@cosmicdrift/kumiko-framework 0.362.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -0,0 +1,187 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/realtime-sse. Do not edit. -->
2
+
3
+ # realtime-sse
4
+
5
+ Server-sent events demo. Goodbye F5.
6
+
7
+ ## README
8
+
9
+ # Realtime SSE
10
+
11
+ Server-sent events — live updates without F5.
12
+
13
+ ## What it shows
14
+
15
+ - Entity writes that fan out over SSE
16
+ - Client subscription shape for live lists
17
+
18
+ ## Source
19
+
20
+ Feature entry point: `src/feature.ts`.
21
+
22
+ ## Tests
23
+
24
+ Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
25
+
26
+ ```bash
27
+ cd samples/recipes/realtime-sse
28
+ bun test
29
+ ```
30
+
31
+ ## Source files
32
+
33
+ ### src/__tests__/feature.integration.test.ts
34
+
35
+ ```ts
36
+ // Realtime SSE Sample — Integration Test
37
+ // Proves: SSE events fire on create, update, delete through the async
38
+ // event-dispatcher (D.3). Shape mirrors the StoredEvent: `type` is the
39
+ // event type ("message.created"), `data` carries id/version only: the
40
+ // frame is a change signal, clients refetch the record through a query.
41
+
42
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
43
+ import type { SaveContext } from "@cosmicdrift/kumiko-framework/engine";
44
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
45
+ import {
46
+ createTestUser,
47
+ setupTestStack,
48
+ type TestStack,
49
+ TestUsers,
50
+ unsafeCreateEntityTable,
51
+ } from "@cosmicdrift/kumiko-framework/stack";
52
+ import { chatFeature, messageEntity } from "../feature";
53
+
54
+ let stack: TestStack;
55
+
56
+ const admin = TestUsers.admin;
57
+ const user = createTestUser({ id: 2, roles: ["User"] });
58
+
59
+ beforeAll(async () => {
60
+ stack = await setupTestStack({ features: [chatFeature] });
61
+ await unsafeCreateEntityTable(stack.db, messageEntity);
62
+ await createEventsTable(stack.db);
63
+ });
64
+
65
+ afterAll(async () => {
66
+ await stack.cleanup();
67
+ });
68
+
69
+ beforeEach(async () => {
70
+ // Advance the event-dispatcher past previous-test events, then reset the
71
+ // collector. Without this, each test would see SSE events stacked up from
72
+ // its predecessors.
73
+ await stack.eventDispatcher?.runOnce();
74
+ stack.events.reset();
75
+ });
76
+
77
+ describe("SSE broadcast on create", () => {
78
+ test("emits message.created event", async () => {
79
+ await stack.http.writeOk<SaveContext>(
80
+ "chat:write:message:create",
81
+ {
82
+ channel: "general",
83
+ text: "Hello world",
84
+ author: "Alice",
85
+ },
86
+ user,
87
+ );
88
+
89
+ // SSE is delivered asynchronously by the event-dispatcher. Drain once so
90
+ // the test observes a deterministic state.
91
+ await stack.eventDispatcher?.runOnce();
92
+
93
+ expect(stack.events.sse).toHaveLength(1);
94
+ expect(stack.events.sse[0]?.type).toBe("message.created");
95
+ expect(stack.events.sse[0]?.data["id"]).toBeDefined();
96
+ });
97
+ });
98
+
99
+ describe("SSE broadcast on update", () => {
100
+ test("emits a message.updated signal without field values", async () => {
101
+ const created = await stack.http.writeOk<SaveContext>(
102
+ "chat:write:message:create",
103
+ {
104
+ channel: "general",
105
+ text: "Original",
106
+ },
107
+ user,
108
+ );
109
+
110
+ // Drain the create event, then reset so the update assertion is isolated.
111
+ await stack.eventDispatcher?.runOnce();
112
+ stack.events.reset();
113
+
114
+ await stack.http.writeOk<SaveContext>(
115
+ "chat:write:message:update",
116
+ {
117
+ id: created.id,
118
+ changes: { text: "Edited" },
119
+ version: 1,
120
+ },
121
+ user,
122
+ );
123
+ await stack.eventDispatcher?.runOnce();
124
+
125
+ const updateEvent = stack.events.sse.find((e) => e.type === "message.updated");
126
+ expect(updateEvent).toBeDefined();
127
+ expect(updateEvent?.data["id"]).toBe(created.id);
128
+ expect(updateEvent?.data["version"]).toBe(2);
129
+ expect(updateEvent?.data).not.toHaveProperty("payload");
130
+ });
131
+ });
132
+
133
+ describe("SSE broadcast on delete", () => {
134
+ test("emits message.deleted event", async () => {
135
+ const created = await stack.http.writeOk<SaveContext>(
136
+ "chat:write:message:create",
137
+ {
138
+ channel: "general",
139
+ text: "Delete me",
140
+ },
141
+ user,
142
+ );
143
+
144
+ await stack.eventDispatcher?.runOnce();
145
+ stack.events.reset();
146
+
147
+ await stack.http.writeOk<SaveContext>("chat:write:message:delete", { id: created.id }, admin);
148
+ await stack.eventDispatcher?.runOnce();
149
+
150
+ const deleteEvent = stack.events.sse.find((e) => e.type === "message.deleted");
151
+ expect(deleteEvent).toBeDefined();
152
+ expect(deleteEvent?.data["id"]).toBe(created.id);
153
+ });
154
+ });
155
+ ```
156
+
157
+ ### src/feature.ts
158
+
159
+ ```ts
160
+ import {
161
+ createEntity,
162
+ createTextField,
163
+ defineEntityDeleteHandler,
164
+ defineFeature,
165
+ } from "@cosmicdrift/kumiko-framework/engine";
166
+
167
+ export const messageEntity = createEntity({
168
+ table: "read_sample_messages",
169
+ fields: {
170
+ channel: createTextField({ personal: false, reason: "is_business_data", required: true }),
171
+ text: createTextField({ personal: { of: "id" }, find: "none", required: true }),
172
+ author: createTextField({ personal: { of: "id" }, find: "none" }),
173
+ },
174
+ softDelete: true,
175
+ });
176
+
177
+ const userWrite = { access: { roles: ["Admin", "User"] } } as const;
178
+ const adminWrite = { access: { roles: ["Admin"] } } as const;
179
+
180
+ export const chatFeature = defineFeature("chat", (r) => {
181
+ r.crud("message", messageEntity, {
182
+ write: userWrite,
183
+ verbs: { list: false, detail: false, restore: false, delete: false },
184
+ });
185
+ r.writeHandler(defineEntityDeleteHandler("message", messageEntity, adminWrite));
186
+ });
187
+ ```
@@ -0,0 +1,297 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/record-detail-layout. Do not edit. -->
2
+
3
+ # record-detail-layout
4
+
5
+ projectionDetail record header, metrics band, and layout.mode: "tabs" — an order screen with two relatedList tabs and a fields tab.
6
+
7
+ ## README
8
+
9
+ # Record Detail Layout
10
+
11
+ Register a `projectionDetail` screen as a tabbed record view ("Akte"): a
12
+ header, a metrics band, and a tabbed layout instead of one long stacked form.
13
+
14
+ ## What it shows
15
+
16
+ - `header` (title/subtitle/status) and `metrics` (labeled via `fieldLabels`)
17
+ rendered from the columns of the screen's own query row
18
+ - `layout.mode: "tabs"` with two `relatedList` tabs (order items, payments)
19
+ and one `fields` tab (master data) — each `relatedList` runs its own query
20
+ - boot-validator catching two common author mistakes: a `metrics` entry
21
+ without a `fieldLabels` override, and a `tabs` layout with fewer than two
22
+ sections
23
+
24
+ ## Source
25
+
26
+ Feature entry point: `src/feature.ts`.
27
+
28
+ ## Tests
29
+
30
+ ```bash
31
+ cd samples/recipes/record-detail-layout
32
+ bun test
33
+ ```
34
+
35
+ ## Source files
36
+
37
+ ### src/__tests__/feature.test.ts
38
+
39
+ ```ts
40
+ // Record Detail Layout Showcase — Unit Test (no DB / HTTP needed).
41
+ // Proves:
42
+ // - header/metrics/tabs land on the registered ProjectionDetailScreenDefinition
43
+ // exactly as declared (column names, not literals)
44
+ // - relatedList columns collapse the string shorthand like entityList columns do
45
+ // - the boot-validator catches the two mistakes the projectionDetail tabs
46
+ // layout introduces: a metric without a fieldLabels entry, and a tabs
47
+ // layout with fewer than two sections
48
+
49
+ import { describe, expect, test } from "bun:test";
50
+ import {
51
+ createRegistry,
52
+ defineFeature,
53
+ normalizeListColumn,
54
+ validateBoot as validateBootRaw,
55
+ } from "@cosmicdrift/kumiko-framework/engine";
56
+ import { withBootValidatorFixture } from "@cosmicdrift/kumiko-framework/testing";
57
+ import { z } from "zod";
58
+ import { createOrderDeskFeature } from "../feature";
59
+
60
+ function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
61
+ validateBootRaw(withBootValidatorFixture(features));
62
+ }
63
+
64
+ const orderDesk = createOrderDeskFeature();
65
+ const registry = createRegistry([orderDesk]);
66
+
67
+ describe("record-detail-layout showcase — registration", () => {
68
+ test("validateBoot accepts the full registered app", () => {
69
+ expect(() => validateBoot([orderDesk])).not.toThrow();
70
+ });
71
+
72
+ test("order-detail registers as projectionDetail with a qualified id", () => {
73
+ const screen = registry.getScreen("order-desk:screen:order-detail");
74
+ expect(screen?.type).toBe("projectionDetail");
75
+ });
76
+
77
+ test("header and metrics store column names, not literals", () => {
78
+ const screen = registry.getScreen("order-desk:screen:order-detail");
79
+ if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
80
+ expect(screen.header).toEqual({
81
+ title: "customerName",
82
+ subtitle: "orderNumber",
83
+ status: "status",
84
+ });
85
+ expect(screen.metrics).toEqual(["totalAmount", "outstandingAmount", "itemCount", "placedAt"]);
86
+ });
87
+
88
+ test("layout.mode is tabs with two relatedList sections, one fields section, and one extension section", () => {
89
+ const screen = registry.getScreen("order-desk:screen:order-detail");
90
+ if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
91
+ expect(screen.layout.mode).toBe("tabs");
92
+ expect(screen.layout.sections.map((s) => s.kind)).toEqual([
93
+ "relatedList",
94
+ "relatedList",
95
+ "fields",
96
+ "extension",
97
+ ]);
98
+ });
99
+
100
+ test("screen-level actions carry a refresh-order navigate action", () => {
101
+ const screen = registry.getScreen("order-desk:screen:order-detail");
102
+ if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
103
+ expect(screen.actions?.map((a) => a.id)).toEqual(["refresh-order"]);
104
+ });
105
+
106
+ test("relatedList columns collapse string shorthand into the object form", () => {
107
+ const screen = registry.getScreen("order-desk:screen:order-detail");
108
+ if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
109
+ const itemsSection = screen.layout.sections[0];
110
+ if (itemsSection?.kind !== "relatedList") throw new Error("expected relatedList");
111
+ expect(normalizeListColumn(itemsSection.columns[0]!)).toEqual({ field: "sku" });
112
+ });
113
+ });
114
+
115
+ describe("record-detail-layout showcase — boot-validator catches author mistakes", () => {
116
+ test("metric without a fieldLabels entry fails boot — no fallback to the raw column name", () => {
117
+ const broken = defineFeature("broken-order-desk", (r) => {
118
+ r.queryHandler("order:detail", z.object({ id: z.string() }), async () => ({ id: "1" }), {
119
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
120
+ });
121
+ r.screen({
122
+ id: "order-detail",
123
+ type: "projectionDetail",
124
+ query: "broken-order-desk:query:order:detail",
125
+ metrics: ["totalAmount"],
126
+ layout: { sections: [{ title: "Details", fields: ["id"] }] },
127
+ });
128
+ });
129
+ expect(() => validateBoot([broken])).toThrow(
130
+ /metric "totalAmount" has no entry in fieldLabels/,
131
+ );
132
+ });
133
+
134
+ test("tabs with only one section fails boot", () => {
135
+ const broken = defineFeature("broken-order-desk-tabs", (r) => {
136
+ r.queryHandler("order:detail", z.object({ id: z.string() }), async () => ({ id: "1" }), {
137
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
138
+ });
139
+ r.screen({
140
+ id: "order-detail",
141
+ type: "projectionDetail",
142
+ query: "broken-order-desk-tabs:query:order:detail",
143
+ layout: {
144
+ mode: "tabs",
145
+ sections: [{ id: "only", title: "Only", fields: ["id"] }],
146
+ },
147
+ });
148
+ });
149
+ expect(() => validateBoot([broken])).toThrow(/tabs need at least 2 sections/);
150
+ });
151
+ });
152
+ ```
153
+
154
+ ### src/feature.ts
155
+
156
+ ```ts
157
+ // Record Detail Layout Showcase — a projectionDetail screen with the
158
+ // "Akte" (case file) layout:
159
+ // 1. `header` renders the query row's customer/order/status columns above
160
+ // the layout — the identity a support agent scans first.
161
+ // 2. `metrics` renders a labeled band of key numbers from the same row —
162
+ // labels come from `fieldLabels`, there is no fallback to the raw
163
+ // column name.
164
+ // 3. `layout.mode: "tabs"` splits the record into four tabs: two
165
+ // `relatedList` sections (own query, own pagination, mounts only when
166
+ // active), one `fields` section (master data from the detail row), and
167
+ // one `extension` section (a plugin-mounted component, fw#3234 padding-
168
+ // parity check — every tab kind must sit in the same Card padding, no
169
+ // section carries extra chrome of its own).
170
+ // 4. A screen-level `actions` entry (fw#3234) renders next to the header,
171
+ // not inside any tab's Card.
172
+ //
173
+ // projectionDetail has no entity — it binds to an explicit query instead, so
174
+ // every field name below (header, metrics, fields) is a column of that
175
+ // query's row, not an entity field.
176
+
177
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
178
+ import { z } from "zod";
179
+
180
+ const rowIdSchema = z.object({ id: z.string() });
181
+
182
+ export function createOrderDeskFeature(): FeatureDefinition {
183
+ return defineFeature("order-desk", (r) => {
184
+ r.systemScope();
185
+
186
+ r.queryHandler(
187
+ "order:detail",
188
+ rowIdSchema,
189
+ async (query) => ({
190
+ id: query.payload.id,
191
+ customerName: "Aiko Tanaka",
192
+ orderNumber: "SO-10482",
193
+ status: "processing",
194
+ totalAmount: "€ 1,248.00",
195
+ outstandingAmount: "€ 312.00",
196
+ itemCount: "3",
197
+ placedAt: "2026-08-21",
198
+ shippingAddress: "Karl-Marx-Straße 12, 12043 Berlin",
199
+ billingAddress: "Karl-Marx-Straße 12, 12043 Berlin",
200
+ salesRep: "Jonas Weber",
201
+ }),
202
+ { access: { roles: ["Support", "Admin"] } },
203
+ );
204
+
205
+ r.queryHandler(
206
+ "order:items",
207
+ rowIdSchema,
208
+ async () => ({
209
+ rows: [
210
+ { sku: "KB-100", description: "Mechanical keyboard", quantity: 1, unitPrice: 148 },
211
+ { sku: "MS-220", description: "Wireless mouse", quantity: 2, unitPrice: 45 },
212
+ { sku: "PAD-01", description: "Desk mat", quantity: 1, unitPrice: 22 },
213
+ ],
214
+ }),
215
+ { access: { roles: ["Support", "Admin"] } },
216
+ );
217
+
218
+ r.queryHandler(
219
+ "order:payments",
220
+ rowIdSchema,
221
+ async () => ({
222
+ rows: [
223
+ { paidAt: "2026-08-21", amount: 936, method: "card", status: "settled" },
224
+ { paidAt: "2026-08-25", amount: 312, method: "invoice", status: "pending" },
225
+ ],
226
+ }),
227
+ { access: { roles: ["Support", "Admin"] } },
228
+ );
229
+
230
+ r.screen({
231
+ id: "order-detail",
232
+ type: "projectionDetail",
233
+ query: "order-desk:query:order:detail",
234
+ header: { title: "customerName", subtitle: "orderNumber", status: "status" },
235
+ metrics: ["totalAmount", "outstandingAmount", "itemCount", "placedAt"],
236
+ fieldLabels: {
237
+ totalAmount: "order-desk.metric.totalAmount",
238
+ outstandingAmount: "order-desk.metric.outstandingAmount",
239
+ itemCount: "order-desk.metric.itemCount",
240
+ placedAt: "order-desk.metric.placedAt",
241
+ },
242
+ layout: {
243
+ mode: "tabs",
244
+ sections: [
245
+ {
246
+ id: "items",
247
+ kind: "relatedList",
248
+ title: "order-desk.tab.items",
249
+ query: "order-desk:query:order:items",
250
+ columns: [
251
+ "sku",
252
+ "description",
253
+ "quantity",
254
+ { field: "unitPrice", renderer: { format: "currency", symbol: "€" } },
255
+ ],
256
+ },
257
+ {
258
+ id: "payments",
259
+ kind: "relatedList",
260
+ title: "order-desk.tab.payments",
261
+ query: "order-desk:query:order:payments",
262
+ columns: [
263
+ "paidAt",
264
+ { field: "amount", renderer: { format: "currency", symbol: "€" } },
265
+ "method",
266
+ "status",
267
+ ],
268
+ },
269
+ {
270
+ id: "details",
271
+ kind: "fields",
272
+ title: "order-desk.tab.details",
273
+ fields: ["shippingAddress", "billingAddress", "salesRep"],
274
+ },
275
+ {
276
+ id: "internal-note",
277
+ kind: "extension",
278
+ title: "order-desk.tab.internalNote",
279
+ component: { react: { __component: "OrderInternalNote" } },
280
+ entityName: "order",
281
+ },
282
+ ],
283
+ },
284
+ actions: [
285
+ {
286
+ kind: "navigate",
287
+ id: "refresh-order",
288
+ label: "order-desk.action.refreshOrder",
289
+ screen: "order-detail",
290
+ icon: "refresh",
291
+ },
292
+ ],
293
+ access: { roles: ["Support", "Admin"] },
294
+ });
295
+ });
296
+ }
297
+ ```
@@ -0,0 +1,151 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/reference-data. Do not edit. -->
2
+
3
+ # reference-data
4
+
5
+ Upsert-based seeds. Lookup tables done right.
6
+
7
+ ## README
8
+
9
+ # Reference Data
10
+
11
+ Seed reference data with `r.referenceData`.
12
+
13
+ ## What it shows
14
+
15
+ - Declarative reference rows loaded at boot
16
+ - Idempotent seeding for lookup tables
17
+
18
+ ## Source
19
+
20
+ Feature entry point: `src/feature.ts`.
21
+
22
+ ## Tests
23
+
24
+ Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
25
+
26
+ ```bash
27
+ cd samples/recipes/reference-data
28
+ bun test
29
+ ```
30
+
31
+ ## Source files
32
+
33
+ ### src/__tests__/feature.integration.test.ts
34
+
35
+ ```ts
36
+ // Reference Data Sample — Integration Test
37
+ // Proves: seed data is inserted, upsert updates existing rows, new rows added
38
+
39
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
40
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
41
+ import { seedReferenceData } from "@cosmicdrift/kumiko-framework/db";
42
+ import type { ReferenceDataDef } from "@cosmicdrift/kumiko-framework/engine";
43
+ import { createTestDb, type TestDb, unsafePushTables } from "@cosmicdrift/kumiko-framework/stack";
44
+ import { categoryFeature, categoryTable } from "../feature";
45
+
46
+ let testDb: TestDb;
47
+
48
+ beforeAll(async () => {
49
+ testDb = await createTestDb();
50
+ await unsafePushTables(testDb.db, { categoryTable });
51
+ });
52
+
53
+ afterAll(async () => {
54
+ await testDb?.cleanup();
55
+ });
56
+
57
+ type CategoryRow = { code: string; name: string; description: string | null };
58
+
59
+ async function readCategories(): Promise<CategoryRow[]> {
60
+ const rows = await selectMany(testDb.db, categoryTable);
61
+ return [...rows].sort((a: CategoryRow, b: CategoryRow) => a.code.localeCompare(b.code));
62
+ }
63
+
64
+ describe("reference data seeding", () => {
65
+ test("inserts initial seed data", async () => {
66
+ const tables = new Map([["category", categoryTable]]);
67
+
68
+ // seedReferenceData(defs, tables, db)
69
+ await seedReferenceData(categoryFeature.referenceData, tables, testDb.db);
70
+
71
+ const rows = await readCategories();
72
+ expect(rows).toHaveLength(4);
73
+ expect(rows[0]?.code).toBe("books");
74
+ expect(rows[1]?.code).toBe("clothing");
75
+ expect(rows[2]?.code).toBe("electronics");
76
+ expect(rows[3]?.code).toBe("food");
77
+ });
78
+
79
+ test("upsert updates existing rows without duplicating", async () => {
80
+ const tables = new Map([["category", categoryTable]]);
81
+
82
+ // Modified seed data: changed name for "books", added new category
83
+ const updatedDefs: ReferenceDataDef[] = [
84
+ {
85
+ entityName: "category",
86
+ data: [
87
+ { code: "electronics", name: "Electronics", description: "Phones, laptops, tablets" },
88
+ { code: "clothing", name: "Clothing", description: "Shirts, pants, shoes" },
89
+ { code: "books", name: "Books & Media", description: "Books, audiobooks, magazines" },
90
+ { code: "food", name: "Food & Drinks", description: "Groceries and beverages" },
91
+ { code: "sports", name: "Sports", description: "Equipment and gear" },
92
+ ],
93
+ upsertKey: "code",
94
+ },
95
+ ];
96
+
97
+ await seedReferenceData(updatedDefs, tables, testDb.db);
98
+
99
+ const rows = await readCategories();
100
+ expect(rows).toHaveLength(5);
101
+
102
+ const books = rows.find((r: CategoryRow) => r.code === "books");
103
+ expect(books?.name).toBe("Books & Media");
104
+
105
+ const sports = rows.find((r: CategoryRow) => r.code === "sports");
106
+ expect(sports?.name).toBe("Sports");
107
+ });
108
+ });
109
+ ```
110
+
111
+ ### src/feature.ts
112
+
113
+ ```ts
114
+ // Reference Data Sample
115
+ // Shows: r.referenceData() for static seed data with upsert logic
116
+
117
+ import { table, text } from "@cosmicdrift/kumiko-framework/db";
118
+ import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
119
+
120
+ export const categoryEntity = createEntity({
121
+ table: "read_sample_categories",
122
+ fields: {
123
+ code: createTextField({ personal: false, reason: "is_reference_data", required: true }),
124
+ name: createTextField({ personal: false, reason: "is_reference_data", required: true }),
125
+ description: createTextField({ personal: false, reason: "is_reference_data" }),
126
+ },
127
+ });
128
+
129
+ // Drizzle table for direct DB queries in tests
130
+ export const categoryTable = table("read_sample_categories", {
131
+ code: text("code").notNull(),
132
+ name: text("name").notNull(),
133
+ description: text("description"),
134
+ });
135
+
136
+ export const categoryFeature = defineFeature("catalog", (r) => {
137
+ r.entity("category", categoryEntity);
138
+
139
+ // Static reference data — seeded on boot, upserted on changes
140
+ r.referenceData(
141
+ "category",
142
+ [
143
+ { code: "electronics", name: "Electronics", description: "Phones, laptops, tablets" },
144
+ { code: "clothing", name: "Clothing", description: "Shirts, pants, shoes" },
145
+ { code: "books", name: "Books", description: "Fiction and non-fiction" },
146
+ { code: "food", name: "Food & Drinks", description: "Groceries and beverages" },
147
+ ],
148
+ { upsertKey: "code" },
149
+ );
150
+ });
151
+ ```