ADR-009: GraphQL Yoga với Schema động trên ItemService
Date: 2026-06-17 Status: Accepted
Context
LumiBase đã phát hành một REST API hoàn chỉnh trên Hono, và tầm nhìn của nó
định vị các consumer Delivery truy cập nội dung qua "REST/GraphQL/WS". Lộ trình
consumer SDK (docs/en/roadmap/consumer-sdk.md) dành sẵn một adapter
.with(graphql()) có thể kết hợp. Cho đến nay, GraphQL vẫn chưa được hiện thực
trên cả API lẫn Studio.
Chúng ta muốn một bề mặt GraphQL cho content items (query + mutation) mà:
- Chạy edge-native trên Cloudflare Workers cũng như Node/Docker.
- Tái dùng — không bao giờ bỏ qua — các cơ chế quản trị đã được tích hợp sẵn trong
ItemService: đa tenant (site_id), mặt nạ row/field phân quyền, RLS, soft-delete, revision/provenance, HITL pin, phát sóng realtime, và lập chỉ mục search. - Phản ánh schema động, hậu thuẫn bởi JSONB của LumiBase: collection và field được khai báo lúc runtime, không phải table-per-type, và khác nhau theo từng tenant.
Các phương án đã cân nhắc:
- GraphQL Yoga + graphql-js — nhẹ, tương thích Workers, tích hợp với Hono qua một fetch handler duy nhất, và hỗ trợ một schema factory theo từng request (cần cho schema động theo từng tenant).
- Pothos (code-first) — tuyệt vời cho schema tĩnh, nhưng schema của LumiBase
được dựng lúc runtime từ
collections/fields, nên việc dựng kiểu lúc biên dịch của Pothos không phù hợp. - Apollo Server — nặng hơn, ít tối ưu cho runtime edge của Workers.
Decision
Áp dụng GraphQL Yoga + graphql-js, gắn bên trong sub-app Hono /api/v1
đã xác thực tại /api/v1/graphql.
- Bảo mật kế thừa: vì route nằm dưới cùng chuỗi
withTenant → withDb → withAuth → … → withRlsnhư REST, GraphQL được giải quyết tenant, xác thực và RLS miễn phí. - Resolver mỏng: mọi resolver ủy quyền cho
ItemService(apps/cms/src/services/item-service.ts). Không resolver nào chạm trực tiếp vào cơ sở dữ liệu, nên toàn bộ quản trị được kế thừa (các quy tắc bất di bất dịch #2, #4, #5). - Schema động:
buildSiteSchema()đọc manifestcollections/fieldstheo từng site quaSchemaServicevà dựng mộtGraphQLSchematheo lập trình (một object type cho mỗi collection;Query.<collection>,Query.<collection>_by_id;Mutation.create_/update_/delete_<collection>). Các đối số filter ánh xạ 1:1 lên các toán tửItemFilterhiện có củaItemService. - Cache schema theo từng site: các object
GraphQLSchemakhông tuần tự hóa được, nên chúng được cache trong tiến trình, khóa theositeIdvới TTL ngắn (60 giây);invalidateSiteSchema(siteId)loại bỏ một cái ngay lập tức. - Lỗi: các
ItemServiceErrorđược ném ra được ánh xạ sangGraphQLErrorvớiextensions.codekhớp với từ vựng lỗi REST, nên client xử lý lỗi giống hệt trên mọi bề mặt. - SDK: một plugin
graphql()có thể kết hợp (packages/sdk/src/graphql) thêm.query()/.mutate()trên nềnrawRequesthiện có, tái dùng các header auth/tenant và xử lý 401 của nó.
Consequences
Tích cực
- Một nguồn chân lý duy nhất cho quản trị nội dung (ItemService); GraphQL không thể trôi khỏi hoặc bỏ qua các quy tắc phân quyền/tenant của REST.
- Tương thích edge, bề mặt phụ thuộc tối thiểu (
graphql,graphql-yoga). - Schema tự động bám theo các thay đổi collection/field của tenant.
Tiêu cực / đánh đổi
- Giải quyết schema theo từng request tốn vài lần đọc manifest; được giảm thiểu bằng cache TTL trong tiến trình.
- Introspection theo kiểu field là nỗ lực tốt nhất: các field cấu trúc/quan hệ
lùi về scalar
JSONthay vì các kiểu GraphQL lồng nhau đầy đủ.
Các mục tiếp theo đã hoàn thành
- Gia cố: giới hạn độ sâu truy vấn + tắt introspection trong production.
- Quan hệ lồng nhau: m2o/o2m được đưa ra dưới dạng field lồng nhau, giải quyết lười qua
ItemService(m2m/m2a vẫn dùng lối thoát JSON). - Subscriptions:
Subscription.<collection>_eventsqua SSE, được bắc cầu từ kênh realtime của SiteRoom Durable Object.
Công việc tương lai
- Các kiểu quan hệ lồng nhau m2m/m2a.
- Che phân quyền cấp field cho payload subscription.
- Persisted query và giới hạn chi phí truy vấn.
- Mở rộng bề mặt ra ngoài items (collections/users/admin) nếu cần.