From 6f215162a35acfff613f9ba569894571808c1575 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 11 Mar 2026 13:54:14 +0800 Subject: [PATCH] feat(page): add page table and CRUD handlers --- Cargo.toml | 2 +- migrations/20260311090000_create_page.sql | 11 ++ src/handler.rs | 1 + src/handler/page.rs | 169 ++++++++++++++++++++++ src/main.rs | 2 + src/model.rs | 13 ++ 6 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 migrations/20260311090000_create_page.sql create mode 100644 src/handler/page.rs diff --git a/Cargo.toml b/Cargo.toml index 13b8973..b8320d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ axum = { version = "0.8", features = ["ws"] } tower-http = { version = "0.6", features = ["cors", "fs"] } # Database -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid", "json"] } # Serialization serde = { version = "1.0", features = ["derive"] } diff --git a/migrations/20260311090000_create_page.sql b/migrations/20260311090000_create_page.sql new file mode 100644 index 0000000..f8ac0c2 --- /dev/null +++ b/migrations/20260311090000_create_page.sql @@ -0,0 +1,11 @@ +-- Page table for UI bindings +CREATE TABLE page ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + data JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Common indexes +CREATE INDEX idx_page_name ON page(name); diff --git a/src/handler.rs b/src/handler.rs index dc2be6b..235a9b7 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -2,3 +2,4 @@ pub mod source; pub mod point; pub mod tag; pub mod log; +pub mod page; diff --git a/src/handler/page.rs b/src/handler/page.rs new file mode 100644 index 0000000..b9a89fe --- /dev/null +++ b/src/handler/page.rs @@ -0,0 +1,169 @@ +use axum::{Json, extract::{Path, Query, State}, http::StatusCode, response::IntoResponse}; +use serde::Deserialize; +use std::collections::HashMap; +use sqlx::types::Json as SqlxJson; +use uuid::Uuid; +use validator::Validate; + +use crate::model::Page; +use crate::util::response::ApiErr; +use crate::AppState; + +#[derive(Deserialize, Validate)] +pub struct GetPageListQuery { + #[validate(length(min = 1, max = 100))] + pub name: Option, +} + +pub async fn get_page_list( + State(state): State, + Query(query): Query, +) -> Result { + query.validate()?; + let pool = &state.pool; + + let pages: Vec = if let Some(name) = query.name { + sqlx::query_as::<_, Page>( + r#" + SELECT * FROM page + WHERE name ILIKE $1 + ORDER BY created_at + "#, + ) + .bind(format!("%{}%", name)) + .fetch_all(pool) + .await? + } else { + sqlx::query_as::<_, Page>( + r#"SELECT * FROM page ORDER BY created_at"#, + ) + .fetch_all(pool) + .await? + }; + + Ok(Json(pages)) +} + +pub async fn get_page( + State(state): State, + Path(page_id): Path, +) -> Result { + let page = sqlx::query_as::<_, Page>("SELECT * FROM page WHERE id = $1") + .bind(page_id) + .fetch_optional(&state.pool) + .await?; + + match page { + Some(p) => Ok(Json(p)), + None => Err(ApiErr::NotFound("Page not found".to_string(), None)), + } +} + +#[derive(Debug, Deserialize, Validate)] +pub struct CreatePageReq { + #[validate(length(min = 1, max = 100))] + pub name: String, + pub data: HashMap, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct UpdatePageReq { + #[validate(length(min = 1, max = 100))] + pub name: Option, + pub data: Option>, +} + +pub async fn create_page( + State(state): State, + Json(payload): Json, +) -> Result { + payload.validate()?; + + let page_id = sqlx::query_scalar::<_, Uuid>( + r#" + INSERT INTO page (name, data) + VALUES ($1, $2) + RETURNING id + "#, + ) + .bind(&payload.name) + .bind(SqlxJson(payload.data)) + .fetch_one(&state.pool) + .await?; + + Ok((StatusCode::CREATED, Json(serde_json::json!({ + "id": page_id, + "ok_msg": "Page created successfully" + })))) +} + +pub async fn update_page( + State(state): State, + Path(page_id): Path, + Json(payload): Json, +) -> Result { + payload.validate()?; + + let exists = sqlx::query("SELECT 1 FROM page WHERE id = $1") + .bind(page_id) + .fetch_optional(&state.pool) + .await?; + if exists.is_none() { + return Err(ApiErr::NotFound("Page not found".to_string(), None)); + } + + if payload.name.is_none() && payload.data.is_none() { + return Ok(Json(serde_json::json!({"ok_msg": "No fields to update"}))); + } + + let mut updates = Vec::new(); + let mut param_count = 1; + + if payload.name.is_some() { + updates.push(format!("name = ${}", param_count)); + param_count += 1; + } + if payload.data.is_some() { + updates.push(format!("data = ${}", param_count)); + param_count += 1; + } + + updates.push("updated_at = NOW()".to_string()); + + let sql = format!( + r#"UPDATE page SET {} WHERE id = ${}"#, + updates.join(", "), + param_count + ); + + let mut query = sqlx::query(&sql); + if let Some(name) = payload.name { + query = query.bind(name); + } + if let Some(data) = payload.data { + query = query.bind(SqlxJson(data)); + } + query = query.bind(page_id); + + query.execute(&state.pool).await?; + + Ok(Json(serde_json::json!({ + "ok_msg": "Page updated successfully" + }))) +} + +pub async fn delete_page( + State(state): State, + Path(page_id): Path, +) -> Result { + let result = sqlx::query("DELETE FROM page WHERE id = $1") + .bind(page_id) + .execute(&state.pool) + .await?; + + if result.rows_affected() == 0 { + return Err(ApiErr::NotFound("Page not found".to_string(), None)); + } + + Ok(StatusCode::NO_CONTENT) +} diff --git a/src/main.rs b/src/main.rs index 54e47b0..ebd145c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,6 +142,8 @@ fn build_router(state: AppState) -> Router { .route("/api/point/batch/set-tags", put(handler::point::batch_set_point_tags)) .route("/api/tag", get(handler::tag::get_tag_list).post(handler::tag::create_tag)) .route("/api/tag/{tag_id}", get(handler::tag::get_tag_points).put(handler::tag::update_tag).delete(handler::tag::delete_tag)) + .route("/api/page", get(handler::page::get_page_list).post(handler::page::create_page)) + .route("/api/page/{page_id}", get(handler::page::get_page).put(handler::page::update_page).delete(handler::page::delete_page)) .route("/api/logs", get(handler::log::get_logs)) .route("/api/logs/stream", get(handler::log::stream_logs)); diff --git a/src/model.rs b/src/model.rs index d7b95f8..243a2f2 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,6 +1,8 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use sqlx::types::Json; +use std::collections::HashMap; use uuid::Uuid; use crate::util::datetime::utc_to_local_str; @@ -116,3 +118,14 @@ pub struct Tag { pub updated_at: DateTime, } +#[derive(Debug, Serialize, Deserialize, FromRow, Clone)] +pub struct Page { + pub id: Uuid, + pub name: String, + pub data: Json>, + #[serde(serialize_with = "utc_to_local_str")] + pub created_at: DateTime, + #[serde(serialize_with = "utc_to_local_str")] + pub updated_at: DateTime, +} +