@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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/admin-shell/feature.ts +9 -9
- package/packages/bundled-features/src/agent-tools/changes.json +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +87 -5
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +13 -0
- package/packages/bundled-features/src/auth-email-password/constants.ts +0 -2
- package/packages/bundled-features/src/auth-email-password/feature.ts +3 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/feature.ts +4 -4
- package/packages/bundled-features/src/auth-mfa-user-data/index.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +89 -28
- package/packages/bundled-features/src/cap-counter/changes.json +7 -0
- package/packages/bundled-features/src/cap-counter/constants.ts +3 -0
- package/packages/bundled-features/src/cap-counter/feature.ts +2 -2
- package/packages/bundled-features/src/cap-counter/tables.ts +5 -2
- package/packages/bundled-features/src/cap-overview/feature.ts +3 -3
- package/packages/bundled-features/src/channel-discord/feature.ts +2 -2
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
- package/packages/bundled-features/src/channel-push/feature.ts +1 -1
- package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
- package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
- package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
- package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
- package/packages/bundled-features/src/config/feature.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
- package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
- package/packages/bundled-features/src/data-retention/feature.ts +1 -1
- package/packages/bundled-features/src/data-retention/presets.ts +4 -4
- package/packages/bundled-features/src/delivery/changes.json +6 -0
- package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
- package/packages/bundled-features/src/delivery/delivery-service.ts +17 -2
- package/packages/bundled-features/src/delivery/feature.ts +9 -5
- package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
- package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
- package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
- package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
- package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
- package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/folders/feature.ts +2 -2
- package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +2 -2
- package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
- package/packages/bundled-features/src/ledger/feature.ts +5 -5
- package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
- package/packages/bundled-features/src/mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
- package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
- package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
- package/packages/bundled-features/src/metrics/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
- package/packages/bundled-features/src/readiness/feature.ts +2 -2
- package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
- package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/seo/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +17 -13
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
- package/packages/bundled-features/src/tags/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
- package/packages/bundled-features/src/tenant/feature.ts +4 -4
- package/packages/bundled-features/src/tenant/seeding.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
- package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
- package/packages/bundled-features/src/user/feature.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/README.md +1 -1
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
- package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
- package/packages/bundled-features/src/user-profile/feature.ts +4 -4
- package/packages/bundled-features/src/waitlist/feature.ts +2 -2
- package/samples/apps/cap-billing-demo/README.md +1 -1
- package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
- package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
- package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
- package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
- package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
- package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
- package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
- package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
- package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
- package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
- package/samples/recipes/access-control/src/feature.ts +21 -25
- package/samples/recipes/anonymous-access/src/feature.ts +36 -43
- package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
- package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
- package/samples/recipes/basic-entity/README.md +1 -1
- package/samples/recipes/boot-check/README.md +1 -1
- package/samples/recipes/boot-check/src/feature.ts +2 -2
- package/samples/recipes/chat-channels/src/feature.ts +1 -1
- package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
- package/samples/recipes/currencies-global/src/feature.ts +3 -3
- package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
- package/samples/recipes/custom-fields-basic/README.md +1 -1
- package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
- package/samples/recipes/custom-handlers/README.md +3 -3
- package/samples/recipes/custom-handlers/src/feature.ts +37 -41
- package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
- package/samples/recipes/delivery-notifications/README.md +1 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
- package/samples/recipes/designer-demo/src/feature.ts +2 -1
- package/samples/recipes/embedded/src/feature.ts +1 -1
- package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
- package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
- package/samples/recipes/error-contract/src/feature.ts +25 -25
- package/samples/recipes/event-sourcing/src/feature.ts +70 -76
- package/samples/recipes/feature-toggles/src/feature.ts +28 -20
- package/samples/recipes/files-post-processing/src/feature.ts +22 -26
- package/samples/recipes/idempotency/src/feature.ts +16 -18
- package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
- package/samples/recipes/lane-routing/src/feature.ts +10 -12
- package/samples/recipes/legal-pages/src/feature.ts +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -4
- package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
- package/samples/recipes/managed-config/src/feature.ts +8 -8
- package/samples/recipes/managed-pages/src/feature.ts +1 -1
- package/samples/recipes/notes-history-basic/README.md +1 -1
- package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
- package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
- package/samples/recipes/pipeline-basics/README.md +1 -1
- package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/rate-limiting/src/feature.ts +7 -9
- package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
- package/samples/recipes/reference-data/src/feature.ts +1 -1
- package/samples/recipes/relations/src/feature.ts +2 -2
- package/samples/recipes/screens-nav/src/feature.ts +1 -1
- package/samples/recipes/search/src/feature.ts +17 -19
- package/samples/recipes/security-baseline/README.md +1 -1
- package/samples/recipes/state-machine/src/feature.ts +1 -1
- package/samples/recipes/tags-basic/README.md +1 -1
- package/samples/recipes/tags-basic/src/feature.ts +2 -2
- package/samples/recipes/tenant-settings/README.md +1 -1
- package/samples/recipes/tenant-settings/src/feature.ts +1 -1
- package/samples/recipes/timezones/src/feature.ts +12 -12
- package/samples/recipes/user-data-rights/README.md +2 -2
- package/samples/recipes/user-data-rights/src/feature.ts +1 -1
- package/samples/recipes/user-profile/src/feature.ts +1 -1
- package/samples/recipes/webhook-step/src/feature.ts +1 -1
- package/samples/recipes/writeform-section/src/feature.ts +12 -12
|
@@ -74,17 +74,17 @@ export const orderEntity = createEntity({
|
|
|
74
74
|
const orderTable = buildEntityTable("order", orderEntity);
|
|
75
75
|
|
|
76
76
|
export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
|
|
77
|
-
r.entity("order", orderEntity);
|
|
77
|
+
r.entity({ name: "order", ...orderEntity });
|
|
78
78
|
|
|
79
79
|
// 1) Create — standard path. Zod schema rejects an empty cart; the
|
|
80
80
|
// dispatcher converts the ZodIssue into a ValidationError with
|
|
81
81
|
// details.fields[] (no handler code needed).
|
|
82
|
-
r.writeHandler(
|
|
83
|
-
"order:create",
|
|
84
|
-
z.object({
|
|
82
|
+
r.writeHandler({
|
|
83
|
+
name: "order:create",
|
|
84
|
+
schema: z.object({
|
|
85
85
|
totalCents: z.number().int().min(1, "cart_is_empty"),
|
|
86
86
|
}),
|
|
87
|
-
async (event, ctx) => {
|
|
87
|
+
handler: async (event, ctx) => {
|
|
88
88
|
const crud = createEventStoreExecutor(orderTable, orderEntity, { entityName: "order" });
|
|
89
89
|
return crud.create(
|
|
90
90
|
{ ...event.payload, ownerId: event.user.id, status: "draft" },
|
|
@@ -92,8 +92,8 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
|
|
|
92
92
|
ctx.db,
|
|
93
93
|
);
|
|
94
94
|
},
|
|
95
|
-
|
|
96
|
-
);
|
|
95
|
+
access: { roles: ["User", "Admin"] },
|
|
96
|
+
});
|
|
97
97
|
|
|
98
98
|
// 2) Pay — business rule: only placed orders can be paid. Shows three
|
|
99
99
|
// different failure shapes in a single handler:
|
|
@@ -103,10 +103,10 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
|
|
|
103
103
|
// - UnprocessableError with reason from OrdersLiteReasons
|
|
104
104
|
// - Framework-generated UnprocessableError with reason
|
|
105
105
|
// FrameworkReasons.invalidTransition via guardTransition
|
|
106
|
-
r.writeHandler(
|
|
107
|
-
"order:pay",
|
|
108
|
-
z.object({ id: z.uuid() }),
|
|
109
|
-
async (event, ctx) => {
|
|
106
|
+
r.writeHandler({
|
|
107
|
+
name: "order:pay",
|
|
108
|
+
schema: z.object({ id: z.uuid() }),
|
|
109
|
+
handler: async (event, ctx) => {
|
|
110
110
|
const [current] = await ctx.db.selectMany(orderTable, { id: event.payload.id });
|
|
111
111
|
|
|
112
112
|
// NotFoundError — automatic reason = "order_not_found" via snake-case
|
|
@@ -153,16 +153,16 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
|
|
|
153
153
|
ctx.db,
|
|
154
154
|
);
|
|
155
155
|
},
|
|
156
|
-
|
|
157
|
-
);
|
|
156
|
+
access: { roles: ["User", "Admin"] },
|
|
157
|
+
});
|
|
158
158
|
|
|
159
159
|
// 3) Cancel — shows ConflictError for a business conflict that isn't a
|
|
160
160
|
// transition or a stale write. E.g. a refund window expired; the row
|
|
161
161
|
// is fine but the action is no longer allowed.
|
|
162
|
-
r.writeHandler(
|
|
163
|
-
"order:cancel",
|
|
164
|
-
z.object({ id: z.uuid() }),
|
|
165
|
-
async (event, ctx) => {
|
|
162
|
+
r.writeHandler({
|
|
163
|
+
name: "order:cancel",
|
|
164
|
+
schema: z.object({ id: z.uuid() }),
|
|
165
|
+
handler: async (event, ctx) => {
|
|
166
166
|
const [current] = await ctx.db.selectMany(orderTable, { id: event.payload.id });
|
|
167
167
|
if (!current) return failNotFound("order", event.payload.id);
|
|
168
168
|
|
|
@@ -192,18 +192,18 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
|
|
|
192
192
|
ctx.db,
|
|
193
193
|
);
|
|
194
194
|
},
|
|
195
|
-
|
|
196
|
-
);
|
|
195
|
+
access: { roles: ["User", "Admin"] },
|
|
196
|
+
});
|
|
197
197
|
|
|
198
198
|
// 4) Update — demonstrates the throw-based path (KumikoError raised
|
|
199
199
|
// directly, not via writeFailure return). The dispatcher catches it
|
|
200
200
|
// and wraps it in a WriteErrorInfo the same as the return path.
|
|
201
201
|
// Use this style when a helper deep in the call tree needs to abort
|
|
202
202
|
// without threading a WriteResult back up.
|
|
203
|
-
r.writeHandler(
|
|
204
|
-
"order:rename",
|
|
205
|
-
z.object({ id: z.uuid(), nickname: z.string().min(1) }),
|
|
206
|
-
async (event, ctx) => {
|
|
203
|
+
r.writeHandler({
|
|
204
|
+
name: "order:rename",
|
|
205
|
+
schema: z.object({ id: z.uuid(), nickname: z.string().min(1) }),
|
|
206
|
+
handler: async (event, ctx) => {
|
|
207
207
|
const [current] = await ctx.db.selectMany(orderTable, { id: event.payload.id });
|
|
208
208
|
|
|
209
209
|
if (!current) {
|
|
@@ -233,6 +233,6 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
|
|
|
233
233
|
},
|
|
234
234
|
};
|
|
235
235
|
},
|
|
236
|
-
|
|
237
|
-
);
|
|
236
|
+
access: { roles: ["User", "Admin"] },
|
|
237
|
+
});
|
|
238
238
|
});
|
|
@@ -126,7 +126,7 @@ export const approverDirectoryTable = table("read_showcase_approver_directory",
|
|
|
126
126
|
// --- Feature ---
|
|
127
127
|
|
|
128
128
|
export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
129
|
-
r.entity("showcase-invoice", invoiceEntity);
|
|
129
|
+
r.entity({ name: "showcase-invoice", ...invoiceEntity });
|
|
130
130
|
|
|
131
131
|
// Two domain events. "approved" is versioned — v1 stored `amount` as
|
|
132
132
|
// string, v2 uses `amountCents` integer. The DECLARATIVE migration form
|
|
@@ -259,21 +259,21 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
259
259
|
|
|
260
260
|
// --- Write handlers ---
|
|
261
261
|
|
|
262
|
-
r.writeHandler(
|
|
263
|
-
"invoice:create",
|
|
264
|
-
z.object({ customer: z.string() }),
|
|
265
|
-
async (event, ctx) =>
|
|
262
|
+
r.writeHandler({
|
|
263
|
+
name: "invoice:create",
|
|
264
|
+
schema: z.object({ customer: z.string() }),
|
|
265
|
+
handler: async (event, ctx) =>
|
|
266
266
|
invoiceExecutor.create(
|
|
267
267
|
{ customer: event.payload.customer, status: "draft" },
|
|
268
268
|
event.user,
|
|
269
269
|
ctx.db,
|
|
270
270
|
),
|
|
271
|
-
|
|
272
|
-
);
|
|
271
|
+
access: { roles: ["Admin"] },
|
|
272
|
+
});
|
|
273
273
|
|
|
274
|
-
r.writeHandler(
|
|
275
|
-
"invoice:approve",
|
|
276
|
-
z.object({
|
|
274
|
+
r.writeHandler({
|
|
275
|
+
name: "invoice:approve",
|
|
276
|
+
schema: z.object({
|
|
277
277
|
id: z.uuid(),
|
|
278
278
|
amountCents: z.number().int(),
|
|
279
279
|
approvedBy: z.string(),
|
|
@@ -282,7 +282,7 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
282
282
|
geoRegion: z.string().optional(),
|
|
283
283
|
abTestBucket: z.string().optional(),
|
|
284
284
|
}),
|
|
285
|
-
async (event, ctx) => {
|
|
285
|
+
handler: async (event, ctx) => {
|
|
286
286
|
const headers: Record<string, string> = {};
|
|
287
287
|
if (event.payload.geoRegion) headers["geoRegion"] = event.payload.geoRegion;
|
|
288
288
|
if (event.payload.abTestBucket) headers["abTestBucket"] = event.payload.abTestBucket;
|
|
@@ -295,13 +295,13 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
295
295
|
});
|
|
296
296
|
return { isSuccess: true as const, data: { id: event.payload.id } };
|
|
297
297
|
},
|
|
298
|
-
|
|
299
|
-
);
|
|
298
|
+
access: { roles: ["Admin"] },
|
|
299
|
+
});
|
|
300
300
|
|
|
301
|
-
r.writeHandler(
|
|
302
|
-
"invoice:acknowledge",
|
|
303
|
-
z.object({ id: z.uuid(), approverId: z.string() }),
|
|
304
|
-
async (event, ctx) => {
|
|
301
|
+
r.writeHandler({
|
|
302
|
+
name: "invoice:acknowledge",
|
|
303
|
+
schema: z.object({ id: z.uuid(), approverId: z.string() }),
|
|
304
|
+
handler: async (event, ctx) => {
|
|
305
305
|
const row = await fetchOne<{ displayName: string }>(ctx.db, approverDirectoryTable, {
|
|
306
306
|
approverId: event.payload.approverId,
|
|
307
307
|
});
|
|
@@ -314,13 +314,13 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
314
314
|
});
|
|
315
315
|
return { isSuccess: true as const, data: { id: event.payload.id } };
|
|
316
316
|
},
|
|
317
|
-
|
|
318
|
-
);
|
|
317
|
+
access: { roles: ["Admin"] },
|
|
318
|
+
});
|
|
319
319
|
|
|
320
|
-
r.writeHandler(
|
|
321
|
-
"invoice:pay",
|
|
322
|
-
z.object({ id: z.uuid(), amountCents: z.number().int() }),
|
|
323
|
-
async (event, ctx) => {
|
|
320
|
+
r.writeHandler({
|
|
321
|
+
name: "invoice:pay",
|
|
322
|
+
schema: z.object({ id: z.uuid(), amountCents: z.number().int() }),
|
|
323
|
+
handler: async (event, ctx) => {
|
|
324
324
|
await ctx.appendEvent({
|
|
325
325
|
aggregateId: event.payload.id,
|
|
326
326
|
aggregateType: "showcase-invoice",
|
|
@@ -329,35 +329,33 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
329
329
|
});
|
|
330
330
|
return { isSuccess: true as const, data: { id: event.payload.id } };
|
|
331
331
|
},
|
|
332
|
-
{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
},
|
|
342
|
-
);
|
|
332
|
+
access: { roles: ["Admin"] },
|
|
333
|
+
// Per-user cap: a single admin shouldn't fire more than 5 pay
|
|
334
|
+
// operations per minute. Real production caps would be per
|
|
335
|
+
// tenant + handler ("tenant+handler") to keep one admin from
|
|
336
|
+
// monopolising the tenant's quota — kept simple here for the
|
|
337
|
+
// sample. Demonstrates the fourth dispatcher gate (rate-limit
|
|
338
|
+
// → access → validation → handler).
|
|
339
|
+
rateLimit: { per: "user", limit: 5, windowSeconds: 60 },
|
|
340
|
+
});
|
|
343
341
|
|
|
344
|
-
r.writeHandler(
|
|
345
|
-
"invoice:archive",
|
|
346
|
-
z.object({ id: z.uuid() }),
|
|
347
|
-
async (event, ctx) => {
|
|
342
|
+
r.writeHandler({
|
|
343
|
+
name: "invoice:archive",
|
|
344
|
+
schema: z.object({ id: z.uuid() }),
|
|
345
|
+
handler: async (event, ctx) => {
|
|
348
346
|
await ctx.archiveStream(event.payload.id, { aggregateType: "showcase-invoice" });
|
|
349
347
|
return { isSuccess: true as const, data: { id: event.payload.id } };
|
|
350
348
|
},
|
|
351
|
-
|
|
352
|
-
);
|
|
349
|
+
access: { roles: ["Admin"] },
|
|
350
|
+
});
|
|
353
351
|
|
|
354
352
|
// Persist a snapshot of the current state. In practice, features schedule
|
|
355
353
|
// this from a lifecycle hook (every N events, every M minutes) — here it's
|
|
356
354
|
// an explicit write-handler so the integration test can drive it.
|
|
357
|
-
r.writeHandler(
|
|
358
|
-
"invoice:take-snapshot",
|
|
359
|
-
z.object({ id: z.uuid() }),
|
|
360
|
-
async (event, ctx) => {
|
|
355
|
+
r.writeHandler({
|
|
356
|
+
name: "invoice:take-snapshot",
|
|
357
|
+
schema: z.object({ id: z.uuid() }),
|
|
358
|
+
handler: async (event, ctx) => {
|
|
361
359
|
// Rebuild the state from the current stream so the snapshot reflects
|
|
362
360
|
// the committed event log, not an in-memory guess. Use the upcaster-
|
|
363
361
|
// aware ctx.loadAggregate to stay consistent with read-time semantics.
|
|
@@ -375,17 +373,17 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
375
373
|
});
|
|
376
374
|
return { isSuccess: true as const, data: { id: event.payload.id, snapshotVersion: version } };
|
|
377
375
|
},
|
|
378
|
-
|
|
379
|
-
);
|
|
376
|
+
access: { roles: ["Admin"] },
|
|
377
|
+
});
|
|
380
378
|
|
|
381
379
|
// --- Query handlers ---
|
|
382
380
|
|
|
383
381
|
// Live aggregation via ctx.loadAggregate — reduces events into a state
|
|
384
382
|
// snapshot. Supports asOf for point-in-time reads.
|
|
385
|
-
r.queryHandler(
|
|
386
|
-
"invoice:state",
|
|
387
|
-
z.object({ id: z.uuid(), asOf: z.iso.datetime().optional() }),
|
|
388
|
-
async (query, ctx) => {
|
|
383
|
+
r.queryHandler({
|
|
384
|
+
name: "invoice:state",
|
|
385
|
+
schema: z.object({ id: z.uuid(), asOf: z.iso.datetime().optional() }),
|
|
386
|
+
handler: async (query, ctx) => {
|
|
389
387
|
const events = await ctx.loadAggregate(query.payload.id, {
|
|
390
388
|
...(query.payload.asOf ? { asOf: Temporal.Instant.from(query.payload.asOf) } : {}),
|
|
391
389
|
});
|
|
@@ -395,24 +393,22 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
395
393
|
}
|
|
396
394
|
return state;
|
|
397
395
|
},
|
|
398
|
-
{
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
"there is no per-user invoice ownership in this sample",
|
|
404
|
-
},
|
|
396
|
+
access: {
|
|
397
|
+
openToAll: {
|
|
398
|
+
reason:
|
|
399
|
+
"demo recipe: any signed-in user may read the event-sourced invoice state; " +
|
|
400
|
+
"there is no per-user invoice ownership in this sample",
|
|
405
401
|
},
|
|
406
402
|
},
|
|
407
|
-
);
|
|
403
|
+
});
|
|
408
404
|
|
|
409
405
|
// Snapshot-aware fast path. Uses the latest snapshot if available and
|
|
410
406
|
// only replays delta events past it. Identical state shape to
|
|
411
407
|
// invoice:state — the two are interchangeable modulo the perf profile.
|
|
412
|
-
r.queryHandler(
|
|
413
|
-
"invoice:fast-state",
|
|
414
|
-
z.object({ id: z.uuid() }),
|
|
415
|
-
async (query, ctx) => {
|
|
408
|
+
r.queryHandler({
|
|
409
|
+
name: "invoice:fast-state",
|
|
410
|
+
schema: z.object({ id: z.uuid() }),
|
|
411
|
+
handler: async (query, ctx) => {
|
|
416
412
|
const result = await ctx.loadAggregateWithSnapshot<InvoiceStateRecord>(
|
|
417
413
|
query.payload.id,
|
|
418
414
|
(state, evt) => {
|
|
@@ -433,16 +429,14 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
433
429
|
snapshotHit: result.snapshotHit,
|
|
434
430
|
};
|
|
435
431
|
},
|
|
436
|
-
{
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"state; there is no per-user invoice ownership in this sample",
|
|
442
|
-
},
|
|
432
|
+
access: {
|
|
433
|
+
openToAll: {
|
|
434
|
+
reason:
|
|
435
|
+
"demo recipe: any signed-in user may read the snapshot-accelerated invoice " +
|
|
436
|
+
"state; there is no per-user invoice ownership in this sample",
|
|
443
437
|
},
|
|
444
438
|
},
|
|
445
|
-
);
|
|
439
|
+
});
|
|
446
440
|
|
|
447
441
|
// Read-model query via ctx.queryProjection — auto-tenant-scoped. The
|
|
448
442
|
// helper collapses the zero-argument "return this projection" pattern
|
|
@@ -467,10 +461,10 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
467
461
|
// bytes nobody will read. The handler checks ctx.signal at every
|
|
468
462
|
// iteration; on abort, throws and the dispatcher returns an error to
|
|
469
463
|
// the (already-disconnected) client.
|
|
470
|
-
r.queryHandler(
|
|
471
|
-
"ops:slow-export",
|
|
472
|
-
z.object({ chunks: z.number().int().min(1).max(1000) }),
|
|
473
|
-
async (query, ctx) => {
|
|
464
|
+
r.queryHandler({
|
|
465
|
+
name: "ops:slow-export",
|
|
466
|
+
schema: z.object({ chunks: z.number().int().min(1).max(1000) }),
|
|
467
|
+
handler: async (query, ctx) => {
|
|
474
468
|
const accumulated: number[] = [];
|
|
475
469
|
for (let i = 0; i < query.payload.chunks; i++) {
|
|
476
470
|
ctx.signal?.throwIfAborted();
|
|
@@ -481,6 +475,6 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
|
|
|
481
475
|
}
|
|
482
476
|
return { processed: accumulated.length };
|
|
483
477
|
},
|
|
484
|
-
|
|
485
|
-
);
|
|
478
|
+
access: { roles: ["Admin"] },
|
|
479
|
+
});
|
|
486
480
|
});
|
|
@@ -47,11 +47,11 @@ export function createProductFeature(): FeatureDefinition {
|
|
|
47
47
|
return defineFeature("product", (r) => {
|
|
48
48
|
r.systemScope();
|
|
49
49
|
r.toggleable({ default: true });
|
|
50
|
-
r.entity("product", productEntity);
|
|
51
|
-
r.writeHandler(
|
|
52
|
-
"product:create",
|
|
53
|
-
z.object({ name: z.string().min(1).max(100) }),
|
|
54
|
-
async (event, ctx) => {
|
|
50
|
+
r.entity({ name: "product", ...productEntity });
|
|
51
|
+
r.writeHandler({
|
|
52
|
+
name: "product:create",
|
|
53
|
+
schema: z.object({ name: z.string().min(1).max(100) }),
|
|
54
|
+
handler: async (event, ctx) => {
|
|
55
55
|
if (!ctx.systemDb) {
|
|
56
56
|
throw new InternalError({
|
|
57
57
|
message: "product:create requires ctx.systemDb — is r.systemScope() still set?",
|
|
@@ -62,8 +62,8 @@ export function createProductFeature(): FeatureDefinition {
|
|
|
62
62
|
);
|
|
63
63
|
return productCrud.create(event.payload, event.user, db);
|
|
64
64
|
},
|
|
65
|
-
|
|
66
|
-
);
|
|
65
|
+
access: { roles: ["SystemAdmin"] },
|
|
66
|
+
});
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -92,20 +92,28 @@ export function createProductAuditFeature(): FeatureDefinition {
|
|
|
92
92
|
return defineFeature("product-audit", (r) => {
|
|
93
93
|
r.systemScope();
|
|
94
94
|
r.toggleable({ default: true });
|
|
95
|
-
r.entity("product-audit", productAuditEntity);
|
|
95
|
+
r.entity({ name: "product-audit", ...productAuditEntity });
|
|
96
96
|
|
|
97
|
-
r.hook(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
r.hook({
|
|
98
|
+
type: "postSave",
|
|
99
|
+
target: { allOf: "product" },
|
|
100
|
+
handler: async (result, ctx) => {
|
|
101
|
+
if (result.kind !== "save" || !result.isNew) return;
|
|
102
|
+
if (!ctx.systemDb) return;
|
|
103
|
+
const name = (result.changes as Record<string, unknown>)["name"] as string | undefined;
|
|
104
|
+
if (!name) return;
|
|
105
|
+
const db = ctx.systemDb.acknowledgeCrossTenant(
|
|
106
|
+
"audit sink is system-wide, mirrors the system-wide product catalog",
|
|
107
|
+
);
|
|
108
|
+
// Cross-feature audit sink is itself an r.entity projection — write it via
|
|
109
|
+
// the executor (system actor, no human caller) so the row is event-backed
|
|
110
|
+
// and survives a rebuild, not a direct method-form insert.
|
|
111
|
+
await productAuditCrud.create(
|
|
112
|
+
{ productName: name },
|
|
113
|
+
createSystemUser(SYSTEM_TENANT_ID),
|
|
114
|
+
db,
|
|
115
|
+
);
|
|
116
|
+
},
|
|
109
117
|
});
|
|
110
118
|
});
|
|
111
119
|
}
|
|
@@ -59,8 +59,8 @@ export const publishedPhotoIds = new Set<string>();
|
|
|
59
59
|
export const coverOrder: string[] = [];
|
|
60
60
|
|
|
61
61
|
export const filesPostProcessingFeature = defineFeature("files-post-processing", (r) => {
|
|
62
|
-
r.requires("file-derivatives");
|
|
63
|
-
r.entity("photo", photoEntity);
|
|
62
|
+
r.requires({ features: ["file-derivatives"] });
|
|
63
|
+
r.entity({ name: "photo", ...photoEntity });
|
|
64
64
|
|
|
65
65
|
// Uploading a file to a photo's field needs the same rights as saving the
|
|
66
66
|
// photo, so the photo must exist and the uploader needs update access.
|
|
@@ -71,9 +71,9 @@ export const filesPostProcessingFeature = defineFeature("files-post-processing",
|
|
|
71
71
|
|
|
72
72
|
// Entry point 2 — imperative. A real app would gate this behind real
|
|
73
73
|
// roles; `openToAll` keeps the recipe's HTTP wiring to one line.
|
|
74
|
-
r.writeHandler(
|
|
75
|
-
"set-cover",
|
|
76
|
-
z.object({
|
|
74
|
+
r.writeHandler({
|
|
75
|
+
name: "set-cover",
|
|
76
|
+
schema: z.object({
|
|
77
77
|
fileRefId: z.string(),
|
|
78
78
|
// Relative to the ORIGINAL image's pixels, clamped before resize —
|
|
79
79
|
// runtime data (e.g. a detected face box), which is exactly why this
|
|
@@ -87,7 +87,7 @@ export const filesPostProcessingFeature = defineFeature("files-post-processing",
|
|
|
87
87
|
})
|
|
88
88
|
.optional(),
|
|
89
89
|
}),
|
|
90
|
-
async (event, ctx) => {
|
|
90
|
+
handler: async (event, ctx) => {
|
|
91
91
|
if (!ctx.derivatives) {
|
|
92
92
|
return writeFailure(
|
|
93
93
|
new InternalError({ message: "ctx.derivatives missing — is file-derivatives mounted?" }),
|
|
@@ -107,36 +107,32 @@ export const filesPostProcessingFeature = defineFeature("files-post-processing",
|
|
|
107
107
|
coverOrder.push(fileRefId);
|
|
108
108
|
return { isSuccess: true as const, data: { fileRefId, storageKey: result.storageKey } };
|
|
109
109
|
},
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"a file; there is no per-user file ownership check in this sample",
|
|
116
|
-
},
|
|
110
|
+
access: {
|
|
111
|
+
openToAll: {
|
|
112
|
+
reason:
|
|
113
|
+
"demo recipe: any signed-in user may generate a cover-image derivative for " +
|
|
114
|
+
"a file; there is no per-user file ownership check in this sample",
|
|
117
115
|
},
|
|
118
116
|
},
|
|
119
|
-
);
|
|
117
|
+
});
|
|
120
118
|
|
|
121
119
|
// A real app would gate this behind real roles; `openToAll` keeps the
|
|
122
120
|
// recipe's HTTP wiring to one line.
|
|
123
|
-
r.writeHandler(
|
|
124
|
-
"publish",
|
|
125
|
-
z.object({ entityId: z.string() }),
|
|
126
|
-
async (event) => {
|
|
121
|
+
r.writeHandler({
|
|
122
|
+
name: "publish",
|
|
123
|
+
schema: z.object({ entityId: z.string() }),
|
|
124
|
+
handler: async (event) => {
|
|
127
125
|
publishedPhotoIds.add(`${event.user.tenantId}:${event.payload.entityId}`);
|
|
128
126
|
return { isSuccess: true as const, data: { entityId: event.payload.entityId } };
|
|
129
127
|
},
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"app would replace this with a role check as noted above",
|
|
136
|
-
},
|
|
128
|
+
access: {
|
|
129
|
+
openToAll: {
|
|
130
|
+
reason:
|
|
131
|
+
"demo recipe: any signed-in user may mark a photo entity published; a real " +
|
|
132
|
+
"app would replace this with a role check as noted above",
|
|
137
133
|
},
|
|
138
134
|
},
|
|
139
|
-
);
|
|
135
|
+
});
|
|
140
136
|
|
|
141
137
|
// Entry point 3 — default-deny. No registration here means every entityId
|
|
142
138
|
// 404s; a registration that returns false does the same. Only an explicit
|
|
@@ -19,32 +19,30 @@ export const orderEntity = createEntity({
|
|
|
19
19
|
|
|
20
20
|
const orderTable = buildEntityTable("order", orderEntity);
|
|
21
21
|
export const orderFeature = defineFeature("orders", (r) => {
|
|
22
|
-
r.entity("order", orderEntity);
|
|
22
|
+
r.entity({ name: "order", ...orderEntity });
|
|
23
23
|
|
|
24
24
|
const orderExecutor = createEventStoreExecutor(orderTable, orderEntity, { entityName: "order" });
|
|
25
25
|
|
|
26
|
-
r.writeHandler(
|
|
27
|
-
"order:place",
|
|
28
|
-
z.object({
|
|
26
|
+
r.writeHandler({
|
|
27
|
+
name: "order:place",
|
|
28
|
+
schema: z.object({
|
|
29
29
|
customerName: z.string().min(1),
|
|
30
30
|
product: z.string().min(1),
|
|
31
31
|
}),
|
|
32
|
-
async (event, ctx) =>
|
|
32
|
+
handler: async (event, ctx) =>
|
|
33
33
|
orderExecutor.create({ ...event.payload, status: "pending" }, event.user, ctx.db),
|
|
34
|
-
|
|
35
|
-
);
|
|
34
|
+
access: { roles: ["Admin", "Customer"] },
|
|
35
|
+
});
|
|
36
36
|
|
|
37
|
-
r.queryHandler(
|
|
38
|
-
"order:list",
|
|
39
|
-
z.object({ limit: z.number().optional() }),
|
|
40
|
-
async (query, ctx) => orderExecutor.list(query.payload, query.user, ctx.db),
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"any signed-in user may list orders in this recipe; placing an order still requires a role above",
|
|
46
|
-
},
|
|
37
|
+
r.queryHandler({
|
|
38
|
+
name: "order:list",
|
|
39
|
+
schema: z.object({ limit: z.number().optional() }),
|
|
40
|
+
handler: async (query, ctx) => orderExecutor.list(query.payload, query.user, ctx.db),
|
|
41
|
+
access: {
|
|
42
|
+
openToAll: {
|
|
43
|
+
reason:
|
|
44
|
+
"any signed-in user may list orders in this recipe; placing an order still requires a role above",
|
|
47
45
|
},
|
|
48
46
|
},
|
|
49
|
-
);
|
|
47
|
+
});
|
|
50
48
|
});
|
|
@@ -40,7 +40,7 @@ export function createMailTriageFeature(): FeatureDefinition {
|
|
|
40
40
|
r.describe(
|
|
41
41
|
"Sample consumer for inbound-mail-foundation: a worker-lane job triggered by the ingest-message write-handler turns every inbound mail into a triage item.",
|
|
42
42
|
);
|
|
43
|
-
r.requires("inbound-mail-foundation");
|
|
43
|
+
r.requires({ features: ["inbound-mail-foundation"] });
|
|
44
44
|
|
|
45
45
|
// The job receives the dispatcher-validated ingest payload — i.e.
|
|
46
46
|
// the PLAINTEXT normalized mail (PII encryption happens inside the
|
|
@@ -39,18 +39,18 @@ const orderEntity = createEntity({
|
|
|
39
39
|
|
|
40
40
|
export function createLaneRoutingFeature(): FeatureDefinition {
|
|
41
41
|
return defineFeature("orders", (r) => {
|
|
42
|
-
r.entity("order", orderEntity);
|
|
42
|
+
r.entity({ name: "order", ...orderEntity });
|
|
43
43
|
|
|
44
44
|
// Write handler — runs in the API process (or wherever the command
|
|
45
45
|
// lands). Event "orders:write:order:create" is the auto-generated
|
|
46
46
|
// write-event both jobs trigger on.
|
|
47
|
-
r.writeHandler(
|
|
48
|
-
"order:create",
|
|
49
|
-
z.object({
|
|
47
|
+
r.writeHandler({
|
|
48
|
+
name: "order:create",
|
|
49
|
+
schema: z.object({
|
|
50
50
|
customerName: z.string().min(1),
|
|
51
51
|
amount: z.number().positive(),
|
|
52
52
|
}),
|
|
53
|
-
async (event) => ({
|
|
53
|
+
handler: async (event) => ({
|
|
54
54
|
isSuccess: true as const,
|
|
55
55
|
data: {
|
|
56
56
|
id: 1,
|
|
@@ -58,15 +58,13 @@ export function createLaneRoutingFeature(): FeatureDefinition {
|
|
|
58
58
|
amount: event.payload.amount,
|
|
59
59
|
},
|
|
60
60
|
}),
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"demo recipe: any signed-in user may place an order; the handler is a stub with no persistence",
|
|
66
|
-
},
|
|
61
|
+
access: {
|
|
62
|
+
openToAll: {
|
|
63
|
+
reason:
|
|
64
|
+
"demo recipe: any signed-in user may place an order; the handler is a stub with no persistence",
|
|
67
65
|
},
|
|
68
66
|
},
|
|
69
|
-
);
|
|
67
|
+
});
|
|
70
68
|
|
|
71
69
|
// Receipt-rendering: heavy CPU work. Pinned to `worker` so the
|
|
72
70
|
// BullMQ consumer on the API process never picks it up — even if the
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
42
42
|
|
|
43
43
|
// Beide Features aktivieren — template-resolver ist Foundation, legal-pages
|
|
44
|
-
// requires sie. r.requires("template-resolver") greift automatisch im
|
|
44
|
+
// requires sie. r.requires({ features: ["template-resolver"] }) greift automatisch im
|
|
45
45
|
// legal-pages-Feature.
|
|
46
46
|
export const templateResolverFeature = createTemplateResolverFeature();
|
|
47
47
|
export const legalPagesFeature = createLegalPagesFeature();
|
|
@@ -12,19 +12,19 @@ back.
|
|
|
12
12
|
|
|
13
13
|
## What it shows
|
|
14
14
|
|
|
15
|
-
- **`r.hook("validation", handlerRef,
|
|
15
|
+
- **`r.hook({ type: "validation", target: handlerRef, handler })`** — runs before the
|
|
16
16
|
handler body executes. Returns either `null` (pass) or an array of
|
|
17
17
|
`{ field, error }` objects (fail). Used here to reject titles
|
|
18
18
|
containing the word "spam" and titles longer than 200 characters.
|
|
19
|
-
- **`r.hook("postSave", { allOf: entityRef },
|
|
19
|
+
- **`r.hook({ type: "postSave", target: { allOf: entityRef }, handler })`** — fires after every
|
|
20
20
|
successful save (create or update) in the same transaction.
|
|
21
21
|
`result.isNew` distinguishes create from update; `result.changes`
|
|
22
22
|
carries the changed fields only.
|
|
23
|
-
- **`r.hook("preDelete", { allOf: entityRef },
|
|
23
|
+
- **`r.hook({ type: "preDelete", target: { allOf: entityRef }, handler })`** — fires inside the
|
|
24
24
|
delete transaction. Throws on invariant violations (here: "published
|
|
25
25
|
articles cannot be deleted") to roll the delete back. The handler
|
|
26
26
|
receives the full row snapshot — no extra load needed.
|
|
27
|
-
- **`r.hook("postDelete", { allOf: entityRef },
|
|
27
|
+
- **`r.hook({ type: "postDelete", target: { allOf: entityRef }, handler })`** — fires after commit.
|
|
28
28
|
External side effects (logs, notifications) belong here because
|
|
29
29
|
failures cannot roll the delete back.
|
|
30
30
|
|