@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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/AGENTS.md +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +14 -5
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +9 -9
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- package/src/changes.json +251 -8
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/writeform-section. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# writeform-section
|
|
4
|
+
|
|
5
|
+
A projectionDetail `writeForm` section rendered beside an established entityEdit form on the same page, so the shared form chrome (footer, submit affordance) between the two can be verified side by side.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# WriteForm Section
|
|
10
|
+
|
|
11
|
+
Register a `projectionDetail` screen with a `kind: "writeForm"` section: a
|
|
12
|
+
self-persisting form embedded inside a record detail page, next to the
|
|
13
|
+
established `entityEdit` form for the same fields — so the two form kinds'
|
|
14
|
+
shared chrome (footer, submit button) can be compared side by side.
|
|
15
|
+
|
|
16
|
+
## What it shows
|
|
17
|
+
|
|
18
|
+
- `note-edit`, an `entityEdit` screen with one `title`/`category`/`priority`/
|
|
19
|
+
`body` section
|
|
20
|
+
- `note-detail`, a `projectionDetail` screen whose only section is
|
|
21
|
+
`kind: "writeForm"` with the identical field set, dispatching its own
|
|
22
|
+
`note-desk:write:note:comment` handler on submit
|
|
23
|
+
- an e2e layout-parity spec (`e2e/writeform-parity.spec.ts`) proving both
|
|
24
|
+
submit buttons render the same width, sit right-aligned in their form's
|
|
25
|
+
footer, and never stretch full-width — the regression this recipe guards
|
|
26
|
+
against (fw#2681)
|
|
27
|
+
|
|
28
|
+
## Source
|
|
29
|
+
|
|
30
|
+
Feature entry point: `src/feature.ts`.
|
|
31
|
+
|
|
32
|
+
## Tests
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd samples/recipes/writeform-section
|
|
36
|
+
bun test
|
|
37
|
+
bun run e2e # layout-parity spec + regenerates the committed screenshot
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Source files
|
|
41
|
+
|
|
42
|
+
### src/__tests__/feature.test.ts
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// WriteForm Section Sample — Unit Test (no DB / HTTP needed).
|
|
46
|
+
// Proves:
|
|
47
|
+
// - note-desk boots clean (r.translations covers the writeForm
|
|
48
|
+
// pseudo-entity keys required by fw editable-detail-screens)
|
|
49
|
+
// - the writeForm section's field list on note-detail is identical to
|
|
50
|
+
// the entityEdit section's field list on note-edit — the precondition
|
|
51
|
+
// for the e2e layout-parity comparison between the two forms
|
|
52
|
+
|
|
53
|
+
import { describe, expect, test } from "bun:test";
|
|
54
|
+
import {
|
|
55
|
+
createRegistry,
|
|
56
|
+
createTextField,
|
|
57
|
+
defineFeature,
|
|
58
|
+
validateBoot as validateBootRaw,
|
|
59
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
60
|
+
import { withBootValidatorFixture } from "@cosmicdrift/kumiko-framework/testing";
|
|
61
|
+
import { z } from "zod";
|
|
62
|
+
import { noteDeskFeature } from "../feature";
|
|
63
|
+
|
|
64
|
+
function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
|
|
65
|
+
validateBootRaw(withBootValidatorFixture(features));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fieldName(spec: string | { readonly field: string }): string {
|
|
69
|
+
return typeof spec === "string" ? spec : spec.field;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const registry = createRegistry([noteDeskFeature]);
|
|
73
|
+
|
|
74
|
+
describe("writeform-section sample — registration", () => {
|
|
75
|
+
test("validateBoot accepts the full registered app", () => {
|
|
76
|
+
expect(() => validateBoot([noteDeskFeature])).not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("note-edit registers as entityEdit with one fields section", () => {
|
|
80
|
+
const screen = registry.getScreen("note-desk:screen:note-edit");
|
|
81
|
+
if (screen?.type !== "entityEdit") throw new Error("expected entityEdit");
|
|
82
|
+
expect(screen.layout.sections).toHaveLength(1);
|
|
83
|
+
expect(screen.layout.sections[0]?.kind).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("note-detail registers as projectionDetail with one writeForm section", () => {
|
|
87
|
+
const screen = registry.getScreen("note-desk:screen:note-detail");
|
|
88
|
+
if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
|
|
89
|
+
expect(screen.layout.sections).toHaveLength(1);
|
|
90
|
+
expect(screen.layout.sections[0]?.kind).toBe("writeForm");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("the writeForm section and the entityEdit section render the same field list", () => {
|
|
94
|
+
const editScreen = registry.getScreen("note-desk:screen:note-edit");
|
|
95
|
+
const detailScreen = registry.getScreen("note-desk:screen:note-detail");
|
|
96
|
+
if (editScreen?.type !== "entityEdit") throw new Error("expected entityEdit");
|
|
97
|
+
if (detailScreen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
|
|
98
|
+
|
|
99
|
+
const editSection = editScreen.layout.sections[0];
|
|
100
|
+
const detailSection = detailScreen.layout.sections[0];
|
|
101
|
+
if (
|
|
102
|
+
editSection === undefined ||
|
|
103
|
+
editSection.kind === "extension" ||
|
|
104
|
+
editSection.kind === "relatedList" ||
|
|
105
|
+
detailSection?.kind !== "writeForm"
|
|
106
|
+
) {
|
|
107
|
+
throw new Error("expected an entityEdit fields section and a writeForm section");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const editFields = editSection.fields.map(fieldName);
|
|
111
|
+
const writeFormFields = detailSection.fields.map(fieldName);
|
|
112
|
+
expect(writeFormFields).toEqual(editFields);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("the writeForm section's fieldDefs cover every field it renders", () => {
|
|
116
|
+
const detailScreen = registry.getScreen("note-desk:screen:note-detail");
|
|
117
|
+
if (detailScreen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
|
|
118
|
+
const section = detailScreen.layout.sections[0];
|
|
119
|
+
if (section?.kind !== "writeForm") throw new Error("expected writeForm section");
|
|
120
|
+
for (const spec of section.fields) {
|
|
121
|
+
expect(Object.keys(section.fieldDefs)).toContain(fieldName(spec));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("writeform-section sample — boot-validator catches author mistakes", () => {
|
|
127
|
+
test("a writeForm field without its pseudo-entity translation key fails boot", () => {
|
|
128
|
+
const broken = defineFeature("broken-note-desk", (r) => {
|
|
129
|
+
r.translations({ keys: { "screen:x.title": { en: "X" } } });
|
|
130
|
+
r.queryHandler("foo:detail", z.object({ id: z.string() }), async () => ({}), {
|
|
131
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
132
|
+
});
|
|
133
|
+
r.writeHandler("save", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
134
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
135
|
+
});
|
|
136
|
+
r.screen({
|
|
137
|
+
id: "x",
|
|
138
|
+
type: "projectionDetail",
|
|
139
|
+
query: "broken-note-desk:query:foo:detail",
|
|
140
|
+
layout: {
|
|
141
|
+
sections: [
|
|
142
|
+
{
|
|
143
|
+
kind: "writeForm",
|
|
144
|
+
title: "s",
|
|
145
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "is_business_data" }) },
|
|
146
|
+
fields: ["name"],
|
|
147
|
+
handler: "broken-note-desk:write:save",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(() => validateBootRaw([broken])).toThrow(
|
|
155
|
+
'[i18n] Feature "broken-note-desk": required translation key missing: "broken-note-desk:entity:__write-form-section__:field:name"',
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### src/feature.ts
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// WriteForm Section Sample
|
|
165
|
+
// Shows: a projectionDetail screen's `kind: "writeForm"` section — a
|
|
166
|
+
// self-persisting form embedded in a record detail page — rendered next to
|
|
167
|
+
// an established `entityEdit` form for the same fields. That side-by-side
|
|
168
|
+
// placement (see e2e/fixtures/client.tsx) is what makes a layout-parity
|
|
169
|
+
// comparison between the two form kinds possible.
|
|
170
|
+
|
|
171
|
+
import {
|
|
172
|
+
createEntity,
|
|
173
|
+
createNumberField,
|
|
174
|
+
createSelectField,
|
|
175
|
+
createTextField,
|
|
176
|
+
defineFeature,
|
|
177
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
178
|
+
import { z } from "zod";
|
|
179
|
+
import { NOTE_DETAIL_FIELDS } from "./note-fixture";
|
|
180
|
+
|
|
181
|
+
const NOTE_CATEGORIES = ["question", "bug", "idea"] as const;
|
|
182
|
+
|
|
183
|
+
const BODY_NOT_PERSONAL = {
|
|
184
|
+
personal: false,
|
|
185
|
+
reason: "is_business_data",
|
|
186
|
+
} as const;
|
|
187
|
+
|
|
188
|
+
// Shared by the entityEdit form and the writeForm section so the parity
|
|
189
|
+
// comparison can't drift through one-sided edits.
|
|
190
|
+
const NOTE_FIELDS = {
|
|
191
|
+
title: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
192
|
+
body: createTextField(BODY_NOT_PERSONAL),
|
|
193
|
+
category: createSelectField({ options: NOTE_CATEGORIES, default: "question" }),
|
|
194
|
+
priority: createNumberField(),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const noteEntity = createEntity({
|
|
198
|
+
table: "read_sample_writeform_notes",
|
|
199
|
+
fields: NOTE_FIELDS,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const open = {
|
|
203
|
+
access: {
|
|
204
|
+
openToAll: {
|
|
205
|
+
reason:
|
|
206
|
+
"demo recipe: any signed-in user manages every note; there is no per-user ownership in this sample",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
} as const;
|
|
210
|
+
|
|
211
|
+
const commentPayloadSchema = z.object({
|
|
212
|
+
title: z.string().optional(),
|
|
213
|
+
body: z.string().optional(),
|
|
214
|
+
category: z.enum(NOTE_CATEGORIES).optional(),
|
|
215
|
+
priority: z.number().optional(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
export const noteDeskFeature = defineFeature("note-desk", (r) => {
|
|
219
|
+
r.translations({
|
|
220
|
+
keys: {
|
|
221
|
+
"screen:note-edit.title": { en: "Edit note" },
|
|
222
|
+
"screen:note-detail.title": { en: "Note" },
|
|
223
|
+
"note-desk:entity:note:field:title": { en: "Title" },
|
|
224
|
+
"note-desk:entity:note:field:body": { en: "Body" },
|
|
225
|
+
"note-desk:entity:note:field:category": { en: "Category" },
|
|
226
|
+
"note-desk:entity:note:field:priority": { en: "Priority" },
|
|
227
|
+
"note-desk:entity:__write-form-section__:field:title": { en: "Title" },
|
|
228
|
+
"note-desk:entity:__write-form-section__:field:body": { en: "Body" },
|
|
229
|
+
"note-desk:entity:__write-form-section__:field:category": { en: "Category" },
|
|
230
|
+
"note-desk:entity:__write-form-section__:field:priority": { en: "Priority" },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
r.crud("note", noteEntity, { write: open, read: open });
|
|
235
|
+
|
|
236
|
+
r.screen({
|
|
237
|
+
id: "note-edit",
|
|
238
|
+
type: "entityEdit",
|
|
239
|
+
entity: "note",
|
|
240
|
+
layout: {
|
|
241
|
+
sections: [
|
|
242
|
+
{
|
|
243
|
+
title: "Note",
|
|
244
|
+
columns: 2,
|
|
245
|
+
fields: [{ field: "title", span: 2 }, "category", "priority", { field: "body", span: 2 }],
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
access: open.access,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
r.queryHandler(
|
|
253
|
+
"note:summary",
|
|
254
|
+
z.object({ id: z.string() }),
|
|
255
|
+
async (query) => ({ id: query.payload.id, ...NOTE_DETAIL_FIELDS }),
|
|
256
|
+
open,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
r.writeHandler(
|
|
260
|
+
"note:comment",
|
|
261
|
+
commentPayloadSchema,
|
|
262
|
+
async () => ({ isSuccess: true as const, data: null }),
|
|
263
|
+
open,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
r.screen({
|
|
267
|
+
id: "note-detail",
|
|
268
|
+
type: "projectionDetail",
|
|
269
|
+
query: "note-desk:query:note:summary",
|
|
270
|
+
layout: {
|
|
271
|
+
sections: [
|
|
272
|
+
{
|
|
273
|
+
// Same field set as note-edit's section — that's what makes the
|
|
274
|
+
// two forms comparable in the e2e layout-parity spec.
|
|
275
|
+
kind: "writeForm",
|
|
276
|
+
title: "Add comment",
|
|
277
|
+
columns: 2,
|
|
278
|
+
fieldDefs: NOTE_FIELDS,
|
|
279
|
+
fields: [{ field: "title", span: 2 }, "category", "priority", { field: "body", span: 2 }],
|
|
280
|
+
handler: "note-desk:write:note:comment",
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
access: open.access,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### src/note-fixture.ts
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
// Server-free on purpose: the e2e mock dispatcher (browser bundle) imports
|
|
293
|
+
// this, so it must not pull in the feature's server-side code.
|
|
294
|
+
export const NOTE_DETAIL_FIELDS = {
|
|
295
|
+
title: "Sample note",
|
|
296
|
+
category: "question",
|
|
297
|
+
priority: 2,
|
|
298
|
+
body: "...",
|
|
299
|
+
};
|
|
300
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/*/package.json. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# Examples
|
|
4
|
+
|
|
5
|
+
| Example | Description | File |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `access-control` | Default-deny access rules, openToAll vs. role-based handlers, and auto-indices for foreign keys. | `examples/access-control.md` |
|
|
8
|
+
| `anonymous-access` | Public endpoints (anonymous role) for shop / CMS-style apps. Tenant resolution via header, cookie, or default. | `examples/anonymous-access.md` |
|
|
9
|
+
| `anonymous-access-multitenant` | Subdomain-based tenant resolution for anonymous-access — SaaS shop / CMS pattern with per-tenant public surface. | `examples/anonymous-access-multitenant.md` |
|
|
10
|
+
| `apex-landing` | Apex landing page composed from other features — hero copy from template-resolver, prices and caps from tier-engine — rendered to one static HTML string. | `examples/apex-landing.md` |
|
|
11
|
+
| `apex-surface-auth` | Apex-Surface: öffentliche Account-Flows (Login/Register/PW/Löschen) via createPublicSurface + AuthShell, inkl. anonymem email-verifiziertem Deletion-Flow. | `examples/apex-surface-auth.md` |
|
|
12
|
+
| `api-worker-split` | Split deploy topology: API process (runSingleInstance:false, enqueue-only) + worker process (consumes worker-lane jobs, applies multiStreamProjections, writes results back through the worker dispatcher). | `examples/api-worker-split.md` |
|
|
13
|
+
| `auth-claims` | r.authClaims() — features contribute identity facts into the JWT at login time with auto-prefixed keys. | `examples/auth-claims.md` |
|
|
14
|
+
| `auth-foundation-providers` | Register custom tenantResolver / tenantExistence via auth-foundation extension points (no anonymousAccess callbacks). | `examples/auth-foundation-providers.md` |
|
|
15
|
+
| `auth-mfa` | createAuthMfaFeature — TOTP/2FA enrollment, login challenge, and recovery codes wired into auth-email-password. | `examples/auth-mfa.md` |
|
|
16
|
+
| `basic-entity` | The simplest possible CRUD demo. About six lines of actual code. | `examples/basic-entity.md` |
|
|
17
|
+
| `boot-check` | A feature declares its own mount-invariant via r.bootCheck. | `examples/boot-check.md` |
|
|
18
|
+
| `chat-channels` | Chat channels. Delivery notifications to Slack, minus the browser tab. | `examples/chat-channels.md` |
|
|
19
|
+
| `compliance-profiles-demo` | Two-tenant demo: EU + Swiss compliance profiles deliver different authority contacts, languages, and grace periods through the same App-code. | `examples/compliance-profiles-demo.md` |
|
|
20
|
+
| `cross-feature-events` | Feature appends a typed domain event via ctx.appendEvent; a r.multiStreamProjection reacts asynchronously. | `examples/cross-feature-events.md` |
|
|
21
|
+
| `crypto-shredding-hr` | Crypto-shredding: PII fields encrypted per subject key; forgetting the subject erases the key and every field it protected. | `examples/crypto-shredding-hr.md` |
|
|
22
|
+
| `currencies-global` | Shared money fields across tenants. Commas still matter. | `examples/currencies-global.md` |
|
|
23
|
+
| `currencies-per-tenant` | Tenant-scoped money fields. Commas matter. | `examples/currencies-per-tenant.md` |
|
|
24
|
+
| `custom-fields-basic` | Tenant-defined extra fields on an entity without writing migrations. | `examples/custom-fields-basic.md` |
|
|
25
|
+
| `custom-handlers` | Beyond CRUD — when your domain refuses to fit a table. | `examples/custom-handlers.md` |
|
|
26
|
+
| `delivery-notifications` | Delivery flows plus notifications. Package tracking, minus the tracking. | `examples/delivery-notifications.md` |
|
|
27
|
+
| `designer-demo` | Kumiko sample feature — scaffolded by `bun kumiko create` | `examples/designer-demo.md` |
|
|
28
|
+
| `e2e-generator` | generateE2ESpec + withBootValidatorFixture — Registry → JSON-Specs für externen Playwright-Worker. Zeigt die 4 Test-Kinds, Feldtyp→Interaktion-Mapping und welche Feldtypen heute noch nicht generisch unterstützt sind. | `examples/e2e-generator.md` |
|
|
29
|
+
| `embedded` | Embedded fields on entities. Structured JSON without the JSON part. | `examples/embedded.md` |
|
|
30
|
+
| `embedded-entity-form` | RenderEdit hosted in a Drawer, no screen or route — externally-sourced prefill, controlled mode, and a custom write handler instead of the built-in CRUD create. | `examples/embedded-entity-form.md` |
|
|
31
|
+
| `encrypted-tenant-config` | Per-tenant secrets via config-key encryption — Stripe-API-key pattern. | `examples/encrypted-tenant-config.md` |
|
|
32
|
+
| `error-contract` | Typed errors on the wire. With snake_case reasons, because we care. | `examples/error-contract.md` |
|
|
33
|
+
| `event-sourcing` | Production-pattern ES sample: r.defineEvent (versioned + migrations) + ctx.appendEvent + r.projection + r.multiStreamProjection + ctx.loadAggregate (asOf) + ctx.archiveStream + ctx.queryProjection. | `examples/event-sourcing.md` |
|
|
34
|
+
| `feature-toggles` | Runtime feature-toggle flip — two toggleable features, one registers a cross-feature hook on the other; disabling either has the documented effect without reboot. | `examples/feature-toggles.md` |
|
|
35
|
+
| `field-access` | Field-level access control. When entity-level roles aren't granular enough. | `examples/field-access.md` |
|
|
36
|
+
| `files-post-processing` | The three ways to generate an image variant: declarative field variants, imperative ctx.derivatives.variant() with region-blur, and the public route behind a default-deny predicate. | `examples/files-post-processing.md` |
|
|
37
|
+
| `i18n` | Feature-scoped translations. 'Hallo' and 'Hello' in the same app. | `examples/i18n.md` |
|
|
38
|
+
| `idempotency` | Same request, same result. Dedupes on request ID. | `examples/idempotency.md` |
|
|
39
|
+
| `inbound-mail-triage` | Inbound-mail consumer pattern: inbound-mail-foundation + in-memory provider, a worker-lane triage job attached to the ingest handler, verified end-to-end through createAllInOneEntrypoint. | `examples/inbound-mail-triage.md` |
|
|
40
|
+
| `lane-routing` | runIn lane-routing: r.job pinned to api/worker deploy-lanes, event-chain via ctx.appendEvent, verified end-to-end through createAllInOneEntrypoint. | `examples/lane-routing.md` |
|
|
41
|
+
| `ledger-banking` | Bank accounts + transfers on the ledger primitive — balances are pure queries, never stored. | `examples/ledger-banking.md` |
|
|
42
|
+
| `ledger-invoicing` | Accrual invoicing on the ledger primitive — revenue at invoice time, VAT as a liability, receivable cleared on payment. | `examples/ledger-invoicing.md` |
|
|
43
|
+
| `legal-pages` | Auto-rendered Impressum + Datenschutzerklärung pages — template-resolver + legal-pages bundled features wired up with anonymousAccess for DACH-compliance. | `examples/legal-pages.md` |
|
|
44
|
+
| `lifecycle-hooks` | preSave, postSave, and the rest. Your logic, our plumbing. | `examples/lifecycle-hooks.md` |
|
|
45
|
+
| `managed-config` | Managed config: one declaration provisions storage + masking + settings-hub, across backings (secrets vs config) and scopes (system vs tenant). | `examples/managed-config.md` |
|
|
46
|
+
| `managed-pages` | Tenant-editable, server-rendered public pages with per-tenant branding + tier-gated sanitized custom CSS — managed-pages wired up with config + anonymousAccess. | `examples/managed-pages.md` |
|
|
47
|
+
| `notes-history-basic` | Append-only note history — who wrote what, when — attached to any entity without changing it. | `examples/notes-history-basic.md` |
|
|
48
|
+
| `ownership` | Entity- and Field-level Row-Ownership: from() helper, Straddle-safe multi-role checks, Field-level Read/Write filtering. | `examples/ownership.md` |
|
|
49
|
+
| `pdf-audit-export` | Exports an audit table as a PDF: a write handler renders rows with renderTablePdf and stores the bytes as a FileRef via createFileRefFromBytes. | `examples/pdf-audit-export.md` |
|
|
50
|
+
| `pipeline-basics` | M.1 Tier-1 step-vocabulary showcase: every step in one inventory-management feature (aggregate.{create,update,appendEvent}, read.{findOne,findMany}, branch, forEach, compute, return, unsafeProjection{Upsert,Delete}). | `examples/pipeline-basics.md` |
|
|
51
|
+
| `public-share-token` | Generic share-token entity: mint hash-stored token, anonymous read-by-token with IP rate-limit. | `examples/public-share-token.md` |
|
|
52
|
+
| `rate-limiting` | End-to-end rate limiting: L1 global-IP + L2 auth-endpoints + L3 handler opt-in. | `examples/rate-limiting.md` |
|
|
53
|
+
| `realtime-sse` | Server-sent events demo. Goodbye F5. | `examples/realtime-sse.md` |
|
|
54
|
+
| `record-detail-layout` | projectionDetail record header, metrics band, and layout.mode: "tabs" — an order screen with two relatedList tabs and a fields tab. | `examples/record-detail-layout.md` |
|
|
55
|
+
| `reference-data` | Upsert-based seeds. Lookup tables done right. | `examples/reference-data.md` |
|
|
56
|
+
| `relations` | belongsTo, hasMany, manyToMany — the good stuff. | `examples/relations.md` |
|
|
57
|
+
| `screens-nav` | r.screen() + r.nav() — entityList/entityEdit/custom screens, cross-feature nav parents, FieldCondition<TData>, boot-validator behaviour. | `examples/screens-nav.md` |
|
|
58
|
+
| `search` | Tenant-scoped Meilisearch. Because ILIKE is not search. | `examples/search.md` |
|
|
59
|
+
| `secrets-demo` | Tenant-owned secrets with envelope encryption, DEK cache, and KEK rotation. | `examples/secrets-demo.md` |
|
|
60
|
+
| `security-baseline` | securityBaselineFeatures() preset: sessions, crypto-shredding, rate-limiting, audit — plus the NODE_ENV=production boot warning when one is missing. | `examples/security-baseline.md` |
|
|
61
|
+
| `seed-migration` | ES-Operations seed-migrations — file-based diff-and-apply für Aggregate-Updates die idempotent-Seeder nicht erfassen. | `examples/seed-migration.md` |
|
|
62
|
+
| `session-revocation` | createSessionCallbacks + sessionChecker — server-side session revocation via JWT jti. | `examples/session-revocation.md` |
|
|
63
|
+
| `state-machine` | Enforced entity state transitions. No more illegal state changes. | `examples/state-machine.md` |
|
|
64
|
+
| `tags-basic` | Tag and group any entity without changing it — host-agnostic, event-sourced. | `examples/tags-basic.md` |
|
|
65
|
+
| `tenant-isolation` | Multi-tenant separation. Your data stays your data. | `examples/tenant-isolation.md` |
|
|
66
|
+
| `tenant-settings` | Tenant-Settings: per-tenant Currency + Locale defaults, auto-filled into a money field / locale field on create instead of a hard-coded literal. | `examples/tenant-settings.md` |
|
|
67
|
+
| `tier-admin` | Manually grant a tier to a tenant without a billing purchase — the SystemAdmin operator flow and how the resolver cache stays in sync. | `examples/tier-admin.md` |
|
|
68
|
+
| `timezones` | Every time-aware field type on one entity, plus the ctx.tz handler API. | `examples/timezones.md` |
|
|
69
|
+
| `user-data-rights` | DSGVO-compliant in 5 LOC: r.useExtension(EXT_USER_DATA, ...) haengt eine Domain an Forget+Export-Pipeline. Forget-Cron, Export-ZIP, Magic-Link kommen alle aus user-data-rights. | `examples/user-data-rights.md` |
|
|
70
|
+
| `user-profile` | user-profile bundled feature — Self-Service-Kontoseite (Passwort/E-Mail ändern, Konto löschen) als App-Screen verdrahten. | `examples/user-profile.md` |
|
|
71
|
+
| `webhook-step` | Tier-2 r.step.webhook.send showcase: deferred webhook dispatch via step-dispatcher subscription, no outbox table. | `examples/webhook-step.md` |
|
|
72
|
+
| `wizard-form` | EditLayout.mode: "wizard" — a multi-step entity form with a review step that reads live form values through an extension section, and draft: true resuming the in-progress form via the bundled form-draft feature. | `examples/wizard-form.md` |
|
|
73
|
+
| `workflow-engine` | M.4 Tier-3 step-vocabulary showcase: defineWorkflow with wait/waitForEvent/retry steps | `examples/workflow-engine.md` |
|
|
74
|
+
| `writeform-section` | A projectionDetail `writeForm` section rendered beside an established entityEdit form on the same page, so the shared form chrome (footer, submit affordance) between the two can be verified side by side. | `examples/writeform-section.md` |
|