feat: identity PATCH update, alias system, name UNIQUE removal
- Add PATCH /api/v1/identity/:identity_uuid endpoint - Migration 030: remove name UNIQUE, add tmdb_id index - TMDb upsert: ON CONFLICT (name) -> ON CONFLICT (tmdb_id) - get_or_create_identity: pre-check by name - upload_identity: ON CONFLICT (name) -> ON CONFLICT (uuid) - Search: include aliases in identity text search - Add scripts/llm_metadata_enhancer.py - Add DESIGN/IdentityUpdateAndAliasSystem.md
This commit is contained in:
@@ -2,7 +2,7 @@ use axum::{
|
||||
extract::{Multipart, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::{Html, Json},
|
||||
routing::{get, post},
|
||||
routing::{get, patch, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -20,7 +20,9 @@ pub fn identity_routes() -> Router<crate::api::types::AppState> {
|
||||
)
|
||||
.route(
|
||||
"/api/v1/identity/:identity_uuid",
|
||||
get(get_identity_detail).delete(delete_identity),
|
||||
get(get_identity_detail)
|
||||
.delete(delete_identity)
|
||||
.patch(update_identity),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/identity/:identity_uuid/files",
|
||||
@@ -785,8 +787,8 @@ async fn upload_identity(
|
||||
let result = sqlx::query_as::<_, (String,)>(&format!(
|
||||
"INSERT INTO {} (uuid, name, identity_type, source, status, tmdb_id, tmdb_profile, metadata) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) \
|
||||
ON CONFLICT (name) DO UPDATE SET \
|
||||
source = EXCLUDED.source, status = EXCLUDED.status, \
|
||||
ON CONFLICT (uuid) DO UPDATE SET \
|
||||
name = EXCLUDED.name, source = EXCLUDED.source, status = EXCLUDED.status, \
|
||||
tmdb_id = EXCLUDED.tmdb_id, tmdb_profile = EXCLUDED.tmdb_profile, \
|
||||
metadata = EXCLUDED.metadata \
|
||||
RETURNING uuid::text", identities_table
|
||||
@@ -1167,8 +1169,12 @@ async fn search_identities_by_text(
|
||||
JOIN {} c ON c.file_uuid = fd.file_uuid
|
||||
AND c.start_time <= fd.frame_number / COALESCE(c.fps, 25.0)
|
||||
AND c.end_time >= fd.frame_number / COALESCE(c.fps, 25.0)
|
||||
WHERE i.name ILIKE $1
|
||||
AND ($2::text IS NULL OR fd.file_uuid = $2)
|
||||
WHERE (i.name ILIKE $1
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(i.metadata->'aliases') AS a
|
||||
WHERE a->>'name' ILIKE $1
|
||||
))
|
||||
AND ($2::text IS NULL OR fd.file_uuid = $2)
|
||||
ORDER BY i.name, c.start_time
|
||||
LIMIT $3"#,
|
||||
id_table, fd_table, chunk_table
|
||||
@@ -1222,3 +1228,141 @@ async fn search_identities_by_text(
|
||||
results,
|
||||
}))
|
||||
}
|
||||
|
||||
// ── PATCH /api/v1/identity/:identity_uuid ────────────────────
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UpdateIdentityRequest {
|
||||
name: Option<String>,
|
||||
metadata: Option<serde_json::Value>,
|
||||
status: Option<String>,
|
||||
identity_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct UpdateIdentityResponse {
|
||||
success: bool,
|
||||
identity_uuid: String,
|
||||
updated_fields: Vec<String>,
|
||||
}
|
||||
|
||||
async fn update_identity(
|
||||
State(state): State<crate::api::types::AppState>,
|
||||
Path(identity_uuid): Path<String>,
|
||||
Json(req): Json<UpdateIdentityRequest>,
|
||||
) -> Result<Json<UpdateIdentityResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let uuid_clean = identity_uuid.replace('-', "");
|
||||
let uuid_parsed = uuid::Uuid::parse_str(&uuid_clean).map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({
|
||||
"success": false, "error": "Invalid identity_uuid"
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
|
||||
let table = crate::core::db::schema::table_name("identities");
|
||||
let existing: Option<(i32, String)> = sqlx::query_as(&format!(
|
||||
"SELECT id, name FROM {} WHERE uuid = $1::uuid",
|
||||
table
|
||||
))
|
||||
.bind(uuid_parsed)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"success": false, "error": format!("DB error: {}", e)
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
|
||||
let (identity_id, old_name) = existing.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({
|
||||
"success": false, "error": "Identity not found"
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut updated_fields: Vec<String> = Vec::new();
|
||||
let mut set_clauses: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(ref name) = req.name {
|
||||
set_clauses.push(format!("name = ${}", set_clauses.len() + 1));
|
||||
updated_fields.push("name".to_string());
|
||||
}
|
||||
if let Some(ref metadata) = req.metadata {
|
||||
set_clauses.push(format!("metadata = ${}::jsonb", set_clauses.len() + 1));
|
||||
updated_fields.push("metadata".to_string());
|
||||
}
|
||||
if let Some(ref status) = req.status {
|
||||
set_clauses.push(format!("status = ${}", set_clauses.len() + 1));
|
||||
updated_fields.push("status".to_string());
|
||||
}
|
||||
if let Some(ref identity_type) = req.identity_type {
|
||||
set_clauses.push(format!("identity_type = ${}", set_clauses.len() + 1));
|
||||
updated_fields.push("identity_type".to_string());
|
||||
}
|
||||
|
||||
if set_clauses.is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({
|
||||
"success": false, "error": "No fields to update"
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
let set_sql = set_clauses.join(", ");
|
||||
let uuid_param = set_clauses.len() + 1;
|
||||
let update_sql = format!(
|
||||
"UPDATE {} SET {} WHERE uuid = ${}::uuid",
|
||||
table, set_sql, uuid_param
|
||||
);
|
||||
|
||||
let mut query = sqlx::query(&update_sql);
|
||||
|
||||
if let Some(ref name) = req.name {
|
||||
query = query.bind(name);
|
||||
}
|
||||
if let Some(ref metadata) = req.metadata {
|
||||
query = query.bind(metadata);
|
||||
}
|
||||
if let Some(ref status) = req.status {
|
||||
query = query.bind(status);
|
||||
}
|
||||
if let Some(ref identity_type) = req.identity_type {
|
||||
query = query.bind(identity_type);
|
||||
}
|
||||
|
||||
query = query.bind(uuid_parsed);
|
||||
|
||||
query.execute(state.db.pool()).await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"success": false, "error": format!("Update failed: {}", e)
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Sync identity.json to disk
|
||||
let _ =
|
||||
crate::core::identity::storage::save_identity_file_by_pool(state.db.pool(), &uuid_clean)
|
||||
.await;
|
||||
|
||||
// If name changed, update _index.json
|
||||
if req.name.is_some() {
|
||||
let new_name = req.name.as_deref().unwrap_or(&old_name);
|
||||
let _ = crate::core::identity::storage::update_index(&uuid_clean, new_name);
|
||||
}
|
||||
|
||||
Ok(Json(UpdateIdentityResponse {
|
||||
success: true,
|
||||
identity_uuid: uuid_clean,
|
||||
updated_fields,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user