D28 — Generated Table Map Design (Option C)
D28 — Generated Table Map Design (Option C)
Date: 2026-05-09 Author: claude-opus-4-7 (VPS reverify + design only) Status: DESIGN — chờ GPT/User duyệt trước implementation Scope: thay 3 hardcoded maps bằng generated module duy nhất sinh từ
table_registry+ convention rules. KHÔNG implement trong pack này.
1. Problem statement (verified on VPS 2026-05-09)
Có 3 hardcoded maps trên Nuxt repo (/opt/incomex/docker/nuxt-repo/web) chứa cùng một sự thật về registry routes nhưng được sao chép thủ công:
| # | File | Symbol | Entries | Pattern |
|---|---|---|---|---|
| 1 | pages/knowledge/registries/[entityType]/index.vue (~L39–58) |
tableIdMap |
18 | entityType → tbl_xxx |
| 2 | config/detail-sections.ts (~L227) |
collectionMap |
20 | entityType → collection |
| 3 | config/detail-sections.ts (~L252) |
reverseCollectionMap |
20 | collection → entityType (derived Object.fromEntries) |
| 4 | server/api/discovery/relations.get.ts (~L18) |
COLLECTION_ENTITY_MAP |
14 | collection → entityType (mirror, partial) |
Drift đã tồn tại: relations.get.ts mirror chỉ có 14/20 entries (thiếu system_issues, registry_changelog, tasks, meta_catalog đôi khi, …) — đúng motivation của Option C.
Live table_registry có 21 rows. Schema KHÔNG có column entity_type (xem Phần 4.1). Đây là data problem, không chỉ code problem: entityType chỉ tồn tại trong tableIdMap ở Nuxt.
2. Goals / Non-goals
Goals
- 1 nguồn sự thật cho mapping (registry rows + convention).
- Generated artifact import được từ cả
pages/*.vue(client/SSR) vàserver/api/*.ts. - Build-time deterministic, drift-detectable bằng CI check.
- Không lộ secret trong artifact, không phá quy ước file existing.
Non-goals
- Không thay registry data model toàn cục.
- Không thay route resolution thành dynamic runtime fetch (giữ static).
- Không xử lý 3 entity-only entries (
trigger,comment,taxonomy) trongcollectionMapmà không có row tương ứng trongtable_registry(xem §6 — handle quastatic_extrastrong script config).
3. Hard boundaries
NO_NUXT_CODE_MODIFICATION=true
NO_GENERATED_FILE_CREATION_NOW=true # design pack only
NO_DIRECTUS_MUTATION=true
NO_PG_MUTATION=true
NO_SECRET_IN_REPORT=true
4. Decision matrix
4.1 entityType derivation strategy — RECOMMEND E4 (Phase 1) → E3 (Phase 2)
table_registry columns hiện tại (live, verified):
id, table_id, name, collection, fields(json), default_sort, default_filter(json),
page_url, enable_insert_marks, enable_proposals, enable_search, enable_pagination,
rows_per_page, status, module, description, _dot_origin
Không có entity_type. Cần derive.
| Option | Pros | Cons | Verdict |
|---|---|---|---|
| E1 — Seed từ tableIdMap hiện tại | Zero risk, exact preserve | Hardcode lại trong script, không tự nhiên cho row mới | ❌ chỉ tốt làm starting set, không làm rule |
E2 — Derive từ page_url last segment |
Tự nhiên cho REGISTRY_ENTITY_ROUTE | Fail cho 8/21 non-registry rows (workflow tabs, admin, index) | ❌ không cover toàn 21 rows |
E3 — Add entity_type column vào table_registry |
Cleanest, single source of truth | Cần Directus field-add + backfill 21 rows; out-of-scope cho design pack | ⏭ Phase 2 target |
| E4 — Convention script + per-row override (recommended) | Không cần schema migration; rule-based + explicit overrides; backfill mechanism dễ chuyển sang E3 sau | Convention nằm trong script, không trong DB | ✅ Phase 1 recommendation |
| E5 — Drop entityType, dùng page_url làm key | Eliminates concept | Fundamental redesign; cần rewrite cả routing + Nuxt page params | ❌ blast radius quá lớn |
E4 derivation rules (đề xuất, in script config):
# Step 1 — convention: tbl_xxx → entityType
tbl_registry_<X> → <X> (ví dụ tbl_registry_dot_tools → dot_tool — phải singularize)
tbl_<X>_list → <X> (tbl_workflow_list → workflow, tbl_modules_list → module)
tbl_<X> → <X> (tbl_table_registry → table; explicit override needed)
# Step 2 — explicit overrides (script config dict, seeded từ tableIdMap):
tbl_meta_catalog → catalog
tbl_table_registry → table
tbl_proposals_list → table_proposal
tbl_tasks_list → task
tbl_modules_list → module
tbl_system_issues → system_issue
tbl_event_outbox → event_outbox
tbl_workflow_timeline → (skipped — duplicate collection workflow_steps; xem §6)
# Step 3 — fail-fast: nếu row không match convention và không có override → script lỗi (build break).
Migration path Phase 1 → Phase 2:
- Phase 1 land script (E4) + generated file. Existing hardcoded maps removed.
- Phase 2 (separate pack
D28_TABLE_REGISTRY_SCHEMA_EXTENSION): addentity_typecolumn, backfill bằng chính output dict của E4 script, switch script sang đọc DB cell. Convention rules giữ làm fallback validation.
4.2 Generation script
| Decision | Recommendation | Rationale |
|---|---|---|
| Location | web/scripts/generate-table-maps.ts |
Cùng package, chạy được qua tsx, không cần monorepo cross-link. Existing web/scripts/ đã có ts conventions. |
| Language | TypeScript via tsx |
Output là .ts file → cùng ngôn ngữ giảm typing error; jq path requires bash plumbing và mất type safety. |
| Input | GET /items/table_registry?fields=id,table_id,collection,page_url,status&limit=-1 |
Đủ cho derivation; tránh fields(json) vì lớn và không dùng. |
| Output | Single file web/generated/table-maps.generated.ts (new dir web/generated/ — currently absent, an established Nuxt convention) |
Pure static, 3 named exports + metadata header; one file dễ verify hash. |
Output skeleton:
// AUTO-GENERATED — DO NOT EDIT
// source: directus.table_registry @ 2026-05-09T...Z
// row_count: 21 status_filter: active|published(+overrides)
// content_hash: sha256:...
export const tableIdMap: Record<string, string> = { ... };
export const collectionMap: Record<string, string> = { ...registryDerived, ...staticExtras };
export const reverseCollectionMap: Record<string, string> = Object.fromEntries(
Object.entries(collectionMap).map(([k, v]) => [v, k])
);
export const __META__ = { generatedAt: '...', rowCount: 21, contentHash: '...' };
4.3 Client/server import safety
Verified pure-static design: file không reference process.env, không runtime fetch, không browser/Node-only API. Importable an toàn từ:
pages/knowledge/registries/[entityType]/index.vue(Vue SFC, client+SSR) — replaces localtableIdMap.config/detail-sections.ts(universal config module) — replacescollectionMap+reverseCollectionMap.server/api/discovery/relations.get.ts(Nitro server) — replacesCOLLECTION_ENTITY_MAP(usesreverseCollectionMap).
No split needed. Import alias ~/generated/table-maps.generated works in cả 3 contexts (~ resolves to web/ từ Nuxt config defaults; auto-import alias xác nhận trong nuxt.config.ts không có override).
4.4 Build-time credentials — RECOMMEND C2 (build token via env)
| Option | Verdict |
|---|---|
C1 — Public read on table_registry |
❌ table_registry chứa schema metadata (fields json, filters) — không nên expose Public. |
C2 — Build token in env (NUXT_DIRECTUS_SERVICE_TOKEN) |
✅ token đã tồn tại trong VPS .env cho relations.get.ts runtime; reuse cho build script; token KHÔNG bundled vào generated file (chỉ values). |
| C3 — VPS-only generation | ❌ ràng buộc dev local — họ muốn regen sau khi sửa script phải SSH; tăng friction. |
Token hygiene: script đọc NUXT_DIRECTUS_SERVICE_TOKEN (đã có trong runtimeConfig), không in vào output, không log; trong CI dùng GitHub Actions secret.
4.5 Build/deploy timing — RECOMMEND A (prebuild) + commit fallback
| Option | Verdict |
|---|---|
A — "prebuild": "tsx scripts/generate-table-maps.ts" in web/package.json |
✅ chạy trước nuxt build mọi nơi (local, CI, VPS). Chuẩn Nuxt convention. |
| B — GitHub Actions step | Redundant nếu A có; dùng làm --check step thay vì regen. |
| C — Commit generated file | ✅ kèm theo A — commit để diff visible trong PR; prebuild đảm bảo update; CI verify hash khớp. |
| D — Deploy hook | ❌ trễ; build artifact đã shipped khi đến đó. |
| E — VPS-only script | ❌ same friction như C3. |
Kết hợp A+C: prebuild regen → developer commit; CI runs --check to fail nếu file lệch.
4.6 Status/draft handling — RECOMMEND --include-draft flag + explicit force_include
Live status distribution của 21 rows:
active: 6 (id 1-6, 8)published: 14 (id 7, 9-20)draft: 1 (id 21 —tbl_event_outbox)
Default (production build): include status IN (active, published). Exclude draft.
Chicken-egg cho tbl_event_outbox: route smoke cần map → map cần row → row đang draft.
Đề xuất sequence (safe):
- Dev chạy
npm run generate:table-maps -- --include-draftlocal. - Smoke
/knowledge/registries/event_outboxtrên dev preview. - Nếu OK → publish row trong Directus (status='published').
- CI prebuild regen với default flag → row tự nhiên có mặt → deploy.
Backup mechanism: script config có force_include: string[] để hardcode-include row theo table_id ngay cả khi draft (fallback nếu publish chưa kịp). Không recommend dùng làm long-term.
4.7 Drift verification
Modes:
generate (default) : write artifact + update header
--check : compute fresh content, exit 1 nếu khác file hiện tại; in diff vắn tắt (no secret)
--print-hash : print content_hash only (CI summary)
Header (in artifact):
// generatedAt, rowCount, statusFilter, contentHash (sha256 over canonicalized JSON of 3 maps)
CI integration:
nuxt-ci.yml job step: `tsx scripts/generate-table-maps.ts --check`
deploy-vps.yml: same --check before build (block deploy on mismatch)
post-deploy: optional smoke fetch /knowledge/registries/<each entityType> head request
Manual edit detection:
--check recompute hash; nếu file hash header ≠ recomputed → fail.
4.8 Rollback
- Vì artifact committed →
git revert <generation-commit>đủ. - Không cần backup directory: git history là backup.
- Nếu schema bug làm regen sai: tạm thêm row vào
force_includehoặc revert + hotfix script.
5. Row-by-row classification (21 rows)
| id | table_id | collection | status | page_url | derived_entity_type | row_class | in_tableIdMap | in_collectionMap | notes |
|---|---|---|---|---|---|---|---|---|---|
| 1 | tbl_workflow_list | workflows | active | /knowledge/workflows | workflow | NON_REGISTRY_PAGE | YES | YES | dùng map cho DirectusTable lookup nội dung, không cho route |
| 2 | tbl_workflow_steps | workflow_steps | active | /knowledge/workflows/[id]?tab=matrix | workflow_step | WORKFLOW_TAB | YES | YES | tab inside workflow detail |
| 3 | tbl_workflow_timeline | workflow_steps | active | /knowledge/workflows/[id]?tab=diagram | (skip — collection collision) | WORKFLOW_TAB | NO | NO | cùng collection với id 2; không thêm vào generated map |
| 4 | tbl_wcr_list | workflow_change_requests | active | /knowledge/workflows/[id]?tab=wcr | wcr | WORKFLOW_TAB | YES | YES | |
| 5 | tbl_modules_list | tasks ⚠ | active | /knowledge/modules | (override → module) | NON_REGISTRY_PAGE | YES (module) | YES (module) | ⚠ collection field= 'tasks' is suspicious; entityType=module per existing maps; flag for data fix |
| 6 | tbl_tasks_list | tasks | active | /knowledge/current-tasks | task | NON_REGISTRY_PAGE | YES | YES | |
| 7 | tbl_proposals_list | table_proposals | published | /admin/proposals | table_proposal | ADMIN_PAGE | YES | NO (collectionMap missing 'table_proposal') | drift: collectionMap thiếu entry này — generated map sẽ tự fix |
| 8 | tbl_meta_catalog | meta_catalog | active | /knowledge/registries | catalog | INDEX_PAGE | YES | YES | index page |
| 9 | tbl_registry_dot_tools | dot_tools | published | /knowledge/registries/dot_tool | dot_tool | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 10 | tbl_registry_ui_pages | ui_pages | published | /knowledge/registries/page | page | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 11 | tbl_registry_collections | collection_registry | published | /knowledge/registries/collection | collection | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 12 | tbl_registry_agents | agents | published | /knowledge/registries/agent | agent | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 13 | tbl_registry_modules | modules | published | /knowledge/registries/module | module | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 14 | tbl_registry_checkpoint_types | checkpoint_types | published | /knowledge/registries/checkpoint_type | checkpoint_type | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 15 | tbl_registry_checkpoint_sets | checkpoint_sets | published | /knowledge/registries/checkpoint_set | checkpoint_set | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 16 | tbl_registry_entity_dependencies | entity_dependencies | published | /knowledge/registries/entity_dependency | entity_dependency | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 17 | tbl_checkpoint_instances | checkpoint_instances | published | /knowledge/registries/checkpoint_instance | checkpoint_instance | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 18 | tbl_registry_changelog | registry_changelog | published | /knowledge/registries/changelog | changelog | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 19 | tbl_table_registry | table_registry | published | /knowledge/registries/table | table | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 20 | tbl_system_issues | system_issues | published | /knowledge/registries/system_issue | system_issue | REGISTRY_ENTITY_ROUTE | YES | YES | |
| 21 | tbl_event_outbox | event_outbox | draft | /knowledge/registries/event_outbox | event_outbox | REGISTRY_ENTITY_ROUTE | NO | NO | new row; cần --include-draft hoặc publish trước build |
Counts:
- REGISTRY_ENTITY_ROUTE: 13 (id 9-21)
- NON_REGISTRY_PAGE: 3 (id 1, 5, 6)
- WORKFLOW_TAB: 3 (id 2, 3, 4)
- ADMIN_PAGE: 1 (id 7)
- INDEX_PAGE: 1 (id 8)
- Total: 21 ✓
6. Static extras (non-registry collections)
collectionMap hiện chứa 3 entries không tương ứng row nào trong table_registry:
trigger → trigger_registry
comment → task_comments
taxonomy → taxonomy
Đây là collections dùng cho relations resolution nhưng chưa được "promote" thành registry-managed table. Đề xuất:
- Script config có
static_extras: Record<string,string>chứa 3 entries trên. - Generated
collectionMap={...derivedFromRegistry, ...staticExtras}. - Conflict detection: nếu key xuất hiện cả 2 → script fail (build break).
- Theo dõi: nếu sau này có row
table_registrycho 3 collections này → xóa entry tương ứng khỏistatic_extras. Add comment cảnh báo trong script config.
7. Open data quality issues (flag, không sửa trong pack này)
- id 5 —
tbl_modules_list.collection = 'tasks'— gần như chắc là sai (nên làmodules). Đề xuất pack riêng: data fix + verify. - id 3 —
tbl_workflow_timeline— cùng collection với id 2 (workflow_steps). Không có entityType riêng. Generated script skip — không gây regression vì hiện tại cũng không xuất hiện trong tableIdMap. relations.get.tsmirror drift — thiếu 6 entries vscollectionMap. Generated map sẽ tự fix khi swap import.
8. Implementation handoff checklist (cho pack tiếp theo)
- Tạo
web/scripts/generate-table-maps.ts(E4 strategy). - Tạo dir + file
web/generated/table-maps.generated.ts(initial run committed). - Update
web/package.jsonscripts:prebuild,generate:table-maps. - Update 3 consumer files import từ
~/generated/table-maps.generated, xóa local hardcoded. - CI: thêm step
tsx scripts/generate-table-maps.ts --checkvàonuxt-ci.yml+deploy-vps.yml. - Add
web/generated/vào.gitignoreexception (commit generated file). - Smoke test 21 routes (13 registry + 3 non-registry + 3 workflow tab + 1 admin + 1 index).
- Phase 2 follow-up pack:
entity_typecolumn add + backfill.
9. Risks
| Risk | Mitigation |
|---|---|
| Convention E4 mismatch cho row mới | Script fail-fast; CI catches. |
| Token leak in artifact | Pure-static check + --check includes "no process.env reference" assertion. |
| Hash drift after manual edit | CI --check block deploy. |
event_outbox draft never published |
Smoke + publish workflow ghi rõ trong runbook. |
module=null cho 14 rows |
Out of scope; không ảnh hưởng map. |
D28 Generated Table Map Design | 2026-05-09 | READ + DESIGN ONLY