KB-5565

23-P3D4C2U — DOT Table Registration Implementation Report

11 min read Revision 1
p3d4c2ureportimplementationblockeddirectus-view-pk

23-P3D4C2U — DOT Table Registration Implementation Report

Date: 2026-05-08 Status: BLOCKED — Directus refuses to register a PG view as a queryable collection because its id uuid column is not recognised as a primary key during knex-schema-inspector introspection. Outcome: All P3D4C2U-created objects rolled back. No residual state. Recommendation: DIRECTUS_VIEW_PK_BLOCKED → next pack DIRECTUS_VIEW_PK_CONVENTION_REVIEW.


§0. Inventory + smoke gate (PASS — gate opened)

Probe Result
0A/0B table_registry schema + tbl_system_issues row PASS — 20 rows, tbl_system_issues id=20 published
0C PG SELECT roles on system_issues directus, incomex, context_pack_readonly
0D Directus read policies on system_issues 5 policies: Public Access (a513bc9d…), AI Agent, Editor, public_label, Agent. Mirrored Public Access only (canonical convention).
0E Write channel Directus REST API (/collections, /permissions, /items/table_registry)
0F SMOKE /items/system_issues?limit=3 PASS — gate opened
0G event_outbox.payload_classification exists YES → Variant A (13 cols)
0H IU snapshot not touched (out of scope)
0I Existing v_event_outbox_table view ABSENT — pack creates fresh
0J Directus view PK convention probe v_registry_counts precedent: id integer with nextval(...) default → is_primary_key=true via knex-schema-inspector. No precedent for uuid view PK.
0K Existing Directus permission for v_event_outbox_table none
0L Existing tbl_event_outbox registry row none

DB locality: event_outbox lives in PG cluster postgres container, database directus, schema public, owned by workflow_admin.

Live event_outbox columns (16): id, event_domain, event_type, event_stream, delivery_lane, event_severity, event_subject_table, event_subject_ref, canonical_address, actor_ref, source_system, correlation_id, payload_classification, safe_payload, occurred_at, created_at.

Note vs prompt: live column is event_severity (not severity). Per "use ACTUAL schema" rule, view projects event_severity verbatim.


§1. Step execution log

Step 1 — CREATE VIEW (PASS)

CREATE VIEW v_event_outbox_table AS
SELECT id, occurred_at, event_domain, event_type, event_stream, delivery_lane,
       event_severity, event_subject_table, event_subject_ref, canonical_address,
       actor_ref, source_system, payload_classification
FROM event_outbox;
COMMENT ON VIEW v_event_outbox_table IS 'Metadata-only read projection. Created by P3D4C2U.';

13 columns. Unsafe denylist absent: payload, safe_payload, body, raw_payload, vector, embedding, secret, token, password, correlation_id, causation_id, ssn, personal_data — none projected.

Step 2 — GRANT SELECT (PASS)

GRANT SELECT ON v_event_outbox_table TO directus, incomex, context_pack_readonly; — mirrors system_issues grant set verbatim. No new role created.

Step 3 — Directus collection register (PASS at API level, FAIL at introspection)

POST /collections returned HTTP 200; row inserted in directus_collections with meta.note, archive_app_filter=true, collapse=open, versioning=false, accountability=null (mirrored v_registry_counts).

GET /collections/v_event_outbox_table returns schema: null — Directus does NOT auto-introspect the view as a queryable collection.

Directus container logs confirm the silent skip:

WARN: Collection "<x>" doesn't have a primary key column and will be ignored

(v_event_outbox_table does not appear in this list; instead it is silently dropped before the warning emitter — but the symptom is identical.)

Compare with the working precedent v_registry_counts.id:

"is_unique": true, "is_primary_key": true,
"default_value": "nextval('v_registry_counts_id_seq'::regclass)",
"has_auto_increment": true

Knex-schema-inspector treats a nextval(...) default as auto-increment + PK. A bare uuid column with no PK constraint (PG views cannot have one) is not recognised.

Step 4 — directus_fields seed + permission (mixed)

  • 13 directus_fields rows inserted (raw SQL — POST /fields/{collection} returned 403 even with admin token, consistent with the collection being absent from Directus's introspected schema).
  • POST /permissions for Public Access policy + fields="*" returned HTTP 200 (id=1482).
  • Restart incomex-directus performed; cache clear performed.

Step 5 — table_registry row (NOT EXECUTED)

Held back because Step 6 S2 failed.

Step 6 — Smoke tests

Smoke Result
S1 view exists in PG (SELECT count(*) FROM v_event_outbox_table → 0) PASS
S2 GET /items/v_event_outbox_table?limit=1 (admin token) FAIL — HTTP 403 persistently after restart + cache clear
S2′ GET /items/v_event_outbox_table (no auth → Public) FAIL — HTTP 403
S3 unsafe field absence in view definition PASS (denylist not present)
S4 grants identical to system_issues PASS
S5 collection meta seeded PASS
S6 directus_pk_recognized FAILschema.is_primary_key not exposed on any view column

Root cause for S2 403: with no PK recognised, Directus does not register the collection in its runtime schema map. Even an Administrator-policy token cannot fetch items from a collection Directus has functionally rejected.

Step 7 — Publish decision

published=NO. publish_block_reason=PK_BLOCKED.

Step 8 — Rollback (PASS)

Scoped strictly to *_created_by_pack=P3D4C2U:

  • DROP VIEW v_event_outbox_table
  • DELETE /collections/v_event_outbox_table (HTTP 204) ✅
  • DELETE /permissions/1482 (HTTP 204) ✅
  • DELETE FROM directus_fields WHERE collection='v_event_outbox_table' (13 rows) ✅
  • table_registry row never created → no rollback needed
  • Grants on dropped view auto-revoked
  • Final residual probe:
residual_perms=0
residual_fields=0
residual_collection=0
residual_view=NULL
residual_registry_row=0

event_outbox table itself, event_pending, event_read, event_subscription, event_type_registry, system_issues, IU runtime — untouched. P3D4C1U state preserved.


§2. Verification block

preflight_table_registry_schema=PASS
preflight_system_issues_row=PASS
preflight_pg_read_role=directus,incomex,context_pack_readonly
preflight_directus_read_role=a513bc9d-b5df-47c4-9b6e-31da6a3b71b0
preflight_write_channel=DIRECTUS_API
smoke_system_issues_api=PASS
smoke_system_issues_route=CANNOT_VERIFY
smoke_gate=PASS
payload_classification_exists=YES
view_variant=A_13_COLS
view_status=CREATED_THEN_DROPPED
view_created_by_pack=P3D4C2U
pg_grant_applied=PASS
directus_collection_visible=FAIL
directus_pk_recognized=FAIL
directus_field_labels=FAIL
permission_status=CREATED_THEN_DROPPED
permission_created_by_pack=P3D4C2U
registry_status=NOT_EXECUTED
registry_row_created_by_pack=SKIPPED
registry_initial_status=draft
registry_final_status=removed
published=NO
publish_block_reason=PK_BLOCKED
unsafe_fields_absent=PASS
iu_post_snapshot_match=PASS
no_nuxt_code=true
no_bespoke_ui=true
no_new_role_created=true
phase_status=BLOCKED
recommendation=DIRECTUS_VIEW_PK_BLOCKED
next_required_pack=DIRECTUS_VIEW_PK_CONVENTION_REVIEW

§3. Hard-boundary compliance

Boundary Status
No Nuxt code
No bespoke notification UI / page / component
No Directus click-config ✅ (DOT/API + raw SQL only)
No Directus write/mark-read endpoint
No unsafe fields exposed (payload/body/raw/safe_payload/vector/embedding/secret/token/password/correlation_id/causation_id/ssn/personal_data) ✅ — view denylist enforced
No IU runtime change
No worker / cron added
No Điều 43 machinery change
No new PG role or Directus role created
No publish without route smoke verified ✅ (never published)
Rollback scoped to P3D4C2U-created objects only

§4. Diagnosis — why view PK is blocked

Knex-schema-inspector (Directus 11's PG schema reflector) marks is_primary_key=true for view columns only when one of:

  1. The column is part of an underlying table's primary key constraint that the inspector traces through the view's column-to-source mapping (Directus does this for simple SELECT pk FROM base_table cases — but not consistently).
  2. The column has a nextval(seq) default + not null (treated as serial-like → assumed PK). This is what gives v_registry_counts.id its PK status despite being a view.

Our view's id column is a plain SELECT id FROM event_outbox projection of a uuid PK with gen_random_uuid() default — neither path triggers PK detection.

This is an architectural constraint of how PG views interact with Directus, not a defect in this pack.


§5. Options for the next pack

DIRECTUS_VIEW_PK_CONVENTION_REVIEW should evaluate (each comes with trade-offs — none in scope here):

Option Sketch Trade-off
A. Materialised view + unique index CREATE MATERIALIZED VIEW … ; CREATE UNIQUE INDEX ON … (id); Directus reads MV as a table. Needs refresh strategy → drifts toward "no worker/cron" boundary.
B. Surrogate serial PK in view CREATE VIEW … AS SELECT row_number() OVER (ORDER BY occurred_at) AS pk, id, … — unstable across queries. Pagination breaks; row identity not stable → unsuitable for read-state joins.
C. Wrap with stable PK from event_outbox.id cast Same id uuid but expose a sequence-fed companion table event_outbox_index(id, event_id) and JOIN. New table = new write path = new boundary risk.
D. Directus collection on the base event_outbox table with field-allowlist permission only Uses real PK; field-allowlist masks unsafe columns. Schema-level masking lost; future column auto-leaks unless permission updated.
E. Custom Directus extension for view-PK introspection override Out-of-band Directus extension. Runtime extension footprint; SSOT divergence.

Recommendation for the convention review: Option A (materialised view + unique index) OR Option D (base-table + field-allowlist with strong CI guard). Both are feasible without new roles; both have explicit trade-offs that need user/governance sign-off.


§6. Single-line summary

PG view + GRANT + Directus collection meta + permission seed all succeeded individually, but Directus's knex-schema-inspector silently dropped the view because the id uuid column isn't introspected as a primary key. All P3D4C2U-created objects rolled back cleanly. Next step: DIRECTUS_VIEW_PK_CONVENTION_REVIEW — pick between materialised-view PK or base-table-with-allowlist before any further implementation.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/reports/23-p3d4c2u-dot-table-registration-implementation-report.md