diff --git a/api/Cargo.toml b/api/Cargo.toml index 91e8ad9..c8fd479 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT" repository = "https://github.com/teaxyz/chai-oss" [dependencies] +uuid = { version = "1.11.0", features = ["serde", "v4"] } actix-web = "4.3" dotenv = "0.15" tokio = { version = "1", features = ["full"] } @@ -20,4 +21,5 @@ chrono = { version = "0.4", features = ["serde"] } tokio-postgres = { version = "0.7", features = [ "with-serde_json-1", "with-chrono-0_4", + "with-uuid-1", ] } diff --git a/api/src/handlers.rs b/api/src/handlers.rs index 19326fd..ecbc746 100644 --- a/api/src/handlers.rs +++ b/api/src/handlers.rs @@ -1,6 +1,8 @@ use actix_web::{get, web, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use tokio_postgres::error::SqlState; +use uuid::Uuid; use crate::app_state::AppState; use crate::utils::{get_column_names, rows_to_json}; @@ -88,3 +90,47 @@ pub async fn get_table( } } } + +#[get("/{table}/{id}")] +pub async fn get_table_row( + path: web::Path<(String, Uuid)>, + data: web::Data, +) -> impl Responder { + let (table_name, id) = path.into_inner(); + + if !data.tables.contains(&table_name) { + return HttpResponse::NotFound().json(json!({ + "error": format!("Table '{}' not found", table_name) + })); + } + + let query = format!("SELECT * FROM {} WHERE id = $1", table_name); + + match data.client.query_one(&query, &[&id]).await { + Ok(row) => { + let json = rows_to_json(&[row]); + let value = json.first().unwrap(); + HttpResponse::Ok().json(value) + } + Err(e) => { + if e.as_db_error() + .map_or(false, |e| e.code() == &SqlState::UNDEFINED_TABLE) + { + HttpResponse::NotFound().json(json!({ + "error": format!("Table '{}' not found", table_name) + })) + } else if e + .as_db_error() + .map_or(false, |e| e.code() == &SqlState::NO_DATA_FOUND) + { + HttpResponse::NotFound().json(json!({ + "error": format!("No row found with id '{}' in table '{}'", id, table_name) + })) + } else { + HttpResponse::InternalServerError().json(json!({ + "error": format!("Database error: {}", e) + })) + } + } + } +} diff --git a/api/src/main.rs b/api/src/main.rs index 647d6a8..991084b 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use crate::app_state::AppState; use crate::db::create_db_client; -use crate::handlers::{get_table, heartbeat, list_tables}; +use crate::handlers::{get_table, get_table_row, heartbeat, list_tables}; use crate::logging::setup_logger; #[actix_web::main] @@ -40,6 +40,7 @@ async fn main() -> std::io::Result<()> { .service(list_tables) .service(heartbeat) .service(get_table) + .service(get_table_row) }) .bind(&bind_address)? .run() diff --git a/api/src/utils.rs b/api/src/utils.rs index 264ca33..d2a5cf2 100644 --- a/api/src/utils.rs +++ b/api/src/utils.rs @@ -1,6 +1,7 @@ +use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc}; use serde_json::{json, Value}; -use tokio_postgres::types::{Json, Type}; -use tokio_postgres::Row; +use tokio_postgres::{types::Type, Row}; +use uuid::Uuid; pub fn get_column_names(rows: &[Row]) -> Vec { if let Some(row) = rows.first() { @@ -27,20 +28,24 @@ pub fn rows_to_json(rows: &[Row]) -> Vec { Type::BOOL => json!(row.get::<_, bool>(i)), Type::VARCHAR | Type::TEXT | Type::BPCHAR => json!(row.get::<_, String>(i)), Type::TIMESTAMP => { - let ts: chrono::NaiveDateTime = row.get(i); + let ts: NaiveDateTime = row.get(i); json!(ts.to_string()) } Type::TIMESTAMPTZ => { - let ts: chrono::DateTime = row.get(i); + let ts: DateTime = row.get(i); json!(ts.to_rfc3339()) } Type::DATE => { - let date: chrono::NaiveDate = row.get(i); + let date: NaiveDate = row.get(i); json!(date.to_string()) } Type::JSON | Type::JSONB => { - let json_value: Json = row.get(i); - json_value.0 + let json_value: serde_json::Value = row.get(i); + json_value + } + Type::UUID => { + let uuid: Uuid = row.get(i); + json!(uuid.to_string()) } _ => Value::Null, }; diff --git a/docker-compose.yml b/docker-compose.yml index 99cad6b..5a9439f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,6 +64,29 @@ services: alembic: condition: service_completed_successfully + api: + build: + context: ./api + dockerfile: Dockerfile + environment: + - DATABASE_URL=postgresql://postgres:s3cr3t@db:5432/chai + - HOST=0.0.0.0 + - PORT=8080 + ports: + - "8080:8080" + depends_on: + db: + condition: service_healthy + alembic: + condition: service_completed_successfully + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/heartbeat"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + monitor: build: context: ./monitor