@cosmicdrift/kumiko-framework 0.367.0 → 0.369.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 (240) hide show
  1. package/AGENTS.md +1 -7
  2. package/README.md +1 -1
  3. package/agents/examples/access-control.md +2 -2
  4. package/agents/examples/anonymous-access-multitenant.md +2 -2
  5. package/agents/examples/anonymous-access.md +3 -3
  6. package/agents/examples/apex-landing.md +1 -1
  7. package/agents/examples/apex-surface-auth.md +6 -7
  8. package/agents/examples/api-worker-split.md +7 -8
  9. package/agents/examples/auth-claims.md +2 -1
  10. package/agents/examples/auth-foundation-providers.md +3 -3
  11. package/agents/examples/auth-mfa.md +3 -4
  12. package/agents/examples/basic-entity.md +3 -4
  13. package/agents/examples/chat-channels.md +4 -4
  14. package/agents/examples/compliance-profiles-demo.md +2 -3
  15. package/agents/examples/cross-feature-events.md +4 -4
  16. package/agents/examples/crypto-shredding-hr.md +14 -9
  17. package/agents/examples/currencies-global.md +3 -4
  18. package/agents/examples/currencies-per-tenant.md +3 -4
  19. package/agents/examples/custom-fields-basic.md +2 -3
  20. package/agents/examples/custom-handlers.md +3 -5
  21. package/agents/examples/dashboard-stats.md +2 -3
  22. package/agents/examples/delivery-notifications.md +5 -6
  23. package/agents/examples/e2e-generator.md +1 -1
  24. package/agents/examples/embedded-entity-form.md +2 -3
  25. package/agents/examples/embedded.md +2 -3
  26. package/agents/examples/encrypted-tenant-config.md +5 -6
  27. package/agents/examples/error-contract.md +3 -4
  28. package/agents/examples/event-sourcing.md +19 -23
  29. package/agents/examples/feature-toggles.md +4 -4
  30. package/agents/examples/field-access.md +3 -4
  31. package/agents/examples/files-post-processing.md +12 -13
  32. package/agents/examples/golden-path.md +208 -0
  33. package/agents/examples/idempotency.md +2 -2
  34. package/agents/examples/inbound-mail-triage.md +7 -7
  35. package/agents/examples/lane-routing.md +3 -3
  36. package/agents/examples/ledger-banking.md +2 -3
  37. package/agents/examples/ledger-invoicing.md +2 -3
  38. package/agents/examples/legal-pages.md +1 -1
  39. package/agents/examples/lifecycle-hooks.md +3 -3
  40. package/agents/examples/managed-config.md +3 -4
  41. package/agents/examples/managed-pages.md +2 -2
  42. package/agents/examples/notes-history-basic.md +4 -5
  43. package/agents/examples/ownership.md +3 -5
  44. package/agents/examples/pdf-audit-export.md +2 -3
  45. package/agents/examples/pipeline-basics.md +2 -2
  46. package/agents/examples/public-share-token.md +3 -3
  47. package/agents/examples/rate-limiting.md +2 -1
  48. package/agents/examples/realtime-sse.md +2 -3
  49. package/agents/examples/record-detail-layout.md +1 -1
  50. package/agents/examples/reference-data.md +3 -3
  51. package/agents/examples/relations.md +2 -2
  52. package/agents/examples/screens-nav.md +1 -1
  53. package/agents/examples/search.md +2 -2
  54. package/agents/examples/secrets-demo.md +8 -8
  55. package/agents/examples/security-baseline.md +2 -2
  56. package/agents/examples/session-revocation.md +7 -8
  57. package/agents/examples/state-machine.md +181 -165
  58. package/agents/examples/tags-basic.md +2 -3
  59. package/agents/examples/tenant-isolation.md +2 -3
  60. package/agents/examples/tenant-settings.md +2 -2
  61. package/agents/examples/tier-admin.md +3 -4
  62. package/agents/examples/timezones.md +3 -3
  63. package/agents/examples/user-data-rights.md +12 -8
  64. package/agents/examples/user-profile.md +3 -3
  65. package/agents/examples/webhook-step.md +2 -3
  66. package/agents/examples/wizard-form.md +4 -5
  67. package/agents/examples/workflow-engine.md +7 -3
  68. package/agents/examples/writeform-section.md +1 -1
  69. package/agents/examples.md +1 -0
  70. package/agents/features.md +4 -4
  71. package/dist/consumer-cli.js +2 -3
  72. package/dist/crypto/index.d.ts +1 -1
  73. package/dist/crypto/index.js +1 -1
  74. package/dist/crypto/owner-transition-rekey.js +2 -0
  75. package/dist/crypto/pg-kms-adapter.js +1 -1
  76. package/dist/crypto/subject-resolver.d.ts +2 -0
  77. package/dist/crypto/subject-resolver.js +30 -15
  78. package/dist/db/bun-db-deprecated.d.ts +10 -0
  79. package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
  80. package/dist/db/config-seed.js +1 -1
  81. package/dist/db/connection.d.ts +23 -5
  82. package/dist/db/connection.js +10 -5
  83. package/dist/db/dialect.js +1 -1
  84. package/dist/db/entity-field-encryption.d.ts +5 -0
  85. package/dist/db/entity-field-encryption.js +29 -0
  86. package/dist/db/entity-table-meta.js +6 -5
  87. package/dist/db/event-store-executor-read.js +26 -5
  88. package/dist/db/index.d.ts +4 -4
  89. package/dist/db/index.js +1 -1
  90. package/dist/db/migrate-runner.js +1 -1
  91. package/dist/db/queries/backfill-pii.js +16 -9
  92. package/dist/db/queries/ddl.js +1 -1
  93. package/dist/db/queries/event-store.d.ts +2 -0
  94. package/dist/db/queries/event-store.js +26 -0
  95. package/dist/db/queries/test-stack.js +1 -1
  96. package/dist/db/query.d.ts +105 -1
  97. package/dist/db/query.js +1450 -3
  98. package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
  99. package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
  100. package/dist/db/tenant-db.d.ts +11 -0
  101. package/dist/db/tenant-db.js +51 -12
  102. package/dist/derivatives/derivatives-context.d.ts +1 -1
  103. package/dist/derivatives/derivatives-context.js +1 -1
  104. package/dist/engine/boot-validator/gdpr-storage.js +3 -3
  105. package/dist/engine/boot-validator/pii-retention.js +3 -3
  106. package/dist/engine/boot-validator/record-owned.js +3 -3
  107. package/dist/engine/boot-validator/screens.js +37 -1
  108. package/dist/engine/bridge-stub.d.ts +4 -0
  109. package/dist/engine/bridge-stub.js +62 -0
  110. package/dist/engine/build-config-feature-schema.js +197 -65
  111. package/dist/engine/event-helpers.d.ts +2 -1
  112. package/dist/engine/event-helpers.js +1 -0
  113. package/dist/engine/extension-selector-plugins.d.ts +3 -0
  114. package/dist/engine/extension-selector-plugins.js +14 -0
  115. package/dist/engine/feature-config-events-jobs.d.ts +2 -0
  116. package/dist/engine/feature-config-events-jobs.js +3 -2
  117. package/dist/engine/index.d.ts +1 -0
  118. package/dist/engine/index.js +1 -0
  119. package/dist/es-ops/operations-schema.js +1 -1
  120. package/dist/event-store/admin-api.js +1 -1
  121. package/dist/event-store/archive.js +1 -1
  122. package/dist/event-store/errors.d.ts +2 -1
  123. package/dist/event-store/errors.js +6 -2
  124. package/dist/event-store/event-store.js +17 -9
  125. package/dist/event-store/events-schema.js +1 -1
  126. package/dist/event-store/rebuild-dead-letter.js +3 -3
  127. package/dist/event-store/snapshot.js +1 -1
  128. package/dist/event-store/upcaster-dead-letter.js +3 -3
  129. package/dist/files/create-file-ref-from-bytes.js +1 -1
  130. package/dist/files/file-routes.js +1 -1
  131. package/dist/i18n/settings-hub-keys.js +1 -2
  132. package/dist/internal/stack-support.d.ts +12 -0
  133. package/dist/internal/stack-support.js +12 -0
  134. package/dist/internal/testing.d.ts +4 -0
  135. package/dist/internal/testing.js +5 -0
  136. package/dist/jobs/job-runner.js +23 -11
  137. package/dist/migrations/pending-rebuilds.js +1 -1
  138. package/dist/migrations/pii-event-backfill.js +2 -2
  139. package/dist/pipeline/dispatch-shared.js +43 -23
  140. package/dist/pipeline/dispatch-write.js +6 -2
  141. package/dist/pipeline/dispatcher-utils.js +6 -2
  142. package/dist/pipeline/event-consumer-state.js +1 -1
  143. package/dist/pipeline/projection-rebuild.d.ts +1 -0
  144. package/dist/pipeline/projection-rebuild.js +1 -1
  145. package/dist/pipeline/projection-state.js +1 -1
  146. package/dist/pipeline/system-hooks.js +1 -1
  147. package/dist/schema-cli.js +3 -3
  148. package/dist/search/purge-subject.js +20 -10
  149. package/dist/seeding/index.d.ts +1 -0
  150. package/dist/seeding/index.js +1 -0
  151. package/dist/stack/index.d.ts +10 -9
  152. package/dist/stack/index.js +11 -19
  153. package/dist/testing/e2e-generator.d.ts +5 -49
  154. package/dist/testing/e2e-generator.js +5 -382
  155. package/dist/testing/handler-context.d.ts +5 -4
  156. package/dist/testing/handler-context.js +5 -69
  157. package/dist/testing/index.d.ts +10 -26
  158. package/dist/testing/index.js +10 -34
  159. package/dist/testing/real-providers.d.ts +5 -3
  160. package/dist/testing/real-providers.js +5 -21
  161. package/dist/upgrade-cli.js +6 -4
  162. package/package.json +37 -9
  163. package/src/changes.json +64 -0
  164. package/src/scripts/codemod/README.md +43 -0
  165. package/src/scripts/codemod/codemod-support.ts +276 -0
  166. package/src/scripts/codemod/db-factory.ts +810 -0
  167. package/src/scripts/codemod/feature-factory.ts +429 -0
  168. package/src/scripts/codemod/s3-imports.ts +274 -0
  169. package/src/scripts/codemod/tenant-db-options.ts +454 -0
  170. package/src/scripts/codemod/testing-move-targets.ts +161 -0
  171. package/src/scripts/codemod/testing-move.ts +520 -0
  172. package/dist/bun-db/connection.d.ts +0 -17
  173. package/dist/bun-db/connection.js +0 -40
  174. package/dist/bun-db/index.d.ts +0 -4
  175. package/dist/bun-db/query.d.ts +0 -106
  176. package/dist/bun-db/query.js +0 -1457
  177. package/dist/db/api.d.ts +0 -19
  178. package/dist/db/api.js +0 -23
  179. package/dist/db/bun-provider.d.ts +0 -2
  180. package/dist/db/bun-provider.js +0 -28
  181. package/dist/db/postgres-provider.d.ts +0 -2
  182. package/dist/db/postgres-provider.js +0 -21
  183. package/dist/db/query-api.d.ts +0 -2
  184. package/dist/db/query-api.js +0 -6
  185. package/dist/stack/db.d.ts +0 -22
  186. package/dist/stack/db.js +0 -67
  187. package/dist/stack/drain-event-consumers.d.ts +0 -4
  188. package/dist/stack/drain-event-consumers.js +0 -51
  189. package/dist/stack/drain-jobs.d.ts +0 -22
  190. package/dist/stack/drain-jobs.js +0 -141
  191. package/dist/stack/event-collector.d.ts +0 -9
  192. package/dist/stack/event-collector.js +0 -12
  193. package/dist/stack/push-entity-projection-tables.d.ts +0 -24
  194. package/dist/stack/push-entity-projection-tables.js +0 -74
  195. package/dist/stack/redis.d.ts +0 -17
  196. package/dist/stack/redis.js +0 -52
  197. package/dist/stack/request-helper.d.ts +0 -35
  198. package/dist/stack/request-helper.js +0 -144
  199. package/dist/stack/test-stack.d.ts +0 -192
  200. package/dist/stack/test-stack.js +0 -429
  201. package/dist/testing/access-assertions.d.ts +0 -2
  202. package/dist/testing/access-assertions.js +0 -13
  203. package/dist/testing/assertions.d.ts +0 -10
  204. package/dist/testing/assertions.js +0 -22
  205. package/dist/testing/boot-validator-fixture.d.ts +0 -3
  206. package/dist/testing/boot-validator-fixture.js +0 -101
  207. package/dist/testing/closed-connection-error.d.ts +0 -7
  208. package/dist/testing/closed-connection-error.js +0 -46
  209. package/dist/testing/db-cleanup.d.ts +0 -9
  210. package/dist/testing/db-cleanup.js +0 -31
  211. package/dist/testing/expect-error.d.ts +0 -2
  212. package/dist/testing/expect-error.js +0 -22
  213. package/dist/testing/file-provider-contract.d.ts +0 -2
  214. package/dist/testing/file-provider-contract.js +0 -102
  215. package/dist/testing/http-cookies.d.ts +0 -7
  216. package/dist/testing/http-cookies.js +0 -47
  217. package/dist/testing/late-bound.d.ts +0 -9
  218. package/dist/testing/late-bound.js +0 -31
  219. package/dist/testing/manual-clock.d.ts +0 -14
  220. package/dist/testing/manual-clock.js +0 -12
  221. package/dist/testing/multipart-helper.d.ts +0 -22
  222. package/dist/testing/multipart-helper.js +0 -77
  223. package/dist/testing/mutable-master-key-provider.d.ts +0 -7
  224. package/dist/testing/mutable-master-key-provider.js +0 -35
  225. package/dist/testing/observability-recorder.d.ts +0 -9
  226. package/dist/testing/observability-recorder.js +0 -29
  227. package/dist/testing/seed.d.ts +0 -9
  228. package/dist/testing/seed.js +0 -21
  229. package/dist/testing/shared-entities.d.ts +0 -286
  230. package/dist/testing/shared-entities.js +0 -56
  231. package/dist/testing/utils.d.ts +0 -1
  232. package/dist/testing/utils.js +0 -1
  233. package/dist/testing/wait-for.d.ts +0 -20
  234. package/dist/testing/wait-for.js +0 -47
  235. package/dist/testing/without-ambient-temporal.d.ts +0 -5
  236. package/dist/testing/without-ambient-temporal.js +0 -17
  237. package/dist/testing/zip-fixture.d.ts +0 -1
  238. package/dist/testing/zip-fixture.js +0 -33
  239. /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
  240. /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
@@ -96,8 +96,8 @@ Six integration tests pin everything at once:
96
96
  // - getAllProjectionProgress (projection lag for ops dashboards)
97
97
 
98
98
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
99
- import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
100
99
  import type { DbTx } from "@cosmicdrift/kumiko-framework/db";
100
+ import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
101
101
  import {
102
102
  append,
103
103
  loadAggregate as loadAggregateRaw,
@@ -106,14 +106,14 @@ import {
106
106
  upcastStoredEvent,
107
107
  } from "@cosmicdrift/kumiko-framework/event-store";
108
108
  import { getAllProjectionProgress } from "@cosmicdrift/kumiko-framework/pipeline";
109
+ import { createTestUser } from "@cosmicdrift/kumiko-framework/seeding";
109
110
  import {
110
- createTestUser,
111
111
  resetEventStore,
112
112
  setupTestStack,
113
113
  type TestStack,
114
114
  unsafeCreateEntityTable,
115
115
  unsafePushTables,
116
- } from "@cosmicdrift/kumiko-framework/stack";
116
+ } from "@cosmicdrift/kumiko-testing/stack";
117
117
  import {
118
118
  approverDirectoryTable,
119
119
  invoiceDetailTable,
@@ -831,8 +831,8 @@ describe("role-restricted write handlers", () => {
831
831
  //
832
832
  // - r.defineEvent with { version, migrations } — evolving event shapes +
833
833
  // upcast older payloads on read (declarative + sync/async transforms)
834
- // - ctx.appendEvent — write domain events onto the aggregate stream
835
- // - ctx.appendEvent with headers — Marten free key/value metadata
834
+ // - emitEvent — write domain events onto the aggregate stream, payload typed by the EventDef
835
+ // - emitEvent with headers — Marten free key/value metadata
836
836
  // - r.projection — single-stream read model (inline in the write TX)
837
837
  // - r.multiStreamProjection — cross-aggregate async read model
838
838
  // - ctx.loadAggregate with { asOf } — point-in-time aggregate state
@@ -845,26 +845,25 @@ describe("role-restricted write handlers", () => {
845
845
  // - getAllProjectionProgress — projection lag for ops dashboards
846
846
 
847
847
  import {
848
- createEntity,
849
- createTextField,
850
- defineFeature,
851
- defineProjectionQueryHandler,
852
- typedPayload,
853
- } from "@app/define";
854
- import {
848
+ buildEntityTable,
849
+ createEventStoreExecutor,
855
850
  fetchOne,
856
851
  incrementCounter,
857
852
  insertOne,
858
- updateMany,
859
- } from "@cosmicdrift/kumiko-framework/bun-db";
860
- import {
861
- buildEntityTable,
862
- createEventStoreExecutor,
863
853
  integer,
864
854
  table,
865
855
  text,
856
+ updateMany,
866
857
  uuid,
867
858
  } from "@cosmicdrift/kumiko-framework/db";
859
+ import {
860
+ createEntity,
861
+ createTextField,
862
+ defineFeature,
863
+ defineProjectionQueryHandler,
864
+ emitEvent,
865
+ typedPayload,
866
+ } from "@cosmicdrift/kumiko-framework/engine";
868
867
  import { z } from "zod";
869
868
 
870
869
  // --- Reducer: shared by live + snapshot-aware query handlers ---
@@ -1112,10 +1111,9 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
1112
1111
  const headers: Record<string, string> = {};
1113
1112
  if (event.payload.geoRegion) headers["geoRegion"] = event.payload.geoRegion;
1114
1113
  if (event.payload.abTestBucket) headers["abTestBucket"] = event.payload.abTestBucket;
1115
- await ctx.appendEvent({
1114
+ await emitEvent(ctx, approved, {
1116
1115
  aggregateId: event.payload.id,
1117
1116
  aggregateType: "showcase-invoice",
1118
- type: approved.name,
1119
1117
  payload: { amountCents: event.payload.amountCents, approvedBy: event.payload.approvedBy },
1120
1118
  ...(Object.keys(headers).length > 0 ? { headers } : {}),
1121
1119
  });
@@ -1132,10 +1130,9 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
1132
1130
  approverId: event.payload.approverId,
1133
1131
  });
1134
1132
  const approverDisplayName: string = row?.displayName ?? `unknown:${event.payload.approverId}`;
1135
- await ctx.appendEvent({
1133
+ await emitEvent(ctx, acknowledged, {
1136
1134
  aggregateId: event.payload.id,
1137
1135
  aggregateType: "showcase-invoice",
1138
- type: acknowledged.name,
1139
1136
  payload: { approverId: event.payload.approverId, approverDisplayName },
1140
1137
  });
1141
1138
  return { isSuccess: true as const, data: { id: event.payload.id } };
@@ -1147,10 +1144,9 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
1147
1144
  name: "invoice:pay",
1148
1145
  schema: z.object({ id: z.uuid(), amountCents: z.number().int() }),
1149
1146
  handler: async (event, ctx) => {
1150
- await ctx.appendEvent({
1147
+ await emitEvent(ctx, paid, {
1151
1148
  aggregateId: event.payload.id,
1152
1149
  aggregateType: "showcase-invoice",
1153
- type: paid.name,
1154
1150
  payload: { amountCents: event.payload.amountCents },
1155
1151
  });
1156
1152
  return { isSuccess: true as const, data: { id: event.payload.id } };
@@ -39,16 +39,16 @@ import {
39
39
  GlobalFeatureToggleRuntime,
40
40
  globalFeatureStateTable,
41
41
  } from "@cosmicdrift/kumiko-bundled-features/feature-toggles";
42
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
42
+ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
43
43
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
44
+ import { createTestUser } from "@cosmicdrift/kumiko-framework/seeding";
45
+ import { createLateBoundHolder } from "@cosmicdrift/kumiko-testing/helpers";
44
46
  import {
45
- createTestUser,
46
47
  setupTestStack,
47
48
  type TestStack,
48
49
  unsafeCreateEntityTable,
49
50
  unsafePushTables,
50
- } from "@cosmicdrift/kumiko-framework/stack";
51
- import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
51
+ } from "@cosmicdrift/kumiko-testing/stack";
52
52
  import {
53
53
  createProductAuditFeature,
54
54
  createProductFeature,
@@ -89,14 +89,13 @@ Covers all three roles reading and writing, including the expected
89
89
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
90
90
  import type { EntityId, SaveContext } from "@cosmicdrift/kumiko-framework/engine";
91
91
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
92
+ import { createTestUser, TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
93
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-testing/helpers";
92
94
  import {
93
- createTestUser,
94
95
  setupTestStack,
95
96
  type TestStack,
96
- TestUsers,
97
97
  unsafeCreateEntityTable,
98
- } from "@cosmicdrift/kumiko-framework/stack";
99
- import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
98
+ } from "@cosmicdrift/kumiko-testing/stack";
100
99
  import { employeeEntity, employeeFeature } from "../feature";
101
100
 
102
101
  let stack: TestStack;
@@ -47,27 +47,26 @@ bun test
47
47
 
48
48
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
49
49
  import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
50
- import { derivativesSharpFeature } from "@cosmicdrift/kumiko-bundled-features/derivatives-sharp";
50
+ import { createDerivativesSharpFeature } from "@cosmicdrift/kumiko-bundled-features/derivatives-sharp";
51
51
  import { createFileDerivativesFeature } from "@cosmicdrift/kumiko-bundled-features/file-derivatives";
52
- import { fileFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
53
- import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
54
- import { entityEventName } from "@cosmicdrift/kumiko-framework/db";
52
+ import { createFileFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
53
+
54
+ import { asRawClient, entityEventName } from "@cosmicdrift/kumiko-framework/db";
55
55
  import {
56
56
  createFilesFeature,
57
57
  createInMemoryFileProvider,
58
58
  type InMemoryFileProvider,
59
59
  } from "@cosmicdrift/kumiko-framework/files";
60
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
61
+ import {
62
+ buildMultipartBody,
63
+ patchFileInstanceofForBunTest,
64
+ } from "@cosmicdrift/kumiko-testing/helpers";
60
65
  import {
61
- createTestUser,
62
66
  setupTestStack,
63
67
  type TestStack,
64
- testTenantId,
65
68
  unsafeCreateEntityTable,
66
- } from "@cosmicdrift/kumiko-framework/stack";
67
- import {
68
- buildMultipartBody,
69
- patchFileInstanceofForBunTest,
70
- } from "@cosmicdrift/kumiko-framework/testing";
69
+ } from "@cosmicdrift/kumiko-testing/stack";
71
70
  import sharp from "sharp";
72
71
  import { coverOrder, filesPostProcessingFeature, photoEntity, publishedPhotoIds } from "../feature";
73
72
 
@@ -97,7 +96,7 @@ beforeAll(async () => {
97
96
  stack = await setupTestStack({
98
97
  features: [
99
98
  createConfigFeature(),
100
- fileFoundationFeature,
99
+ createFileFoundationFeature(),
101
100
  createFilesFeature(),
102
101
  createFileDerivativesFeature({
103
102
  resolveApexTenant: (host) => {
@@ -106,7 +105,7 @@ beforeAll(async () => {
106
105
  return null;
107
106
  },
108
107
  }),
109
- derivativesSharpFeature,
108
+ createDerivativesSharpFeature(),
110
109
  filesPostProcessingFeature,
111
110
  ],
112
111
  files: { storageProvider: provider },
@@ -0,0 +1,208 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/golden-path. Do not edit. -->
2
+
3
+ # golden-path
4
+
5
+ The smallest complete feature: entity, write handler, event, projection, screen and an integration test.
6
+
7
+ ## README
8
+
9
+ # Golden path
10
+
11
+ The smallest complete Kumiko feature: one entity, one write handler that emits
12
+ an event, one projection, one screen, and an integration test that writes
13
+ through HTTP and reads the result back. Every other recipe is a variation on
14
+ this shape. The walkthrough is in [`docs/golden-path.md`](../../../docs/golden-path.md).
15
+
16
+ ## What it shows
17
+
18
+ - **Entity** — `ticketEntity` declares `title` and `isClosed`. `r.crud` registers
19
+ `ticket:create`, `ticket:list` and `ticket:detail` for it; update and delete
20
+ are switched off with `verbs`.
21
+ - **Write handler** — `ticket:close` checks that the ticket exists and emits the
22
+ `ticket-closed` event. It never touches the table.
23
+ - **Event** — `r.defineEvent` declares `ticket-closed` with a Zod payload;
24
+ `emitEvent` appends it to the ticket's stream in the handler's transaction.
25
+ - **Projection** — `ticket-status` folds `ticket-closed` into the ticket row
26
+ (`isClosed: true`), in the same transaction as the write.
27
+ - **Screen** — `ticket-list` is a declarative `entityList`; the renderer builds
28
+ the UI from it.
29
+ - **Test** — `setupTestStack` boots the real server and database;
30
+ `stack.http.writeOk` and `queryOk` go over HTTP. Each test runs in its own
31
+ tenant.
32
+
33
+ ## Files
34
+
35
+ ```
36
+ src/feature.ts entity, event, handler, projection, screen
37
+ src/__tests__/feature.integration.test.ts write → read back, access, not found
38
+ ```
39
+
40
+ ## Source files
41
+
42
+ ### src/__tests__/feature.integration.test.ts
43
+
44
+ ```ts
45
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
46
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
47
+ import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-testing/stack";
48
+ import { helpdeskFeature } from "../feature";
49
+
50
+ type TicketRow = { id: string; title: string; isClosed: boolean };
51
+
52
+ let stack: TestStack;
53
+ let nextTenant = 1;
54
+ let admin = createTestUser({ roles: ["Admin"], tenantId: testTenantId(nextTenant) });
55
+ let agent = createTestUser({ id: 2, roles: ["User"], tenantId: testTenantId(nextTenant) });
56
+
57
+ beforeAll(async () => {
58
+ stack = await setupTestStack({ features: [helpdeskFeature], systemHooks: [] });
59
+ });
60
+
61
+ afterAll(async () => {
62
+ await stack.cleanup();
63
+ });
64
+
65
+ // A fresh tenant per test keeps rows from other tests out of every list.
66
+ beforeEach(() => {
67
+ nextTenant += 1;
68
+ admin = createTestUser({ roles: ["Admin"], tenantId: testTenantId(nextTenant) });
69
+ agent = createTestUser({ id: 2, roles: ["User"], tenantId: testTenantId(nextTenant) });
70
+ });
71
+
72
+ async function openTicket(title: string): Promise<string> {
73
+ const created = await stack.http.writeOk<{ id: string }>(
74
+ "helpdesk:write:ticket:create",
75
+ { title },
76
+ agent,
77
+ );
78
+ return created.id;
79
+ }
80
+
81
+ async function readTicket(id: string): Promise<TicketRow> {
82
+ return stack.http.queryOk<TicketRow>("helpdesk:query:ticket:detail", { id }, agent);
83
+ }
84
+
85
+ describe("golden path: ticket", () => {
86
+ test("a new ticket starts open", async () => {
87
+ const id = await openTicket("Printer is on fire");
88
+
89
+ expect(await readTicket(id)).toMatchObject({ title: "Printer is on fire", isClosed: false });
90
+ });
91
+
92
+ test("closing a ticket emits an event that the projection folds into the read model", async () => {
93
+ const id = await openTicket("Login fails");
94
+
95
+ await stack.http.writeOk(
96
+ "helpdesk:write:ticket:close",
97
+ { id, resolution: "Password reset" },
98
+ admin,
99
+ );
100
+
101
+ expect(await readTicket(id)).toMatchObject({ title: "Login fails", isClosed: true });
102
+ const list = await stack.http.queryOk<{ rows: TicketRow[] }>(
103
+ "helpdesk:query:ticket:list",
104
+ { filter: { field: "isClosed", op: "eq", value: true } },
105
+ agent,
106
+ );
107
+ expect(list.rows.map((row) => row.id)).toEqual([id]);
108
+ });
109
+
110
+ test("only an Admin may close a ticket", async () => {
111
+ const id = await openTicket("Slow wifi");
112
+
113
+ const denied = await stack.http.writeErr(
114
+ "helpdesk:write:ticket:close",
115
+ { id, resolution: "Moved the router" },
116
+ agent,
117
+ );
118
+
119
+ expect(denied.code).toBe("access_denied");
120
+ expect((await readTicket(id)).isClosed).toBe(false);
121
+ });
122
+
123
+ test("closing an unknown ticket fails with not_found", async () => {
124
+ const missing = await stack.http.writeErr(
125
+ "helpdesk:write:ticket:close",
126
+ { id: "99999999-9999-4999-8999-999999999999", resolution: "n/a" },
127
+ admin,
128
+ );
129
+
130
+ expect(missing.code).toBe("not_found");
131
+ });
132
+ });
133
+ ```
134
+
135
+ ### src/feature.ts
136
+
137
+ ```ts
138
+ import {
139
+ createBooleanField,
140
+ createEntity,
141
+ createEntityExecutor,
142
+ createTextField,
143
+ defineFeature,
144
+ emitEvent,
145
+ setFields,
146
+ } from "@cosmicdrift/kumiko-framework/engine";
147
+ import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
148
+ import { z } from "zod";
149
+
150
+ export const ticketEntity = createEntity({
151
+ table: "read_golden_path_tickets",
152
+ fields: {
153
+ title: createTextField({ personal: false, reason: "is_business_data", required: true }),
154
+ isClosed: createBooleanField({ default: false }),
155
+ },
156
+ });
157
+
158
+ export const helpdeskFeature = defineFeature("helpdesk", (r) => {
159
+ const { table: ticketTable, executor: ticketExecutor } = createEntityExecutor(
160
+ "ticket",
161
+ ticketEntity,
162
+ );
163
+
164
+ r.crud("ticket", ticketEntity, {
165
+ write: { access: { roles: ["Admin", "User"] } },
166
+ read: { access: { roles: ["Admin", "User"] } },
167
+ verbs: { update: false, delete: false },
168
+ });
169
+
170
+ const ticketClosed = r.defineEvent("ticket-closed", z.object({ resolution: z.string().min(1) }), {
171
+ piiFields: "none",
172
+ });
173
+
174
+ r.writeHandler({
175
+ name: "ticket:close",
176
+ schema: z.object({ id: z.uuid(), resolution: z.string().min(1) }),
177
+ access: { roles: ["Admin"] },
178
+ handler: async (event, ctx) => {
179
+ const ticket = await ticketExecutor.detail({ id: event.payload.id }, event.user, ctx.db);
180
+ if (!ticket) {
181
+ return failNotFound("ticket", event.payload.id);
182
+ }
183
+ await emitEvent(ctx, ticketClosed, {
184
+ aggregateId: event.payload.id,
185
+ aggregateType: "ticket",
186
+ payload: { resolution: event.payload.resolution },
187
+ });
188
+ return { isSuccess: true as const, data: { id: event.payload.id } };
189
+ },
190
+ });
191
+
192
+ r.projection({
193
+ name: "ticket-status",
194
+ source: "ticket",
195
+ table: ticketTable,
196
+ apply: {
197
+ [ticketClosed.name]: setFields(ticketTable, { isClosed: true }),
198
+ },
199
+ });
200
+
201
+ r.screen({
202
+ id: "ticket-list",
203
+ type: "entityList",
204
+ entity: "ticket",
205
+ columns: ["title", "isClosed"],
206
+ });
207
+ });
208
+ ```
@@ -39,12 +39,12 @@ bun test
39
39
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
40
40
  import type { SaveContext } from "@cosmicdrift/kumiko-framework/engine";
41
41
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
42
+ import { createTestUser } from "@cosmicdrift/kumiko-framework/seeding";
42
43
  import {
43
- createTestUser,
44
44
  setupTestStack,
45
45
  type TestStack,
46
46
  unsafeCreateEntityTable,
47
- } from "@cosmicdrift/kumiko-framework/stack";
47
+ } from "@cosmicdrift/kumiko-testing/stack";
48
48
  import { orderEntity, orderFeature } from "../feature";
49
49
 
50
50
  let stack: TestStack;
@@ -63,15 +63,15 @@ import {
63
63
  } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
64
64
  import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
65
65
  import {
66
+ createInboundMailFoundationFeature,
66
67
  InboundMailFoundationHandlers,
67
- inboundMailFoundationFeature,
68
68
  inboundMessageEntity,
69
69
  mailAccountEntity,
70
70
  mailThreadEntity,
71
71
  seenMessageEntity,
72
72
  syncCursorEntity,
73
73
  } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
74
- import { inboundProviderInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/inbound-provider-inmemory";
74
+ import { createInboundProviderInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/inbound-provider-inmemory";
75
75
  import { createTenantFeature, tenantEntity } from "@cosmicdrift/kumiko-bundled-features/tenant";
76
76
  import { createTenantLifecycleFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
77
77
  import { createRegistry } from "@cosmicdrift/kumiko-framework/engine";
@@ -81,15 +81,15 @@ import {
81
81
  createEventsTable,
82
82
  } from "@cosmicdrift/kumiko-framework/event-store";
83
83
  import { createEventConsumerStateTable } from "@cosmicdrift/kumiko-framework/pipeline";
84
+ import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
85
+ import { waitFor } from "@cosmicdrift/kumiko-testing/helpers";
84
86
  import {
85
87
  createTestDb,
86
88
  createTestRedis,
87
89
  type TestDb,
88
90
  type TestRedis,
89
- TestUsers,
90
91
  unsafeCreateEntityTable,
91
- } from "@cosmicdrift/kumiko-framework/stack";
92
- import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
92
+ } from "@cosmicdrift/kumiko-testing/stack";
93
93
  import { createMailTriageFeature, triageInbox } from "../feature";
94
94
 
95
95
  const JWT = "inbound-mail-triage-secret-minimum-32-chars!";
@@ -147,8 +147,8 @@ describe("inbound-mail triage sample", () => {
147
147
  createTenantFeature(),
148
148
  createComplianceProfilesFeature(),
149
149
  createTenantLifecycleFeature(),
150
- inboundMailFoundationFeature,
151
- inboundProviderInMemoryFeature,
150
+ createInboundMailFoundationFeature(),
151
+ createInboundProviderInMemoryFeature(),
152
152
  createMailTriageFeature(),
153
153
  ]);
154
154
  const redisUrl = `redis://${testRedis.redis.options.host}:${testRedis.redis.options.port}/${testRedis.redis.options.db}`;
@@ -57,14 +57,14 @@ import {
57
57
  createEventsTable,
58
58
  } from "@cosmicdrift/kumiko-framework/event-store";
59
59
  import { createEventConsumerStateTable } from "@cosmicdrift/kumiko-framework/pipeline";
60
+ import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
61
+ import { waitFor } from "@cosmicdrift/kumiko-testing/helpers";
60
62
  import {
61
63
  createTestDb,
62
64
  createTestRedis,
63
65
  type TestDb,
64
66
  type TestRedis,
65
- TestUsers,
66
- } from "@cosmicdrift/kumiko-framework/stack";
67
- import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
67
+ } from "@cosmicdrift/kumiko-testing/stack";
68
68
  import { createLaneRoutingFeature, renderedReceipts, sentConfirmations } from "../feature";
69
69
 
70
70
  const JWT = "lane-routing-sample-secret-minimum-32-chars!";
@@ -73,13 +73,12 @@ import {
73
73
  transactionEntity,
74
74
  } from "@cosmicdrift/kumiko-bundled-features/ledger";
75
75
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
76
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
76
77
  import {
77
- createTestUser,
78
78
  setupTestStack,
79
79
  type TestStack,
80
- testTenantId,
81
80
  unsafeCreateEntityTable,
82
- } from "@cosmicdrift/kumiko-framework/stack";
81
+ } from "@cosmicdrift/kumiko-testing/stack";
83
82
  import { bankingFlow, type LedgerClient } from "../usage";
84
83
 
85
84
  // Each test runs in its own tenant, so rows from other tests never show up.
@@ -82,13 +82,12 @@ import {
82
82
  transactionEntity,
83
83
  } from "@cosmicdrift/kumiko-bundled-features/ledger";
84
84
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
85
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
85
86
  import {
86
- createTestUser,
87
87
  setupTestStack,
88
88
  type TestStack,
89
- testTenantId,
90
89
  unsafeCreateEntityTable,
91
- } from "@cosmicdrift/kumiko-framework/stack";
90
+ } from "@cosmicdrift/kumiko-testing/stack";
92
91
  import { invoicingFlow, type LedgerClient } from "../usage";
93
92
 
94
93
  // Each test runs in its own tenant, so rows from other tests never show up.
@@ -228,7 +228,7 @@ import {
228
228
  setupTestStack,
229
229
  type TestStack,
230
230
  unsafeCreateEntityTable,
231
- } from "@cosmicdrift/kumiko-framework/stack";
231
+ } from "@cosmicdrift/kumiko-testing/stack";
232
232
  import { LEGAL_ROUTES, legalPagesFeature, templateResolverFeature } from "../feature";
233
233
 
234
234
  let stack: TestStack;
@@ -64,13 +64,13 @@ bun kumiko test integration samples/lifecycle-hooks
64
64
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
65
65
  import type { SaveContext } from "@cosmicdrift/kumiko-framework/engine";
66
66
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
67
+ import { createTestUser } from "@cosmicdrift/kumiko-framework/seeding";
68
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-testing/helpers";
67
69
  import {
68
- createTestUser,
69
70
  setupTestStack,
70
71
  type TestStack,
71
72
  unsafeCreateEntityTable,
72
- } from "@cosmicdrift/kumiko-framework/stack";
73
- import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
73
+ } from "@cosmicdrift/kumiko-testing/stack";
74
74
  import { articleEntity, articlesFeature, hookLog } from "../feature";
75
75
 
76
76
  let stack: TestStack;
@@ -169,7 +169,7 @@ import {
169
169
  createSecretsFeature,
170
170
  tenantSecretsTable,
171
171
  } from "@cosmicdrift/kumiko-bundled-features/secrets";
172
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
172
+ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
173
173
  import {
174
174
  type ConfigCascade,
175
175
  createRegistry,
@@ -177,13 +177,12 @@ import {
177
177
  } from "@cosmicdrift/kumiko-framework/engine";
178
178
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
179
179
  import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
180
+ import { TestUsers, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
180
181
  import {
181
182
  setupTestStack,
182
183
  type TestStack,
183
- TestUsers,
184
- testTenantId,
185
184
  unsafePushTables,
186
- } from "@cosmicdrift/kumiko-framework/stack";
185
+ } from "@cosmicdrift/kumiko-testing/stack";
187
186
  import { integrationsFeature, paymentApiKeyHandle, smtpHostHandle } from "../feature";
188
187
 
189
188
  const TENANT_A = testTenantId(1);
@@ -180,13 +180,13 @@ import { BRANDING_QN, pageEntity } from "@cosmicdrift/kumiko-bundled-features/ma
180
180
  import { seedPage } from "@cosmicdrift/kumiko-bundled-features/managed-pages/seeding";
181
181
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
182
182
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
183
+ import { createTestUser } from "@cosmicdrift/kumiko-framework/seeding";
183
184
  import {
184
- createTestUser,
185
185
  setupTestStack,
186
186
  type TestStack,
187
187
  unsafeCreateEntityTable,
188
188
  unsafePushTables,
189
- } from "@cosmicdrift/kumiko-framework/stack";
189
+ } from "@cosmicdrift/kumiko-testing/stack";
190
190
  import { configFeature, managedPagesCssFeature, managedPagesFeature } from "../feature";
191
191
 
192
192
  // Branding writes are tenant-scoped — the admin's tenantId must equal the
@@ -117,18 +117,17 @@ bun test src/__tests__/feature.integration.test.ts
117
117
 
118
118
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
119
119
  import {
120
+ createNotesHistoryFeature,
120
121
  noteEntryEntity,
121
122
  noteMentionEntity,
122
- notesHistoryFeature,
123
123
  } from "@cosmicdrift/kumiko-bundled-features/notes-history";
124
124
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
125
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
125
126
  import {
126
- createTestUser,
127
127
  setupTestStack,
128
128
  type TestStack,
129
- testTenantId,
130
129
  unsafeCreateEntityTable,
131
- } from "@cosmicdrift/kumiko-framework/stack";
130
+ } from "@cosmicdrift/kumiko-testing/stack";
132
131
  import { taskEntity, taskFeature } from "../feature";
133
132
  import { type NotesClient, notesFlow } from "../usage";
134
133
 
@@ -145,7 +144,7 @@ const notesClient: NotesClient = {
145
144
  let stack: TestStack;
146
145
 
147
146
  beforeAll(async () => {
148
- stack = await setupTestStack({ features: [notesHistoryFeature, taskFeature] });
147
+ stack = await setupTestStack({ features: [createNotesHistoryFeature(), taskFeature] });
149
148
  await unsafeCreateEntityTable(stack.db, noteEntryEntity);
150
149
  await unsafeCreateEntityTable(stack.db, noteMentionEntity);
151
150
  await unsafeCreateEntityTable(stack.db, taskEntity);
@@ -40,15 +40,13 @@ bun test
40
40
 
41
41
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
42
42
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
43
+ import { createTestUser, TestUsers, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
44
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-testing/helpers";
43
45
  import {
44
- createTestUser,
45
46
  setupTestStack,
46
47
  type TestStack,
47
- TestUsers,
48
- testTenantId,
49
48
  unsafeCreateEntityTable,
50
- } from "@cosmicdrift/kumiko-framework/stack";
51
- import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
49
+ } from "@cosmicdrift/kumiko-testing/stack";
52
50
  import { contractEntity, contractsFeature, teamsFeature } from "../feature";
53
51
 
54
52
  let stack: TestStack;
@@ -46,13 +46,12 @@ import {
46
46
  createFilesFeature,
47
47
  createInMemoryFileProvider,
48
48
  } from "@cosmicdrift/kumiko-framework/files";
49
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
49
50
  import {
50
- createTestUser,
51
51
  setupTestStack,
52
52
  type TestStack,
53
- testTenantId,
54
53
  unsafeCreateEntityTable,
55
- } from "@cosmicdrift/kumiko-framework/stack";
54
+ } from "@cosmicdrift/kumiko-testing/stack";
56
55
  import { extractText, getDocumentProxy } from "unpdf";
57
56
  import { inspectionEntity, pdfAuditExportFeature } from "../feature";
58
57