feat: backup architecture docs, source code, and scripts
This commit is contained in:
288
src/api/identities.rs
Normal file
288
src/api/identities.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::db::{schema, Database, PostgresDb};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RegisterFromPersonRequest {
|
||||
pub video_uuid: String,
|
||||
pub person_id: String,
|
||||
pub identity_name: String,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RegisterFromPersonResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub identity_id: i32,
|
||||
pub identity_name: String,
|
||||
pub person_id: String,
|
||||
}
|
||||
|
||||
pub fn identity_routes() -> Router<crate::api::server::AppState> {
|
||||
Router::new()
|
||||
.route("/api/v1/identities/from-person", post(register_from_person))
|
||||
.route("/api/v1/identities", get(list_identities))
|
||||
}
|
||||
|
||||
/// Register a Global Identity from a specific Person in a video.
|
||||
/// This creates/updates the Identity record, links the Person to the Identity,
|
||||
/// and updates the Person's name to match the Identity.
|
||||
async fn register_from_person(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(req): Json<RegisterFromPersonRequest>,
|
||||
) -> Result<Json<RegisterFromPersonResponse>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("DB error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let mut tx = match db.pool().begin().await {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Tx error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Check if Person exists
|
||||
let person_query =
|
||||
"SELECT id, name FROM person_identities WHERE person_id = $1 AND video_uuid = $2";
|
||||
let person: Option<(i32, Option<String>)> = match sqlx::query_as(person_query)
|
||||
.bind(&req.person_id)
|
||||
.bind(&req.video_uuid)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let (person_db_id, _old_name) = match person {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"Person '{}' not found in video '{}'",
|
||||
req.person_id, req.video_uuid
|
||||
),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Check if Identity exists
|
||||
let identity_query = "SELECT id FROM identities WHERE name = $1";
|
||||
let identity_id: Option<i32> = match sqlx::query_scalar(identity_query)
|
||||
.bind(&req.identity_name)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let final_identity_id = if let Some(id) = identity_id {
|
||||
id
|
||||
} else {
|
||||
// Create new Identity
|
||||
let meta_json = req.metadata.clone().unwrap_or(serde_json::json!({}));
|
||||
|
||||
let new_id: i32 = match sqlx::query_scalar(
|
||||
r#"
|
||||
INSERT INTO identities (name, embedding, metadata)
|
||||
VALUES ($1, NULLIF($2, '')::public.vector, $3)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(&req.identity_name)
|
||||
.bind("".to_string()) // No embedding for now via this API
|
||||
.bind(&meta_json)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Insert identity error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
new_id
|
||||
};
|
||||
|
||||
// 3. Create Binding
|
||||
// Columns: id, identity_id, identity_type, identity_value, confidence, metadata, created_at
|
||||
let binding_query = r#"
|
||||
INSERT INTO identity_bindings (identity_id, identity_type, identity_value, confidence, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#;
|
||||
|
||||
match sqlx::query(binding_query)
|
||||
.bind(final_identity_id)
|
||||
.bind("person_id") // identity_type
|
||||
.bind(&req.person_id) // identity_value
|
||||
.bind(1.0) // confidence
|
||||
.bind(&serde_json::json!({"auto_updated": true}))
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Binding error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Update Person Name
|
||||
let update_person = "UPDATE person_identities SET name = $1 WHERE id = $2";
|
||||
match sqlx::query(update_person)
|
||||
.bind(&req.identity_name)
|
||||
.bind(person_db_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Update person error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match tx.commit().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Commit error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RegisterFromPersonResponse {
|
||||
success: true,
|
||||
message: format!(
|
||||
"Successfully registered identity '{}' and linked to person '{}'",
|
||||
req.identity_name, req.person_id
|
||||
),
|
||||
identity_id: final_identity_id,
|
||||
identity_name: req.identity_name,
|
||||
person_id: req.person_id,
|
||||
}))
|
||||
}
|
||||
|
||||
/// List all global identities
|
||||
async fn list_identities(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Query(query): Query<ListIdentitiesQuery>,
|
||||
) -> Result<Json<IdentityListResponse>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("DB error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
// 獲取總數
|
||||
let count_sql = "SELECT COUNT(*) FROM identities";
|
||||
let total: i64 = match sqlx::query_scalar(count_sql).fetch_one(db.pool()).await {
|
||||
Ok(count) => count,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Count error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let sql = "SELECT id, name, metadata FROM identities ORDER BY id DESC LIMIT $1 OFFSET $2";
|
||||
|
||||
let rows: Vec<(i32, String, Option<serde_json::Value>)> = match sqlx::query_as(sql)
|
||||
.bind(page_size as i64)
|
||||
.bind(offset)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let identities: Vec<IdentityResponse> = rows
|
||||
.into_iter()
|
||||
.map(|r| IdentityResponse {
|
||||
id: r.0,
|
||||
name: r.1,
|
||||
metadata: r.2,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(IdentityListResponse {
|
||||
identities,
|
||||
count: total,
|
||||
page,
|
||||
page_size,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListIdentitiesQuery {
|
||||
pub page: Option<usize>,
|
||||
pub page_size: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityListResponse {
|
||||
pub identities: Vec<IdentityResponse>,
|
||||
pub count: i64,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
}
|
||||
Reference in New Issue
Block a user