Directus Data Model Parity Tasks
Mục tiêu: đưa LumiBase Data Model / Collections Builder lên mức đáp ứng các thành phần cơ bản khi so sánh với Directus: tạo collection, tạo field/model, quan hệ, quyền schema, metadata, validation, item API, SDK và trải nghiệm Studio. Tài liệu này tập trung vào phần còn thiếu hoặc chưa đủ chặt trong codebase hiện tại.
1. Baseline hiện tại
LumiBase đã có nền tảng chính:
[DB]collections,fields,relations,items,revisions,activitytrongpackages/database/src/schema/cms.ts.[BE]SchemaServicequản lý CRUD collection, field, relation và compiled schema cache.[BE]ItemServiceCRUD item trên JSONB store, có validation, encryption, revisions, activity, realtime/search hooks.[BE]Routes/api/v1/collections,/api/v1/collections/:name/fields,/api/v1/relations,/api/v1/items/:collection.[FE]Studio Data Model có collection wizard, detail tabs, field inspector, display tab, raw JSON tab.[SDK]Client có nhóm schema/items cơ bản.
Điểm khác biệt kiến trúc cần giữ rõ: Directus tạo hoặc introspect table vật lý trong database; LumiBase MVP hiện dùng virtual schema + items.data JSONB, materialized collections là opt-in optimization. Nếu muốn cạnh tranh trực tiếp, phải hoặc hỗ trợ DDL/introspection, hoặc biến virtual schema thành lợi thế sản phẩm với contract rõ ràng.
2. Product contract cần đạt
Một collection trong LumiBase phải là một "model" đầy đủ, gồm:
- Machine identity:
name, immutable sau khi tạo. - Display identity:
label,pluralLabel,icon,color,note, translations. - Storage identity:
storageMode,primaryKey, materialized target tuỳ chọn. - Behavior:
singleton,accountability,versioning,archiveField,archiveValue,unarchiveValue,sortField. - Chính sách system field:
status,sort,createdAt,updatedAt,createdBy,updatedBy,deletedAt. - Định nghĩa field: storage type, interface, display, validation, conditions, options, display options, translations, metadata layout.
- Relations: many-to-one, one-to-many, many-to-many, many-to-any, file relation, metadata junction.
- Access: quyền quản lý schema và quyền ở mức item.
- Hình dạng API: REST, SDK typegen, OpenAPI, export/import, diff/apply.
3. Task group A: Contract metadata của collection
A1. Thêm các cột collection còn thiếu
Scope: [DB] packages/database, [BE] apps/cms, [SDK] packages/sdk, [FE] apps/studio.
Thêm các cột first-class vào collections thay vì giấu các field quan trọng của sản phẩm trong meta:
label: text('label')
pluralLabel: text('plural_label')
hidden: boolean('hidden').default(false).notNull()
system: boolean('system').default(false).notNull()
primaryKeyField: text('primary_key_field').default('id').notNull()
primaryKeyType: text('primary_key_type').default('nanoid').notNull()
storageMode: text('storage_mode').default('jsonb').notNull()
unarchiveValue: text('unarchive_value')
itemDuplicationFields: jsonb('item_duplication_fields').default([]).notNull()
translations: jsonb('translations').default({}).notNull()
Các giá trị primaryKeyType được chấp nhận:
nanoid: mặc định hiện tại của LumiBase.uuid: UUID v4 hoặc v7.integer: sequence integer được sinh tự động. Chỉ hợp lệ với mode physical/materialized, trừ khi được emulate tường minh.bigInteger: sequence bigint được sinh tự động. Cùng ràng buộc nhưinteger.string: ID chuỗi do người dùng cung cấp.
Các giá trị storageMode được chấp nhận:
jsonb: item store chung hiện tại.materialized: virtual schema với projection vật lý được quản lý.physical: mode tương lai, tạo và migrate một table thật.external: mode tương lai cho table ngoài được introspect.
Ghi chú triển khai:
- Tạo một Drizzle migration cho các cột mới.
- Backfill các row hiện có:
label = titleCase(name)pluralLabel = titleCase(name)primaryKeyField = 'id'primaryKeyType = 'nanoid'storageMode = 'jsonb'
- Cập nhật
CollectionInput, validation ở route, SDK types, config export/import và OpenAPI. - Chỉ giữ
metacho UI hint của extension/custom, không dùng cho hành vi cốt lõi của collection.
A2. Sửa payload của collection wizard trong Studio
Vấn đề hiện tại: wizard của Studio gửi note, versioning, accountability bên trong meta, trong khi backend nhận chúng như field top-level của collection.
Thay đổi cần thiết:
client.schema.createCollection({
name,
label,
pluralLabel,
singleton,
icon,
color,
note: note || null,
accountability,
versioning,
primaryKeyType,
storageMode,
})
Các bước của wizard:
- Identity: name, label, plural label, icon, color, hidden.
- Storage: primary key type, storage mode, singleton.
- System fields: status, sort, các field created/updated, hành vi archive.
- Mặc định permissions: preset role/policy hoặc "inherit default".
- Review: JSON preview, cảnh báo, create.
Tiêu chí chấp nhận:
- Tạo collection từ Studio lưu được
note,accountability,versioning,singletonở mức top-level. - Bước review hiển thị đúng payload API.
- Nếu
primaryKeyTypekhông được storage mode hiện tại hỗ trợ, UI chặn submit kèm thông báo chính xác.
4. Task group B: Primary key và system fields
B1. Định nghĩa chiến lược primary key trong item storage
Scope: [DB], [BE] ItemService, [SDK] typegen.
Với storage mode jsonb, triển khai chính sách sinh ID trong ItemService.create():
nanoid: sinh bởi default của database hoặc helper của service.uuid: sinh bởi service.string: bắt buộc cóidtrong request body hoặcdata.id, từ chối trùng lặp.integer/bigInteger: hoãn lại cho đến khi có bảng sequence.
Thêm một helper nhỏ ở tầng service:
interface PrimaryKeyStrategy {
field: string;
type: 'nanoid' | 'uuid' | 'string' | 'integer' | 'bigInteger';
generate(input: Record<string, unknown>): string | number;
validate(value: unknown): void;
}
Tiêu chí chấp nhận:
- Tạo item tôn trọng chiến lược primary key của collection.
- Detail/update/delete nhận item ID theo chiến lược đã cấu hình.
- Typegen phơi ra kiểu primary key theo từng collection.
- ID do người dùng cung cấp bị trùng thì trả
409.
B2. Đưa system field thành field nhìn thấy được trong schema
Directus coi các field ID/status/sort/audit là field nhìn thấy trong model. LumiBase hiện có các cột cấu trúc, nhưng UI Data Model không phơi chúng ra như field model cấu hình được.
Thêm định nghĩa virtual field được sinh tự động cho:
idstatussortuser_createduser_updatedcreated_atupdated_atdeleted_at
Triển khai:
- Mở rộng
CompiledCollectionvớisystemFields. SchemaService.compile()nên nối descriptor của system field trước user field, hoặc phơi ra một arraysystemFieldsriêng.- Tab Fields của Studio nên hiển thị system field trong một nhóm bị khoá.
- Người dùng có thể cấu hình display, hidden, readonly, translations và width cho system field, nhưng không thể xoá chúng.
- Nếu collection tắt hành vi của một system field, đánh dấu field đó hidden thay vì bỏ cột nền.
Tiêu chí chấp nhận:
- Content list có thể include/hide system field một cách nhất quán.
- Typegen bao gồm system field.
- Raw JSON schema tách rõ
fieldsvàsystemFields.
5. Task group C: Ngang bằng về cấu hình field
C1. Mở rộng contract DB của field
Thêm metadata field ở dạng first-class:
label: text('label')
note: text('note')
defaultValue: jsonb('default_value')
nullable: boolean('nullable').default(true).notNull()
unique: boolean('unique').default(false).notNull()
indexed: boolean('indexed').default(false).notNull()
searchable: boolean('searchable').default(false).notNull()
length: integer('length')
precision: integer('precision')
scale: integer('scale')
special: jsonb('special').default([]).notNull()
Quy tắc:
name,typevà các field ảnh hưởng tới storage là immutable khi đã có dữ liệu, trừ khi có kế hoạch migration đi kèm.interface,display,options,displayOptions,validation,conditions,translations,width,group,sortOrdersửa được.uniquevàindexedchỉ ở mức advisory trong modejsonb, trừ khi có index materialized/physical. UI phải ghi rõ điều này.
C2. Triển khai các tab nâng cao cho FieldInspector
Thay inspector một-form hiện tại bằng các tab:
- Basics: name, label, note, type, interface, required, readonly, hidden.
- Options: option riêng theo interface, sinh từ schema của registry.
- Display: display key, display options, live preview.
- Validation: rule builder có sẵn và mode raw JSON.
- Conditions: rule builder và mode raw JSON.
- Layout: width, group, sort, presentation field.
- Storage: default value, nullable, unique, indexed, searchable, encrypted, versioned, raw enabled.
- Translations: label/help theo từng locale.
Ghi chú triển khai:
- Interface registry phải phơi option schema ở dạng UI đọc được.
- Option lạ phải vẫn sửa được qua raw JSON để interface của extension không mất config.
- Validation builder phải ghi ra đúng DSL JSON mà
validateItem()tiêu thụ.
Tiêu chí chấp nhận:
- Người dùng có thể cấu hình mọi cột đang có trong
fields. - Người dùng có thể cấu hình các phần mới thêm:
label,note, default, hint uniqueness/index, searchability, translations. - Không lần lưu field nào làm mất
options,displayOptions,validationhayconditionslạ.
C3. Enforce các thay đổi field immutable và rủi ro
upsertField() ở backend hiện đang cho update type/interface của field có sẵn một cách tự do.
Thêm:
createField(collection, input)cho field mới.updateField(collection, field, patch)cho các update không phá vỡ.planFieldMigration(collection, field, proposal)cho thay đổi type/default/nullability/index.applyFieldMigration(planId)cho thay đổi đã xác nhận.
Hành vi MVP tối thiểu:
- Từ chối thay đổi
typenếu collection đang có item. - Từ chối thay đổi
name; yêu cầu endpointrenameField()tường minh. - Từ chối xoá field đang có dữ liệu, trừ khi
force=truevà một revision backup đã được ghi.
Tiêu chí chấp nhận:
- API hiện có vẫn tương thích ngược qua
PUT, nhưng bên trong phân nhánh sang create/update an toàn. - Update rủi ro trả
409kèm hướng dẫn migration. - Studio hiện dialog xác nhận trước các thay đổi phá huỷ.
6. Task group D: Ngang bằng về relations
D1. Validate tham chiếu của relation
Route relation hiện tại nhận string mà không verify collection/field.
Thêm validation trong SchemaService.createRelation():
manyCollectiontồn tại.manyFieldtồn tại trên many collection.oneCollectiontồn tại.oneFieldtồn tại khi được cung cấp.junctionCollectiontồn tại với M2M nếu được cung cấp.onDeleteđược storage mode hỗ trợ.- Tên relation đủ unique để tránh edge trùng lặp.
Tiêu chí chấp nhận:
- Relation không hợp lệ trả
400kèm đúng path collection/field bị thiếu. - Xoá collection kiểm tra cả
manyCollectionvàoneCollection, không chỉ một phía. - Xoá field đang được relation tham chiếu bị chặn cho đến khi relation được bỏ hoặc migrate.
D2. Thêm loại relation và metadata
Mở rộng schema relation với:
type: text('type').notNull() // m2o | o2m | m2m | m2a | file
aliasField: text('alias_field')
relatedDisplayTemplate: text('related_display_template')
junctionManyField: text('junction_many_field')
junctionOneField: text('junction_one_field')
Quy tắc:
- M2O lưu giá trị foreign key trên
manyField. - O2M là alias được tính từ M2O nghịch đảo.
- M2M cần một junction collection và hai relation M2O.
- M2A có thể để sau, nhưng phải giữ chỗ cho type này và từ chối với "not implemented" nếu được chọn.
D3. Triển khai deep read và relation expansion
Thêm hỗ trợ query item:
fields=title,author.name,categories.*deep[author][fields]=id,namedeep[categories][limit]=10
Triển khai:
- Dựng relation graph từ compiled schema.
- Resolve các relation expansion sau query item cơ sở.
- Áp permission cho mọi collection liên quan.
- Tránh N+1 bằng cách batch các lần tra relation.
Tiêu chí chấp nhận:
- Giá trị M2O resolve thành item liên quan khi được yêu cầu.
- O2M/M2M trả về array.
- Masking permission ở mức field vẫn áp lên các item liên quan.
7. Task group E: Quyền schema và bảo mật
E1. Bảo vệ các endpoint quản lý schema
Route collections hiện đang tài liệu hoá việc enforce quyền schema như một stub. Thay bằng kiểm tra thật.
Các quyền cần có:
schema:read: list/get collection, field, relation, compiled schema.schema:create: tạo collection, tạo field, tạo relation.schema:update: patch collection, update field, update relation, apply schema.schema:delete: xoá collection, xoá field, xoá relation.schema:migrate: thay đổi type phá vỡ, rename field, thay đổi storage mode.
Triển khai:
- Thêm các action quyền schema vào shared policy types.
- Thêm middleware/helper
requireSchemaPermission(c, action). - Áp nó vào
collectionsRoutervàrelationsRouter. - Bảo đảm các AI skill tạo schema dùng cùng bộ kiểm tra capability.
Tiêu chí chấp nhận:
- User đã đăng nhập nhưng không phải admin không thể mutate schema mà không có quyền tường minh.
- Việc từ chối quyền chỉ được làm cho khó phân biệt với lỗi API khác ở nơi chính sách bảo mật yêu cầu; còn lại trả
403. - Test bao phủ role read-only, schema editor và admin.
E2. Sửa quyền update/delete của item
ItemService.list/detail/create có kiểm tra permission, nhưng update/delete cũng phải enforce.
Thay đổi cần thiết:
- Trong
patch(): gọiperm(collectionName, 'update'). - Trong
replace(): dùng lại quyền update. - Trong
softDelete(): gọiperm(collectionName, 'delete'). - Áp
whereFor()ở mức row cho việc tra cứu target của update/delete. - Áp allowlist update ở mức field: từ chối patch vào field mà permission hiện tại không cho ghi.
Tiêu chí chấp nhận:
- User read-only không thể patch/delete bằng lệnh gọi API trực tiếp.
- User có quyền update giới hạn theo row không thể update row ngoài phạm vi.
- User không thể update field hidden/readonly qua raw JSON.
8. Task group F: Lifecycle collection và schema diff
F1. Làm cho schema diff đầy đủ
Diff hiện tại chỉ so sánh một vài field của collection cùng các field được thêm/bỏ/đổi.
Mở rộng diff để bao gồm:
- Thay đổi metadata collection.
- Thay đổi metadata field.
- Thêm/update/bỏ relation.
- Phân loại rủi ro:
safe,requires-confirmation,destructive,unsupported. - Ảnh hưởng runtime: cache invalidation, migrate item, refresh materialized, cập nhật typegen.
Hình dạng response:
{
"summary": {
"safe": 3,
"requiresConfirmation": 1,
"destructive": 0,
"unsupported": 0
},
"changes": [
{
"kind": "field.update",
"path": "posts.title.label",
"before": "Title",
"after": "Headline",
"risk": "safe"
}
]
}
F2. Apply schema theo kiểu atomic
PUT /collections/:name/schema phải là atomic từ góc nhìn người dùng.
Triển khai:
- Validate toàn bộ schema trước.
- Tính diff.
- Từ chối các thay đổi unsupported/destructive trừ khi được xác nhận tường minh.
- Áp collection, field, relation trong một transaction khi runtime hỗ trợ.
- Invalidate compiled schema, cache permission và cache typegen.
- Phát event realtime
schema.changed.
Tiêu chí chấp nhận:
- Apply schema một phần không thể để collection ở trạng thái nửa vời.
- Tab Raw JSON của Studio hiển thị diff trước khi apply.
- Preview typegen cập nhật sau khi apply.
9. Task group G: Storage mode và định vị so với Directus
G1. Tài liệu hoá và triển khai hành vi của storage mode
Thêm docs sản phẩm và metadata API giải thích:
jsonb: tiến hoá nhanh nhất, không cần DDL runtime, tương thích SQL-native thấp hơn.materialized: JSONB là source of truth, kèm tối ưu đọc vật lý.physical: table thật kiểu Directus, cột mốc tương lai.external: table được introspect, cột mốc tương lai.
Để ngang bằng ở mức MVP, đừng giả vờ jsonb giống table vật lý của Directus. Studio nên hiện một badge storage kèm các giới hạn.
G2. Spike cho mode physical
Tạo một thiết kế kỹ thuật trước khi triển khai:
- Cách tạo table vật lý scope theo tenant một cách an toàn.
- Quy ước đặt tên:
site_<siteId>_<collectionName>hoặc schema-per-site. - Lập kế hoạch migration cho add/drop/rename/đổi type.
- Tương tác giữa RLS và permission.
- Ranh giới Drizzle/raw SQL.
- Hành vi backup/export/import.
- Tương thích Cloudflare Hyperdrive/Postgres.
Sản phẩm giao:
docs/en/architecture/physical-collections.md- Quyết định: triển khai ngay, hoãn, hay giữ chiến lược chỉ-materialized.
10. Task group H: SDK, typegen, OpenAPI và docs
H1. Các resource schema của SDK
SDK phải phơi ra:
client.schema.collections.list()
client.schema.collections.create(input)
client.schema.collections.update(name, patch)
client.schema.collections.delete(name)
client.schema.fields.create(collection, input)
client.schema.fields.update(collection, field, patch)
client.schema.fields.rename(collection, field, nextName)
client.schema.fields.delete(collection, field, options)
client.schema.relations.list(params)
client.schema.relations.create(input)
client.schema.relations.update(id, patch)
client.schema.relations.delete(id)
client.schema.diff(input)
client.schema.apply(input, options)
Tiêu chí chấp nhận:
- Các method SDK legacy hiện có vẫn chạy, hoặc phát cảnh báo deprecation.
- Kiểu TypeScript khớp với schema validation ở backend.
- Lỗi SDK giữ được metadata code/path/risk.
H2. Cải tiến typegen
Typegen nên bao gồm:
- Kiểu primary key.
- System fields.
- Field nullable so với required.
- Field readonly/generated.
- Response type khi relation được expand.
- Hành vi đọc field encrypted: không thể biết ở compile time là kiểu đã giải mã hay string bị mask (phụ thuộc permission), nên hãy tài liệu hoá dưới dạng
T | '***'nếu cần.
H3. OpenAPI và docs công khai
Cập nhật:
apps/cms/openapi.yamldocs/en/features/collections-builder.mddocs/en/features/field-types-and-config.mddocs/en/data-model.md- Bản mirror tiếng Việt sau khi contract tiếng Anh đã ổn định.
11. Task group I: Tests
Các test tối thiểu cần có trước khi đánh dấu công việc parity là hoàn thành:
Test backend
- Tạo collection lưu được toàn bộ metadata top-level.
- Tên collection trùng trả
409. - Chiến lược primary key không hợp lệ trả
400. - Các endpoint schema enforce quyền schema.
- Đổi type field khi đang có dữ liệu trả
409. - Xoá field đang có dữ liệu yêu cầu force.
- Tạo relation validate sự tồn tại của collection/field.
- Xoá collection bị chặn bởi relation ở cả hai phía.
- Update/delete item enforce permission và phạm vi row.
- Schema diff phân loại đúng thay đổi safe/destructive.
- Schema apply invalidate cache.
Test frontend
- Wizard gửi đúng payload.
- Wizard chặn các tổ hợp storage/primary key không được hỗ trợ.
- FieldInspector giữ nguyên các option raw JSON lạ.
- Thay đổi field rủi ro hiện xác nhận.
- System field render ở trạng thái khoá.
- Relation builder chặn tham chiếu không hợp lệ trước khi submit.
- Apply raw schema hiển thị diff.
Test SDK/typegen
- Type được sinh ra bao gồm primary key và system field.
- Các type có relation được expand compile được.
- Method schema của SDK gọi đúng route và giữ được metadata lỗi.
12. Thứ tự triển khai đề xuất
- Sửa các bug đúng-sai hiện tại:
- Payload metadata top-level của wizard.
- Kiểm tra permission update/delete của item.
- Kiểm tra delete/dependency của relation ở cả hai phía.
- Thêm các cột metadata của collection cùng type ở API/SDK.
- Thêm contract chiến lược primary key cho mode
jsonb. - Mở rộng DB cấu hình field + các tab nâng cao của FieldInspector.
- Tách các endpoint create/update/rename/delete/migration của field.
- Làm chặt validation relation và metadata relation.
- Thêm kiểm tra quyền schema.
- Mở rộng schema diff/apply.
- Thêm system field vào compiled schema/typegen/Studio.
- Triển khai relation expansion trong query item.
- Cập nhật OpenAPI/docs và thêm bộ test parity.
- Quyết định chiến lược storage physical/external.
13. Definition of done
Công việc này hoàn thành khi một người dùng LumiBase mới có thể:
- Tạo một collection từ Studio với identity, storage, system fields, archive/sort, mặc định permissions và review JSON.
- Thêm field với đầy đủ cấu hình type/interface/display/validation/condition/storage.
- Thêm relation M2O/O2M/M2M có validation.
- Quản lý item qua REST và SDK với permission được áp nhất quán.
- Export/import một schema, xem trước diff và apply an toàn.
- Sinh type phản ánh chính xác model của collection.
- Hiểu được collection đang là JSONB, materialized, physical hay external, kèm các đánh đổi.
Không thuộc mục tiêu của cột mốc parity đầu tiên:
- Introspect đầy đủ database có sẵn.
- Engine migration DDL table vật lý đầy đủ.
- Runtime cho relation many-to-any.
- Trình lập kế hoạch migration trực quan cho mọi phép chuyển đổi type.