@cosmicdrift/kumiko-framework 0.367.0 → 0.368.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 (233) 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/dist/consumer-cli.js +2 -3
  71. package/dist/crypto/index.d.ts +1 -1
  72. package/dist/crypto/index.js +1 -1
  73. package/dist/crypto/owner-transition-rekey.js +2 -0
  74. package/dist/crypto/pg-kms-adapter.js +1 -1
  75. package/dist/crypto/subject-resolver.d.ts +2 -0
  76. package/dist/crypto/subject-resolver.js +30 -15
  77. package/dist/db/bun-db-deprecated.d.ts +10 -0
  78. package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
  79. package/dist/db/config-seed.js +1 -1
  80. package/dist/db/connection.d.ts +23 -5
  81. package/dist/db/connection.js +10 -5
  82. package/dist/db/dialect.js +1 -1
  83. package/dist/db/entity-field-encryption.d.ts +5 -0
  84. package/dist/db/entity-field-encryption.js +29 -0
  85. package/dist/db/entity-table-meta.js +6 -5
  86. package/dist/db/event-store-executor-read.js +26 -5
  87. package/dist/db/index.d.ts +4 -4
  88. package/dist/db/index.js +1 -1
  89. package/dist/db/migrate-runner.js +1 -1
  90. package/dist/db/queries/backfill-pii.js +16 -9
  91. package/dist/db/queries/ddl.js +1 -1
  92. package/dist/db/queries/event-store.d.ts +2 -0
  93. package/dist/db/queries/event-store.js +26 -0
  94. package/dist/db/queries/test-stack.js +1 -1
  95. package/dist/db/query.d.ts +105 -1
  96. package/dist/db/query.js +1450 -3
  97. package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
  98. package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
  99. package/dist/db/tenant-db.d.ts +11 -0
  100. package/dist/db/tenant-db.js +51 -12
  101. package/dist/derivatives/derivatives-context.d.ts +1 -1
  102. package/dist/derivatives/derivatives-context.js +1 -1
  103. package/dist/engine/boot-validator/gdpr-storage.js +3 -3
  104. package/dist/engine/boot-validator/pii-retention.js +3 -3
  105. package/dist/engine/boot-validator/record-owned.js +3 -3
  106. package/dist/engine/boot-validator/screens.js +8 -1
  107. package/dist/engine/bridge-stub.d.ts +4 -0
  108. package/dist/engine/bridge-stub.js +62 -0
  109. package/dist/engine/event-helpers.d.ts +2 -1
  110. package/dist/engine/event-helpers.js +1 -0
  111. package/dist/engine/feature-config-events-jobs.js +2 -2
  112. package/dist/engine/index.d.ts +1 -0
  113. package/dist/engine/index.js +1 -0
  114. package/dist/es-ops/operations-schema.js +1 -1
  115. package/dist/event-store/admin-api.js +1 -1
  116. package/dist/event-store/archive.js +1 -1
  117. package/dist/event-store/errors.d.ts +2 -1
  118. package/dist/event-store/errors.js +6 -2
  119. package/dist/event-store/event-store.js +17 -9
  120. package/dist/event-store/events-schema.js +1 -1
  121. package/dist/event-store/rebuild-dead-letter.js +3 -3
  122. package/dist/event-store/snapshot.js +1 -1
  123. package/dist/event-store/upcaster-dead-letter.js +3 -3
  124. package/dist/files/create-file-ref-from-bytes.js +1 -1
  125. package/dist/files/file-routes.js +1 -1
  126. package/dist/internal/stack-support.d.ts +12 -0
  127. package/dist/internal/stack-support.js +12 -0
  128. package/dist/internal/testing.d.ts +4 -0
  129. package/dist/internal/testing.js +5 -0
  130. package/dist/jobs/job-runner.js +23 -11
  131. package/dist/migrations/pending-rebuilds.js +1 -1
  132. package/dist/migrations/pii-event-backfill.js +2 -2
  133. package/dist/pipeline/dispatch-shared.js +43 -23
  134. package/dist/pipeline/dispatch-write.js +6 -2
  135. package/dist/pipeline/dispatcher-utils.js +6 -2
  136. package/dist/pipeline/event-consumer-state.js +1 -1
  137. package/dist/pipeline/projection-rebuild.d.ts +1 -0
  138. package/dist/pipeline/projection-rebuild.js +1 -1
  139. package/dist/pipeline/projection-state.js +1 -1
  140. package/dist/pipeline/system-hooks.js +1 -1
  141. package/dist/schema-cli.js +3 -3
  142. package/dist/search/purge-subject.js +20 -10
  143. package/dist/seeding/index.d.ts +1 -0
  144. package/dist/seeding/index.js +1 -0
  145. package/dist/stack/index.d.ts +10 -9
  146. package/dist/stack/index.js +11 -19
  147. package/dist/testing/e2e-generator.d.ts +5 -49
  148. package/dist/testing/e2e-generator.js +5 -382
  149. package/dist/testing/handler-context.d.ts +5 -4
  150. package/dist/testing/handler-context.js +5 -69
  151. package/dist/testing/index.d.ts +10 -26
  152. package/dist/testing/index.js +10 -34
  153. package/dist/testing/real-providers.d.ts +5 -3
  154. package/dist/testing/real-providers.js +5 -21
  155. package/package.json +37 -9
  156. package/src/changes.json +58 -0
  157. package/src/scripts/codemod/README.md +43 -0
  158. package/src/scripts/codemod/codemod-support.ts +276 -0
  159. package/src/scripts/codemod/db-factory.ts +810 -0
  160. package/src/scripts/codemod/feature-factory.ts +429 -0
  161. package/src/scripts/codemod/s3-imports.ts +274 -0
  162. package/src/scripts/codemod/tenant-db-options.ts +454 -0
  163. package/src/scripts/codemod/testing-move-targets.ts +161 -0
  164. package/src/scripts/codemod/testing-move.ts +520 -0
  165. package/dist/bun-db/connection.d.ts +0 -17
  166. package/dist/bun-db/connection.js +0 -40
  167. package/dist/bun-db/index.d.ts +0 -4
  168. package/dist/bun-db/query.d.ts +0 -106
  169. package/dist/bun-db/query.js +0 -1457
  170. package/dist/db/api.d.ts +0 -19
  171. package/dist/db/api.js +0 -23
  172. package/dist/db/bun-provider.d.ts +0 -2
  173. package/dist/db/bun-provider.js +0 -28
  174. package/dist/db/postgres-provider.d.ts +0 -2
  175. package/dist/db/postgres-provider.js +0 -21
  176. package/dist/db/query-api.d.ts +0 -2
  177. package/dist/db/query-api.js +0 -6
  178. package/dist/stack/db.d.ts +0 -22
  179. package/dist/stack/db.js +0 -67
  180. package/dist/stack/drain-event-consumers.d.ts +0 -4
  181. package/dist/stack/drain-event-consumers.js +0 -51
  182. package/dist/stack/drain-jobs.d.ts +0 -22
  183. package/dist/stack/drain-jobs.js +0 -141
  184. package/dist/stack/event-collector.d.ts +0 -9
  185. package/dist/stack/event-collector.js +0 -12
  186. package/dist/stack/push-entity-projection-tables.d.ts +0 -24
  187. package/dist/stack/push-entity-projection-tables.js +0 -74
  188. package/dist/stack/redis.d.ts +0 -17
  189. package/dist/stack/redis.js +0 -52
  190. package/dist/stack/request-helper.d.ts +0 -35
  191. package/dist/stack/request-helper.js +0 -144
  192. package/dist/stack/test-stack.d.ts +0 -192
  193. package/dist/stack/test-stack.js +0 -429
  194. package/dist/testing/access-assertions.d.ts +0 -2
  195. package/dist/testing/access-assertions.js +0 -13
  196. package/dist/testing/assertions.d.ts +0 -10
  197. package/dist/testing/assertions.js +0 -22
  198. package/dist/testing/boot-validator-fixture.d.ts +0 -3
  199. package/dist/testing/boot-validator-fixture.js +0 -101
  200. package/dist/testing/closed-connection-error.d.ts +0 -7
  201. package/dist/testing/closed-connection-error.js +0 -46
  202. package/dist/testing/db-cleanup.d.ts +0 -9
  203. package/dist/testing/db-cleanup.js +0 -31
  204. package/dist/testing/expect-error.d.ts +0 -2
  205. package/dist/testing/expect-error.js +0 -22
  206. package/dist/testing/file-provider-contract.d.ts +0 -2
  207. package/dist/testing/file-provider-contract.js +0 -102
  208. package/dist/testing/http-cookies.d.ts +0 -7
  209. package/dist/testing/http-cookies.js +0 -47
  210. package/dist/testing/late-bound.d.ts +0 -9
  211. package/dist/testing/late-bound.js +0 -31
  212. package/dist/testing/manual-clock.d.ts +0 -14
  213. package/dist/testing/manual-clock.js +0 -12
  214. package/dist/testing/multipart-helper.d.ts +0 -22
  215. package/dist/testing/multipart-helper.js +0 -77
  216. package/dist/testing/mutable-master-key-provider.d.ts +0 -7
  217. package/dist/testing/mutable-master-key-provider.js +0 -35
  218. package/dist/testing/observability-recorder.d.ts +0 -9
  219. package/dist/testing/observability-recorder.js +0 -29
  220. package/dist/testing/seed.d.ts +0 -9
  221. package/dist/testing/seed.js +0 -21
  222. package/dist/testing/shared-entities.d.ts +0 -286
  223. package/dist/testing/shared-entities.js +0 -56
  224. package/dist/testing/utils.d.ts +0 -1
  225. package/dist/testing/utils.js +0 -1
  226. package/dist/testing/wait-for.d.ts +0 -20
  227. package/dist/testing/wait-for.js +0 -47
  228. package/dist/testing/without-ambient-temporal.d.ts +0 -5
  229. package/dist/testing/without-ambient-temporal.js +0 -17
  230. package/dist/testing/zip-fixture.d.ts +0 -1
  231. package/dist/testing/zip-fixture.js +0 -33
  232. /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
  233. /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
@@ -43,13 +43,9 @@ bun test
43
43
  // → cancelled → draft (reopen)
44
44
 
45
45
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
46
- import {
47
- createTestUser,
48
- setupTestStack,
49
- type TestStack,
50
- TestUsers,
51
- } from "@cosmicdrift/kumiko-framework/stack";
52
- import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
46
+ import { createTestUser, TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
47
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-testing/helpers";
48
+ import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-testing/stack";
53
49
  import { stateMachineFeature } from "../feature";
54
50
 
55
51
  let stack: TestStack;
@@ -449,14 +445,14 @@ describe("reduceInvoice", () => {
449
445
  ### src/entities/invoice.ts
450
446
 
451
447
  ```ts
448
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
452
449
  import {
453
450
  createEntity,
454
451
  createMoneyField,
455
452
  createSelectField,
456
453
  createTextField,
457
454
  defineTransitions,
458
- } from "@app/define";
459
- import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
455
+ } from "@cosmicdrift/kumiko-framework/engine";
460
456
 
461
457
  export const INVOICE_STATES = ["draft", "sent", "paid", "cancelled"] as const;
462
458
 
@@ -494,6 +490,9 @@ export const invoiceTable = buildEntityTable("invoice", invoiceEntity);
494
490
  // strings, so a rename here propagates everywhere instead of silently
495
491
  // breaking the reducer.
496
492
 
493
+ import type { EventDef } from "@cosmicdrift/kumiko-framework/engine";
494
+ import { z } from "zod";
495
+
497
496
  export const FEATURE_NAME = "billing";
498
497
  export const ENTITY_NAME = "invoice";
499
498
 
@@ -521,6 +520,22 @@ export function eventName<const TShort extends string>(
521
520
  // The auto-generated event the executor emits on create — entity name dot
522
521
  // "created", no feature prefix (executor convention).
523
522
  export const INVOICE_CREATED_EVENT = `${ENTITY_NAME}.created` as const;
523
+
524
+ export const emptyEventPayload = z.object({});
525
+ export const statusForcedPayload = z.object({
526
+ newStatus: z.enum(["draft", "sent", "paid", "cancelled"]),
527
+ fromStatus: z.enum(["draft", "sent", "paid", "cancelled"]),
528
+ });
529
+
530
+ // The EventDefs r.defineEvent returns inside the feature setup. Handlers receive them so that
531
+ // emitEvent checks each payload against the event's own schema.
532
+ export type InvoiceEvents = {
533
+ readonly sent: EventDef<z.infer<typeof emptyEventPayload>>;
534
+ readonly markedPaid: EventDef<z.infer<typeof emptyEventPayload>>;
535
+ readonly cancelled: EventDef<z.infer<typeof emptyEventPayload>>;
536
+ readonly reopened: EventDef<z.infer<typeof emptyEventPayload>>;
537
+ readonly statusForced: EventDef<z.infer<typeof statusForcedPayload>>;
538
+ };
524
539
  ```
525
540
 
526
541
  ### src/feature.ts
@@ -549,20 +564,17 @@ export const INVOICE_CREATED_EVENT = `${ENTITY_NAME}.created` as const;
549
564
  // handlers/create.ts — invoice:create (uses executor)
550
565
  // handlers/transitions.ts — the six state-change handlers
551
566
 
552
- import { defineFeature, setFields } from "@app/define";
553
- import { z } from "zod";
567
+ import { defineFeature, setFields, typedPayload } from "@cosmicdrift/kumiko-framework/engine";
554
568
  import { invoiceEntity, invoiceTable } from "./entities/invoice";
555
- import { eventName, INVOICE_EVENTS } from "./events";
556
- import { invoiceCreate } from "./handlers/create";
557
569
  import {
558
- invoiceCancel,
559
- invoiceForceStatus,
560
- invoiceMarkPaid,
561
- invoiceReopen,
562
- invoiceSend,
563
- invoiceUpdateStatus,
564
- } from "./handlers/transitions";
565
- import type { InvoiceStatus } from "./reducer";
570
+ emptyEventPayload,
571
+ eventName,
572
+ INVOICE_EVENTS,
573
+ type InvoiceEvents,
574
+ statusForcedPayload,
575
+ } from "./events";
576
+ import { invoiceCreate } from "./handlers/create";
577
+ import { createInvoiceTransitionHandlers } from "./handlers/transitions";
566
578
 
567
579
  export { invoiceEntity } from "./entities/invoice";
568
580
 
@@ -573,18 +585,15 @@ export const stateMachineFeature = defineFeature("billing", (r) => {
573
585
  // and the projection stay in sync. The auto "invoice.created" event from
574
586
  // the executor's create-path covers the initial insert, so we only declare
575
587
  // the state-change events here.
576
- r.defineEvent(INVOICE_EVENTS.sent, z.object({}), { piiFields: "none" });
577
- r.defineEvent(INVOICE_EVENTS.markedPaid, z.object({}), { piiFields: "none" });
578
- r.defineEvent(INVOICE_EVENTS.cancelled, z.object({}), { piiFields: "none" });
579
- r.defineEvent(INVOICE_EVENTS.reopened, z.object({}), { piiFields: "none" });
580
- r.defineEvent(
581
- INVOICE_EVENTS.statusForced,
582
- z.object({
583
- newStatus: z.enum(["draft", "sent", "paid", "cancelled"]),
584
- fromStatus: z.enum(["draft", "sent", "paid", "cancelled"]),
588
+ const events: InvoiceEvents = {
589
+ sent: r.defineEvent(INVOICE_EVENTS.sent, emptyEventPayload, { piiFields: "none" }),
590
+ markedPaid: r.defineEvent(INVOICE_EVENTS.markedPaid, emptyEventPayload, { piiFields: "none" }),
591
+ cancelled: r.defineEvent(INVOICE_EVENTS.cancelled, emptyEventPayload, { piiFields: "none" }),
592
+ reopened: r.defineEvent(INVOICE_EVENTS.reopened, emptyEventPayload, { piiFields: "none" }),
593
+ statusForced: r.defineEvent(INVOICE_EVENTS.statusForced, statusForcedPayload, {
594
+ piiFields: "none",
585
595
  }),
586
- { piiFields: "none" },
587
- );
596
+ };
588
597
 
589
598
  // The aggregate table (sample_sm_invoices) is INSERTed by the executor on
590
599
  // invoice:create. State-change events update the same row via this inline
@@ -602,18 +611,19 @@ export const stateMachineFeature = defineFeature("billing", (r) => {
602
611
  [eventName(INVOICE_EVENTS.cancelled)]: setFields(invoiceTable, { status: "cancelled" }),
603
612
  [eventName(INVOICE_EVENTS.reopened)]: setFields(invoiceTable, { status: "draft" }),
604
613
  [eventName(INVOICE_EVENTS.statusForced)]: setFields(invoiceTable, (e) => ({
605
- status: (e.payload as { newStatus: InvoiceStatus }).newStatus,
614
+ status: typedPayload(e, events.statusForced).newStatus,
606
615
  })),
607
616
  },
608
617
  });
609
618
 
619
+ const transitions = createInvoiceTransitionHandlers(events);
610
620
  r.writeHandler(invoiceCreate);
611
- r.writeHandler(invoiceSend);
612
- r.writeHandler(invoiceMarkPaid);
613
- r.writeHandler(invoiceCancel);
614
- r.writeHandler(invoiceReopen);
615
- r.writeHandler(invoiceUpdateStatus);
616
- r.writeHandler(invoiceForceStatus);
621
+ r.writeHandler(transitions.invoiceSend);
622
+ r.writeHandler(transitions.invoiceMarkPaid);
623
+ r.writeHandler(transitions.invoiceCancel);
624
+ r.writeHandler(transitions.invoiceReopen);
625
+ r.writeHandler(transitions.invoiceUpdateStatus);
626
+ r.writeHandler(transitions.invoiceForceStatus);
617
627
  });
618
628
  ```
619
629
 
@@ -624,25 +634,25 @@ export const stateMachineFeature = defineFeature("billing", (r) => {
624
634
  // optionally guardTransition, append the domain event, return a WriteResult
625
635
  // that reflects the post-write status.
626
636
 
627
- import type { AccessRule, KumikoEventTypeMap } from "@app/define";
628
- import { defineWriteHandler, guardTransition } from "@app/define";
637
+ import {
638
+ type AccessRule,
639
+ defineWriteHandler,
640
+ type EventDef,
641
+ emitEvent,
642
+ guardTransition,
643
+ } from "@cosmicdrift/kumiko-framework/engine";
629
644
  import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
630
645
  import { z } from "zod";
631
646
  import { INVOICE_TRANSITIONS } from "../entities/invoice";
632
- import { ENTITY_NAME } from "../events";
647
+ import { ENTITY_NAME, type emptyEventPayload } from "../events";
633
648
  import { type InvoiceStatus, loadInvoiceState } from "../reducer";
634
649
 
635
- // Narrow TEvent to event-types whose payload is `{}` — this helper
636
- // always emits an empty payload. Anyone trying to use it with a non-
637
- // empty-payload event-type gets caught at the call-site, not at runtime.
638
- type EmptyPayloadEvent = {
639
- [K in keyof KumikoEventTypeMap]: KumikoEventTypeMap[K] extends Record<string, never> ? K : never;
640
- }[keyof KumikoEventTypeMap];
641
-
642
- export function transitionHandler<const TEvent extends EmptyPayloadEvent>(opts: {
650
+ // The event carries an empty payload — this helper always emits `{}`, so an EventDef with a
651
+ // non-empty payload is a compile error at the call site.
652
+ export function transitionHandler(opts: {
643
653
  name: string;
644
654
  toStatus: InvoiceStatus;
645
- eventType: TEvent;
655
+ event: EventDef<z.infer<typeof emptyEventPayload>>;
646
656
  access: AccessRule;
647
657
  skipGuard?: boolean;
648
658
  }) {
@@ -663,10 +673,9 @@ export function transitionHandler<const TEvent extends EmptyPayloadEvent>(opts:
663
673
  guardTransition(INVOICE_TRANSITIONS, state.status as InvoiceStatus, opts.toStatus);
664
674
  }
665
675
 
666
- await ctx.appendEvent({
676
+ await emitEvent(ctx, opts.event, {
667
677
  aggregateId: event.payload.id,
668
678
  aggregateType: ENTITY_NAME,
669
- type: opts.eventType,
670
679
  payload: {},
671
680
  });
672
681
 
@@ -701,8 +710,8 @@ export function successResult(id: string, newStatus: InvoiceStatus, previousStat
701
710
  // keeps amount + amountCurrency flat, while the entity stores them as a
702
711
  // money pair.
703
712
 
704
- import { defineWriteHandler } from "@app/define";
705
713
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
714
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
706
715
  import { z } from "zod";
707
716
  import { invoiceEntity, invoiceTable } from "../entities/invoice";
708
717
 
@@ -730,138 +739,145 @@ export const invoiceCreate = defineWriteHandler({
730
739
  // little extra logic (amount-check, verb routing, dedicated forced-event)
731
740
  // and stay inline so the variation is visible at the call site.
732
741
 
733
- import { defineWriteHandler, guardTransition } from "@app/define";
742
+ import {
743
+ defineWriteHandler,
744
+ emitEvent,
745
+ guardTransition,
746
+ } from "@cosmicdrift/kumiko-framework/engine";
734
747
  import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
735
748
  import { z } from "zod";
736
749
  import { INVOICE_STATES, INVOICE_TRANSITIONS } from "../entities/invoice";
737
- import { ENTITY_NAME, eventName, INVOICE_EVENTS } from "../events";
750
+ import { ENTITY_NAME, type InvoiceEvents } from "../events";
738
751
  import { type InvoiceStatus, loadInvoiceState } from "../reducer";
739
752
  import { successResult, transitionHandler } from "./_helpers";
740
753
 
741
754
  const adminOnly = { roles: ["Admin"] } as const;
742
755
 
743
- // --- Standard transitions (same shape, different target status) ---
756
+ export function createInvoiceTransitionHandlers(events: InvoiceEvents) {
757
+ // --- Standard transitions (same shape, different target status) ---
744
758
 
745
- export const invoiceSend = transitionHandler({
746
- name: "invoice:send",
747
- toStatus: "sent",
748
- eventType: eventName(INVOICE_EVENTS.sent),
749
- access: adminOnly,
750
- });
759
+ const invoiceSend = transitionHandler({
760
+ name: "invoice:send",
761
+ toStatus: "sent",
762
+ event: events.sent,
763
+ access: adminOnly,
764
+ });
751
765
 
752
- export const invoiceCancel = transitionHandler({
753
- name: "invoice:cancel",
754
- toStatus: "cancelled",
755
- eventType: eventName(INVOICE_EVENTS.cancelled),
756
- access: adminOnly,
757
- });
766
+ const invoiceCancel = transitionHandler({
767
+ name: "invoice:cancel",
768
+ toStatus: "cancelled",
769
+ event: events.cancelled,
770
+ access: adminOnly,
771
+ });
758
772
 
759
- export const invoiceReopen = transitionHandler({
760
- name: "invoice:reopen",
761
- toStatus: "draft",
762
- eventType: eventName(INVOICE_EVENTS.reopened),
763
- access: adminOnly,
764
- });
773
+ const invoiceReopen = transitionHandler({
774
+ name: "invoice:reopen",
775
+ toStatus: "draft",
776
+ event: events.reopened,
777
+ access: adminOnly,
778
+ });
765
779
 
766
- // --- markPaid: same shape + business rule (amount must be > 0).
767
- // The amount is reduced from the event stream, so the check stays
768
- // consistent with everything else (no out-of-band DB query).
780
+ // --- markPaid: same shape + business rule (amount must be > 0).
781
+ // The amount is reduced from the event stream, so the check stays
782
+ // consistent with everything else (no out-of-band DB query).
769
783
 
770
- export const invoiceMarkPaid = defineWriteHandler({
771
- name: "invoice:mark-paid",
772
- schema: z.object({ id: z.uuid() }),
773
- access: { roles: ["Accounting"] },
774
- handler: async (event, ctx) => {
775
- const state = await loadInvoiceState(ctx, event.payload.id);
776
- if (!state) return failNotFound(ENTITY_NAME, event.payload.id);
784
+ const invoiceMarkPaid = defineWriteHandler({
785
+ name: "invoice:mark-paid",
786
+ schema: z.object({ id: z.uuid() }),
787
+ access: { roles: ["Accounting"] },
788
+ handler: async (event, ctx) => {
789
+ const state = await loadInvoiceState(ctx, event.payload.id);
790
+ if (!state) return failNotFound(ENTITY_NAME, event.payload.id);
777
791
 
778
- guardTransition(INVOICE_TRANSITIONS, state.status as InvoiceStatus, "paid");
792
+ guardTransition(INVOICE_TRANSITIONS, state.status as InvoiceStatus, "paid");
779
793
 
780
- if (state.amount === 0) {
781
- return failUnprocessable("cannot_pay_zero_amount");
782
- }
794
+ if (state.amount === 0) {
795
+ return failUnprocessable("cannot_pay_zero_amount");
796
+ }
783
797
 
784
- await ctx.appendEvent({
785
- aggregateId: event.payload.id,
786
- aggregateType: ENTITY_NAME,
787
- type: eventName(INVOICE_EVENTS.markedPaid),
788
- payload: {},
789
- });
798
+ await emitEvent(ctx, events.markedPaid, {
799
+ aggregateId: event.payload.id,
800
+ aggregateType: ENTITY_NAME,
801
+ payload: {},
802
+ });
790
803
 
791
- return successResult(event.payload.id, "paid", state.status);
792
- },
793
- });
804
+ return successResult(event.payload.id, "paid", state.status);
805
+ },
806
+ });
794
807
 
795
- // --- updateStatus: relies on the pipeline's auto transition guard.
796
- // Routes the requested status to the matching domain event so the audit
797
- // log still reads as a workflow step ("invoice-sent"), not a generic
798
- // "status set to ...".
799
-
800
- // `as const satisfies` keeps the literal-strings alive (so each value is
801
- // `"billing:event:invoice-..."` literally typed, not just `string`) AND
802
- // type-checks the shape against `Record<InvoiceStatus, string>` —
803
- // missing-status entries fail at compile-time. Strict-mode for
804
- // ctx.appendEvent kicks in because `VERB_TO_EVENT[target]` resolves to
805
- // a union of literal event-names, all of which are augmented keys.
806
- const VERB_TO_EVENT = {
807
- draft: eventName(INVOICE_EVENTS.reopened),
808
- sent: eventName(INVOICE_EVENTS.sent),
809
- paid: eventName(INVOICE_EVENTS.markedPaid),
810
- cancelled: eventName(INVOICE_EVENTS.cancelled),
811
- } as const satisfies Record<InvoiceStatus, string>;
812
-
813
- export const invoiceUpdateStatus = defineWriteHandler({
814
- name: "invoice:update-status",
815
- schema: z.object({
816
- id: z.uuid(),
817
- changes: z.object({
808
+ // --- updateStatus: relies on the pipeline's auto transition guard.
809
+ // Routes the requested status to the matching domain event so the audit
810
+ // log still reads as a workflow step ("invoice-sent"), not a generic
811
+ // "status set to ...".
812
+
813
+ // `satisfies Record<InvoiceStatus, ...>` makes a missing status a compile error.
814
+ const verbToEvent = {
815
+ draft: events.reopened,
816
+ sent: events.sent,
817
+ paid: events.markedPaid,
818
+ cancelled: events.cancelled,
819
+ } satisfies Record<InvoiceStatus, InvoiceEvents["sent"]>;
820
+
821
+ const invoiceUpdateStatus = defineWriteHandler({
822
+ name: "invoice:update-status",
823
+ schema: z.object({
824
+ id: z.uuid(),
825
+ changes: z.object({
826
+ status: z.enum(INVOICE_STATES),
827
+ }),
828
+ }),
829
+ access: adminOnly,
830
+ // No skipTransitionGuard — pipeline auto-guards.
831
+ handler: async (event, ctx) => {
832
+ const state = await loadInvoiceState(ctx, event.payload.id);
833
+ if (!state) return failNotFound(ENTITY_NAME, event.payload.id);
834
+
835
+ const target = event.payload.changes.status;
836
+ await emitEvent(ctx, verbToEvent[target], {
837
+ aggregateId: event.payload.id,
838
+ aggregateType: ENTITY_NAME,
839
+ payload: {},
840
+ });
841
+
842
+ return successResult(event.payload.id, target, state.status);
843
+ },
844
+ });
845
+
846
+ // --- forceStatus: skips the guard and emits a dedicated "forced" event so
847
+ // admin overrides stay visually distinct from normal workflow events in
848
+ // the audit log.
849
+
850
+ const invoiceForceStatus = defineWriteHandler({
851
+ name: "invoice:force-status",
852
+ schema: z.object({
853
+ id: z.uuid(),
818
854
  status: z.enum(INVOICE_STATES),
819
855
  }),
820
- }),
821
- access: adminOnly,
822
- // No skipTransitionGuard — pipeline auto-guards.
823
- handler: async (event, ctx) => {
824
- const state = await loadInvoiceState(ctx, event.payload.id);
825
- if (!state) return failNotFound(ENTITY_NAME, event.payload.id);
826
-
827
- const target = event.payload.changes.status;
828
- await ctx.appendEvent({
829
- aggregateId: event.payload.id,
830
- aggregateType: ENTITY_NAME,
831
- type: VERB_TO_EVENT[target],
832
- payload: {},
833
- });
834
-
835
- return successResult(event.payload.id, target, state.status);
836
- },
837
- });
856
+ access: adminOnly,
857
+ unsafeSkipTransitionGuard: true,
858
+ handler: async (event, ctx) => {
859
+ const state = await loadInvoiceState(ctx, event.payload.id);
860
+ if (!state) return failNotFound(ENTITY_NAME, event.payload.id);
838
861
 
839
- // --- forceStatus: skips the guard and emits a dedicated "forced" event so
840
- // admin overrides stay visually distinct from normal workflow events in
841
- // the audit log.
862
+ await emitEvent(ctx, events.statusForced, {
863
+ aggregateId: event.payload.id,
864
+ aggregateType: ENTITY_NAME,
865
+ payload: { newStatus: event.payload.status, fromStatus: state.status as InvoiceStatus },
866
+ });
842
867
 
843
- export const invoiceForceStatus = defineWriteHandler({
844
- name: "invoice:force-status",
845
- schema: z.object({
846
- id: z.uuid(),
847
- status: z.enum(INVOICE_STATES),
848
- }),
849
- access: adminOnly,
850
- unsafeSkipTransitionGuard: true,
851
- handler: async (event, ctx) => {
852
- const state = await loadInvoiceState(ctx, event.payload.id);
853
- if (!state) return failNotFound(ENTITY_NAME, event.payload.id);
854
-
855
- await ctx.appendEvent({
856
- aggregateId: event.payload.id,
857
- aggregateType: ENTITY_NAME,
858
- type: eventName(INVOICE_EVENTS.statusForced),
859
- payload: { newStatus: event.payload.status, fromStatus: state.status as InvoiceStatus },
860
- });
861
-
862
- return successResult(event.payload.id, event.payload.status, state.status);
863
- },
864
- });
868
+ return successResult(event.payload.id, event.payload.status, state.status);
869
+ },
870
+ });
871
+
872
+ return {
873
+ invoiceSend,
874
+ invoiceCancel,
875
+ invoiceReopen,
876
+ invoiceMarkPaid,
877
+ invoiceUpdateStatus,
878
+ invoiceForceStatus,
879
+ };
880
+ }
865
881
  ```
866
882
 
867
883
  ### src/reducer.ts
@@ -876,7 +892,7 @@ export const invoiceForceStatus = defineWriteHandler({
876
892
  // and update the relevant `case` in `reduceInvoice` — that's the whole
877
893
  // contract.
878
894
 
879
- import type { HandlerContext } from "@app/define";
895
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
880
896
  import { eventName, INVOICE_CREATED_EVENT, INVOICE_EVENTS } from "./events";
881
897
 
882
898
  export type InvoiceStatus = "draft" | "sent" | "paid" | "cancelled";
@@ -131,13 +131,12 @@ import {
131
131
  tagEntity,
132
132
  } from "@cosmicdrift/kumiko-bundled-features/tags";
133
133
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
134
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
134
135
  import {
135
- createTestUser,
136
136
  setupTestStack,
137
137
  type TestStack,
138
- testTenantId,
139
138
  unsafeCreateEntityTable,
140
- } from "@cosmicdrift/kumiko-framework/stack";
139
+ } from "@cosmicdrift/kumiko-testing/stack";
141
140
  import { noteEntity, noteFeature } from "../feature";
142
141
  import { type TagClient, tagFlow } from "../usage";
143
142
 
@@ -39,13 +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, TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
42
43
  import {
43
- createTestUser,
44
44
  setupTestStack,
45
45
  type TestStack,
46
- TestUsers,
47
46
  unsafeCreateEntityTable,
48
- } from "@cosmicdrift/kumiko-framework/stack";
47
+ } from "@cosmicdrift/kumiko-testing/stack";
49
48
  import { noteEntity, noteFeature } from "../feature";
50
49
 
51
50
  let stack: TestStack;
@@ -137,14 +137,14 @@ import {
137
137
  import { createTenantSettingsFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-settings";
138
138
  import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
139
139
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
140
+ import { testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
140
141
  import {
141
142
  pushEntityProjectionTables,
142
143
  setupTestStack,
143
144
  type TestStack,
144
- testTenantId,
145
145
  unsafeCreateEntityTable,
146
146
  unsafePushTables,
147
- } from "@cosmicdrift/kumiko-framework/stack";
147
+ } from "@cosmicdrift/kumiko-testing/stack";
148
148
  import { invoiceEntity, invoiceFeature } from "../feature";
149
149
 
150
150
  const tenantId = testTenantId(1);
@@ -119,15 +119,14 @@ import {
119
119
  import { asRawClient } from "@cosmicdrift/kumiko-framework/db";
120
120
  import { findTierResolverUsage, isTierResolverPlugin } from "@cosmicdrift/kumiko-framework/engine";
121
121
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
122
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
123
+ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-testing/helpers";
122
124
  import {
123
- createTestUser,
124
125
  setupTestStack,
125
126
  type TestStack,
126
- testTenantId,
127
127
  unsafeCreateEntityTable,
128
128
  unsafePushTables,
129
- } from "@cosmicdrift/kumiko-framework/stack";
130
- import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing";
129
+ } from "@cosmicdrift/kumiko-testing/stack";
131
130
  import { appTierMap, NOTES_EXPORT_FEATURE, notesExportFeature, tierEngineForApp } from "../feature";
132
131
 
133
132
  const encryptionKey = randomBytes(32).toString("base64");
@@ -117,13 +117,13 @@ boundary, the `todayRange` day-window, and the `fromCoordinates` provider seam.
117
117
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
118
118
  import type { SaveContext } from "@cosmicdrift/kumiko-framework/engine";
119
119
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
120
+ import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
121
+ import type { GeoTzProvider } from "@cosmicdrift/kumiko-framework/time";
120
122
  import {
121
123
  setupTestStack,
122
124
  type TestStack,
123
- TestUsers,
124
125
  unsafeCreateEntityTable,
125
- } from "@cosmicdrift/kumiko-framework/stack";
126
- import type { GeoTzProvider } from "@cosmicdrift/kumiko-framework/time";
126
+ } from "@cosmicdrift/kumiko-testing/stack";
127
127
  import { deliveryEntity, timezonesFeature } from "../feature";
128
128
 
129
129
  let stack: TestStack;
@@ -123,7 +123,7 @@ export const features = [
123
123
  // löschen — Row bleibt für Multi-User-Refs intakt.
124
124
 
125
125
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
126
- import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
126
+ import { createAuthFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
127
127
  import {
128
128
  createComplianceProfilesFeature,
129
129
  tenantComplianceProfileEntity,
@@ -148,15 +148,15 @@ import {
148
148
  runForgetCleanup,
149
149
  runUserExport,
150
150
  } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
151
- import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
151
+ import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
152
152
  import { EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
153
+ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
154
+ import { seedRow } from "@cosmicdrift/kumiko-testing/helpers";
153
155
  import {
154
156
  setupTestStack,
155
157
  type TestStack,
156
158
  unsafeCreateEntityTable,
157
- } from "@cosmicdrift/kumiko-framework/stack";
158
- import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
159
- import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
159
+ } from "@cosmicdrift/kumiko-testing/stack";
160
160
  import { noteEntity, notesFeature, notesTable } from "../feature";
161
161
 
162
162
  let stack: TestStack;
@@ -177,7 +177,7 @@ beforeAll(async () => {
177
177
  stack = await setupTestStack({
178
178
  features: [
179
179
  createUserFeature(),
180
- authFoundationFeature,
180
+ createAuthFoundationFeature(),
181
181
  createSessionsFeature(),
182
182
  createDataRetentionFeature(),
183
183
  createComplianceProfilesFeature(),
@@ -321,8 +321,12 @@ describe("user-data-rights recipe :: EXT_USER_DATA-Hooks integrieren Notes-Domai
321
321
  // - HTTP-Endpoints fuer create/list
322
322
  // - Compliance-Profile-Wiring + Cron-Scheduling
323
323
 
324
- import { deleteMany, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
325
- import { deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
324
+ import {
325
+ deleteMany,
326
+ deriveEntityTableMeta,
327
+ selectMany,
328
+ updateMany,
329
+ } from "@cosmicdrift/kumiko-framework/db";
326
330
  import {
327
331
  createEntity,
328
332
  createTextField,
@@ -109,7 +109,7 @@ import {
109
109
  createRegistry,
110
110
  validateBoot as validateBootRaw,
111
111
  } from "@cosmicdrift/kumiko-framework/engine";
112
- import { withBootValidatorFixture } from "@cosmicdrift/kumiko-framework/testing";
112
+ import { withBootValidatorFixture } from "@cosmicdrift/kumiko-testing/helpers";
113
113
  import { composeAccountApp } from "../feature";
114
114
 
115
115
  function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
@@ -166,7 +166,7 @@ describe("user-profile recipe — boot validation", () => {
166
166
  // })
167
167
 
168
168
  import { createAuthEmailPasswordFeature } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
169
- import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
169
+ import { createAuthFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
170
170
  import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
171
171
  import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
172
172
  import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention";
@@ -211,7 +211,7 @@ export function composeAccountApp(): FeatureDefinition[] {
211
211
  createAuthEmailPasswordFeature(),
212
212
  createDataRetentionFeature(),
213
213
  createComplianceProfilesFeature(),
214
- authFoundationFeature,
214
+ createAuthFoundationFeature(),
215
215
  createPersonalAccessTokensFeature({ scopes: {} }),
216
216
  createSessionsFeature(),
217
217
  createFilesFeature(),