@cosmicdrift/kumiko-samples 0.365.0 → 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 (180) 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 +13 -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/changes.json +8 -1
  86. package/packages/bundled-features/src/step-dispatcher/constants.ts +22 -0
  87. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +236 -0
  88. package/packages/bundled-features/src/step-dispatcher/feature.ts +66 -119
  89. package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +40 -14
  90. package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
  91. package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
  92. package/packages/bundled-features/src/tags/feature.ts +2 -2
  93. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  94. package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
  95. package/packages/bundled-features/src/tenant/feature.ts +4 -4
  96. package/packages/bundled-features/src/tenant/seeding.ts +1 -1
  97. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
  98. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +6 -1
  99. package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
  100. package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
  101. package/packages/bundled-features/src/user/feature.ts +1 -1
  102. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  103. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
  104. package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
  105. package/packages/bundled-features/src/user-profile/feature.ts +4 -4
  106. package/packages/bundled-features/src/waitlist/feature.ts +2 -2
  107. package/samples/apps/cap-billing-demo/README.md +1 -1
  108. package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
  109. package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
  110. package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
  111. package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
  112. package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
  113. package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
  114. package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
  115. package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
  116. package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
  117. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
  118. package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
  119. package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
  120. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
  121. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
  122. package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
  123. package/samples/recipes/access-control/src/feature.ts +21 -25
  124. package/samples/recipes/anonymous-access/src/feature.ts +36 -43
  125. package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
  126. package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
  127. package/samples/recipes/basic-entity/README.md +1 -1
  128. package/samples/recipes/boot-check/README.md +1 -1
  129. package/samples/recipes/boot-check/src/feature.ts +2 -2
  130. package/samples/recipes/chat-channels/src/feature.ts +1 -1
  131. package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
  132. package/samples/recipes/currencies-global/src/feature.ts +3 -3
  133. package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
  134. package/samples/recipes/custom-fields-basic/README.md +1 -1
  135. package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
  136. package/samples/recipes/custom-handlers/README.md +3 -3
  137. package/samples/recipes/custom-handlers/src/feature.ts +37 -41
  138. package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
  139. package/samples/recipes/delivery-notifications/README.md +1 -1
  140. package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
  141. package/samples/recipes/designer-demo/src/feature.ts +2 -1
  142. package/samples/recipes/embedded/src/feature.ts +1 -1
  143. package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
  144. package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
  145. package/samples/recipes/error-contract/src/feature.ts +25 -25
  146. package/samples/recipes/event-sourcing/src/feature.ts +70 -76
  147. package/samples/recipes/feature-toggles/src/feature.ts +28 -20
  148. package/samples/recipes/files-post-processing/src/feature.ts +22 -26
  149. package/samples/recipes/idempotency/src/feature.ts +16 -18
  150. package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
  151. package/samples/recipes/lane-routing/src/feature.ts +10 -12
  152. package/samples/recipes/legal-pages/src/feature.ts +1 -1
  153. package/samples/recipes/lifecycle-hooks/README.md +4 -4
  154. package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
  155. package/samples/recipes/managed-config/src/feature.ts +8 -8
  156. package/samples/recipes/managed-pages/src/feature.ts +1 -1
  157. package/samples/recipes/notes-history-basic/README.md +1 -1
  158. package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
  159. package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
  160. package/samples/recipes/pipeline-basics/README.md +1 -1
  161. package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
  162. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  163. package/samples/recipes/rate-limiting/src/feature.ts +7 -9
  164. package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
  165. package/samples/recipes/reference-data/src/feature.ts +1 -1
  166. package/samples/recipes/relations/src/feature.ts +2 -2
  167. package/samples/recipes/screens-nav/src/feature.ts +1 -1
  168. package/samples/recipes/search/src/feature.ts +17 -19
  169. package/samples/recipes/security-baseline/README.md +1 -1
  170. package/samples/recipes/state-machine/src/feature.ts +1 -1
  171. package/samples/recipes/tags-basic/README.md +1 -1
  172. package/samples/recipes/tags-basic/src/feature.ts +2 -2
  173. package/samples/recipes/tenant-settings/README.md +1 -1
  174. package/samples/recipes/tenant-settings/src/feature.ts +1 -1
  175. package/samples/recipes/timezones/src/feature.ts +12 -12
  176. package/samples/recipes/user-data-rights/README.md +2 -2
  177. package/samples/recipes/user-data-rights/src/feature.ts +1 -1
  178. package/samples/recipes/user-profile/src/feature.ts +1 -1
  179. package/samples/recipes/webhook-step/src/feature.ts +1 -1
  180. package/samples/recipes/writeform-section/src/feature.ts +12 -12
@@ -52,41 +52,37 @@ export const productTable = buildEntityTable("product", productEntity);
52
52
  export const guestOrderTable = buildEntityTable("guest-order", guestOrderEntity);
53
53
 
54
54
  export const anonymousAccessFeature = defineFeature("shop", (r) => {
55
- r.entity("product", productEntity);
56
- r.entity("guest-order", guestOrderEntity);
55
+ r.entity({ name: "product", ...productEntity });
56
+ r.entity({ name: "guest-order", ...guestOrderEntity });
57
57
 
58
58
  // Public listing: anonymous + authenticated customers see it. Combining
59
59
  // the roles in one handler is preferred over duplicating the read path —
60
60
  // the handler body can branch on `event.user.id === "anonymous"` if it
61
61
  // needs to personalise (e.g. show a sign-in CTA, hide a wishlist button).
62
- r.queryHandler(
63
- "product:list",
64
- z.object({}),
65
- async (_event, ctx) => ctx.db.selectMany(productTable),
66
- {
67
- access: { roles: [...access.anonymous, "User", "Admin"] },
68
- rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
69
- },
70
- );
62
+ r.queryHandler({
63
+ name: "product:list",
64
+ schema: z.object({}),
65
+ handler: async (_event, ctx) => ctx.db.selectMany(productTable),
66
+ access: { roles: [...access.anonymous, "User", "Admin"] },
67
+ rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
68
+ });
71
69
 
72
70
  // Authenticated-only listing — same data, different access rule. The
73
71
  // openToAll declaration here is the regression-guard: even with
74
72
  // anonymousAccess enabled on the server, this endpoint stays gated to
75
73
  // logged-in users.
76
- r.queryHandler(
77
- "product:authenticated-only",
78
- z.object({}),
79
- async (_event, ctx) => ctx.db.selectMany(productTable),
80
- {
81
- access: {
82
- openToAll: {
83
- reason:
84
- "any signed-in (non-anonymous) user may list products here; this handler is " +
85
- "the regression-guard proving anonymousAccess never silently exposes an authenticated-only endpoint",
86
- },
74
+ r.queryHandler({
75
+ name: "product:authenticated-only",
76
+ schema: z.object({}),
77
+ handler: async (_event, ctx) => ctx.db.selectMany(productTable),
78
+ access: {
79
+ openToAll: {
80
+ reason:
81
+ "any signed-in (non-anonymous) user may list products here; this handler is " +
82
+ "the regression-guard proving anonymousAccess never silently exposes an authenticated-only endpoint",
87
83
  },
88
84
  },
89
- );
85
+ });
90
86
 
91
87
  // Anonymous + authenticated guest checkout. Stores the synthesised user-id
92
88
  // ("anonymous") in `placedBy` so audit-trail / support queries can tell
@@ -97,36 +93,33 @@ export const anonymousAccessFeature = defineFeature("shop", (r) => {
97
93
  const guestOrderExecutor = createEventStoreExecutor(guestOrderTable, guestOrderEntity, {
98
94
  entityName: "guest-order",
99
95
  });
100
- r.writeHandler(
101
- "guest-order:place",
102
- z.object({
96
+ r.writeHandler({
97
+ name: "guest-order:place",
98
+ schema: z.object({
103
99
  productId: z.uuid(),
104
100
  email: z.string().email(),
105
101
  }),
106
- async (event, ctx) =>
107
- guestOrderExecutor.create({ ...event.payload, placedBy: event.user.id }, event.user, ctx.db),
108
- {
109
- // `email` is recordOwned PII (personal: { of: "id" }) written by an
110
- // anonymous root handler — the runtime personal-data gate
111
- // (write-origin.ts) requires this explicit opt-in, the same way the
112
- // boot validator would reject an openToAll declaration missing it.
113
- access: { roles: [...access.anonymous, "User"], personalData: "public-intake" },
114
- rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
115
- },
116
- );
102
+ handler: async (event, ctx) =>
103
+ guestOrderExecutor.create({ ...event.payload, placedBy: event.user.id }, event.user, ctx.db), // `email` is recordOwned PII (personal: { of: "id" }) written by an
104
+ // anonymous root handler — the runtime personal-data gate
105
+ // (write-origin.ts) requires this explicit opt-in, the same way the
106
+ // boot validator would reject an openToAll declaration missing it.
107
+ access: { roles: [...access.anonymous, "User"], personalData: "public-intake" },
108
+ rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
109
+ });
117
110
 
118
111
  // Admin-only product CRUD — proves role-gated handlers still reject
119
112
  // anonymous callers. Same shape as any other handler in the framework:
120
113
  // anonymous is just a regular role, not a special case.
121
- r.writeHandler(
122
- "product:create",
123
- z.object({ name: z.string().min(1), priceCents: z.string() }),
124
- async (event, ctx) => {
114
+ r.writeHandler({
115
+ name: "product:create",
116
+ schema: z.object({ name: z.string().min(1), priceCents: z.string() }),
117
+ handler: async (event, ctx) => {
125
118
  const exec = createEventStoreExecutor(productTable, productEntity, {
126
119
  entityName: "product",
127
120
  });
128
121
  return exec.create(event.payload, event.user, ctx.db);
129
122
  },
130
- { access: { roles: ["Admin"] } },
131
- );
123
+ access: { roles: ["Admin"] },
124
+ });
132
125
  });
@@ -34,32 +34,30 @@ export const productEntity = createEntity({
34
34
  export const productTable = buildEntityTable("product", productEntity);
35
35
 
36
36
  export const multiTenantShopFeature = defineFeature("mtshop", (r) => {
37
- r.entity("product", productEntity);
37
+ r.entity({ name: "product", ...productEntity });
38
38
 
39
39
  // Public listing — every tenant has its own products, scoped by tenantId
40
40
  // automatically via the framework's tenant-isolation. Anonymous visitors
41
41
  // see only the tenant they landed on.
42
- r.queryHandler(
43
- "product:list",
44
- z.object({}),
45
- async (_event, ctx) => ctx.db.selectMany(productTable),
46
- {
47
- access: { roles: [...access.anonymous, "User", "Admin"] },
48
- rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
49
- },
50
- );
42
+ r.queryHandler({
43
+ name: "product:list",
44
+ schema: z.object({}),
45
+ handler: async (_event, ctx) => ctx.db.selectMany(productTable),
46
+ access: { roles: [...access.anonymous, "User", "Admin"] },
47
+ rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
48
+ });
51
49
 
52
50
  // Admin-only — proves role-gating still works on top of the multi-tenant
53
51
  // anonymous resolution.
54
- r.writeHandler(
55
- "product:create",
56
- z.object({ name: z.string().min(1) }),
57
- async (event, ctx) => {
52
+ r.writeHandler({
53
+ name: "product:create",
54
+ schema: z.object({ name: z.string().min(1) }),
55
+ handler: async (event, ctx) => {
58
56
  const exec = createEventStoreExecutor(productTable, productEntity, {
59
57
  entityName: "product",
60
58
  });
61
59
  return exec.create(event.payload, event.user, ctx.db);
62
60
  },
63
- { access: { roles: ["Admin"] } },
64
- );
61
+ access: { roles: ["Admin"] },
62
+ });
65
63
  });
@@ -47,7 +47,7 @@ export function createSubdomainTenantRoutingFeature(
47
47
  const reserved = new Set(opts.reservedSubdomains ?? ["www", "app", "api", "admin"]);
48
48
 
49
49
  return defineFeature("sample-subdomain-tenant-routing", (r) => {
50
- r.requires("auth-foundation");
50
+ r.requires({ features: ["auth-foundation"] });
51
51
 
52
52
  const resolverPlugin: TenantResolverProvider = {
53
53
  trust,
@@ -28,7 +28,7 @@ task-management → single feature, single `task` entity, six CRUD handlers
28
28
  ```
29
29
 
30
30
  No bundled features required — this is the baseline before you add
31
- `r.requires("tenant")`, auth, or cross-feature extensions.
31
+ `r.requires({ features: ["tenant"] })`, auth, or cross-feature extensions.
32
32
 
33
33
  ## Flow
34
34
 
@@ -13,7 +13,7 @@ never mounted, and nothing caught it — until now.
13
13
  feature-prefixed message.
14
14
  - **Conditional invariants `r.requires` can't express** — the check only
15
15
  fails when *this* feature's own shape demands it (has a PII field), not
16
- unconditionally. A plain `r.requires("user-data-hook")` would fail even
16
+ unconditionally. A plain `r.requires({ features: ["user-data-hook"] })` would fail even
17
17
  for a prompt-store variant with no PII fields at all.
18
18
  - **`validateBoot`** — the framework's boot-time validator, run directly
19
19
  against a feature list. `bootCheck` has no DB dependency, so this is the
@@ -6,7 +6,7 @@ import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumik
6
6
 
7
7
  // A prompt-store integration trap: a feature with a PII field was mounted
8
8
  // without its required companion feature, and nothing caught it at boot.
9
- // r.requires("user-data-hook") can't express this — it would
9
+ // r.requires({ features: ["user-data-hook"] }) can't express this — it would
10
10
  // fail even for a prompt-store variant with no PII fields at all.
11
11
  const promptFields = {
12
12
  text: createTextField({
@@ -16,7 +16,7 @@ const promptFields = {
16
16
  };
17
17
 
18
18
  export const promptStoreFeature = defineFeature("prompt-store", (r) => {
19
- r.entity("prompt", createEntity({ fields: promptFields }));
19
+ r.entity({ name: "prompt", ...createEntity({ fields: promptFields }) });
20
20
  r.bootCheck(({ features }) => {
21
21
  const hasPiiField = Object.values(promptFields).some((field) => field.pii);
22
22
  const hasUserDataHook = features.some((f) => f.name === "user-data-hook");
@@ -16,7 +16,7 @@ import * as z from "zod";
16
16
  export const OPS_ANNOUNCEMENT_TYPE = qn("ops", "notify", "announcement");
17
17
 
18
18
  export const opsFeature = defineFeature("ops", (r) => {
19
- r.requires("delivery");
19
+ r.requires({ features: ["delivery"] });
20
20
 
21
21
  r.writeHandler(
22
22
  defineWriteHandler({
@@ -64,7 +64,7 @@ export const capturedEvents: CapturedEvent[] = [];
64
64
  // --- Feature ---
65
65
 
66
66
  export const pubsubOrderFeature = defineFeature("pubsubOrders", (r) => {
67
- r.entity("pubsub-order", orderEntity);
67
+ r.entity({ name: "pubsub-order", ...orderEntity });
68
68
 
69
69
  // Define the event shape once. The returned `.name` is the qualified
70
70
  // name ("pubsub-orders:event:order-placed") — pass it to ctx.appendEvent.
@@ -76,10 +76,10 @@ export const pubsubOrderFeature = defineFeature("pubsubOrders", (r) => {
76
76
 
77
77
  const { executor: orderExecutor } = createEntityExecutor("pubsub-order", orderEntity);
78
78
 
79
- r.writeHandler(
80
- "order:place",
81
- z.object({ customer: z.string().min(1), product: z.string().min(1) }),
82
- async (event, ctx) => {
79
+ r.writeHandler({
80
+ name: "order:place",
81
+ schema: z.object({ customer: z.string().min(1), product: z.string().min(1) }),
82
+ handler: async (event, ctx) => {
83
83
  const result = await orderExecutor.create(event.payload, event.user, ctx.db);
84
84
  if (result.isSuccess) {
85
85
  // emitEvent runs inside the executor's TX and is typed against
@@ -100,8 +100,8 @@ export const pubsubOrderFeature = defineFeature("pubsubOrders", (r) => {
100
100
  }
101
101
  return result;
102
102
  },
103
- { access: { roles: ["Admin", "Customer"] } },
104
- );
103
+ access: { roles: ["Admin", "Customer"] },
104
+ });
105
105
 
106
106
  // Async consumer — Marten-style. Fires after commit, via the event-
107
107
  // dispatcher. No `table` means this MSP is pure side-effect (no state
@@ -49,9 +49,9 @@ const CURRENCY_CATALOG = [
49
49
  ] as const;
50
50
 
51
51
  export const currenciesGlobalFeature = defineFeature("currenciesGlobal", (r) => {
52
- const currency = r.entity("currency", currencyEntity);
53
- r.entity("tenant-currency", tenantCurrencyEntity);
54
- r.entity("invoice", invoiceEntity);
52
+ const currency = r.entity({ name: "currency", ...currencyEntity });
53
+ r.entity({ name: "tenant-currency", ...tenantCurrencyEntity });
54
+ r.entity({ name: "invoice", ...invoiceEntity });
55
55
 
56
56
  // Seed global currency table — available for all tenants to pick from
57
57
  r.referenceData(currency, [...CURRENCY_CATALOG], { upsertKey: "code" });
@@ -19,8 +19,8 @@ export { currencyEntity } from "./entities/currency";
19
19
  export { invoiceEntity } from "./entities/invoice";
20
20
 
21
21
  export const currenciesPerTenantFeature = defineFeature("currenciesPerTenant", (r) => {
22
- r.entity("currency", currencyEntity);
23
- r.entity("invoice", invoiceEntity);
22
+ r.entity({ name: "currency", ...currencyEntity });
23
+ r.entity({ name: "invoice", ...invoiceEntity });
24
24
 
25
25
  // No r.referenceData() — tenants create currencies themselves
26
26
  r.writeHandler(currencyCreate);
@@ -36,7 +36,7 @@ property-management → our feature: opts the `property` entity into
36
36
  custom-fields via wireCustomFieldsFor
37
37
  ```
38
38
 
39
- The `property-management` feature `r.requires("custom-fields")` — the
39
+ The `property-management` feature `r.requires({ features: ["custom-fields"] })` — the
40
40
  bundle is non-optional for this recipe because the wired entity would
41
41
  otherwise have an empty jsonb column and no way to write into it.
42
42
 
@@ -53,9 +53,10 @@ function propertyExecutor() {
53
53
  // --- Feature ---
54
54
 
55
55
  export const propertyFeature = defineFeature("property-management", (r) => {
56
- r.requires("custom-fields");
56
+ r.requires({ features: ["custom-fields"] });
57
57
 
58
- r.entity("property", {
58
+ r.entity({
59
+ name: "property",
59
60
  table: "read_sample_cf_properties",
60
61
  fields: {
61
62
  name: { type: "text", required: true, maxLength: 200 },
@@ -13,9 +13,9 @@ logic is yours.
13
13
 
14
14
  ## What it shows
15
15
 
16
- - **`r.writeHandler(name, schema, handler, options)`** — the positional
17
- inline form, useful when the handler body is short and stays close to
18
- its registration.
16
+ - **`r.writeHandler({ name, schema, handler, access })`** — the inline
17
+ form, useful when the handler body is short and stays close to its
18
+ registration.
19
19
  - **Read-modify-write with optimistic locking** — `increment` reads the
20
20
  current count, computes the new one, and updates with the version it
21
21
  read. Two concurrent increments cannot both succeed.
@@ -25,7 +25,7 @@ export const counterEntity = createEntity({
25
25
  });
26
26
 
27
27
  export const counterFeature = defineFeature("counters", (r) => {
28
- r.entity("counter", counterEntity);
28
+ r.entity({ name: "counter", ...counterEntity });
29
29
 
30
30
  // createEntityExecutor bundles buildEntityTable + createEventStoreExecutor —
31
31
  // the same pair every custom write-handler opens with. Collapses 3 lines
@@ -33,19 +33,19 @@ export const counterFeature = defineFeature("counters", (r) => {
33
33
  const { executor: counterExecutor } = createEntityExecutor("counter", counterEntity);
34
34
 
35
35
  // Standard create
36
- r.writeHandler(
37
- "counter:create",
38
- z.object({ name: z.string().min(1) }),
39
- async (event, ctx) =>
36
+ r.writeHandler({
37
+ name: "counter:create",
38
+ schema: z.object({ name: z.string().min(1) }),
39
+ handler: async (event, ctx) =>
40
40
  counterExecutor.create({ ...event.payload, count: 0 }, event.user, ctx.db),
41
- { access: { roles: ["Admin"] } },
42
- );
41
+ access: { roles: ["Admin"] },
42
+ });
43
43
 
44
44
  // Custom handler: increment with business logic
45
- r.writeHandler(
46
- "counter:increment",
47
- z.object({ id: z.uuid(), amount: z.number().min(1).max(100) }),
48
- async (event, ctx) => {
45
+ r.writeHandler({
46
+ name: "counter:increment",
47
+ schema: z.object({ id: z.uuid(), amount: z.number().min(1).max(100) }),
48
+ handler: async (event, ctx) => {
49
49
  const current = await counterExecutor.detail({ id: event.payload.id }, event.user, ctx.db);
50
50
  if (!current) {
51
51
  return failNotFound("counter", event.payload.id);
@@ -65,14 +65,14 @@ export const counterFeature = defineFeature("counters", (r) => {
65
65
  ctx.db,
66
66
  );
67
67
  },
68
- { access: { roles: ["Admin", "User"] } },
69
- );
68
+ access: { roles: ["Admin", "User"] },
69
+ });
70
70
 
71
71
  // Custom handler: reset to zero
72
- r.writeHandler(
73
- "counter:reset",
74
- z.object({ id: z.uuid() }),
75
- async (event, ctx) =>
72
+ r.writeHandler({
73
+ name: "counter:reset",
74
+ schema: z.object({ id: z.uuid() }),
75
+ handler: async (event, ctx) =>
76
76
  // Admin reset: last-writer-wins is intentional — there's no useful
77
77
  // concurrent-reset race to guard against.
78
78
  counterExecutor.update(
@@ -81,16 +81,16 @@ export const counterFeature = defineFeature("counters", (r) => {
81
81
  ctx.db,
82
82
  { skipOptimisticLock: true },
83
83
  ),
84
- { access: { roles: ["Admin"] } },
85
- );
84
+ access: { roles: ["Admin"] },
85
+ });
86
86
 
87
87
  // Custom query: only counters above a threshold. list() returns one page
88
88
  // (default 50, max 200, MAX_LIST_LIMIT), so a JS filter would miss rows after
89
89
  // page 1; filter in the query instead. totalCount counts over the whole WHERE.
90
- r.queryHandler(
91
- "counter:active",
92
- z.object({ minCount: z.number().int().default(1), cursor: z.string().optional() }),
93
- async (query, ctx) =>
90
+ r.queryHandler({
91
+ name: "counter:active",
92
+ schema: z.object({ minCount: z.number().int().default(1), cursor: z.string().optional() }),
93
+ handler: async (query, ctx) =>
94
94
  counterExecutor.list(
95
95
  {
96
96
  filter: { field: "count", op: "gte", value: query.payload.minCount },
@@ -100,28 +100,24 @@ export const counterFeature = defineFeature("counters", (r) => {
100
100
  query.user,
101
101
  ctx.db,
102
102
  ),
103
- {
104
- access: {
105
- openToAll: {
106
- reason:
107
- "any signed-in user may list active counters; the sample has no per-user counter ownership",
108
- },
103
+ access: {
104
+ openToAll: {
105
+ reason:
106
+ "any signed-in user may list active counters; the sample has no per-user counter ownership",
109
107
  },
110
108
  },
111
- );
109
+ });
112
110
 
113
111
  // Standard detail
114
- r.queryHandler(
115
- "counter:detail",
116
- z.object({ id: z.uuid() }),
117
- async (query, ctx) => counterExecutor.detail(query.payload, query.user, ctx.db),
118
- {
119
- access: {
120
- openToAll: {
121
- reason:
122
- "any signed-in user may read a counter by id; the sample has no per-user counter ownership",
123
- },
112
+ r.queryHandler({
113
+ name: "counter:detail",
114
+ schema: z.object({ id: z.uuid() }),
115
+ handler: async (query, ctx) => counterExecutor.detail(query.payload, query.user, ctx.db),
116
+ access: {
117
+ openToAll: {
118
+ reason:
119
+ "any signed-in user may read a counter by id; the sample has no per-user counter ownership",
124
120
  },
125
121
  },
126
- );
122
+ });
127
123
  });
@@ -36,10 +36,10 @@ export const ticketFeature = defineFeature("tickets", (r) => {
36
36
  // Flat result: each dashboard stat reads one field via its valueField.
37
37
  // limit: 1 keeps the page tiny; totalCount counts over the whole WHERE in
38
38
  // the database, so no rows are loaded just to be counted.
39
- r.queryHandler(
40
- "ticket:stats",
41
- z.object({}),
42
- async (query, ctx) => {
39
+ r.queryHandler({
40
+ name: "ticket:stats",
41
+ schema: z.object({}),
42
+ handler: async (query, ctx) => {
43
43
  const countWithStatus = async (status: (typeof TICKET_STATUSES)[number]): Promise<number> => {
44
44
  const page = await ticketExecutor.list(
45
45
  { filter: { field: "status", op: "eq", value: status }, limit: 1, totalCount: true },
@@ -54,8 +54,8 @@ export const ticketFeature = defineFeature("tickets", (r) => {
54
54
  ]);
55
55
  return { open, closed, total: open + closed };
56
56
  },
57
- { access: readerRoles },
58
- );
57
+ access: readerRoles,
58
+ });
59
59
 
60
60
  r.translations({
61
61
  keys: {
@@ -28,7 +28,7 @@ channel-inApp / channel-email / channel-push
28
28
  support → our business logic (tickets + the notification declaration)
29
29
  ```
30
30
 
31
- The `support` feature `r.requires("delivery")`: channels are features that attach to delivery, but `support` doesn't need to know them.
31
+ The `support` feature `r.requires({ features: ["delivery"] })`: channels are features that attach to delivery, but `support` doesn't need to know them.
32
32
 
33
33
  ## Flow
34
34
 
@@ -47,9 +47,9 @@ function ticketExecutor() {
47
47
  export const TICKET_ASSIGNED = qn("support", QnTypes.notify, "ticket-assigned");
48
48
 
49
49
  export const supportFeature = defineFeature("support", (r) => {
50
- r.requires("delivery");
50
+ r.requires({ features: ["delivery"] });
51
51
 
52
- r.entity("ticket", ticketEntity);
52
+ r.entity({ name: "ticket", ...ticketEntity });
53
53
 
54
54
  r.notification("ticket-assigned", {
55
55
  variables: {
@@ -79,16 +79,16 @@ export const supportFeature = defineFeature("support", (r) => {
79
79
  },
80
80
  });
81
81
 
82
- r.writeHandler(
83
- "ticket:create",
84
- z.object({
82
+ r.writeHandler({
83
+ name: "ticket:create",
84
+ schema: z.object({
85
85
  title: z.string().min(1),
86
86
  description: z.string().optional(),
87
87
  assigneeId: z.uuid().optional(),
88
88
  priority: z.enum(["low", "normal", "critical"]),
89
89
  status: z.string().default("open"),
90
90
  }),
91
- async (event, ctx) => {
91
+ handler: async (event, ctx) => {
92
92
  const created = await ticketExecutor().create(event.payload, event.user, ctx.db);
93
93
  const assigneeId = event.payload.assigneeId;
94
94
  // No assignee, no notification.
@@ -109,6 +109,6 @@ export const supportFeature = defineFeature("support", (r) => {
109
109
  });
110
110
  return created;
111
111
  },
112
- { access: { roles: ["Admin", "Support"] } },
113
- );
112
+ access: { roles: ["Admin", "Support"] },
113
+ });
114
114
  });
@@ -12,7 +12,8 @@ import { z } from "zod";
12
12
  defineFeature("designerDemo", (r) => {
13
13
  r.optionalRequires("analytics");
14
14
 
15
- r.entity("task", {
15
+ r.entity({
16
+ name: "task",
16
17
  fields: {
17
18
  title: { type: "text", required: true },
18
19
  done: { type: "boolean", default: false },
@@ -39,7 +39,7 @@ const openRead = {
39
39
  } as const;
40
40
 
41
41
  export const embeddedFeature = defineFeature("contacts", (r) => {
42
- r.entity("contact", contactEntity);
42
+ r.entity({ name: "contact", ...contactEntity });
43
43
 
44
44
  r.writeHandler(contactCreate);
45
45
  r.queryHandler(contactDetail);
@@ -55,7 +55,7 @@ export const prospectsFeature = defineFeature("prospects", (r) => {
55
55
  read: openRead,
56
56
  verbs: { update: false },
57
57
  });
58
- r.entity("prospect", prospectEntity);
58
+ r.entity({ name: "prospect", ...prospectEntity });
59
59
 
60
60
  const { executor: suggestionExecutor } = createEntityExecutor("suggestion", suggestionEntity);
61
61
  const { executor: prospectExecutor } = createEntityExecutor("prospect", prospectEntity);
@@ -64,10 +64,10 @@ export const prospectsFeature = defineFeature("prospects", (r) => {
64
64
  // edited (RenderEdit's onChange `changes`, same delta the controlled mode
65
65
  // reports) — the handler owns merging that onto the suggestion it already
66
66
  // has, so an untouched field never has to round-trip through the browser.
67
- r.writeHandler(
68
- "prospect:accept",
69
- z.object({ suggestionId: z.uuid(), changes: acceptChangesSchema }),
70
- async (event, ctx) => {
67
+ r.writeHandler({
68
+ name: "prospect:accept",
69
+ schema: z.object({ suggestionId: z.uuid(), changes: acceptChangesSchema }),
70
+ handler: async (event, ctx) => {
71
71
  const suggestion = await suggestionExecutor.detail(
72
72
  { id: event.payload.suggestionId },
73
73
  event.user,
@@ -108,13 +108,13 @@ export const prospectsFeature = defineFeature("prospects", (r) => {
108
108
  if (!flipped.isSuccess) return flipped;
109
109
  return created;
110
110
  },
111
- adminWrite,
112
- );
111
+ ...adminWrite,
112
+ });
113
113
 
114
- r.queryHandler(
115
- "prospect:detail",
116
- z.object({ id: z.uuid() }),
117
- async (query, ctx) => prospectExecutor.detail(query.payload, query.user, ctx.db),
118
- openRead,
119
- );
114
+ r.queryHandler({
115
+ name: "prospect:detail",
116
+ schema: z.object({ id: z.uuid() }),
117
+ handler: async (query, ctx) => prospectExecutor.detail(query.payload, query.user, ctx.db),
118
+ ...openRead,
119
+ });
120
120
  });
@@ -80,7 +80,7 @@ const chargeHandler = defineWriteHandler({
80
80
  });
81
81
 
82
82
  export const billingFeature = defineFeature(FEATURE, (r) => {
83
- r.requires("config");
83
+ r.requires({ features: ["config"] });
84
84
  r.config("stripe-api-key", stripeApiKeyDef);
85
85
  r.writeHandler(chargeHandler);
86
86
  // Kein r.screen/r.nav: der `mask`-Eintrag auf dem Key lässt