KB-280B
09 — Directus / API Exposure Readiness (PG views → read-only API, no math)
5 min read Revision 1
registries-pivotdirectusapiexposureread-onlyno-mathdieu28gated-P22026-05-31
title: 09 — Directus / API Exposure Readiness date: 2026-05-31 gate: P2 (after P1 committed). Design-only; no Directus mutation this session.
09 — Directus / API Exposure Readiness
Principle (Đ28): the API/Directus is a shaper, not a calculator. It exposes committed PG views/functions verbatim. No per-count math, no gap arithmetic, no hardcoded arrays in the API layer — those are exactly the legacy violations (doc 11).
A. What gets exposed (after P1 commit)
| contract field | PG source | shape |
|---|---|---|
| leaf list | v_living_lists |
rows {code,name,entity_type,composition_level,list_count,count_source,pivot_code,pivot_backed} |
| integrity summary | v_count_integrity (aggregate) |
{leaf_rows,sum_record,sum_actual,net_gap,drift_rows,unverified_rows,pivot_backed,pivot_missing,status} |
| drift detail | v_count_drift |
rows {code,gap,drift_side,drift_classification,...} |
| drill tree | v_registries_pivot_tree |
rows {node_code,parent_code,is_root,has_children,...} |
| node substrate | fn_registries_pivot_node_substrate(code) |
1 row {…,record_count,actual_count,pivot_backed,pivot_count} |
| any count | pivot_count(code) / pivot_query(code) |
{code,name,source,value} — PIVOT_MISSING if no pivot |
B. Endpoint candidates (/api/registries-pivot/* — note: /api/registries-pivot/health is 404 today)
GET /api/registries-pivot/lists→v_living_listsGET /api/registries-pivot/integrity→v_count_integrityaggregate (+?detail=drift→v_count_drift)GET /api/registries-pivot/tree→v_registries_pivot_treeGET /api/registries-pivot/node/:code→fn_registries_pivot_node_substrate(:code)GET /api/registries-pivot/count/:pivot_code→pivot_count(:pivot_code)Each handler is a singleSELECT * FROM <view/fn>(or Directus collection read) → JSON. Zero business logic. Contrast: the legacyhealth.get.tscomputesgap/totalGapin the handler (doc 11).
C. Two exposure routes (choose at P2)
- Directus collections over the views — register
v_count_integrity,v_living_lists,v_count_drift,v_registries_pivot_treeas read-only Directus collections (Directus can map to views). Pros: permissions/UI for free. The fn needs a thin endpoint. - Nuxt server routes under
/api/registries-pivot/*thatSELECTfrom the views/fn. Pros: functions exposable; total control. Either way the shaper does no counting.
D. Permissions / read-only role
- Reuse a read-only DB role (like
context_pack_readonly) for these endpoints; no write grant. - Directus: public/role read on the four view-collections; no create/update/delete.
- Functions:
GRANT EXECUTEonfn_registries_pivot_node_substrate(text)andpivot_count(text)to the read role only.
E. Response shapes & failure states (explicit, not silent)
- Missing pivot → field
pivot_count: null,pivot_backed: false, badgePIVOT_MISSING. - Unmeasured leaf →
count_integrity_status: "unverified",list_countmay be null withcount_source: "unmeasured". - Unknown node code →
fnreturns 0 rows → API 404 with{error:"unknown_node"}(never a guessed count). - Stale pivot → include
refreshed_at; never recompute client-side.
F. Security risks
| risk | mitigation |
|---|---|
| write via API | read-only role + read-only Directus permissions; no INSERT/UPDATE endpoints |
| count math creeping into handler | code review + no-hardcode CI (doc 12) greps for reduce/Math.abs/gap in handlers |
| info exposure (985k birth rows) | endpoints return counts/aggregates, not row dumps; node endpoint returns substrate metadata only |
injection on :code |
parameterized; validate against `^(CAT |
| over-fetch / DoS | pagination on list endpoints; views are cheap (read meta_catalog/pivot_results, not live 985k scans) |
G. Gate
P2, after P1 commit. Design-only here; no Directus mutation performed this session. The contract field→source map above is the acceptance artifact for P2.