Coverage for node / src / stigmem_node / routes / aliases.py: 88%
50 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Entity alias management routes — spec §2.6 Phase 6.
3POST /v1/aliases — register a user-defined semantic alias
4GET /v1/aliases — list aliases (filterable by kind / canonical_uri)
5DELETE /v1/aliases/{raw_uri} — remove a user-defined alias (migration aliases protected)
6"""
8from __future__ import annotations
10from typing import Annotated, Any
11from urllib.parse import unquote
13from fastapi import APIRouter, Depends, HTTPException, Query, status
15from ..auth import Identity, resolve_identity
16from ..db import db
17from ..models.aliases import AliasRecord, AliasRequest
18from ..recall.fuzzy_resolver import register_alias
20router = APIRouter(prefix="/v1/aliases", tags=["aliases"])
22_VALID_KINDS = {"user", "migration"}
25@router.post("", response_model=AliasRecord, status_code=status.HTTP_201_CREATED)
26def create_alias(
27 req: AliasRequest,
28 identity: Annotated[Identity, Depends(resolve_identity)],
29) -> AliasRecord:
30 """Register a user-defined semantic alias (raw_uri ≡ canonical_uri)."""
31 if not identity.can_write(): 31 ↛ 32line 31 didn't jump to line 32 because the condition on line 31 was never true
32 raise HTTPException(
33 status_code=status.HTTP_403_FORBIDDEN, detail="write permission required"
34 )
36 with db() as conn:
37 try:
38 result = register_alias(conn, req.raw_uri, req.canonical_uri, kind="user")
39 except ValueError as exc:
40 raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
42 return AliasRecord(**result)
45@router.get("", response_model=list[AliasRecord])
46def list_aliases(
47 identity: Annotated[Identity, Depends(resolve_identity)],
48 kind: str | None = Query(None, description="Filter by kind: 'user' or 'migration'"),
49 canonical_uri: str | None = Query(
50 None, description="Return all aliases that resolve to this URI"
51 ),
52) -> list[AliasRecord]:
53 """List registered entity aliases."""
54 if not identity.can_read(): 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true
55 raise HTTPException(
56 status_code=status.HTTP_403_FORBIDDEN, detail="read permission required"
57 )
59 if kind and kind not in _VALID_KINDS:
60 raise HTTPException(
61 status_code=status.HTTP_400_BAD_REQUEST,
62 detail=f"kind must be one of {sorted(_VALID_KINDS)}",
63 )
65 conditions: list[str] = []
66 params: list[Any] = []
67 if kind:
68 conditions.append("kind = ?")
69 params.append(kind)
70 if canonical_uri:
71 conditions.append("canonical_uri = ?")
72 params.append(canonical_uri)
74 where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
76 with db() as conn:
77 rows = conn.execute(
78 f"SELECT raw_uri, canonical_uri, kind, created_at FROM entity_aliases" # nosec B608 — where is built from literal fragments; values in params
79 f" {where} ORDER BY created_at DESC",
80 params,
81 ).fetchall()
83 return [AliasRecord(**dict(r)) for r in rows]
86@router.delete("/{raw_uri:path}", status_code=status.HTTP_204_NO_CONTENT)
87def delete_alias(
88 raw_uri: str,
89 identity: Annotated[Identity, Depends(resolve_identity)],
90) -> None:
91 """Remove a user-defined alias. Migration aliases cannot be deleted via API."""
92 if not identity.can_write(): 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 raise HTTPException(
94 status_code=status.HTTP_403_FORBIDDEN, detail="write permission required"
95 )
97 decoded = unquote(raw_uri)
99 with db() as conn:
100 row = conn.execute(
101 "SELECT kind FROM entity_aliases WHERE raw_uri = ?", (decoded,)
102 ).fetchone()
103 if row is None:
104 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="alias not found")
105 if row["kind"] != "user": 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 raise HTTPException(
107 status_code=status.HTTP_403_FORBIDDEN,
108 detail=(
109 "migration aliases are managed by the migration sweep "
110 "and cannot be deleted via API"
111 ),
112 )
113 conn.execute("DELETE FROM entity_aliases WHERE raw_uri = ?", (decoded,))