@cosmicdrift/kumiko-samples 0.365.1 → 0.366.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 (174) 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/feature.ts +1 -1
  23. package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
  24. package/packages/bundled-features/src/channel-push/feature.ts +1 -1
  25. package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
  26. package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
  27. package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
  28. package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
  29. package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
  30. package/packages/bundled-features/src/config/feature.ts +1 -1
  31. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  32. package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
  33. package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
  34. package/packages/bundled-features/src/data-retention/feature.ts +1 -1
  35. package/packages/bundled-features/src/data-retention/presets.ts +4 -4
  36. package/packages/bundled-features/src/delivery/changes.json +6 -0
  37. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
  38. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
  39. package/packages/bundled-features/src/delivery/delivery-service.ts +17 -2
  40. package/packages/bundled-features/src/delivery/feature.ts +9 -5
  41. package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
  42. package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
  43. package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
  44. package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +3 -1
  45. package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
  46. package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
  47. package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
  48. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
  49. package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
  50. package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
  51. package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
  52. package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
  53. package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
  54. package/packages/bundled-features/src/files/README.md +1 -1
  55. package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
  56. package/packages/bundled-features/src/folders/feature.ts +2 -2
  57. package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
  58. package/packages/bundled-features/src/form-draft/feature.ts +2 -2
  59. package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
  60. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
  61. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
  62. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
  63. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
  64. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
  65. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
  66. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
  67. package/packages/bundled-features/src/ledger/feature.ts +5 -5
  68. package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
  69. package/packages/bundled-features/src/mail-foundation/feature.ts +1 -1
  70. package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
  71. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
  72. package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
  73. package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
  74. package/packages/bundled-features/src/metrics/feature.ts +2 -2
  75. package/packages/bundled-features/src/notes-history/feature.ts +2 -2
  76. package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
  77. package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
  78. package/packages/bundled-features/src/readiness/feature.ts +2 -2
  79. package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
  80. package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
  81. package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
  82. package/packages/bundled-features/src/secrets/feature.ts +1 -1
  83. package/packages/bundled-features/src/seo/feature.ts +1 -1
  84. package/packages/bundled-features/src/sessions/feature.ts +17 -13
  85. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
  86. package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
  87. package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
  88. package/packages/bundled-features/src/tags/feature.ts +2 -2
  89. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  90. package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
  91. package/packages/bundled-features/src/tenant/feature.ts +4 -4
  92. package/packages/bundled-features/src/tenant/seeding.ts +1 -1
  93. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
  94. package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
  95. package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
  96. package/packages/bundled-features/src/user/feature.ts +1 -1
  97. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  98. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
  99. package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
  100. package/packages/bundled-features/src/user-profile/feature.ts +4 -4
  101. package/packages/bundled-features/src/waitlist/feature.ts +2 -2
  102. package/samples/apps/cap-billing-demo/README.md +1 -1
  103. package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
  104. package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
  105. package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
  106. package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
  107. package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
  108. package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
  109. package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
  110. package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
  111. package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
  112. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
  113. package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
  114. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
  115. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
  116. package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
  117. package/samples/recipes/access-control/src/feature.ts +21 -25
  118. package/samples/recipes/anonymous-access/src/feature.ts +36 -43
  119. package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
  120. package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
  121. package/samples/recipes/basic-entity/README.md +1 -1
  122. package/samples/recipes/boot-check/README.md +1 -1
  123. package/samples/recipes/boot-check/src/feature.ts +2 -2
  124. package/samples/recipes/chat-channels/src/feature.ts +1 -1
  125. package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
  126. package/samples/recipes/currencies-global/src/feature.ts +3 -3
  127. package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
  128. package/samples/recipes/custom-fields-basic/README.md +1 -1
  129. package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
  130. package/samples/recipes/custom-handlers/README.md +3 -3
  131. package/samples/recipes/custom-handlers/src/feature.ts +37 -41
  132. package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
  133. package/samples/recipes/delivery-notifications/README.md +1 -1
  134. package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
  135. package/samples/recipes/designer-demo/src/feature.ts +2 -1
  136. package/samples/recipes/embedded/src/feature.ts +1 -1
  137. package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
  138. package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
  139. package/samples/recipes/error-contract/src/feature.ts +25 -25
  140. package/samples/recipes/event-sourcing/src/feature.ts +70 -76
  141. package/samples/recipes/feature-toggles/src/feature.ts +28 -20
  142. package/samples/recipes/files-post-processing/src/feature.ts +22 -26
  143. package/samples/recipes/idempotency/src/feature.ts +16 -18
  144. package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
  145. package/samples/recipes/lane-routing/src/feature.ts +10 -12
  146. package/samples/recipes/legal-pages/src/feature.ts +1 -1
  147. package/samples/recipes/lifecycle-hooks/README.md +4 -4
  148. package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
  149. package/samples/recipes/managed-config/src/feature.ts +8 -8
  150. package/samples/recipes/managed-pages/src/feature.ts +1 -1
  151. package/samples/recipes/notes-history-basic/README.md +1 -1
  152. package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
  153. package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
  154. package/samples/recipes/pipeline-basics/README.md +1 -1
  155. package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
  156. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  157. package/samples/recipes/rate-limiting/src/feature.ts +7 -9
  158. package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
  159. package/samples/recipes/reference-data/src/feature.ts +1 -1
  160. package/samples/recipes/relations/src/feature.ts +2 -2
  161. package/samples/recipes/screens-nav/src/feature.ts +1 -1
  162. package/samples/recipes/search/src/feature.ts +17 -19
  163. package/samples/recipes/security-baseline/README.md +1 -1
  164. package/samples/recipes/state-machine/src/feature.ts +1 -1
  165. package/samples/recipes/tags-basic/README.md +1 -1
  166. package/samples/recipes/tags-basic/src/feature.ts +2 -2
  167. package/samples/recipes/tenant-settings/README.md +1 -1
  168. package/samples/recipes/tenant-settings/src/feature.ts +1 -1
  169. package/samples/recipes/timezones/src/feature.ts +12 -12
  170. package/samples/recipes/user-data-rights/README.md +2 -2
  171. package/samples/recipes/user-data-rights/src/feature.ts +1 -1
  172. package/samples/recipes/user-profile/src/feature.ts +1 -1
  173. package/samples/recipes/webhook-step/src/feature.ts +1 -1
  174. package/samples/recipes/writeform-section/src/feature.ts +12 -12
@@ -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
  });
@@ -11,8 +11,8 @@ forget logic:
11
11
 
12
12
  ```ts illustration
13
13
  defineFeature("notes", (r) => {
14
- r.requires("user-data-rights");
15
- r.entity("note", noteEntity);
14
+ r.requires({ features: ["user-data-rights"] });
15
+ r.entity({ name: "note", ...noteEntity });
16
16
 
17
17
  r.useExtension(EXT_USER_DATA, "note", {
18
18
  export: async (ctx) => ({
@@ -55,7 +55,7 @@ export const noteEntity = createEntity({
55
55
  export const notesTable = deriveEntityTableMeta("note", noteEntity, { source: "unmanaged" });
56
56
 
57
57
  export const notesFeature = defineFeature("notes", (r) => {
58
- r.requires("user-data-rights");
58
+ r.requires({ features: ["user-data-rights"] });
59
59
  // store_notes is a direct-write store: the forget hook below `updateMany`/
60
60
  // `deleteMany`s rows WITHOUT emitting lifecycle events. r.entity would make
61
61
  // it a rebuildable implicit projection whose replay finds zero note events
@@ -35,7 +35,7 @@ export function createAccountFeature(): FeatureDefinition {
35
35
  "entry — user-profile registers the `profile` screen itself " +
36
36
  "(fw#2312 declarative projectionDetail).",
37
37
  );
38
- r.requires("user-profile");
38
+ r.requires({ features: ["user-profile"] });
39
39
 
40
40
  r.nav({
41
41
  id: "profile",
@@ -33,7 +33,7 @@ const incidentExecutor = createEventStoreExecutor(incidentTable, incidentEntity,
33
33
  const INCIDENT_HOOK_URL = "https://hooks.example/incident";
34
34
 
35
35
  export const webhookDemoFeature = defineFeature("webhook-demo", (r) => {
36
- r.entity("incident", incidentEntity);
36
+ r.entity({ name: "incident", ...incidentEntity });
37
37
  r.requires.step("webhook.send");
38
38
  r.requires.step("mail.send");
39
39
  r.requires.step("callFeature");
@@ -86,19 +86,19 @@ export const noteDeskFeature = defineFeature("note-desk", (r) => {
86
86
  access: open.access,
87
87
  });
88
88
 
89
- r.queryHandler(
90
- "note:summary",
91
- z.object({ id: z.string() }),
92
- async (query) => ({ id: query.payload.id, ...NOTE_DETAIL_FIELDS }),
93
- open,
94
- );
89
+ r.queryHandler({
90
+ name: "note:summary",
91
+ schema: z.object({ id: z.string() }),
92
+ handler: async (query) => ({ id: query.payload.id, ...NOTE_DETAIL_FIELDS }),
93
+ ...open,
94
+ });
95
95
 
96
- r.writeHandler(
97
- "note:comment",
98
- commentPayloadSchema,
99
- async () => ({ isSuccess: true as const, data: null }),
100
- open,
101
- );
96
+ r.writeHandler({
97
+ name: "note:comment",
98
+ schema: commentPayloadSchema,
99
+ handler: async () => ({ isSuccess: true as const, data: null }),
100
+ ...open,
101
+ });
102
102
 
103
103
  r.screen({
104
104
  id: "note-detail",