@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.
- 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/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +12 -2
- package/packages/bundled-features/src/channel-email/feature.ts +2 -2
- 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 +18 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +1 -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 +71 -4
- package/packages/bundled-features/src/delivery/feature.ts +10 -6
- 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 +13 -6
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/index.ts +6 -0
- package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +4 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +73 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +17 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +94 -31
- 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 +51 -10
- package/packages/bundled-features/src/mail-foundation/index.ts +3 -0
- 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/changes.json +25 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -9
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +19 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +9 -1
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +8 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +8 -1
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +6 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +21 -1
- package/packages/bundled-features/src/template-resolver/index.ts +4 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +1 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +54 -7
- package/packages/bundled-features/src/template-resolver/template-preview.ts +2 -0
- package/packages/bundled-features/src/template-resolver/template-system-only.ts +21 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +48 -24
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -0
- 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/changes.json +6 -0
- 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/app/seed-templates.ts +2 -0
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +12 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +7 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +2 -0
- 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/feature-manifest.json +4 -4
- package/samples/apps/use-all-bundled/package.json +1 -0
- 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
|
@@ -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)
|
|
10
|
-
// - r.writeHandler("create")
|
|
11
|
-
// - r.queryHandler("list")
|
|
12
|
-
// - r.useExtension(EXT_USER_DATA, "todo")
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
15
|
+
r.entity({
|
|
16
|
+
name: "task",
|
|
16
17
|
fields: {
|
|
17
18
|
title: { type: "text", required: true },
|
|
18
19
|
done: { type: "boolean", default: false },
|