@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
|
@@ -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 },
|
|
@@ -39,7 +39,7 @@ const openRead = {
|
|
|
39
39
|
} as const;
|
|
40
40
|
|
|
41
41
|
export const embeddedFeature = defineFeature("contacts", (r) => {
|
|
42
|
-
r.entity("contact", contactEntity);
|
|
42
|
+
r.entity({ name: "contact", ...contactEntity });
|
|
43
43
|
|
|
44
44
|
r.writeHandler(contactCreate);
|
|
45
45
|
r.queryHandler(contactDetail);
|
|
@@ -55,7 +55,7 @@ export const prospectsFeature = defineFeature("prospects", (r) => {
|
|
|
55
55
|
read: openRead,
|
|
56
56
|
verbs: { update: false },
|
|
57
57
|
});
|
|
58
|
-
r.entity("prospect", prospectEntity);
|
|
58
|
+
r.entity({ name: "prospect", ...prospectEntity });
|
|
59
59
|
|
|
60
60
|
const { executor: suggestionExecutor } = createEntityExecutor("suggestion", suggestionEntity);
|
|
61
61
|
const { executor: prospectExecutor } = createEntityExecutor("prospect", prospectEntity);
|
|
@@ -64,10 +64,10 @@ export const prospectsFeature = defineFeature("prospects", (r) => {
|
|
|
64
64
|
// edited (RenderEdit's onChange `changes`, same delta the controlled mode
|
|
65
65
|
// reports) — the handler owns merging that onto the suggestion it already
|
|
66
66
|
// has, so an untouched field never has to round-trip through the browser.
|
|
67
|
-
r.writeHandler(
|
|
68
|
-
"prospect:accept",
|
|
69
|
-
z.object({ suggestionId: z.uuid(), changes: acceptChangesSchema }),
|
|
70
|
-
async (event, ctx) => {
|
|
67
|
+
r.writeHandler({
|
|
68
|
+
name: "prospect:accept",
|
|
69
|
+
schema: z.object({ suggestionId: z.uuid(), changes: acceptChangesSchema }),
|
|
70
|
+
handler: async (event, ctx) => {
|
|
71
71
|
const suggestion = await suggestionExecutor.detail(
|
|
72
72
|
{ id: event.payload.suggestionId },
|
|
73
73
|
event.user,
|
|
@@ -108,13 +108,13 @@ export const prospectsFeature = defineFeature("prospects", (r) => {
|
|
|
108
108
|
if (!flipped.isSuccess) return flipped;
|
|
109
109
|
return created;
|
|
110
110
|
},
|
|
111
|
-
adminWrite,
|
|
112
|
-
);
|
|
111
|
+
...adminWrite,
|
|
112
|
+
});
|
|
113
113
|
|
|
114
|
-
r.queryHandler(
|
|
115
|
-
"prospect:detail",
|
|
116
|
-
z.object({ id: z.uuid() }),
|
|
117
|
-
async (query, ctx) => prospectExecutor.detail(query.payload, query.user, ctx.db),
|
|
118
|
-
openRead,
|
|
119
|
-
);
|
|
114
|
+
r.queryHandler({
|
|
115
|
+
name: "prospect:detail",
|
|
116
|
+
schema: z.object({ id: z.uuid() }),
|
|
117
|
+
handler: async (query, ctx) => prospectExecutor.detail(query.payload, query.user, ctx.db),
|
|
118
|
+
...openRead,
|
|
119
|
+
});
|
|
120
120
|
});
|
|
@@ -80,7 +80,7 @@ const chargeHandler = defineWriteHandler({
|
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
export const billingFeature = defineFeature(FEATURE, (r) => {
|
|
83
|
-
r.requires("config");
|
|
83
|
+
r.requires({ features: ["config"] });
|
|
84
84
|
r.config("stripe-api-key", stripeApiKeyDef);
|
|
85
85
|
r.writeHandler(chargeHandler);
|
|
86
86
|
// Kein r.screen/r.nav: der `mask`-Eintrag auf dem Key lässt
|