"""STEP 05 — Vocabulary Terms (权威词表) CRUD + lookup + merge.""" from fastapi import APIRouter, Depends, HTTPException from app.auth import CurrentUser from app.config import settings from app.contracts import VocabularyTermCreate, VocabularyTermUpdate from app.db import ( list_vocabulary_terms, create_vocabulary_term, update_vocabulary_term, lookup_vocabulary, get_conn, ) from app.project_context import ProjectContext, get_project_context router = APIRouter() @router.get("/vocabulary") async def _list( entity_type: str | None = None, search: str | None = None, context: ProjectContext = Depends(get_project_context), _user: CurrentUser = None, ): return await list_vocabulary_terms( context.tenant_id, context.project_id, entity_type, search ) @router.post("/vocabulary") async def _create( body: VocabularyTermCreate, user: CurrentUser, context: ProjectContext = Depends(get_project_context), ): data = body.model_dump() data["tenant_id"] = context.tenant_id data["project_id"] = context.project_id data["created_by"] = user["username"] return await create_vocabulary_term(data) @router.patch("/vocabulary/{term_id}") async def _update(term_id: int, body: VocabularyTermUpdate, _user: CurrentUser): row = await update_vocabulary_term(term_id, body.model_dump(exclude_none=True)) if not row: raise HTTPException(404, "Term not found") return row @router.get("/vocabulary/lookup") async def _lookup( name: str, context: ProjectContext = Depends(get_project_context), _user: CurrentUser = None, ): """Normalize a name against the vocabulary.""" result = await lookup_vocabulary(context.tenant_id, context.project_id, name) if not result: return {"found": False, "name": name} return {"found": True, "canonical_name": result["canonical_name"], "term": result} @router.post("/vocabulary/{term_id}/merge-into/{target_id}") async def _merge(term_id: int, target_id: int, _user: CurrentUser): """Merge aliases from term_id into target_id, then delete term_id.""" import json s = settings.db_schema async with get_conn() as conn: async with conn.cursor() as cur: await cur.execute(f"SELECT * FROM {s}.vocabulary_terms WHERE id=%s", (term_id,)) source = await cur.fetchone() await cur.execute(f"SELECT * FROM {s}.vocabulary_terms WHERE id=%s", (target_id,)) target = await cur.fetchone() if not source or not target: raise HTTPException(404, "Term not found") source_aliases = source["aliases"] if isinstance(source["aliases"], list) else json.loads(source["aliases"] or "[]") target_aliases = target["aliases"] if isinstance(target["aliases"], list) else json.loads(target["aliases"] or "[]") merged = list(set(target_aliases + [source["canonical_name"]] + source_aliases)) if target["canonical_name"] in merged: merged.remove(target["canonical_name"]) await update_vocabulary_term(target_id, {"aliases": json.dumps(merged)}) async with conn.cursor() as cur: await cur.execute(f"DELETE FROM {s}.vocabulary_terms WHERE id=%s", (term_id,)) await conn.commit() return {"merged": term_id, "into": target_id, "aliases": merged}