Coverage for node / src / stigmem_node / routes / aliases.py: 88%
51 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +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(
39 conn, req.raw_uri, req.canonical_uri, kind="user", tenant_id=identity.tenant_id
40 )
41 except ValueError as exc:
42 raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
44 return AliasRecord(**result)
47@router.get("", response_model=list[AliasRecord])
48def list_aliases(
49 identity: Annotated[Identity, Depends(resolve_identity)],
50 kind: str | None = Query(None, description="Filter by kind: 'user' or 'migration'"),
51 canonical_uri: str | None = Query(
52 None, description="Return all aliases that resolve to this URI"
53 ),
54) -> list[AliasRecord]:
55 """List registered entity aliases."""
56 if not identity.can_read(): 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 raise HTTPException(
58 status_code=status.HTTP_403_FORBIDDEN, detail="read permission required"
59 )
61 if kind and kind not in _VALID_KINDS:
62 raise HTTPException(
63 status_code=status.HTTP_400_BAD_REQUEST,
64 detail=f"kind must be one of {sorted(_VALID_KINDS)}",
65 )
67 # Always scope to the caller's tenant (aliases are tenant-isolated). The
68 # base `tenant_id = ?` predicate is inlined in the SQL literal (not a
69 # variable) so it is statically verifiable by the tenant-scope CI guard.
70 params: list[Any] = [identity.tenant_id]
71 extra = ""
72 if kind:
73 extra += " AND kind = ?"
74 params.append(kind)
75 if canonical_uri:
76 extra += " AND canonical_uri = ?"
77 params.append(canonical_uri)
79 # Base predicate is a pure literal (incl. `tenant_id = ?` so the CI tenant-scope
80 # guard sees it); `extra` is built only from literal fragments, values in params.
81 base_sql = "SELECT raw_uri, canonical_uri, kind, created_at FROM entity_aliases WHERE tenant_id = ?" # noqa: E501
82 sql = base_sql + extra + " ORDER BY created_at DESC" # noqa: S608 # nosec B608
83 with db() as conn:
84 rows = conn.execute(sql, params).fetchall()
86 return [AliasRecord(**dict(r)) for r in rows]
89@router.delete("/{raw_uri:path}", status_code=status.HTTP_204_NO_CONTENT)
90def delete_alias(
91 raw_uri: str,
92 identity: Annotated[Identity, Depends(resolve_identity)],
93) -> None:
94 """Remove a user-defined alias. Migration aliases cannot be deleted via API."""
95 if not identity.can_write(): 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 raise HTTPException(
97 status_code=status.HTTP_403_FORBIDDEN, detail="write permission required"
98 )
100 decoded = unquote(raw_uri)
102 with db() as conn:
103 row = conn.execute(
104 "SELECT kind FROM entity_aliases WHERE raw_uri = ? AND tenant_id = ?",
105 (decoded, identity.tenant_id),
106 ).fetchone()
107 if row is None:
108 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="alias not found")
109 if row["kind"] != "user": 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 raise HTTPException(
111 status_code=status.HTTP_403_FORBIDDEN,
112 detail=(
113 "migration aliases are managed by the migration sweep "
114 "and cannot be deleted via API"
115 ),
116 )
117 conn.execute(
118 "DELETE FROM entity_aliases WHERE raw_uri = ? AND tenant_id = ?",
119 (decoded, identity.tenant_id),
120 )