@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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 (281) hide show
  1. package/AGENTS.md +338 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +446 -0
  4. package/agents/examples/anonymous-access.md +288 -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 +319 -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 +315 -0
  20. package/agents/examples/custom-handlers.md +444 -0
  21. package/agents/examples/dashboard-stats.md +229 -0
  22. package/agents/examples/delivery-notifications.md +432 -0
  23. package/agents/examples/designer-demo.md +86 -0
  24. package/agents/examples/e2e-generator.md +211 -0
  25. package/agents/examples/embedded-entity-form.md +598 -0
  26. package/agents/examples/embedded.md +649 -0
  27. package/agents/examples/encrypted-tenant-config.md +414 -0
  28. package/agents/examples/error-contract.md +535 -0
  29. package/agents/examples/event-sourcing.md +1287 -0
  30. package/agents/examples/feature-toggles.md +274 -0
  31. package/agents/examples/field-access.md +271 -0
  32. package/agents/examples/files-post-processing.md +433 -0
  33. package/agents/examples/i18n.md +166 -0
  34. package/agents/examples/idempotency.md +179 -0
  35. package/agents/examples/inbound-mail-triage.md +315 -0
  36. package/agents/examples/lane-routing.md +246 -0
  37. package/agents/examples/ledger-banking.md +215 -0
  38. package/agents/examples/ledger-invoicing.md +236 -0
  39. package/agents/examples/legal-pages.md +393 -0
  40. package/agents/examples/lifecycle-hooks.md +368 -0
  41. package/agents/examples/managed-config.md +428 -0
  42. package/agents/examples/managed-pages.md +353 -0
  43. package/agents/examples/notes-history-basic.md +346 -0
  44. package/agents/examples/ownership.md +404 -0
  45. package/agents/examples/pdf-audit-export.md +306 -0
  46. package/agents/examples/pipeline-basics.md +792 -0
  47. package/agents/examples/public-share-token.md +351 -0
  48. package/agents/examples/rate-limiting.md +211 -0
  49. package/agents/examples/realtime-sse.md +187 -0
  50. package/agents/examples/record-detail-layout.md +297 -0
  51. package/agents/examples/reference-data.md +151 -0
  52. package/agents/examples/relations.md +280 -0
  53. package/agents/examples/screens-nav.md +353 -0
  54. package/agents/examples/search.md +233 -0
  55. package/agents/examples/secrets-demo.md +417 -0
  56. package/agents/examples/security-baseline.md +187 -0
  57. package/agents/examples/seed-migration.md +99 -0
  58. package/agents/examples/session-revocation.md +250 -0
  59. package/agents/examples/state-machine.md +920 -0
  60. package/agents/examples/tags-basic.md +401 -0
  61. package/agents/examples/tenant-isolation.md +173 -0
  62. package/agents/examples/tenant-settings.md +275 -0
  63. package/agents/examples/tier-admin.md +347 -0
  64. package/agents/examples/timezones.md +366 -0
  65. package/agents/examples/user-data-rights.md +418 -0
  66. package/agents/examples/user-profile.md +226 -0
  67. package/agents/examples/webhook-step.md +597 -0
  68. package/agents/examples/wizard-form.md +379 -0
  69. package/agents/examples/workflow-engine.md +1731 -0
  70. package/agents/examples/writeform-section.md +300 -0
  71. package/agents/examples.md +75 -0
  72. package/agents/features.md +768 -0
  73. package/agents/widgets.md +181 -0
  74. package/dist/api/auth-middleware.d.ts +2 -0
  75. package/dist/api/auth-middleware.js +7 -0
  76. package/dist/api/auth-routes.d.ts +0 -1
  77. package/dist/api/auth-routes.js +49 -46
  78. package/dist/api/extra-route.d.ts +3 -0
  79. package/dist/api/index.d.ts +2 -2
  80. package/dist/api/index.js +1 -1
  81. package/dist/api/routes.js +46 -17
  82. package/dist/api/server.d.ts +4 -3
  83. package/dist/api/server.js +39 -10
  84. package/dist/api/session-cookies.d.ts +1 -0
  85. package/dist/api/session-cookies.js +20 -5
  86. package/dist/api/sse-route.js +16 -1
  87. package/dist/bun-db/connection.js +3 -16
  88. package/dist/bun-db/index.d.ts +1 -1
  89. package/dist/bun-db/index.js +1 -1
  90. package/dist/bun-db/query.d.ts +15 -3
  91. package/dist/bun-db/query.js +76 -24
  92. package/dist/crypto/owner-transition-rekey.d.ts +1 -0
  93. package/dist/crypto/owner-transition-rekey.js +7 -3
  94. package/dist/crypto/pii-field-encryption.d.ts +1 -0
  95. package/dist/crypto/pii-field-encryption.js +9 -2
  96. package/dist/crypto/subject-resolver.d.ts +1 -0
  97. package/dist/crypto/subject-resolver.js +7 -0
  98. package/dist/db/api.d.ts +4 -3
  99. package/dist/db/apply-entity-event.js +26 -2
  100. package/dist/db/blind-index-cleanup.js +2 -1
  101. package/dist/db/bun-provider.js +9 -18
  102. package/dist/db/collect-table-metas.js +11 -7
  103. package/dist/db/dialect.d.ts +1 -1
  104. package/dist/db/dialect.js +2 -0
  105. package/dist/db/eagerload.js +4 -3
  106. package/dist/db/event-store-executor-context.d.ts +6 -6
  107. package/dist/db/event-store-executor-context.js +25 -22
  108. package/dist/db/event-store-executor-read.js +31 -16
  109. package/dist/db/event-store-executor-write.d.ts +1 -1
  110. package/dist/db/event-store-executor-write.js +84 -63
  111. package/dist/db/index.d.ts +2 -1
  112. package/dist/db/index.js +2 -1
  113. package/dist/db/lock-guard.d.ts +4 -0
  114. package/dist/db/lock-guard.js +14 -0
  115. package/dist/db/migrate-generator.js +1 -1
  116. package/dist/db/migrate-runner.d.ts +1 -0
  117. package/dist/db/migrate-runner.js +64 -38
  118. package/dist/db/queries/backfill-pii.js +63 -25
  119. package/dist/db/queries/event-consumer.d.ts +6 -6
  120. package/dist/db/queries/event-consumer.js +2 -1
  121. package/dist/db/queries/event-store-transfer.js +15 -4
  122. package/dist/db/queries/event-store.d.ts +1 -1
  123. package/dist/db/queries/event-store.js +11 -10
  124. package/dist/db/queries/pii-backfill-state.js +3 -2
  125. package/dist/db/queries/postgres-array-literal.d.ts +2 -0
  126. package/dist/db/queries/postgres-array-literal.js +6 -0
  127. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  128. package/dist/db/queries/projection-rebuild.js +3 -2
  129. package/dist/db/queries/shadow-swap.d.ts +1 -1
  130. package/dist/db/queries/shadow-swap.js +12 -8
  131. package/dist/db/query.d.ts +1 -1
  132. package/dist/db/query.js +1 -1
  133. package/dist/db/retired-framework-tables.js +22 -4
  134. package/dist/db/table-builder.js +6 -2
  135. package/dist/db/tenant-db-runner.d.ts +4 -0
  136. package/dist/db/tenant-db-runner.js +17 -0
  137. package/dist/db/tenant-db.d.ts +4 -0
  138. package/dist/db/tenant-db.js +50 -5
  139. package/dist/derivatives/delete-stored-file.d.ts +4 -0
  140. package/dist/derivatives/delete-stored-file.js +9 -0
  141. package/dist/derivatives/derivatives-context.js +1 -1
  142. package/dist/derivatives/index.d.ts +1 -1
  143. package/dist/derivatives/index.js +1 -1
  144. package/dist/engine/boot-validator/access-declarations.js +2 -2
  145. package/dist/engine/boot-validator/api-ext.js +3 -3
  146. package/dist/engine/boot-validator/boot-check.js +1 -1
  147. package/dist/engine/boot-validator/config-deps.js +13 -13
  148. package/dist/engine/boot-validator/detail-screens.js +1 -1
  149. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  150. package/dist/engine/boot-validator/entity-handler.js +87 -35
  151. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  152. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  153. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  154. package/dist/engine/boot-validator/nav.js +1 -1
  155. package/dist/engine/boot-validator/pii-retention.js +8 -8
  156. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  157. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  158. package/dist/engine/boot-validator/query-refs.js +14 -2
  159. package/dist/engine/boot-validator/screens.d.ts +2 -1
  160. package/dist/engine/boot-validator/screens.js +153 -95
  161. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  162. package/dist/engine/dedupe-features.js +31 -4
  163. package/dist/engine/define-handler.d.ts +2 -2
  164. package/dist/engine/define-handler.js +8 -1
  165. package/dist/engine/entity-handlers.js +14 -7
  166. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  167. package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
  168. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  169. package/dist/engine/feature-ast/render.js +10 -2
  170. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  171. package/dist/engine/feature-entity-handlers.js +12 -3
  172. package/dist/engine/handler-field-references.d.ts +21 -2
  173. package/dist/engine/handler-field-references.js +55 -17
  174. package/dist/engine/index.d.ts +3 -3
  175. package/dist/engine/index.js +1 -1
  176. package/dist/engine/schema-builder.js +2 -1
  177. package/dist/engine/screen-helpers.d.ts +1 -0
  178. package/dist/engine/screen-helpers.js +2 -2
  179. package/dist/engine/soft-delete-cleanup.js +10 -3
  180. package/dist/engine/steps/_read-source.js +5 -2
  181. package/dist/engine/steps/retry.js +2 -1
  182. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  183. package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
  184. package/dist/engine/template-variables.d.ts +3 -0
  185. package/dist/engine/template-variables.js +3 -0
  186. package/dist/engine/tier-resolver-extension.d.ts +1 -0
  187. package/dist/engine/tier-resolver-extension.js +6 -0
  188. package/dist/engine/types/index.d.ts +1 -1
  189. package/dist/entrypoint/index.d.ts +2 -2
  190. package/dist/env/_zod-introspect.d.ts +1 -0
  191. package/dist/errors/classes.js +1 -0
  192. package/dist/errors/docs-url.js +6 -6
  193. package/dist/errors/index.d.ts +1 -1
  194. package/dist/errors/index.js +1 -1
  195. package/dist/errors/kumiko-error.d.ts +2 -0
  196. package/dist/errors/kumiko-error.js +2 -0
  197. package/dist/errors/subject-erased.d.ts +1 -0
  198. package/dist/errors/subject-erased.js +6 -0
  199. package/dist/errors/write-error-info.js +2 -0
  200. package/dist/event-store/admin-api.js +3 -2
  201. package/dist/event-store/errors.d.ts +9 -0
  202. package/dist/event-store/errors.js +20 -0
  203. package/dist/event-store/event-store.d.ts +6 -0
  204. package/dist/event-store/event-store.js +28 -6
  205. package/dist/event-store/index.d.ts +1 -1
  206. package/dist/event-store/index.js +1 -1
  207. package/dist/event-store/tenant-db-append.js +2 -2
  208. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  209. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  210. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  211. package/dist/files/bind-pending-file-refs.js +3 -3
  212. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  213. package/dist/files/bind-referenced-files.write.js +22 -13
  214. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  215. package/dist/files/create-file-ref-from-bytes.js +67 -4
  216. package/dist/files/create-file-ref.d.ts +24 -1
  217. package/dist/files/create-file-ref.js +67 -25
  218. package/dist/files/dispose-removed-file-refs.js +21 -6
  219. package/dist/files/feature.d.ts +1 -0
  220. package/dist/files/feature.js +2 -1
  221. package/dist/files/file-handle.d.ts +1 -1
  222. package/dist/files/file-handle.js +12 -0
  223. package/dist/files/file-routes.js +10 -6
  224. package/dist/files/index.d.ts +3 -3
  225. package/dist/files/index.js +2 -2
  226. package/dist/files/load-save-file-refs.d.ts +10 -0
  227. package/dist/files/load-save-file-refs.js +47 -0
  228. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  229. package/dist/files/purge-expired-file-ref-bytes.js +46 -21
  230. package/dist/files/resolve-content-type.js +40 -4
  231. package/dist/files/storage-tracking.js +5 -4
  232. package/dist/files/types.js +5 -8
  233. package/dist/files/zip-entry-names.d.ts +1 -0
  234. package/dist/files/zip-entry-names.js +46 -0
  235. package/dist/jobs/index.d.ts +1 -1
  236. package/dist/jobs/job-runner.d.ts +5 -1
  237. package/dist/jobs/job-runner.js +124 -37
  238. package/dist/observability/escape-hatch-report.d.ts +2 -1
  239. package/dist/observability/escape-hatch-report.js +48 -7
  240. package/dist/observability/index.d.ts +1 -1
  241. package/dist/observability/index.js +1 -1
  242. package/dist/observability/metrics-handle.d.ts +1 -0
  243. package/dist/observability/metrics-handle.js +7 -0
  244. package/dist/observability/standard-metrics.js +4 -4
  245. package/dist/pipeline/cascade-handler.js +5 -2
  246. package/dist/pipeline/dispatch-batch.js +79 -31
  247. package/dist/pipeline/dispatch-query.d.ts +2 -1
  248. package/dist/pipeline/dispatch-query.js +6 -6
  249. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  250. package/dist/pipeline/dispatch-shared.js +136 -48
  251. package/dist/pipeline/dispatch-stream.js +10 -4
  252. package/dist/pipeline/dispatch-write.js +34 -22
  253. package/dist/pipeline/dispatcher.js +3 -1
  254. package/dist/pipeline/event-dispatcher.d.ts +2 -2
  255. package/dist/pipeline/event-dispatcher.js +11 -9
  256. package/dist/pipeline/event-retention.d.ts +1 -0
  257. package/dist/pipeline/event-retention.js +7 -1
  258. package/dist/pipeline/index.d.ts +1 -0
  259. package/dist/pipeline/index.js +1 -0
  260. package/dist/pipeline/notify-scope.js +3 -1
  261. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  262. package/dist/pipeline/pre-transaction-reservations.js +29 -9
  263. package/dist/rate-limit/bucket.d.ts +3 -3
  264. package/dist/rate-limit/bucket.js +14 -4
  265. package/dist/redis/cache-sync-bus.js +9 -3
  266. package/dist/schema-cli.js +12 -1
  267. package/dist/search/meilisearch-adapter.js +36 -9
  268. package/dist/stack/db.d.ts +2 -2
  269. package/dist/stack/test-stack.js +2 -1
  270. package/dist/testing/index.d.ts +1 -0
  271. package/dist/testing/index.js +1 -0
  272. package/dist/testing/multipart-helper.d.ts +6 -0
  273. package/dist/testing/multipart-helper.js +1 -1
  274. package/dist/testing/zip-fixture.d.ts +1 -0
  275. package/dist/testing/zip-fixture.js +33 -0
  276. package/dist/ui-types/card-column-roles.js +4 -2
  277. package/dist/ui-types/index.d.ts +1 -1
  278. package/dist/ui-types/index.js +1 -1
  279. package/dist/upgrade-cli.js +46 -13
  280. package/package.json +7 -5
  281. package/src/changes.json +512 -8
@@ -0,0 +1,355 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/access-control. Do not edit. -->
2
+
3
+ # access-control
4
+
5
+ Default-deny access rules, openToAll vs. role-based handlers, and auto-indices for foreign keys.
6
+
7
+ ## README
8
+
9
+ # Access Control
10
+
11
+ Default-deny access rules and FK indices via relations.
12
+
13
+ ## What it shows
14
+
15
+ - Handler `access` roles as the default-deny gate
16
+ - Relation-declared FK indices for join-friendly schemas
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/access-control
28
+ bun test
29
+ ```
30
+
31
+ ## Source files
32
+
33
+ ### src/__tests__/feature.integration.test.ts
34
+
35
+ ```ts
36
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
37
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
38
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
39
+ import {
40
+ setupTestStack,
41
+ type TestStack,
42
+ TestUsers,
43
+ unsafeCreateEntityTable,
44
+ } from "@cosmicdrift/kumiko-framework/stack";
45
+ import { z } from "zod";
46
+ import {
47
+ accessControlFeature,
48
+ projectEntity,
49
+ projectTable,
50
+ taskEntity,
51
+ taskTable,
52
+ } from "../feature";
53
+
54
+ let stack: TestStack;
55
+ const admin = TestUsers.admin; // { id: 1, tenantId: 1, roles: ["Admin"] }
56
+ const user = TestUsers.user; // { id: 2, tenantId: 1, roles: ["User"] }
57
+
58
+ beforeAll(async () => {
59
+ stack = await setupTestStack({ features: [accessControlFeature] });
60
+ await unsafeCreateEntityTable(stack.db, projectEntity);
61
+ await unsafeCreateEntityTable(stack.db, taskEntity);
62
+ await createEventsTable(stack.db);
63
+ });
64
+
65
+ afterAll(async () => {
66
+ await stack?.cleanup();
67
+ });
68
+
69
+ describe("default-deny: refuses handlers without access", () => {
70
+ test("throws at registration when a handler lacks an access rule", () => {
71
+ // `access` is required since fw#2855 — the registrar throws before validateBoot ever runs.
72
+ expect(() =>
73
+ defineFeature("broken-feature", (r) => {
74
+ // @ts-expect-error — No { access: ... }: registration must reject this.
75
+ r.writeHandler("forgotAccess", z.object({ name: z.string() }), async () => ({
76
+ isSuccess: true as const,
77
+ data: {},
78
+ }));
79
+ }),
80
+ ).toThrow(/requires schema \+ handler \+ options\.access/);
81
+ });
82
+ });
83
+
84
+ describe("role-based vs openToAll", () => {
85
+ test("Admin-only handler rejects a User caller with access_denied", async () => {
86
+ const raw = await stack.http.write(
87
+ "access-control:write:project:create",
88
+ { name: "Rejected", ownerId: "11111111-0000-4000-8000-000000000001" },
89
+ user,
90
+ );
91
+ const body = (await raw.json()) as { isSuccess: boolean; error?: { code: string } };
92
+ expect(body.isSuccess).toBe(false);
93
+ expect(body.error?.code).toBe("access_denied");
94
+ });
95
+
96
+ test("Admin-only handler accepts an Admin caller", async () => {
97
+ await stack.http.writeOk(
98
+ "access-control:write:project:create",
99
+ { name: "Alpha", ownerId: "11111111-0000-4000-8000-000000000001" },
100
+ admin,
101
+ );
102
+ });
103
+
104
+ test("openToAll handler accepts any authenticated user", async () => {
105
+ const proj = await stack.http.writeOk<{ data: { id: string } }>(
106
+ "access-control:write:project:create",
107
+ { name: "Shared", ownerId: "11111111-0000-4000-8000-000000000001" },
108
+ admin,
109
+ );
110
+ await stack.http.writeOk(
111
+ "access-control:write:task:create",
112
+ { title: "User's task", projectId: proj.data.id },
113
+ user,
114
+ );
115
+ });
116
+ });
117
+
118
+ describe("auto-indices from relations", () => {
119
+ test("task table has an index on project_id because of the belongsTo relation", () => {
120
+ const names = taskTable.indexes.map((i) => i.name);
121
+
122
+ expect(names).toContain("read_ac_tasks_tenant_id_idx");
123
+ expect(names).toContain("read_ac_tasks_project_id_idx");
124
+ });
125
+
126
+ test("project table has only the tenant_id index (no belongsTo relations)", () => {
127
+ expect(projectTable.indexes.map((i) => i.name)).toEqual(["read_ac_projects_tenant_id_idx"]);
128
+ });
129
+ });
130
+
131
+ describe("optimistic locking: version required in update schema", () => {
132
+ test("update without version fails schema validation before reaching the crud executor", async () => {
133
+ const proj = await stack.http.writeOk<{ data: { id: string } }>(
134
+ "access-control:write:project:create",
135
+ { name: "VersionedProject", ownerId: "11111111-0000-4000-8000-000000000001" },
136
+ admin,
137
+ );
138
+ const created = await stack.http.writeOk<{ data: { id: string } }>(
139
+ "access-control:write:task:create",
140
+ { title: "Needs version", projectId: proj.data.id },
141
+ user,
142
+ );
143
+
144
+ const raw = await stack.http.write(
145
+ "access-control:write:task:update",
146
+ // Missing `version` — schema rejects before any DB call.
147
+ { id: created.data.id, changes: { title: "Updated" } },
148
+ user,
149
+ );
150
+ const body = (await raw.json()) as { isSuccess: boolean; error?: { code: string } };
151
+ expect(body.isSuccess).toBe(false);
152
+ expect(body.error?.code).toBe("validation_error");
153
+ });
154
+
155
+ test("update with correct version succeeds", async () => {
156
+ const proj = await stack.http.writeOk<{ data: { id: string } }>(
157
+ "access-control:write:project:create",
158
+ { name: "VersionedOk", ownerId: "11111111-0000-4000-8000-000000000001" },
159
+ admin,
160
+ );
161
+ const created = await stack.http.writeOk<{ data: { id: string } }>(
162
+ "access-control:write:task:create",
163
+ { title: "Task v1", projectId: proj.data.id },
164
+ user,
165
+ );
166
+
167
+ await stack.http.writeOk(
168
+ "access-control:write:task:update",
169
+ { id: created.data.id, version: 1, changes: { title: "Task v2" } },
170
+ user,
171
+ );
172
+ });
173
+ });
174
+
175
+ // Ensures this sample is wired through the full stack — if any of its
176
+ // handlers regress to missing access or broken schemas, boot or this list
177
+ // query will fail loudly.
178
+ describe("end-to-end", () => {
179
+ test("list returns seeded tasks across users", async () => {
180
+ const proj = await stack.http.writeOk<{ data: { id: string } }>(
181
+ "access-control:write:project:create",
182
+ { name: "ListTarget", ownerId: "11111111-0000-4000-8000-000000000001" },
183
+ admin,
184
+ );
185
+
186
+ await stack.http.writeOk(
187
+ "access-control:write:task:create",
188
+ { title: "t1", projectId: proj.data.id },
189
+ user,
190
+ );
191
+ await stack.http.writeOk(
192
+ "access-control:write:task:create",
193
+ { title: "t2", projectId: proj.data.id },
194
+ admin,
195
+ );
196
+
197
+ const list = await stack.http.queryOk<{ rows: Record<string, unknown>[] }>(
198
+ "access-control:query:task:list",
199
+ {},
200
+ user,
201
+ );
202
+ expect(list.rows.length).toBeGreaterThanOrEqual(2);
203
+ });
204
+ });
205
+ ```
206
+
207
+ ### src/feature.ts
208
+
209
+ ```ts
210
+ // Access Control + FK-Indices Sample
211
+ //
212
+ // Demonstrates the framework's security + performance defaults:
213
+ //
214
+ // 1. Default-deny: every handler must declare an access rule. Boot fails
215
+ // loudly when one is missing. Two shapes:
216
+ // { roles: ["Admin", ...] } — role allowlist
217
+ // { openToAll: { reason: "..." } } — any authenticated user, with a required reason
218
+ //
219
+ // 2. Foreign-key relations declared via r.relation() become indexed columns
220
+ // automatically — no manual CREATE INDEX. Inspect the Drizzle table
221
+ // config in tests to verify.
222
+
223
+ import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
224
+ import {
225
+ createEntity,
226
+ createTextField,
227
+ defineEntityCreateHandler,
228
+ defineEntityListHandler,
229
+ defineFeature,
230
+ defineRoles,
231
+ } from "@cosmicdrift/kumiko-framework/engine";
232
+ import { z } from "zod";
233
+
234
+ // Typed role registry — `defineRoles` returns an object keyed by role name
235
+ // so access rules can reference `AccessControlRoles.Admin` instead of the
236
+ // magic string "Admin". Typos become compile errors, renames refactor.
237
+ export const AccessControlRoles = defineRoles(["Admin"] as const);
238
+
239
+ export const projectEntity = createEntity({
240
+ table: "read_ac_projects",
241
+ fields: {
242
+ name: createTextField({ personal: false, reason: "is_business_data", required: true }),
243
+ ownerId: createTextField({ personal: "ref", required: true }),
244
+ },
245
+ });
246
+
247
+ export const taskEntity = createEntity({
248
+ table: "read_ac_tasks",
249
+ fields: {
250
+ title: createTextField({ personal: false, reason: "is_business_data", required: true }),
251
+ assigneeId: createTextField({ personal: "ref" }),
252
+ projectId: createTextField({ personal: false, reason: "is_system_identifier", required: true }),
253
+ },
254
+ });
255
+
256
+ // The "owner" relation points at a hypothetical external `user` entity — in
257
+ // a real app this would be @cosmicdrift/kumiko-bundled-features/user. For the sample we
258
+ // keep it internal: projectRelations only holds FK metadata (no cross-entity
259
+ // resolution) so the sample stays self-contained.
260
+ export const projectRelations = {} as const;
261
+
262
+ export const taskRelations = {
263
+ project: { type: "belongsTo", target: "project", foreignKey: "projectId" },
264
+ } as const;
265
+
266
+ // Relations flow into buildEntityTable — every belongsTo FK gets its own
267
+ // index on this table.
268
+ export const projectTable = buildEntityTable("project", projectEntity, {
269
+ relations: projectRelations,
270
+ });
271
+ export const taskTable = buildEntityTable("task", taskEntity, {
272
+ relations: taskRelations,
273
+ });
274
+
275
+ export const accessControlFeature = defineFeature("access-control", (r) => {
276
+ r.entity("project", projectEntity);
277
+ r.entity("task", taskEntity);
278
+ r.relation("task", "project", taskRelations.project);
279
+
280
+ // Admin-only: standard create — no business logic, helper handles schema +
281
+ // executor + handler-body.
282
+ r.writeHandler(
283
+ defineEntityCreateHandler("project", projectEntity, {
284
+ access: { roles: [AccessControlRoles.Admin] },
285
+ }),
286
+ );
287
+
288
+ // Custom create: assigneeId defaults to the caller when omitted, so we
289
+ // hand-write this handler instead of using the helper. The framework helpers
290
+ // are deliberately scope-limited — opt out the moment you need defaults.
291
+ const taskExecutor = createEventStoreExecutor(taskTable, taskEntity, { entityName: "task" });
292
+ r.writeHandler(
293
+ "task:create",
294
+ z.object({
295
+ title: z.string().min(1),
296
+ projectId: z.uuid(),
297
+ assigneeId: z.uuid().optional(),
298
+ }),
299
+ async (event, ctx) =>
300
+ taskExecutor.create(
301
+ {
302
+ ...event.payload,
303
+ assigneeId: event.payload.assigneeId ?? event.user.id,
304
+ },
305
+ event.user,
306
+ ctx.db,
307
+ ),
308
+ {
309
+ access: {
310
+ openToAll: {
311
+ reason:
312
+ "any signed-in tenant user may create a task; assigneeId defaults to the caller when omitted",
313
+ },
314
+ },
315
+ },
316
+ );
317
+
318
+ // Update requires version — optimistic locking is the default and this
319
+ // narrowed schema (only title + assigneeId mutable) is stricter than the
320
+ // helper's generic update schema, so we keep it hand-written.
321
+ r.writeHandler(
322
+ "task:update",
323
+ z.object({
324
+ id: z.uuid(),
325
+ version: z.number(),
326
+ changes: z.object({
327
+ title: z.string().min(1).optional(),
328
+ assigneeId: z.uuid().optional(),
329
+ }),
330
+ }),
331
+ async (event, ctx) => taskExecutor.update(event.payload, event.user, ctx.db),
332
+ {
333
+ access: {
334
+ openToAll: {
335
+ reason:
336
+ "any signed-in tenant user may update a task; optimistic locking (version) " +
337
+ "guards against concurrent overwrites, not caller identity",
338
+ },
339
+ },
340
+ },
341
+ );
342
+
343
+ // Self-service list — no custom logic.
344
+ r.queryHandler(
345
+ defineEntityListHandler("task", taskEntity, {
346
+ access: {
347
+ openToAll: {
348
+ reason:
349
+ "any signed-in tenant user may list tasks; there is no per-user filter in this recipe",
350
+ },
351
+ },
352
+ }),
353
+ );
354
+ });
355
+ ```