@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
|
@@ -38,7 +38,7 @@ export const hookLog: { type: string; data: Record<string, unknown> }[] = [];
|
|
|
38
38
|
const editorWrite = { access: { roles: ["Admin", "Editor"] } } as const;
|
|
39
39
|
|
|
40
40
|
export const articlesFeature = defineFeature("blog", (r) => {
|
|
41
|
-
const article = r.entity("article", articleEntity);
|
|
41
|
+
const article = r.entity({ name: "article", ...articleEntity });
|
|
42
42
|
|
|
43
43
|
const articleCreate = r.writeHandler(
|
|
44
44
|
defineEntityCreateHandler("article", articleEntity, editorWrite),
|
|
@@ -49,33 +49,45 @@ export const articlesFeature = defineFeature("blog", (r) => {
|
|
|
49
49
|
r.writeHandler(defineEntityDeleteHandler("article", articleEntity, editorWrite));
|
|
50
50
|
|
|
51
51
|
// --- Validation hook on create: reject banned words + length ---
|
|
52
|
-
r.hook(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
r.hook({
|
|
53
|
+
type: "validation",
|
|
54
|
+
target: articleCreate,
|
|
55
|
+
handler: (data) => {
|
|
56
|
+
const title = data["title"] as string;
|
|
57
|
+
if (title.toLowerCase().includes("spam")) {
|
|
58
|
+
return [{ field: "title", error: "title_contains_banned_word" }];
|
|
59
|
+
}
|
|
60
|
+
if (title.length > 200) {
|
|
61
|
+
return [{ field: "title", error: "title_too_long" }];
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
61
65
|
});
|
|
62
66
|
|
|
63
67
|
// --- Validation hook on update: length check on title changes ---
|
|
64
|
-
r.hook(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
r.hook({
|
|
69
|
+
type: "validation",
|
|
70
|
+
target: articleUpdate,
|
|
71
|
+
handler: (data) => {
|
|
72
|
+
const changes = data["changes"] as Record<string, unknown> | undefined;
|
|
73
|
+
const title = changes?.["title"] as string | undefined;
|
|
74
|
+
if (title && title.length > 200) {
|
|
75
|
+
return [{ field: "title", error: "title_too_long" }];
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
},
|
|
71
79
|
});
|
|
72
80
|
|
|
73
81
|
// --- postSave entity hook: log all saves ---
|
|
74
|
-
r.hook(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
r.hook({
|
|
83
|
+
type: "postSave",
|
|
84
|
+
target: { allOf: article },
|
|
85
|
+
handler: async (result: SaveContext) => {
|
|
86
|
+
hookLog.push({
|
|
87
|
+
type: result.isNew ? "created" : "updated",
|
|
88
|
+
data: { id: result.id, changes: result.changes },
|
|
89
|
+
});
|
|
90
|
+
},
|
|
79
91
|
});
|
|
80
92
|
|
|
81
93
|
// --- preDelete entity hook: block delete when the article is still published.
|
|
@@ -83,22 +95,30 @@ export const articlesFeature = defineFeature("blog", (r) => {
|
|
|
83
95
|
// the delete rolls back before any projection or event is written. The
|
|
84
96
|
// payload.data carries the full row snapshot taken just before delete —
|
|
85
97
|
// no extra load needed.
|
|
86
|
-
r.hook(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
r.hook({
|
|
99
|
+
type: "preDelete",
|
|
100
|
+
target: { allOf: article },
|
|
101
|
+
handler: async (payload) => {
|
|
102
|
+
if (payload.data["isPublished"] === true) {
|
|
103
|
+
throw new ConflictError({
|
|
104
|
+
message: `article ${payload.id} is still published`,
|
|
105
|
+
i18nKey: "errors.publishedArticleCannotBeDeleted",
|
|
106
|
+
details: { reason: "published_article_cannot_be_deleted", entityId: payload.id },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
},
|
|
94
110
|
});
|
|
95
111
|
|
|
96
112
|
// --- postDelete entity hook: log deletions after the transaction commits.
|
|
97
113
|
// Default phase is afterCommit so failures here can't roll back the delete.
|
|
98
|
-
r.hook(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
r.hook({
|
|
115
|
+
type: "postDelete",
|
|
116
|
+
target: { allOf: article },
|
|
117
|
+
handler: async (payload) => {
|
|
118
|
+
hookLog.push({
|
|
119
|
+
type: "deleted",
|
|
120
|
+
data: { id: payload.id },
|
|
121
|
+
});
|
|
122
|
+
},
|
|
103
123
|
});
|
|
104
124
|
});
|
|
@@ -36,12 +36,12 @@ export const smtpHostHandle: ConfigKeyHandle<"text"> = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export const integrationsFeature = defineFeature(FEATURE, (r) => {
|
|
39
|
-
r.requires("config");
|
|
39
|
+
r.requires({ features: ["config"] });
|
|
40
40
|
// backing:"secrets" stores payment-api-key in the secrets envelope, which
|
|
41
41
|
// the secrets feature provisions — declare the dependency so registry-build
|
|
42
42
|
// enforces it instead of leaving a consumer to discover the missing
|
|
43
43
|
// tenant_secrets table at runtime.
|
|
44
|
-
r.requires("secrets");
|
|
44
|
+
r.requires({ features: ["secrets"] });
|
|
45
45
|
|
|
46
46
|
r.config({
|
|
47
47
|
keys: {
|
|
@@ -68,15 +68,15 @@ export const integrationsFeature = defineFeature(FEATURE, (r) => {
|
|
|
68
68
|
|
|
69
69
|
// Internal-read probe: the owning feature reads its own secrets-backed key
|
|
70
70
|
// via ctx.config and receives the revealed plaintext, never the mask.
|
|
71
|
-
r.queryHandler(
|
|
72
|
-
"peek-payment-key",
|
|
73
|
-
z.object({}),
|
|
74
|
-
async (_query, ctx) => {
|
|
71
|
+
r.queryHandler({
|
|
72
|
+
name: "peek-payment-key",
|
|
73
|
+
schema: z.object({}),
|
|
74
|
+
handler: async (_query, ctx) => {
|
|
75
75
|
if (!ctx.config) throw new Error("ctx.config not wired");
|
|
76
76
|
return { value: await ctx.config(paymentApiKeyHandle) };
|
|
77
77
|
},
|
|
78
|
-
|
|
79
|
-
);
|
|
78
|
+
access: { roles: ["SystemAdmin"] },
|
|
79
|
+
});
|
|
80
80
|
|
|
81
81
|
r.translations({
|
|
82
82
|
keys: {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "@cosmicdrift/kumiko-bundled-features/managed-pages";
|
|
6
6
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
|
|
8
|
-
// managed-pages declares `r.requires("config")` for its branding keys — the
|
|
8
|
+
// managed-pages declares `r.requires({ features: ["config"] })` for its branding keys — the
|
|
9
9
|
// config feature must be in the stack (runProdApp auto-mounts it; setupTestStack
|
|
10
10
|
// does not, so the recipe lists it explicitly).
|
|
11
11
|
export const configFeature = createConfigFeature();
|
|
@@ -68,7 +68,7 @@ The component itself is `notes-history/web/notes-section.tsx`
|
|
|
68
68
|
```
|
|
69
69
|
notes-history → core bundle: note-entry entity, add-note handler, list query
|
|
70
70
|
task-management → our feature: a plain `task` entity. Declares
|
|
71
|
-
r.requires("notes-history") only so the bundle is
|
|
71
|
+
r.requires({ features: ["notes-history"] }) only so the bundle is
|
|
72
72
|
mounted — the task itself is completely notes-agnostic.
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -48,9 +48,9 @@ export const taskFeature = defineFeature("task-management", (r) => {
|
|
|
48
48
|
// notes-history is non-optional for this recipe: the demo notes tasks.
|
|
49
49
|
// The task feature itself stays completely notes-agnostic — it only
|
|
50
50
|
// declares the dependency so the bundle is mounted.
|
|
51
|
-
r.requires("notes-history");
|
|
51
|
+
r.requires({ features: ["notes-history"] });
|
|
52
52
|
|
|
53
|
-
r.entity("task", taskEntity);
|
|
53
|
+
r.entity({ name: "task", ...taskEntity });
|
|
54
54
|
|
|
55
55
|
r.writeHandler({
|
|
56
56
|
name: "task:create",
|
|
@@ -56,20 +56,20 @@ const AUDIT_ROW_LIMIT = 100;
|
|
|
56
56
|
const access = { access: { roles: ["Admin", "User"] } } as const;
|
|
57
57
|
|
|
58
58
|
export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
|
|
59
|
-
r.entity("inspection", inspectionEntity);
|
|
59
|
+
r.entity({ name: "inspection", ...inspectionEntity });
|
|
60
60
|
const { executor: inspectionExecutor } = createEntityExecutor("inspection", inspectionEntity);
|
|
61
61
|
|
|
62
|
-
r.writeHandler(
|
|
63
|
-
"inspection:create",
|
|
64
|
-
z.object({
|
|
62
|
+
r.writeHandler({
|
|
63
|
+
name: "inspection:create",
|
|
64
|
+
schema: z.object({
|
|
65
65
|
assetName: z.string().min(1),
|
|
66
66
|
inspectedOn: z.iso.date(),
|
|
67
67
|
result: z.enum(INSPECTION_RESULTS),
|
|
68
68
|
inspector: z.string().optional(),
|
|
69
69
|
}),
|
|
70
|
-
async (event, ctx) => inspectionExecutor.create(event.payload, event.user, ctx.db),
|
|
71
|
-
access,
|
|
72
|
-
);
|
|
70
|
+
handler: async (event, ctx) => inspectionExecutor.create(event.payload, event.user, ctx.db),
|
|
71
|
+
...access,
|
|
72
|
+
});
|
|
73
73
|
|
|
74
74
|
r.writeHandler(
|
|
75
75
|
defineWriteHandler({
|
|
@@ -39,7 +39,7 @@ and why the combination is the production-realistic shape (not just
|
|
|
39
39
|
an API tour).
|
|
40
40
|
|
|
41
41
|
If you've only ever seen the free-form handler signature
|
|
42
|
-
(`r.writeHandler(name, schema,
|
|
42
|
+
(`r.writeHandler({ name, schema, handler, access })`), compare:
|
|
43
43
|
|
|
44
44
|
- `samples/recipes/custom-handlers/src/feature.ts` — a similar
|
|
45
45
|
domain, written in the free-form style.
|
|
@@ -72,7 +72,7 @@ const productExecutor = createEventStoreExecutor(productTable, productEntity, {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
export const inventoryFeature = defineFeature("inventory", (r) => {
|
|
75
|
-
r.entity("product", productEntity);
|
|
75
|
+
r.entity({ name: "product", ...productEntity });
|
|
76
76
|
r.requires.projection("read_inventory_low_stock_alerts");
|
|
77
77
|
|
|
78
78
|
// Domain-events on the product stream (alongside the auto-CRUD
|
|
@@ -412,13 +412,13 @@ export const inventoryFeature = defineFeature("inventory", (r) => {
|
|
|
412
412
|
// only; queries get the existing handler signature. The list
|
|
413
413
|
// below is what the integration test reads against.
|
|
414
414
|
// -------------------------------------------------------------
|
|
415
|
-
r.queryHandler(
|
|
416
|
-
"low-stock-alerts:list",
|
|
417
|
-
z.object({}),
|
|
418
|
-
async (_query, ctx) => {
|
|
415
|
+
r.queryHandler({
|
|
416
|
+
name: "low-stock-alerts:list",
|
|
417
|
+
schema: z.object({}),
|
|
418
|
+
handler: async (_query, ctx) => {
|
|
419
419
|
const rows = await selectMany(ctx.db, lowStockAlertsTable);
|
|
420
420
|
return { rows };
|
|
421
421
|
},
|
|
422
|
-
|
|
423
|
-
);
|
|
422
|
+
access: { roles: ["Admin"] },
|
|
423
|
+
});
|
|
424
424
|
});
|
|
@@ -190,7 +190,7 @@ export const shareByTokenQuery = defineQueryHandler({
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
export const publicShareTokenFeature = defineFeature("public-share", (r) => {
|
|
193
|
-
r.entity("share-link", shareLinkEntity);
|
|
193
|
+
r.entity({ name: "share-link", ...shareLinkEntity });
|
|
194
194
|
r.writeHandler(shareLinkCreateWrite);
|
|
195
195
|
r.writeHandler(shareLinkRevokeWrite);
|
|
196
196
|
r.queryHandler(shareByTokenQuery);
|
|
@@ -13,14 +13,12 @@ export function createRateLimitShowcaseFeature(): FeatureDefinition {
|
|
|
13
13
|
// Per-user budget. Real apps tune `limit` to actual handler cost — a
|
|
14
14
|
// search call against a sharded index might warrant 5/min, a full
|
|
15
15
|
// export 1/min. The bucket is `user:<userId>` (see rate-limit/bucket.ts).
|
|
16
|
-
r.queryHandler(
|
|
17
|
-
"expensive-search",
|
|
18
|
-
z.object({ q: z.string().min(1) }),
|
|
19
|
-
async ({ payload }) => ({ q: payload.q, hits: 0 }),
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
);
|
|
16
|
+
r.queryHandler({
|
|
17
|
+
name: "expensive-search",
|
|
18
|
+
schema: z.object({ q: z.string().min(1) }),
|
|
19
|
+
handler: async ({ payload }) => ({ q: payload.q, hits: 0 }),
|
|
20
|
+
access: { roles: ["Admin", "User"] },
|
|
21
|
+
rateLimit: { per: "user", limit: 3, windowSeconds: 60 },
|
|
22
|
+
});
|
|
25
23
|
});
|
|
26
24
|
}
|
|
@@ -27,10 +27,10 @@ export function createOrderDeskFeature(): FeatureDefinition {
|
|
|
27
27
|
return defineFeature("order-desk", (r) => {
|
|
28
28
|
r.systemScope();
|
|
29
29
|
|
|
30
|
-
r.queryHandler(
|
|
31
|
-
"order:detail",
|
|
32
|
-
rowIdSchema,
|
|
33
|
-
async (query) => ({
|
|
30
|
+
r.queryHandler({
|
|
31
|
+
name: "order:detail",
|
|
32
|
+
schema: rowIdSchema,
|
|
33
|
+
handler: async (query) => ({
|
|
34
34
|
id: query.payload.id,
|
|
35
35
|
customerName: "Aiko Tanaka",
|
|
36
36
|
orderNumber: "SO-10482",
|
|
@@ -43,33 +43,33 @@ export function createOrderDeskFeature(): FeatureDefinition {
|
|
|
43
43
|
billingAddress: "Karl-Marx-Straße 12, 12043 Berlin",
|
|
44
44
|
salesRep: "Jonas Weber",
|
|
45
45
|
}),
|
|
46
|
-
|
|
47
|
-
);
|
|
46
|
+
access: { roles: ["Support", "Admin"] },
|
|
47
|
+
});
|
|
48
48
|
|
|
49
|
-
r.queryHandler(
|
|
50
|
-
"order:items",
|
|
51
|
-
rowIdSchema,
|
|
52
|
-
async () => ({
|
|
49
|
+
r.queryHandler({
|
|
50
|
+
name: "order:items",
|
|
51
|
+
schema: rowIdSchema,
|
|
52
|
+
handler: async () => ({
|
|
53
53
|
rows: [
|
|
54
54
|
{ sku: "KB-100", description: "Mechanical keyboard", quantity: 1, unitPrice: 148 },
|
|
55
55
|
{ sku: "MS-220", description: "Wireless mouse", quantity: 2, unitPrice: 45 },
|
|
56
56
|
{ sku: "PAD-01", description: "Desk mat", quantity: 1, unitPrice: 22 },
|
|
57
57
|
],
|
|
58
58
|
}),
|
|
59
|
-
|
|
60
|
-
);
|
|
59
|
+
access: { roles: ["Support", "Admin"] },
|
|
60
|
+
});
|
|
61
61
|
|
|
62
|
-
r.queryHandler(
|
|
63
|
-
"order:payments",
|
|
64
|
-
rowIdSchema,
|
|
65
|
-
async () => ({
|
|
62
|
+
r.queryHandler({
|
|
63
|
+
name: "order:payments",
|
|
64
|
+
schema: rowIdSchema,
|
|
65
|
+
handler: async () => ({
|
|
66
66
|
rows: [
|
|
67
67
|
{ paidAt: "2026-08-21", amount: 936, method: "card", status: "settled" },
|
|
68
68
|
{ paidAt: "2026-08-25", amount: 312, method: "invoice", status: "pending" },
|
|
69
69
|
],
|
|
70
70
|
}),
|
|
71
|
-
|
|
72
|
-
);
|
|
71
|
+
access: { roles: ["Support", "Admin"] },
|
|
72
|
+
});
|
|
73
73
|
|
|
74
74
|
r.screen({
|
|
75
75
|
id: "order-detail",
|
|
@@ -21,7 +21,7 @@ export const categoryTable = table("read_sample_categories", {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
export const categoryFeature = defineFeature("catalog", (r) => {
|
|
24
|
-
r.entity("category", categoryEntity);
|
|
24
|
+
r.entity({ name: "category", ...categoryEntity });
|
|
25
25
|
|
|
26
26
|
// Static reference data — seeded on boot, upserted on changes
|
|
27
27
|
r.referenceData(
|
|
@@ -46,8 +46,8 @@ const createOnly = {
|
|
|
46
46
|
} as const;
|
|
47
47
|
|
|
48
48
|
export const relationsFeature = defineFeature("org", (r) => {
|
|
49
|
-
const team = r.entity("team", teamEntity);
|
|
50
|
-
const member = r.entity("member", memberEntity);
|
|
49
|
+
const team = r.entity({ name: "team", ...teamEntity });
|
|
50
|
+
const member = r.entity({ name: "member", ...memberEntity });
|
|
51
51
|
|
|
52
52
|
r.relation(team, "members", {
|
|
53
53
|
type: "hasMany",
|
|
@@ -46,7 +46,7 @@ export const bookEntity = createEntity({
|
|
|
46
46
|
export function createBookshopFeature(): FeatureDefinition {
|
|
47
47
|
return defineFeature("bookshop", (r) => {
|
|
48
48
|
r.systemScope();
|
|
49
|
-
r.entity("book", bookEntity);
|
|
49
|
+
r.entity({ name: "book", ...bookEntity });
|
|
50
50
|
|
|
51
51
|
// entityList — columns carry string shorthand and the object form
|
|
52
52
|
// side by side; the framework normalizes both through
|
|
@@ -26,47 +26,45 @@ export const productEntity = createEntity({
|
|
|
26
26
|
const productTable = buildEntityTable("product", productEntity);
|
|
27
27
|
|
|
28
28
|
export const productFeature = defineFeature("shop", (r) => {
|
|
29
|
-
r.entity("product", productEntity);
|
|
29
|
+
r.entity({ name: "product", ...productEntity });
|
|
30
30
|
|
|
31
|
-
r.writeHandler(
|
|
32
|
-
"product:create",
|
|
33
|
-
z.object({
|
|
31
|
+
r.writeHandler({
|
|
32
|
+
name: "product:create",
|
|
33
|
+
schema: z.object({
|
|
34
34
|
name: z.string().min(1),
|
|
35
35
|
brand: z.string().optional(),
|
|
36
36
|
sku: z.string().min(1),
|
|
37
37
|
category: z.string().optional(),
|
|
38
38
|
}),
|
|
39
|
-
async (event, ctx) => {
|
|
39
|
+
handler: async (event, ctx) => {
|
|
40
40
|
const crud = createEventStoreExecutor(productTable, productEntity, {
|
|
41
41
|
searchAdapter: ctx.searchAdapter,
|
|
42
42
|
entityName: "product",
|
|
43
43
|
});
|
|
44
44
|
return crud.create(event.payload, event.user, ctx.db);
|
|
45
45
|
},
|
|
46
|
-
|
|
47
|
-
);
|
|
46
|
+
access: { roles: ["Admin"] },
|
|
47
|
+
});
|
|
48
48
|
|
|
49
|
-
r.queryHandler(
|
|
50
|
-
"product:list",
|
|
51
|
-
z.object({
|
|
49
|
+
r.queryHandler({
|
|
50
|
+
name: "product:list",
|
|
51
|
+
schema: z.object({
|
|
52
52
|
search: z.string().optional(),
|
|
53
53
|
limit: z.number().optional(),
|
|
54
54
|
}),
|
|
55
|
-
async (query, ctx) => {
|
|
55
|
+
handler: async (query, ctx) => {
|
|
56
56
|
const crud = createEventStoreExecutor(productTable, productEntity, {
|
|
57
57
|
searchAdapter: ctx.searchAdapter,
|
|
58
58
|
entityName: "product",
|
|
59
59
|
});
|
|
60
60
|
return crud.list(query.payload, query.user, ctx.db);
|
|
61
61
|
},
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"a product still requires the Admin role above",
|
|
68
|
-
},
|
|
62
|
+
access: {
|
|
63
|
+
openToAll: {
|
|
64
|
+
reason:
|
|
65
|
+
"any signed-in user may search and list products in this recipe; creating " +
|
|
66
|
+
"a product still requires the Admin role above",
|
|
69
67
|
},
|
|
70
68
|
},
|
|
71
|
-
);
|
|
69
|
+
});
|
|
72
70
|
});
|
|
@@ -25,7 +25,7 @@ used to be silent; now the boot validator warns at `NODE_ENV=production`.
|
|
|
25
25
|
## Feature composition
|
|
26
26
|
|
|
27
27
|
```
|
|
28
|
-
config → tenant's r.requires("config") dependency
|
|
28
|
+
config → tenant's r.requires({ features: ["config"] }) dependency
|
|
29
29
|
user → cross-tenant identity (sessions' r.requires target)
|
|
30
30
|
tenant → memberships (audit's r.requires target)
|
|
31
31
|
auth-foundation → tokenVerifier extension point (sessions' r.requires target)
|
|
@@ -39,7 +39,7 @@ import type { InvoiceStatus } from "./reducer";
|
|
|
39
39
|
export { invoiceEntity } from "./entities/invoice";
|
|
40
40
|
|
|
41
41
|
export const stateMachineFeature = defineFeature("billing", (r) => {
|
|
42
|
-
r.entity("invoice", invoiceEntity);
|
|
42
|
+
r.entity({ name: "invoice", ...invoiceEntity });
|
|
43
43
|
|
|
44
44
|
// Domain events — one per intent. Names live in events.ts so the reducer
|
|
45
45
|
// and the projection stay in sync. The auto "invoice.created" event from
|
|
@@ -77,7 +77,7 @@ See the [tags feature reference](/en/bundled-features/tags/) for the full set.
|
|
|
77
77
|
tags → core bundle: tag + tag-assignment entities,
|
|
78
78
|
create/assign/remove handlers, list queries
|
|
79
79
|
note-management → our feature: a plain `note` entity. Declares
|
|
80
|
-
r.requires("tags") only so the bundle is mounted —
|
|
80
|
+
r.requires({ features: ["tags"] }) only so the bundle is mounted —
|
|
81
81
|
the note itself is completely tag-agnostic.
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -47,9 +47,9 @@ export const noteFeature = defineFeature("note-management", (r) => {
|
|
|
47
47
|
// tags is non-optional for this recipe: the demo tags notes. The note
|
|
48
48
|
// feature itself stays completely tag-agnostic — it only declares the
|
|
49
49
|
// dependency so the bundle is mounted.
|
|
50
|
-
r.requires("tags");
|
|
50
|
+
r.requires({ features: ["tags"] });
|
|
51
51
|
|
|
52
|
-
r.entity("note", noteEntity);
|
|
52
|
+
r.entity({ name: "note", ...noteEntity });
|
|
53
53
|
|
|
54
54
|
r.writeHandler({
|
|
55
55
|
name: "note:create",
|
|
@@ -27,7 +27,7 @@ export const invoiceEntity = createEntity({
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
export const invoiceFeature = defineFeature("invoice", (r) => {
|
|
30
|
-
r.entity("invoice", invoiceEntity);
|
|
30
|
+
r.entity({ name: "invoice", ...invoiceEntity });
|
|
31
31
|
r.writeHandler(
|
|
32
32
|
defineCreateWithTenantDefaults("invoice", invoiceEntity, {
|
|
33
33
|
access: { roles: ["Admin"] },
|
|
@@ -24,7 +24,7 @@ export const invoiceEntity = createEntity({
|
|
|
24
24
|
const ACCESS = { roles: ["Admin"] } as const;
|
|
25
25
|
|
|
26
26
|
export const invoiceFeature = defineFeature("invoice", (r) => {
|
|
27
|
-
r.entity("invoice", invoiceEntity);
|
|
27
|
+
r.entity({ name: "invoice", ...invoiceEntity });
|
|
28
28
|
r.writeHandler(
|
|
29
29
|
defineCreateWithTenantDefaults("invoice", invoiceEntity, {
|
|
30
30
|
access: ACCESS,
|
|
@@ -43,12 +43,12 @@ export const timezonesFeature = defineFeature("timezones", (r) => {
|
|
|
43
43
|
});
|
|
44
44
|
r.writeHandler(defineEntityDeleteHandler("delivery", deliveryEntity, adminWrite));
|
|
45
45
|
|
|
46
|
-
r.queryHandler(
|
|
47
|
-
"day-window",
|
|
48
|
-
z.object({
|
|
46
|
+
r.queryHandler({
|
|
47
|
+
name: "day-window",
|
|
48
|
+
schema: z.object({
|
|
49
49
|
zone: z.string().refine(isValidIanaTimeZone, { message: "Invalid IANA timezone" }),
|
|
50
50
|
}),
|
|
51
|
-
async (query, ctx) => {
|
|
51
|
+
handler: async (query, ctx) => {
|
|
52
52
|
const { start, end } = ctx.tz.todayRange(query.payload.zone);
|
|
53
53
|
return {
|
|
54
54
|
zone: query.payload.zone,
|
|
@@ -57,16 +57,16 @@ export const timezonesFeature = defineFeature("timezones", (r) => {
|
|
|
57
57
|
end: end.toString(),
|
|
58
58
|
};
|
|
59
59
|
},
|
|
60
|
-
openRead,
|
|
61
|
-
);
|
|
60
|
+
...openRead,
|
|
61
|
+
});
|
|
62
62
|
|
|
63
|
-
r.queryHandler(
|
|
64
|
-
"zone-at",
|
|
65
|
-
z.object({ latitude: z.number(), longitude: z.number() }),
|
|
66
|
-
async (query, ctx) => {
|
|
63
|
+
r.queryHandler({
|
|
64
|
+
name: "zone-at",
|
|
65
|
+
schema: z.object({ latitude: z.number(), longitude: z.number() }),
|
|
66
|
+
handler: async (query, ctx) => {
|
|
67
67
|
const zone = await ctx.tz.fromCoordinates(query.payload);
|
|
68
68
|
return { zone };
|
|
69
69
|
},
|
|
70
|
-
openRead,
|
|
71
|
-
);
|
|
70
|
+
...openRead,
|
|
71
|
+
});
|
|
72
72
|
});
|
|
@@ -11,8 +11,8 @@ forget logic:
|
|
|
11
11
|
|
|
12
12
|
```ts illustration
|
|
13
13
|
defineFeature("notes", (r) => {
|
|
14
|
-
r.requires("user-data-rights");
|
|
15
|
-
r.entity("note", noteEntity);
|
|
14
|
+
r.requires({ features: ["user-data-rights"] });
|
|
15
|
+
r.entity({ name: "note", ...noteEntity });
|
|
16
16
|
|
|
17
17
|
r.useExtension(EXT_USER_DATA, "note", {
|
|
18
18
|
export: async (ctx) => ({
|
|
@@ -55,7 +55,7 @@ export const noteEntity = createEntity({
|
|
|
55
55
|
export const notesTable = deriveEntityTableMeta("note", noteEntity, { source: "unmanaged" });
|
|
56
56
|
|
|
57
57
|
export const notesFeature = defineFeature("notes", (r) => {
|
|
58
|
-
r.requires("user-data-rights");
|
|
58
|
+
r.requires({ features: ["user-data-rights"] });
|
|
59
59
|
// store_notes is a direct-write store: the forget hook below `updateMany`/
|
|
60
60
|
// `deleteMany`s rows WITHOUT emitting lifecycle events. r.entity would make
|
|
61
61
|
// it a rebuildable implicit projection whose replay finds zero note events
|
|
@@ -35,7 +35,7 @@ export function createAccountFeature(): FeatureDefinition {
|
|
|
35
35
|
"entry — user-profile registers the `profile` screen itself " +
|
|
36
36
|
"(fw#2312 declarative projectionDetail).",
|
|
37
37
|
);
|
|
38
|
-
r.requires("user-profile");
|
|
38
|
+
r.requires({ features: ["user-profile"] });
|
|
39
39
|
|
|
40
40
|
r.nav({
|
|
41
41
|
id: "profile",
|
|
@@ -33,7 +33,7 @@ const incidentExecutor = createEventStoreExecutor(incidentTable, incidentEntity,
|
|
|
33
33
|
const INCIDENT_HOOK_URL = "https://hooks.example/incident";
|
|
34
34
|
|
|
35
35
|
export const webhookDemoFeature = defineFeature("webhook-demo", (r) => {
|
|
36
|
-
r.entity("incident", incidentEntity);
|
|
36
|
+
r.entity({ name: "incident", ...incidentEntity });
|
|
37
37
|
r.requires.step("webhook.send");
|
|
38
38
|
r.requires.step("mail.send");
|
|
39
39
|
r.requires.step("callFeature");
|
|
@@ -86,19 +86,19 @@ export const noteDeskFeature = defineFeature("note-desk", (r) => {
|
|
|
86
86
|
access: open.access,
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
r.queryHandler(
|
|
90
|
-
"note:summary",
|
|
91
|
-
z.object({ id: z.string() }),
|
|
92
|
-
async (query) => ({ id: query.payload.id, ...NOTE_DETAIL_FIELDS }),
|
|
93
|
-
open,
|
|
94
|
-
);
|
|
89
|
+
r.queryHandler({
|
|
90
|
+
name: "note:summary",
|
|
91
|
+
schema: z.object({ id: z.string() }),
|
|
92
|
+
handler: async (query) => ({ id: query.payload.id, ...NOTE_DETAIL_FIELDS }),
|
|
93
|
+
...open,
|
|
94
|
+
});
|
|
95
95
|
|
|
96
|
-
r.writeHandler(
|
|
97
|
-
"note:comment",
|
|
98
|
-
commentPayloadSchema,
|
|
99
|
-
async () => ({ isSuccess: true as const, data: null }),
|
|
100
|
-
open,
|
|
101
|
-
);
|
|
96
|
+
r.writeHandler({
|
|
97
|
+
name: "note:comment",
|
|
98
|
+
schema: commentPayloadSchema,
|
|
99
|
+
handler: async () => ({ isSuccess: true as const, data: null }),
|
|
100
|
+
...open,
|
|
101
|
+
});
|
|
102
102
|
|
|
103
103
|
r.screen({
|
|
104
104
|
id: "note-detail",
|