@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
@@ -6,10 +6,10 @@
6
6
  // DSGVO-Endpoints kommen vollstaendig aus user-data-rights.
7
7
  //
8
8
  // Dieses Feature zeigt:
9
- // - r.entity("todo", todoEntity) → Drizzle-Tabelle wird gebaut
10
- // - r.writeHandler("create") → User legt Todo an
11
- // - r.queryHandler("list") → User sieht seine Todos
12
- // - r.useExtension(EXT_USER_DATA, "todo") → Forget + Export integration
9
+ // - r.entity({ name: "todo", ...todoEntity }) → Drizzle-Tabelle wird gebaut
10
+ // - r.writeHandler({ name: "create", … }) → User legt Todo an
11
+ // - r.queryHandler({ name: "list", … }) → User sieht seine Todos
12
+ // - r.useExtension(EXT_USER_DATA, "todo") → Forget + Export integration
13
13
  //
14
14
  // Was passiert wenn der User dann request-export aufruft:
15
15
  // 1. user-data-rights.request-export.write triggert einen Job
@@ -122,7 +122,7 @@ const listTodosHandler = defineQueryHandler({
122
122
  });
123
123
 
124
124
  export const todosFeature = defineFeature(FEATURE_NAME, (r) => {
125
- r.requires("user-data-rights");
125
+ r.requires({ features: ["user-data-rights"] });
126
126
 
127
127
  // store_todos is a direct-write store: the create handler `insertOne`s and
128
128
  // the forget hook `updateMany`/`deleteMany`s rows WITHOUT emitting lifecycle
@@ -64,8 +64,8 @@ export const taskTable = buildEntityTable("task", taskEntity, {
64
64
  });
65
65
 
66
66
  export const accessControlFeature = defineFeature("access-control", (r) => {
67
- r.entity("project", projectEntity);
68
- r.entity("task", taskEntity);
67
+ r.entity({ name: "project", ...projectEntity });
68
+ r.entity({ name: "task", ...taskEntity });
69
69
  r.relation("task", "project", taskRelations.project);
70
70
 
71
71
  // Admin-only: standard create — no business logic, helper handles schema +
@@ -80,14 +80,14 @@ export const accessControlFeature = defineFeature("access-control", (r) => {
80
80
  // hand-write this handler instead of using the helper. The framework helpers
81
81
  // are deliberately scope-limited — opt out the moment you need defaults.
82
82
  const taskExecutor = createEventStoreExecutor(taskTable, taskEntity, { entityName: "task" });
83
- r.writeHandler(
84
- "task:create",
85
- z.object({
83
+ r.writeHandler({
84
+ name: "task:create",
85
+ schema: z.object({
86
86
  title: z.string().min(1),
87
87
  projectId: z.uuid(),
88
88
  assigneeId: z.uuid().optional(),
89
89
  }),
90
- async (event, ctx) =>
90
+ handler: async (event, ctx) =>
91
91
  taskExecutor.create(
92
92
  {
93
93
  ...event.payload,
@@ -96,22 +96,20 @@ export const accessControlFeature = defineFeature("access-control", (r) => {
96
96
  event.user,
97
97
  ctx.db,
98
98
  ),
99
- {
100
- access: {
101
- openToAll: {
102
- reason:
103
- "any signed-in tenant user may create a task; assigneeId defaults to the caller when omitted",
104
- },
99
+ access: {
100
+ openToAll: {
101
+ reason:
102
+ "any signed-in tenant user may create a task; assigneeId defaults to the caller when omitted",
105
103
  },
106
104
  },
107
- );
105
+ });
108
106
 
109
107
  // Update requires version — optimistic locking is the default and this
110
108
  // narrowed schema (only title + assigneeId mutable) is stricter than the
111
109
  // helper's generic update schema, so we keep it hand-written.
112
- r.writeHandler(
113
- "task:update",
114
- z.object({
110
+ r.writeHandler({
111
+ name: "task:update",
112
+ schema: z.object({
115
113
  id: z.uuid(),
116
114
  version: z.number(),
117
115
  changes: z.object({
@@ -119,17 +117,15 @@ export const accessControlFeature = defineFeature("access-control", (r) => {
119
117
  assigneeId: z.uuid().optional(),
120
118
  }),
121
119
  }),
122
- async (event, ctx) => taskExecutor.update(event.payload, event.user, ctx.db),
123
- {
124
- access: {
125
- openToAll: {
126
- reason:
127
- "any signed-in tenant user may update a task; optimistic locking (version) " +
128
- "guards against concurrent overwrites, not caller identity",
129
- },
120
+ handler: async (event, ctx) => taskExecutor.update(event.payload, event.user, ctx.db),
121
+ access: {
122
+ openToAll: {
123
+ reason:
124
+ "any signed-in tenant user may update a task; optimistic locking (version) " +
125
+ "guards against concurrent overwrites, not caller identity",
130
126
  },
131
127
  },
132
- );
128
+ });
133
129
 
134
130
  // Self-service list — no custom logic.
135
131
  r.queryHandler(
@@ -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 },