@cosmicdrift/kumiko-samples 0.365.1 → 0.367.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 (208) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/admin-shell/feature.ts +9 -9
  4. package/packages/bundled-features/src/agent-tools/changes.json +6 -0
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +87 -5
  6. package/packages/bundled-features/src/audit/feature.ts +1 -1
  7. package/packages/bundled-features/src/auth-email-password/changes.json +13 -0
  8. package/packages/bundled-features/src/auth-email-password/constants.ts +0 -2
  9. package/packages/bundled-features/src/auth-email-password/feature.ts +3 -3
  10. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -0
  11. package/packages/bundled-features/src/auth-mfa/feature.ts +4 -4
  12. package/packages/bundled-features/src/auth-mfa-user-data/index.ts +1 -1
  13. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +1 -1
  14. package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
  15. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +89 -28
  16. package/packages/bundled-features/src/cap-counter/changes.json +7 -0
  17. package/packages/bundled-features/src/cap-counter/constants.ts +3 -0
  18. package/packages/bundled-features/src/cap-counter/feature.ts +2 -2
  19. package/packages/bundled-features/src/cap-counter/tables.ts +5 -2
  20. package/packages/bundled-features/src/cap-overview/feature.ts +3 -3
  21. package/packages/bundled-features/src/channel-discord/feature.ts +2 -2
  22. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  23. package/packages/bundled-features/src/channel-email/email-channel.ts +12 -2
  24. package/packages/bundled-features/src/channel-email/feature.ts +2 -2
  25. package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
  26. package/packages/bundled-features/src/channel-push/feature.ts +1 -1
  27. package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
  28. package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
  29. package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
  30. package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
  31. package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
  32. package/packages/bundled-features/src/config/feature.ts +1 -1
  33. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  34. package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
  35. package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
  36. package/packages/bundled-features/src/data-retention/feature.ts +1 -1
  37. package/packages/bundled-features/src/data-retention/presets.ts +4 -4
  38. package/packages/bundled-features/src/delivery/changes.json +18 -0
  39. package/packages/bundled-features/src/delivery/channel-context.ts +1 -0
  40. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
  41. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
  42. package/packages/bundled-features/src/delivery/delivery-service.ts +71 -4
  43. package/packages/bundled-features/src/delivery/feature.ts +10 -6
  44. package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
  45. package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
  46. package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
  47. package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +13 -6
  48. package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
  49. package/packages/bundled-features/src/delivery/index.ts +6 -0
  50. package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
  51. package/packages/bundled-features/src/delivery/testing.ts +2 -0
  52. package/packages/bundled-features/src/delivery/types.ts +4 -0
  53. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +73 -0
  54. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +17 -0
  55. package/packages/bundled-features/src/delivery/unsubscribe.ts +94 -31
  56. package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
  57. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
  58. package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
  59. package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
  60. package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
  61. package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
  62. package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
  63. package/packages/bundled-features/src/files/README.md +1 -1
  64. package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
  65. package/packages/bundled-features/src/folders/feature.ts +2 -2
  66. package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
  67. package/packages/bundled-features/src/form-draft/feature.ts +2 -2
  68. package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
  69. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
  70. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
  71. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
  72. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
  73. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
  74. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
  75. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
  76. package/packages/bundled-features/src/ledger/feature.ts +5 -5
  77. package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
  78. package/packages/bundled-features/src/mail-foundation/feature.ts +51 -10
  79. package/packages/bundled-features/src/mail-foundation/index.ts +3 -0
  80. package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
  81. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
  82. package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
  83. package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
  84. package/packages/bundled-features/src/metrics/feature.ts +2 -2
  85. package/packages/bundled-features/src/notes-history/feature.ts +2 -2
  86. package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
  87. package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
  88. package/packages/bundled-features/src/readiness/feature.ts +2 -2
  89. package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
  90. package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
  91. package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
  92. package/packages/bundled-features/src/secrets/feature.ts +1 -1
  93. package/packages/bundled-features/src/seo/feature.ts +1 -1
  94. package/packages/bundled-features/src/sessions/feature.ts +17 -13
  95. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
  96. package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
  97. package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
  98. package/packages/bundled-features/src/tags/feature.ts +2 -2
  99. package/packages/bundled-features/src/template-resolver/changes.json +25 -0
  100. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -9
  101. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +19 -0
  102. package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -0
  103. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  104. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +9 -1
  105. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +8 -2
  106. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +8 -1
  107. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +6 -1
  108. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +7 -2
  109. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +21 -1
  110. package/packages/bundled-features/src/template-resolver/index.ts +4 -0
  111. package/packages/bundled-features/src/template-resolver/qualified-names.ts +1 -0
  112. package/packages/bundled-features/src/template-resolver/seeding.ts +54 -7
  113. package/packages/bundled-features/src/template-resolver/template-preview.ts +2 -0
  114. package/packages/bundled-features/src/template-resolver/template-system-only.ts +21 -0
  115. package/packages/bundled-features/src/template-resolver/template-validation.ts +48 -24
  116. package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -0
  117. package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
  118. package/packages/bundled-features/src/tenant/feature.ts +4 -4
  119. package/packages/bundled-features/src/tenant/seeding.ts +1 -1
  120. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
  121. package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
  122. package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
  123. package/packages/bundled-features/src/user/feature.ts +1 -1
  124. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  125. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  126. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
  127. package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
  128. package/packages/bundled-features/src/user-profile/feature.ts +4 -4
  129. package/packages/bundled-features/src/waitlist/feature.ts +2 -2
  130. package/samples/apps/cap-billing-demo/README.md +1 -1
  131. package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
  132. package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
  133. package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
  134. package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
  135. package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
  136. package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
  137. package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
  138. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
  139. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +12 -0
  140. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +7 -0
  141. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +2 -0
  142. package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
  143. package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
  144. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
  145. package/samples/apps/use-all-bundled/feature-manifest.json +4 -4
  146. package/samples/apps/use-all-bundled/package.json +1 -0
  147. package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
  148. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
  149. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
  150. package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
  151. package/samples/recipes/access-control/src/feature.ts +21 -25
  152. package/samples/recipes/anonymous-access/src/feature.ts +36 -43
  153. package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
  154. package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
  155. package/samples/recipes/basic-entity/README.md +1 -1
  156. package/samples/recipes/boot-check/README.md +1 -1
  157. package/samples/recipes/boot-check/src/feature.ts +2 -2
  158. package/samples/recipes/chat-channels/src/feature.ts +1 -1
  159. package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
  160. package/samples/recipes/currencies-global/src/feature.ts +3 -3
  161. package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
  162. package/samples/recipes/custom-fields-basic/README.md +1 -1
  163. package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
  164. package/samples/recipes/custom-handlers/README.md +3 -3
  165. package/samples/recipes/custom-handlers/src/feature.ts +37 -41
  166. package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
  167. package/samples/recipes/delivery-notifications/README.md +1 -1
  168. package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
  169. package/samples/recipes/designer-demo/src/feature.ts +2 -1
  170. package/samples/recipes/embedded/src/feature.ts +1 -1
  171. package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
  172. package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
  173. package/samples/recipes/error-contract/src/feature.ts +25 -25
  174. package/samples/recipes/event-sourcing/src/feature.ts +70 -76
  175. package/samples/recipes/feature-toggles/src/feature.ts +28 -20
  176. package/samples/recipes/files-post-processing/src/feature.ts +22 -26
  177. package/samples/recipes/idempotency/src/feature.ts +16 -18
  178. package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
  179. package/samples/recipes/lane-routing/src/feature.ts +10 -12
  180. package/samples/recipes/legal-pages/src/feature.ts +1 -1
  181. package/samples/recipes/lifecycle-hooks/README.md +4 -4
  182. package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
  183. package/samples/recipes/managed-config/src/feature.ts +8 -8
  184. package/samples/recipes/managed-pages/src/feature.ts +1 -1
  185. package/samples/recipes/notes-history-basic/README.md +1 -1
  186. package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
  187. package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
  188. package/samples/recipes/pipeline-basics/README.md +1 -1
  189. package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
  190. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  191. package/samples/recipes/rate-limiting/src/feature.ts +7 -9
  192. package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
  193. package/samples/recipes/reference-data/src/feature.ts +1 -1
  194. package/samples/recipes/relations/src/feature.ts +2 -2
  195. package/samples/recipes/screens-nav/src/feature.ts +1 -1
  196. package/samples/recipes/search/src/feature.ts +17 -19
  197. package/samples/recipes/security-baseline/README.md +1 -1
  198. package/samples/recipes/state-machine/src/feature.ts +1 -1
  199. package/samples/recipes/tags-basic/README.md +1 -1
  200. package/samples/recipes/tags-basic/src/feature.ts +2 -2
  201. package/samples/recipes/tenant-settings/README.md +1 -1
  202. package/samples/recipes/tenant-settings/src/feature.ts +1 -1
  203. package/samples/recipes/timezones/src/feature.ts +12 -12
  204. package/samples/recipes/user-data-rights/README.md +2 -2
  205. package/samples/recipes/user-data-rights/src/feature.ts +1 -1
  206. package/samples/recipes/user-profile/src/feature.ts +1 -1
  207. package/samples/recipes/webhook-step/src/feature.ts +1 -1
  208. package/samples/recipes/writeform-section/src/feature.ts +12 -12
@@ -39,18 +39,18 @@ const orderEntity = createEntity({
39
39
 
40
40
  export function createLaneRoutingFeature(): FeatureDefinition {
41
41
  return defineFeature("orders", (r) => {
42
- r.entity("order", orderEntity);
42
+ r.entity({ name: "order", ...orderEntity });
43
43
 
44
44
  // Write handler — runs in the API process (or wherever the command
45
45
  // lands). Event "orders:write:order:create" is the auto-generated
46
46
  // write-event both jobs trigger on.
47
- r.writeHandler(
48
- "order:create",
49
- z.object({
47
+ r.writeHandler({
48
+ name: "order:create",
49
+ schema: z.object({
50
50
  customerName: z.string().min(1),
51
51
  amount: z.number().positive(),
52
52
  }),
53
- async (event) => ({
53
+ handler: async (event) => ({
54
54
  isSuccess: true as const,
55
55
  data: {
56
56
  id: 1,
@@ -58,15 +58,13 @@ export function createLaneRoutingFeature(): FeatureDefinition {
58
58
  amount: event.payload.amount,
59
59
  },
60
60
  }),
61
- {
62
- access: {
63
- openToAll: {
64
- reason:
65
- "demo recipe: any signed-in user may place an order; the handler is a stub with no persistence",
66
- },
61
+ access: {
62
+ openToAll: {
63
+ reason:
64
+ "demo recipe: any signed-in user may place an order; the handler is a stub with no persistence",
67
65
  },
68
66
  },
69
- );
67
+ });
70
68
 
71
69
  // Receipt-rendering: heavy CPU work. Pinned to `worker` so the
72
70
  // BullMQ consumer on the API process never picks it up — even if the
@@ -41,7 +41,7 @@ import {
41
41
  import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
42
42
 
43
43
  // Beide Features aktivieren — template-resolver ist Foundation, legal-pages
44
- // requires sie. r.requires("template-resolver") greift automatisch im
44
+ // requires sie. r.requires({ features: ["template-resolver"] }) greift automatisch im
45
45
  // legal-pages-Feature.
46
46
  export const templateResolverFeature = createTemplateResolverFeature();
47
47
  export const legalPagesFeature = createLegalPagesFeature();
@@ -12,19 +12,19 @@ back.
12
12
 
13
13
  ## What it shows
14
14
 
15
- - **`r.hook("validation", handlerRef, fn)`** — runs before the
15
+ - **`r.hook({ type: "validation", target: handlerRef, handler })`** — runs before the
16
16
  handler body executes. Returns either `null` (pass) or an array of
17
17
  `{ field, error }` objects (fail). Used here to reject titles
18
18
  containing the word "spam" and titles longer than 200 characters.
19
- - **`r.hook("postSave", { allOf: entityRef }, fn)`** — fires after every
19
+ - **`r.hook({ type: "postSave", target: { allOf: entityRef }, handler })`** — fires after every
20
20
  successful save (create or update) in the same transaction.
21
21
  `result.isNew` distinguishes create from update; `result.changes`
22
22
  carries the changed fields only.
23
- - **`r.hook("preDelete", { allOf: entityRef }, fn)`** — fires inside the
23
+ - **`r.hook({ type: "preDelete", target: { allOf: entityRef }, handler })`** — fires inside the
24
24
  delete transaction. Throws on invariant violations (here: "published
25
25
  articles cannot be deleted") to roll the delete back. The handler
26
26
  receives the full row snapshot — no extra load needed.
27
- - **`r.hook("postDelete", { allOf: entityRef }, fn)`** — fires after commit.
27
+ - **`r.hook({ type: "postDelete", target: { allOf: entityRef }, handler })`** — fires after commit.
28
28
  External side effects (logs, notifications) belong here because
29
29
  failures cannot roll the delete back.
30
30
 
@@ -38,7 +38,7 @@ export const hookLog: { type: string; data: Record<string, unknown> }[] = [];
38
38
  const editorWrite = { access: { roles: ["Admin", "Editor"] } } as const;
39
39
 
40
40
  export const articlesFeature = defineFeature("blog", (r) => {
41
- const article = r.entity("article", articleEntity);
41
+ const article = r.entity({ name: "article", ...articleEntity });
42
42
 
43
43
  const articleCreate = r.writeHandler(
44
44
  defineEntityCreateHandler("article", articleEntity, editorWrite),
@@ -49,33 +49,45 @@ export const articlesFeature = defineFeature("blog", (r) => {
49
49
  r.writeHandler(defineEntityDeleteHandler("article", articleEntity, editorWrite));
50
50
 
51
51
  // --- Validation hook on create: reject banned words + length ---
52
- r.hook("validation", articleCreate, (data) => {
53
- const title = data["title"] as string;
54
- if (title.toLowerCase().includes("spam")) {
55
- return [{ field: "title", error: "title_contains_banned_word" }];
56
- }
57
- if (title.length > 200) {
58
- return [{ field: "title", error: "title_too_long" }];
59
- }
60
- return null;
52
+ r.hook({
53
+ type: "validation",
54
+ target: articleCreate,
55
+ handler: (data) => {
56
+ const title = data["title"] as string;
57
+ if (title.toLowerCase().includes("spam")) {
58
+ return [{ field: "title", error: "title_contains_banned_word" }];
59
+ }
60
+ if (title.length > 200) {
61
+ return [{ field: "title", error: "title_too_long" }];
62
+ }
63
+ return null;
64
+ },
61
65
  });
62
66
 
63
67
  // --- Validation hook on update: length check on title changes ---
64
- r.hook("validation", articleUpdate, (data) => {
65
- const changes = data["changes"] as Record<string, unknown> | undefined;
66
- const title = changes?.["title"] as string | undefined;
67
- if (title && title.length > 200) {
68
- return [{ field: "title", error: "title_too_long" }];
69
- }
70
- return null;
68
+ r.hook({
69
+ type: "validation",
70
+ target: articleUpdate,
71
+ handler: (data) => {
72
+ const changes = data["changes"] as Record<string, unknown> | undefined;
73
+ const title = changes?.["title"] as string | undefined;
74
+ if (title && title.length > 200) {
75
+ return [{ field: "title", error: "title_too_long" }];
76
+ }
77
+ return null;
78
+ },
71
79
  });
72
80
 
73
81
  // --- postSave entity hook: log all saves ---
74
- r.hook("postSave", { allOf: article }, async (result: SaveContext) => {
75
- hookLog.push({
76
- type: result.isNew ? "created" : "updated",
77
- data: { id: result.id, changes: result.changes },
78
- });
82
+ r.hook({
83
+ type: "postSave",
84
+ target: { allOf: article },
85
+ handler: async (result: SaveContext) => {
86
+ hookLog.push({
87
+ type: result.isNew ? "created" : "updated",
88
+ data: { id: result.id, changes: result.changes },
89
+ });
90
+ },
79
91
  });
80
92
 
81
93
  // --- preDelete entity hook: block delete when the article is still published.
@@ -83,22 +95,30 @@ export const articlesFeature = defineFeature("blog", (r) => {
83
95
  // the delete rolls back before any projection or event is written. The
84
96
  // payload.data carries the full row snapshot taken just before delete —
85
97
  // no extra load needed.
86
- r.hook("preDelete", { allOf: article }, async (payload) => {
87
- if (payload.data["isPublished"] === true) {
88
- throw new ConflictError({
89
- message: `article ${payload.id} is still published`,
90
- i18nKey: "errors.publishedArticleCannotBeDeleted",
91
- details: { reason: "published_article_cannot_be_deleted", entityId: payload.id },
92
- });
93
- }
98
+ r.hook({
99
+ type: "preDelete",
100
+ target: { allOf: article },
101
+ handler: async (payload) => {
102
+ if (payload.data["isPublished"] === true) {
103
+ throw new ConflictError({
104
+ message: `article ${payload.id} is still published`,
105
+ i18nKey: "errors.publishedArticleCannotBeDeleted",
106
+ details: { reason: "published_article_cannot_be_deleted", entityId: payload.id },
107
+ });
108
+ }
109
+ },
94
110
  });
95
111
 
96
112
  // --- postDelete entity hook: log deletions after the transaction commits.
97
113
  // Default phase is afterCommit so failures here can't roll back the delete.
98
- r.hook("postDelete", { allOf: article }, async (payload) => {
99
- hookLog.push({
100
- type: "deleted",
101
- data: { id: payload.id },
102
- });
114
+ r.hook({
115
+ type: "postDelete",
116
+ target: { allOf: article },
117
+ handler: async (payload) => {
118
+ hookLog.push({
119
+ type: "deleted",
120
+ data: { id: payload.id },
121
+ });
122
+ },
103
123
  });
104
124
  });
@@ -36,12 +36,12 @@ export const smtpHostHandle: ConfigKeyHandle<"text"> = {
36
36
  };
37
37
 
38
38
  export const integrationsFeature = defineFeature(FEATURE, (r) => {
39
- r.requires("config");
39
+ r.requires({ features: ["config"] });
40
40
  // backing:"secrets" stores payment-api-key in the secrets envelope, which
41
41
  // the secrets feature provisions — declare the dependency so registry-build
42
42
  // enforces it instead of leaving a consumer to discover the missing
43
43
  // tenant_secrets table at runtime.
44
- r.requires("secrets");
44
+ r.requires({ features: ["secrets"] });
45
45
 
46
46
  r.config({
47
47
  keys: {
@@ -68,15 +68,15 @@ export const integrationsFeature = defineFeature(FEATURE, (r) => {
68
68
 
69
69
  // Internal-read probe: the owning feature reads its own secrets-backed key
70
70
  // via ctx.config and receives the revealed plaintext, never the mask.
71
- r.queryHandler(
72
- "peek-payment-key",
73
- z.object({}),
74
- async (_query, ctx) => {
71
+ r.queryHandler({
72
+ name: "peek-payment-key",
73
+ schema: z.object({}),
74
+ handler: async (_query, ctx) => {
75
75
  if (!ctx.config) throw new Error("ctx.config not wired");
76
76
  return { value: await ctx.config(paymentApiKeyHandle) };
77
77
  },
78
- { access: { roles: ["SystemAdmin"] } },
79
- );
78
+ access: { roles: ["SystemAdmin"] },
79
+ });
80
80
 
81
81
  r.translations({
82
82
  keys: {
@@ -5,7 +5,7 @@ import {
5
5
  } from "@cosmicdrift/kumiko-bundled-features/managed-pages";
6
6
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
7
7
 
8
- // managed-pages declares `r.requires("config")` for its branding keys — the
8
+ // managed-pages declares `r.requires({ features: ["config"] })` for its branding keys — the
9
9
  // config feature must be in the stack (runProdApp auto-mounts it; setupTestStack
10
10
  // does not, so the recipe lists it explicitly).
11
11
  export const configFeature = createConfigFeature();
@@ -68,7 +68,7 @@ The component itself is `notes-history/web/notes-section.tsx`
68
68
  ```
69
69
  notes-history → core bundle: note-entry entity, add-note handler, list query
70
70
  task-management → our feature: a plain `task` entity. Declares
71
- r.requires("notes-history") only so the bundle is
71
+ r.requires({ features: ["notes-history"] }) only so the bundle is
72
72
  mounted — the task itself is completely notes-agnostic.
73
73
  ```
74
74
 
@@ -48,9 +48,9 @@ export const taskFeature = defineFeature("task-management", (r) => {
48
48
  // notes-history is non-optional for this recipe: the demo notes tasks.
49
49
  // The task feature itself stays completely notes-agnostic — it only
50
50
  // declares the dependency so the bundle is mounted.
51
- r.requires("notes-history");
51
+ r.requires({ features: ["notes-history"] });
52
52
 
53
- r.entity("task", taskEntity);
53
+ r.entity({ name: "task", ...taskEntity });
54
54
 
55
55
  r.writeHandler({
56
56
  name: "task:create",
@@ -56,20 +56,20 @@ const AUDIT_ROW_LIMIT = 100;
56
56
  const access = { access: { roles: ["Admin", "User"] } } as const;
57
57
 
58
58
  export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
59
- r.entity("inspection", inspectionEntity);
59
+ r.entity({ name: "inspection", ...inspectionEntity });
60
60
  const { executor: inspectionExecutor } = createEntityExecutor("inspection", inspectionEntity);
61
61
 
62
- r.writeHandler(
63
- "inspection:create",
64
- z.object({
62
+ r.writeHandler({
63
+ name: "inspection:create",
64
+ schema: z.object({
65
65
  assetName: z.string().min(1),
66
66
  inspectedOn: z.iso.date(),
67
67
  result: z.enum(INSPECTION_RESULTS),
68
68
  inspector: z.string().optional(),
69
69
  }),
70
- async (event, ctx) => inspectionExecutor.create(event.payload, event.user, ctx.db),
71
- access,
72
- );
70
+ handler: async (event, ctx) => inspectionExecutor.create(event.payload, event.user, ctx.db),
71
+ ...access,
72
+ });
73
73
 
74
74
  r.writeHandler(
75
75
  defineWriteHandler({
@@ -39,7 +39,7 @@ and why the combination is the production-realistic shape (not just
39
39
  an API tour).
40
40
 
41
41
  If you've only ever seen the free-form handler signature
42
- (`r.writeHandler(name, schema, async fn, opts)`), compare:
42
+ (`r.writeHandler({ name, schema, handler, access })`), compare:
43
43
 
44
44
  - `samples/recipes/custom-handlers/src/feature.ts` — a similar
45
45
  domain, written in the free-form style.
@@ -72,7 +72,7 @@ const productExecutor = createEventStoreExecutor(productTable, productEntity, {
72
72
  });
73
73
 
74
74
  export const inventoryFeature = defineFeature("inventory", (r) => {
75
- r.entity("product", productEntity);
75
+ r.entity({ name: "product", ...productEntity });
76
76
  r.requires.projection("read_inventory_low_stock_alerts");
77
77
 
78
78
  // Domain-events on the product stream (alongside the auto-CRUD
@@ -412,13 +412,13 @@ export const inventoryFeature = defineFeature("inventory", (r) => {
412
412
  // only; queries get the existing handler signature. The list
413
413
  // below is what the integration test reads against.
414
414
  // -------------------------------------------------------------
415
- r.queryHandler(
416
- "low-stock-alerts:list",
417
- z.object({}),
418
- async (_query, ctx) => {
415
+ r.queryHandler({
416
+ name: "low-stock-alerts:list",
417
+ schema: z.object({}),
418
+ handler: async (_query, ctx) => {
419
419
  const rows = await selectMany(ctx.db, lowStockAlertsTable);
420
420
  return { rows };
421
421
  },
422
- { access: { roles: ["Admin"] } },
423
- );
422
+ access: { roles: ["Admin"] },
423
+ });
424
424
  });
@@ -190,7 +190,7 @@ export const shareByTokenQuery = defineQueryHandler({
190
190
  });
191
191
 
192
192
  export const publicShareTokenFeature = defineFeature("public-share", (r) => {
193
- r.entity("share-link", shareLinkEntity);
193
+ r.entity({ name: "share-link", ...shareLinkEntity });
194
194
  r.writeHandler(shareLinkCreateWrite);
195
195
  r.writeHandler(shareLinkRevokeWrite);
196
196
  r.queryHandler(shareByTokenQuery);
@@ -13,14 +13,12 @@ export function createRateLimitShowcaseFeature(): FeatureDefinition {
13
13
  // Per-user budget. Real apps tune `limit` to actual handler cost — a
14
14
  // search call against a sharded index might warrant 5/min, a full
15
15
  // export 1/min. The bucket is `user:<userId>` (see rate-limit/bucket.ts).
16
- r.queryHandler(
17
- "expensive-search",
18
- z.object({ q: z.string().min(1) }),
19
- async ({ payload }) => ({ q: payload.q, hits: 0 }),
20
- {
21
- access: { roles: ["Admin", "User"] },
22
- rateLimit: { per: "user", limit: 3, windowSeconds: 60 },
23
- },
24
- );
16
+ r.queryHandler({
17
+ name: "expensive-search",
18
+ schema: z.object({ q: z.string().min(1) }),
19
+ handler: async ({ payload }) => ({ q: payload.q, hits: 0 }),
20
+ access: { roles: ["Admin", "User"] },
21
+ rateLimit: { per: "user", limit: 3, windowSeconds: 60 },
22
+ });
25
23
  });
26
24
  }
@@ -27,10 +27,10 @@ export function createOrderDeskFeature(): FeatureDefinition {
27
27
  return defineFeature("order-desk", (r) => {
28
28
  r.systemScope();
29
29
 
30
- r.queryHandler(
31
- "order:detail",
32
- rowIdSchema,
33
- async (query) => ({
30
+ r.queryHandler({
31
+ name: "order:detail",
32
+ schema: rowIdSchema,
33
+ handler: async (query) => ({
34
34
  id: query.payload.id,
35
35
  customerName: "Aiko Tanaka",
36
36
  orderNumber: "SO-10482",
@@ -43,33 +43,33 @@ export function createOrderDeskFeature(): FeatureDefinition {
43
43
  billingAddress: "Karl-Marx-Straße 12, 12043 Berlin",
44
44
  salesRep: "Jonas Weber",
45
45
  }),
46
- { access: { roles: ["Support", "Admin"] } },
47
- );
46
+ access: { roles: ["Support", "Admin"] },
47
+ });
48
48
 
49
- r.queryHandler(
50
- "order:items",
51
- rowIdSchema,
52
- async () => ({
49
+ r.queryHandler({
50
+ name: "order:items",
51
+ schema: rowIdSchema,
52
+ handler: async () => ({
53
53
  rows: [
54
54
  { sku: "KB-100", description: "Mechanical keyboard", quantity: 1, unitPrice: 148 },
55
55
  { sku: "MS-220", description: "Wireless mouse", quantity: 2, unitPrice: 45 },
56
56
  { sku: "PAD-01", description: "Desk mat", quantity: 1, unitPrice: 22 },
57
57
  ],
58
58
  }),
59
- { access: { roles: ["Support", "Admin"] } },
60
- );
59
+ access: { roles: ["Support", "Admin"] },
60
+ });
61
61
 
62
- r.queryHandler(
63
- "order:payments",
64
- rowIdSchema,
65
- async () => ({
62
+ r.queryHandler({
63
+ name: "order:payments",
64
+ schema: rowIdSchema,
65
+ handler: async () => ({
66
66
  rows: [
67
67
  { paidAt: "2026-08-21", amount: 936, method: "card", status: "settled" },
68
68
  { paidAt: "2026-08-25", amount: 312, method: "invoice", status: "pending" },
69
69
  ],
70
70
  }),
71
- { access: { roles: ["Support", "Admin"] } },
72
- );
71
+ access: { roles: ["Support", "Admin"] },
72
+ });
73
73
 
74
74
  r.screen({
75
75
  id: "order-detail",
@@ -21,7 +21,7 @@ export const categoryTable = table("read_sample_categories", {
21
21
  });
22
22
 
23
23
  export const categoryFeature = defineFeature("catalog", (r) => {
24
- r.entity("category", categoryEntity);
24
+ r.entity({ name: "category", ...categoryEntity });
25
25
 
26
26
  // Static reference data — seeded on boot, upserted on changes
27
27
  r.referenceData(
@@ -46,8 +46,8 @@ const createOnly = {
46
46
  } as const;
47
47
 
48
48
  export const relationsFeature = defineFeature("org", (r) => {
49
- const team = r.entity("team", teamEntity);
50
- const member = r.entity("member", memberEntity);
49
+ const team = r.entity({ name: "team", ...teamEntity });
50
+ const member = r.entity({ name: "member", ...memberEntity });
51
51
 
52
52
  r.relation(team, "members", {
53
53
  type: "hasMany",
@@ -46,7 +46,7 @@ export const bookEntity = createEntity({
46
46
  export function createBookshopFeature(): FeatureDefinition {
47
47
  return defineFeature("bookshop", (r) => {
48
48
  r.systemScope();
49
- r.entity("book", bookEntity);
49
+ r.entity({ name: "book", ...bookEntity });
50
50
 
51
51
  // entityList — columns carry string shorthand and the object form
52
52
  // side by side; the framework normalizes both through
@@ -26,47 +26,45 @@ export const productEntity = createEntity({
26
26
  const productTable = buildEntityTable("product", productEntity);
27
27
 
28
28
  export const productFeature = defineFeature("shop", (r) => {
29
- r.entity("product", productEntity);
29
+ r.entity({ name: "product", ...productEntity });
30
30
 
31
- r.writeHandler(
32
- "product:create",
33
- z.object({
31
+ r.writeHandler({
32
+ name: "product:create",
33
+ schema: z.object({
34
34
  name: z.string().min(1),
35
35
  brand: z.string().optional(),
36
36
  sku: z.string().min(1),
37
37
  category: z.string().optional(),
38
38
  }),
39
- async (event, ctx) => {
39
+ handler: async (event, ctx) => {
40
40
  const crud = createEventStoreExecutor(productTable, productEntity, {
41
41
  searchAdapter: ctx.searchAdapter,
42
42
  entityName: "product",
43
43
  });
44
44
  return crud.create(event.payload, event.user, ctx.db);
45
45
  },
46
- { access: { roles: ["Admin"] } },
47
- );
46
+ access: { roles: ["Admin"] },
47
+ });
48
48
 
49
- r.queryHandler(
50
- "product:list",
51
- z.object({
49
+ r.queryHandler({
50
+ name: "product:list",
51
+ schema: z.object({
52
52
  search: z.string().optional(),
53
53
  limit: z.number().optional(),
54
54
  }),
55
- async (query, ctx) => {
55
+ handler: async (query, ctx) => {
56
56
  const crud = createEventStoreExecutor(productTable, productEntity, {
57
57
  searchAdapter: ctx.searchAdapter,
58
58
  entityName: "product",
59
59
  });
60
60
  return crud.list(query.payload, query.user, ctx.db);
61
61
  },
62
- {
63
- access: {
64
- openToAll: {
65
- reason:
66
- "any signed-in user may search and list products in this recipe; creating " +
67
- "a product still requires the Admin role above",
68
- },
62
+ access: {
63
+ openToAll: {
64
+ reason:
65
+ "any signed-in user may search and list products in this recipe; creating " +
66
+ "a product still requires the Admin role above",
69
67
  },
70
68
  },
71
- );
69
+ });
72
70
  });
@@ -25,7 +25,7 @@ used to be silent; now the boot validator warns at `NODE_ENV=production`.
25
25
  ## Feature composition
26
26
 
27
27
  ```
28
- config → tenant's r.requires("config") dependency
28
+ config → tenant's r.requires({ features: ["config"] }) dependency
29
29
  user → cross-tenant identity (sessions' r.requires target)
30
30
  tenant → memberships (audit's r.requires target)
31
31
  auth-foundation → tokenVerifier extension point (sessions' r.requires target)
@@ -39,7 +39,7 @@ import type { InvoiceStatus } from "./reducer";
39
39
  export { invoiceEntity } from "./entities/invoice";
40
40
 
41
41
  export const stateMachineFeature = defineFeature("billing", (r) => {
42
- r.entity("invoice", invoiceEntity);
42
+ r.entity({ name: "invoice", ...invoiceEntity });
43
43
 
44
44
  // Domain events — one per intent. Names live in events.ts so the reducer
45
45
  // and the projection stay in sync. The auto "invoice.created" event from
@@ -77,7 +77,7 @@ See the [tags feature reference](/en/bundled-features/tags/) for the full set.
77
77
  tags → core bundle: tag + tag-assignment entities,
78
78
  create/assign/remove handlers, list queries
79
79
  note-management → our feature: a plain `note` entity. Declares
80
- r.requires("tags") only so the bundle is mounted —
80
+ r.requires({ features: ["tags"] }) only so the bundle is mounted —
81
81
  the note itself is completely tag-agnostic.
82
82
  ```
83
83
 
@@ -47,9 +47,9 @@ export const noteFeature = defineFeature("note-management", (r) => {
47
47
  // tags is non-optional for this recipe: the demo tags notes. The note
48
48
  // feature itself stays completely tag-agnostic — it only declares the
49
49
  // dependency so the bundle is mounted.
50
- r.requires("tags");
50
+ r.requires({ features: ["tags"] });
51
51
 
52
- r.entity("note", noteEntity);
52
+ r.entity({ name: "note", ...noteEntity });
53
53
 
54
54
  r.writeHandler({
55
55
  name: "note:create",
@@ -27,7 +27,7 @@ export const invoiceEntity = createEntity({
27
27
  });
28
28
 
29
29
  export const invoiceFeature = defineFeature("invoice", (r) => {
30
- r.entity("invoice", invoiceEntity);
30
+ r.entity({ name: "invoice", ...invoiceEntity });
31
31
  r.writeHandler(
32
32
  defineCreateWithTenantDefaults("invoice", invoiceEntity, {
33
33
  access: { roles: ["Admin"] },
@@ -24,7 +24,7 @@ export const invoiceEntity = createEntity({
24
24
  const ACCESS = { roles: ["Admin"] } as const;
25
25
 
26
26
  export const invoiceFeature = defineFeature("invoice", (r) => {
27
- r.entity("invoice", invoiceEntity);
27
+ r.entity({ name: "invoice", ...invoiceEntity });
28
28
  r.writeHandler(
29
29
  defineCreateWithTenantDefaults("invoice", invoiceEntity, {
30
30
  access: ACCESS,
@@ -43,12 +43,12 @@ export const timezonesFeature = defineFeature("timezones", (r) => {
43
43
  });
44
44
  r.writeHandler(defineEntityDeleteHandler("delivery", deliveryEntity, adminWrite));
45
45
 
46
- r.queryHandler(
47
- "day-window",
48
- z.object({
46
+ r.queryHandler({
47
+ name: "day-window",
48
+ schema: z.object({
49
49
  zone: z.string().refine(isValidIanaTimeZone, { message: "Invalid IANA timezone" }),
50
50
  }),
51
- async (query, ctx) => {
51
+ handler: async (query, ctx) => {
52
52
  const { start, end } = ctx.tz.todayRange(query.payload.zone);
53
53
  return {
54
54
  zone: query.payload.zone,
@@ -57,16 +57,16 @@ export const timezonesFeature = defineFeature("timezones", (r) => {
57
57
  end: end.toString(),
58
58
  };
59
59
  },
60
- openRead,
61
- );
60
+ ...openRead,
61
+ });
62
62
 
63
- r.queryHandler(
64
- "zone-at",
65
- z.object({ latitude: z.number(), longitude: z.number() }),
66
- async (query, ctx) => {
63
+ r.queryHandler({
64
+ name: "zone-at",
65
+ schema: z.object({ latitude: z.number(), longitude: z.number() }),
66
+ handler: async (query, ctx) => {
67
67
  const zone = await ctx.tz.fromCoordinates(query.payload);
68
68
  return { zone };
69
69
  },
70
- openRead,
71
- );
70
+ ...openRead,
71
+ });
72
72
  });