@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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 +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -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 +319 -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 +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -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 +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -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 +215 -0
- package/agents/examples/ledger-invoicing.md +236 -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 +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -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 +250 -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 +418 -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 +75 -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 +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- 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/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- 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/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- 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 +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -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 +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- 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 +8 -8
- 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/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -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 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- 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 +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- 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 +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- 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/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- package/src/changes.json +512 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
2
2
|
import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs } from "../screen-helpers.js";
|
|
3
|
-
import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, } from "./screens.js";
|
|
3
|
+
import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, validateDrawerPrefillKeys, } from "./screens.js";
|
|
4
4
|
function isNavigateKind(action) {
|
|
5
5
|
return action.kind === undefined || action.kind === "navigate";
|
|
6
6
|
}
|
|
@@ -59,13 +59,14 @@ function navigateActionsOf(screen) {
|
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
function toNavigateParamsSource(action, params, sourceScreenEntity) {
|
|
62
|
+
function toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel) {
|
|
63
63
|
return {
|
|
64
64
|
...(action.screen !== undefined && { screen: action.screen }),
|
|
65
65
|
...(action.entity !== undefined && { entity: action.entity }),
|
|
66
66
|
...(action.entityId !== undefined && { entityId: action.entityId }),
|
|
67
67
|
params,
|
|
68
68
|
sourceScreenEntity,
|
|
69
|
+
sourceLabel,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
// Every declarative navigate that writes params into the target's URL —
|
|
@@ -73,17 +74,19 @@ function toNavigateParamsSource(action, params, sourceScreenEntity) {
|
|
|
73
74
|
// plus projectionDetail metrics (runMetricNavigate), which reuse that shape —
|
|
74
75
|
// plus a relatedList toolbarAction with no `params` of its own, which still
|
|
75
76
|
// writes its implicit parent-id param at runtime (see `implicitParams`).
|
|
76
|
-
function navigateParamsSources(screen) {
|
|
77
|
+
function navigateParamsSources(screen, sourceLabel) {
|
|
77
78
|
return navigateActionsOf(screen).flatMap(({ action, sourceScreenEntity, implicitParams }) => {
|
|
78
79
|
if (!isNavigateKind(action))
|
|
79
80
|
return [];
|
|
80
81
|
const params = action.params ?? implicitParams;
|
|
81
|
-
return params === undefined
|
|
82
|
+
return params === undefined
|
|
83
|
+
? []
|
|
84
|
+
: [toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel)];
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
|
-
// Per target screen QN,
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
+
// Per target screen QN, every navigate `params` source that writes into the
|
|
88
|
+
// target's URL prefill. A form screen absent from the map accepts no URL prefill.
|
|
89
|
+
function collectUrlPrefillSourcesByScreenQn(features) {
|
|
87
90
|
const screensByShortId = collectScreensByShortId(features);
|
|
88
91
|
const detailForByEntity = new Map();
|
|
89
92
|
for (const feature of features) {
|
|
@@ -95,8 +98,9 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
|
|
|
95
98
|
}
|
|
96
99
|
const result = new Map();
|
|
97
100
|
for (const feature of features) {
|
|
98
|
-
for (const screen of Object.
|
|
99
|
-
|
|
101
|
+
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
102
|
+
const sourceLabel = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) navigate action`;
|
|
103
|
+
for (const source of navigateParamsSources(screen, sourceLabel)) {
|
|
100
104
|
const target = resolveTarget(source, screensByShortId, detailForByEntity);
|
|
101
105
|
if (target === undefined)
|
|
102
106
|
continue;
|
|
@@ -105,15 +109,21 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
|
|
|
105
109
|
continue;
|
|
106
110
|
}
|
|
107
111
|
const qn = qualifyEntityName(target.featureName, "screen", target.shortId);
|
|
108
|
-
|
|
109
|
-
for (const key of rowFieldExtractorKeys(source.params))
|
|
110
|
-
fields.add(key);
|
|
111
|
-
result.set(qn, fields);
|
|
112
|
+
result.set(qn, [...(result.get(qn) ?? []), source]);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
return result;
|
|
116
117
|
}
|
|
118
|
+
// Per target screen QN, the union of field names any navigate `params` may
|
|
119
|
+
// prefill from the URL.
|
|
120
|
+
export function collectUrlPrefillFieldsByScreenQn(features) {
|
|
121
|
+
const result = new Map();
|
|
122
|
+
for (const [qn, sources] of collectUrlPrefillSourcesByScreenQn(features)) {
|
|
123
|
+
result.set(qn, new Set(sources.flatMap((source) => rowFieldExtractorKeys(source.params))));
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
117
127
|
// Runtime resolution: a bare screen id matches the first feature registering
|
|
118
128
|
// it (create-app's router); an entity target opens its detailFor screen with an id.
|
|
119
129
|
function resolveTarget(source, screensByShortId, detailForByEntity) {
|
|
@@ -124,37 +134,46 @@ function resolveTarget(source, screensByShortId, detailForByEntity) {
|
|
|
124
134
|
const match = screensByShortId.get(source.screen)?.[0];
|
|
125
135
|
return match === undefined ? undefined : { ...match, shortId: source.screen };
|
|
126
136
|
}
|
|
137
|
+
function assertHiddenRequiredFieldsSeeded(featureName, screenId, screen, seeded) {
|
|
138
|
+
for (const section of screen.layout.sections) {
|
|
139
|
+
if (!isFieldsEditSection(section))
|
|
140
|
+
continue;
|
|
141
|
+
for (const rawSpec of sectionFieldSpecs(section)) {
|
|
142
|
+
const spec = normalizeEditField(rawSpec);
|
|
143
|
+
if (spec.visible !== false)
|
|
144
|
+
continue;
|
|
145
|
+
const fieldDef = screen.fields[spec.field];
|
|
146
|
+
if (fieldDef === undefined || !("required" in fieldDef) || fieldDef.required !== true) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (seeded.has(spec.field))
|
|
150
|
+
continue;
|
|
151
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screen.type}) sets submitPrefilled but ` +
|
|
152
|
+
`hides required field "${spec.field}" (visible: false) and no navigate action passes it in ` +
|
|
153
|
+
`params — the field could never get a value and the form could never submit. Add ` +
|
|
154
|
+
`"${spec.field}" to the params of a navigate action targeting this screen, make the field ` +
|
|
155
|
+
`visible, or drop required.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
127
159
|
// submitPrefilled lets an unchanged form submit, so a required field the layout
|
|
128
|
-
// hides must be seeded by a navigate param — nothing else can ever fill it.
|
|
160
|
+
// hides must be seeded by a navigate param — nothing else can ever fill it. The
|
|
161
|
+
// param keys of every source (incl. metric/subtitle navigates and the implicit
|
|
162
|
+
// relatedList parent id) must also be fields the renderer would actually prefill.
|
|
129
163
|
export function validateSubmitPrefilledHiddenFields(features) {
|
|
130
|
-
const
|
|
164
|
+
const sourcesByScreenQn = collectUrlPrefillSourcesByScreenQn(features);
|
|
131
165
|
for (const feature of features) {
|
|
132
166
|
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
133
167
|
if (screen.type !== "actionForm" && screen.type !== "secretMint")
|
|
134
168
|
continue;
|
|
135
169
|
if (screen.submitPrefilled !== true)
|
|
136
170
|
continue;
|
|
137
|
-
const
|
|
138
|
-
for (const
|
|
139
|
-
|
|
140
|
-
continue;
|
|
141
|
-
for (const rawSpec of sectionFieldSpecs(section)) {
|
|
142
|
-
const spec = normalizeEditField(rawSpec);
|
|
143
|
-
if (spec.visible !== false)
|
|
144
|
-
continue;
|
|
145
|
-
const fieldDef = screen.fields[spec.field];
|
|
146
|
-
if (fieldDef === undefined || !("required" in fieldDef) || fieldDef.required !== true) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (seeded?.has(spec.field) === true)
|
|
150
|
-
continue;
|
|
151
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets submitPrefilled but ` +
|
|
152
|
-
`hides required field "${spec.field}" (visible: false) and no navigate action passes it in ` +
|
|
153
|
-
`params — the field could never get a value and the form could never submit. Add ` +
|
|
154
|
-
`"${spec.field}" to the params of a navigate action targeting this screen, make the field ` +
|
|
155
|
-
`visible, or drop required.`);
|
|
156
|
-
}
|
|
171
|
+
const sources = sourcesByScreenQn.get(qualifyEntityName(feature.name, "screen", screenId));
|
|
172
|
+
for (const source of sources ?? []) {
|
|
173
|
+
validateDrawerPrefillKeys(source.sourceLabel, "navigate-target", screenId, screen, source.params);
|
|
157
174
|
}
|
|
175
|
+
const seeded = new Set((sources ?? []).flatMap((source) => rowFieldExtractorKeys(source.params)));
|
|
176
|
+
assertHiddenRequiredFieldsSeeded(feature.name, screenId, screen, seeded);
|
|
158
177
|
}
|
|
159
178
|
}
|
|
160
179
|
}
|
|
@@ -11,13 +11,40 @@ function sameKeys(a, b) {
|
|
|
11
11
|
const bKeys = new Set(Object.keys(b ?? {}));
|
|
12
12
|
return aKeys.length === bKeys.size && aKeys.every((key) => bKeys.has(key));
|
|
13
13
|
}
|
|
14
|
+
const KEYED_SURFACE_PROPERTIES = [
|
|
15
|
+
"entities",
|
|
16
|
+
"writeHandlers",
|
|
17
|
+
"queryHandlers",
|
|
18
|
+
"streamHandlers",
|
|
19
|
+
"screens",
|
|
20
|
+
"navs",
|
|
21
|
+
"jobs",
|
|
22
|
+
"events",
|
|
23
|
+
"translations",
|
|
24
|
+
"configKeys",
|
|
25
|
+
"notifications",
|
|
26
|
+
"metrics",
|
|
27
|
+
];
|
|
28
|
+
// Hooks are keyed per phase, then per target: flatten so a hook added under an
|
|
29
|
+
// existing phase still shows up as a difference.
|
|
30
|
+
function hookKeys(feature) {
|
|
31
|
+
const keys = new Set();
|
|
32
|
+
for (const [phase, byTarget] of Object.entries(feature.hooks ?? {})) {
|
|
33
|
+
for (const target of Object.keys(byTarget ?? {}))
|
|
34
|
+
keys.add(`${phase}/${target}`);
|
|
35
|
+
}
|
|
36
|
+
return keys;
|
|
37
|
+
}
|
|
38
|
+
function sameHookKeys(a, b) {
|
|
39
|
+
const aKeys = hookKeys(a);
|
|
40
|
+
const bKeys = hookKeys(b);
|
|
41
|
+
return aKeys.size === bKeys.size && [...aKeys].every((key) => bKeys.has(key));
|
|
42
|
+
}
|
|
14
43
|
// dedupeOptions is a declaration, not proof: two same-named features that both
|
|
15
44
|
// carry `{}` but register different surface must still clash, not silently drop one.
|
|
16
45
|
function sameRegisteredSurface(a, b) {
|
|
17
|
-
return (sameKeys(a
|
|
18
|
-
|
|
19
|
-
sameKeys(a.queryHandlers, b.queryHandlers) &&
|
|
20
|
-
sameKeys(a.streamHandlers, b.streamHandlers));
|
|
46
|
+
return (KEYED_SURFACE_PROPERTIES.every((property) => sameKeys(a[property], b[property])) &&
|
|
47
|
+
sameHookKeys(a, b));
|
|
21
48
|
}
|
|
22
49
|
function isInterchangeable(existing, next) {
|
|
23
50
|
if (existing === next)
|
|
@@ -4,9 +4,9 @@ import type { ContainsSecret } from "../secrets/types.js";
|
|
|
4
4
|
import type { QueryHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput } from "./types/define-handler.js";
|
|
5
5
|
import type { KumikoEventTypeMap } from "./types/index.js";
|
|
6
6
|
export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "./types/define-handler.js";
|
|
7
|
-
export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap>(def: WriteHandlerInput<TName, TSchema, TData, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
|
|
7
|
+
export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap, TPrepared = undefined>(def: WriteHandlerInput<TName, TSchema, TData, TMap, TPrepared>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
|
|
8
8
|
secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
|
|
9
|
-
] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap>;
|
|
9
|
+
] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap, TPrepared>;
|
|
10
10
|
export declare function defineQueryHandler<const TName extends string, TSchema extends ZodType, TResult = unknown, TMap extends object = KumikoEventTypeMap>(def: QueryHandlerDefinition<TName, TSchema, TResult, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TResult> ? [
|
|
11
11
|
secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
|
|
12
12
|
] : []): QueryHandlerDefinition<TName, TSchema, TResult, TMap>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
2
|
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
3
|
import { runPipeline } from "./run-pipeline.js";
|
|
4
|
+
function pickPreTransactionPhases(def) {
|
|
5
|
+
return {
|
|
6
|
+
...(def.reserveBeforeTransaction && { reserveBeforeTransaction: def.reserveBeforeTransaction }),
|
|
7
|
+
...(def.beforeTransaction && { beforeTransaction: def.beforeTransaction }),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
4
10
|
export function defineWriteHandler(def,
|
|
5
11
|
// R6: a phantom rest-param. When the inferred response `TData` carries a
|
|
6
12
|
// Secret<> anywhere, ContainsSecret<TData> is `true` and this resolves to a
|
|
@@ -51,8 +57,9 @@ export function defineWriteHandler(def,
|
|
|
51
57
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
52
58
|
...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
|
|
53
59
|
...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
|
|
54
|
-
...(def
|
|
60
|
+
...pickPreTransactionPhases(def),
|
|
55
61
|
...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
|
|
62
|
+
...(def.transaction && { transaction: def.transaction }),
|
|
56
63
|
};
|
|
57
64
|
if ("perform" in def && def.perform !== undefined) {
|
|
58
65
|
const performDef = def.perform;
|
|
@@ -4,6 +4,7 @@ import { collectReferenceFields, enrichRowWithReferences, enrichWithReferences,
|
|
|
4
4
|
import { createEventStoreExecutor } from "../db/event-store-executor.js";
|
|
5
5
|
import { buildEntityTable } from "../db/table-builder.js";
|
|
6
6
|
import { acknowledgeConventionCrossTenant, reportConventionCrossTenantUse, } from "../db/tenant-db.js";
|
|
7
|
+
import { InternalError } from "../errors/index.js";
|
|
7
8
|
import { isSystemIdentity } from "../pipeline/system-identity-switch.js";
|
|
8
9
|
import { assertUnreachable } from "../utils/index.js";
|
|
9
10
|
import { PAGED_QUERY_HANDLER_BRAND } from "./define-handler.js";
|
|
@@ -49,8 +50,8 @@ const WRITE_VERBS = ["create", "update", "delete", "restore"];
|
|
|
49
50
|
const QUERY_VERBS = ["list", "detail"];
|
|
50
51
|
const idSchema = z.object({ id: z.uuid() });
|
|
51
52
|
const SYSTEM_ADMIN_ROLE = "SystemAdmin";
|
|
52
|
-
// A
|
|
53
|
-
// role may reach it — otherwise any tenant-level role holder could mutate foreign tenants.
|
|
53
|
+
// A handler with escapeHatch reads/writes rows of every tenant, so only an operator
|
|
54
|
+
// role may reach it — otherwise any tenant-level role holder could read or mutate foreign tenants.
|
|
54
55
|
function isSystemAdminOnly(access) {
|
|
55
56
|
return ("roles" in access &&
|
|
56
57
|
access.roles.length > 0 &&
|
|
@@ -65,8 +66,8 @@ function resolveCrossTenantReason(name, options, kind) {
|
|
|
65
66
|
const placeholderProblem = codemodPlaceholderReasonProblem(options.escapeHatch.reason);
|
|
66
67
|
if (placeholderProblem !== undefined)
|
|
67
68
|
throw new Error(`"${name}": ${placeholderProblem}`);
|
|
68
|
-
if (
|
|
69
|
-
throw new Error(`"${name}": a
|
|
69
|
+
if (!isSystemAdminOnly(options.access)) {
|
|
70
|
+
throw new Error(`"${name}": a ${kind} handler with escapeHatch reaches across every tenant, so its access must be SystemAdmin-only ({ roles: ["${SYSTEM_ADMIN_ROLE}"] }); got ${JSON.stringify(options.access)}.`);
|
|
70
71
|
}
|
|
71
72
|
return options.escapeHatch.reason;
|
|
72
73
|
}
|
|
@@ -235,7 +236,13 @@ export function defineEntityWriteHandler(name, entity, options) {
|
|
|
235
236
|
}
|
|
236
237
|
if (typeof rowTenantId !== "string" || rowTenantId === user.tenantId)
|
|
237
238
|
return undefined;
|
|
238
|
-
|
|
239
|
+
const streamTenantId = parseTenantId(rowTenantId);
|
|
240
|
+
if (streamTenantId === null) {
|
|
241
|
+
throw new InternalError({
|
|
242
|
+
message: `"${name}": row ${String(id)} has tenantId "${rowTenantId}" which is not a TenantId; refusing to append to the wrong stream.`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return streamTenantId;
|
|
239
246
|
};
|
|
240
247
|
let schema;
|
|
241
248
|
let handler;
|
|
@@ -569,7 +576,7 @@ export function registerEntityCrud(r, entityName, entity, options) {
|
|
|
569
576
|
...writeOpts,
|
|
570
577
|
access: requireAccess(verb, options?.verbAccess?.[verb] ?? writeOpts?.access),
|
|
571
578
|
description: options?.descriptions?.[verb] ?? writeOpts?.description,
|
|
572
|
-
...(verbAgent !== undefined && { agent: verbAgent }),
|
|
579
|
+
...(verbAgent !== undefined && { agent: { ...writeOpts?.agent, ...verbAgent } }),
|
|
573
580
|
...(excludeFields !== undefined && { excludeFields }),
|
|
574
581
|
};
|
|
575
582
|
};
|
|
@@ -579,7 +586,7 @@ export function registerEntityCrud(r, entityName, entity, options) {
|
|
|
579
586
|
...readOpts,
|
|
580
587
|
access: requireAccess(verb, options?.verbAccess?.[verb] ?? readOpts?.access),
|
|
581
588
|
description: options?.descriptions?.[verb] ?? readOpts?.description,
|
|
582
|
-
...(verbAgent !== undefined && { agent: verbAgent }),
|
|
589
|
+
...(verbAgent !== undefined && { agent: { ...readOpts?.agent, ...verbAgent } }),
|
|
583
590
|
};
|
|
584
591
|
};
|
|
585
592
|
if (verbs.create) {
|
|
@@ -202,6 +202,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
|
|
|
202
202
|
limit: z.ZodNumber;
|
|
203
203
|
windowSeconds: z.ZodNumber;
|
|
204
204
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
205
|
+
normalize: z.ZodOptional<z.ZodLiteral<"email">>;
|
|
205
206
|
}, z.core.$strict>>, z.ZodObject<{
|
|
206
207
|
__raw: z.ZodString;
|
|
207
208
|
}, z.core.$strict>]>>;
|
|
@@ -724,6 +725,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
724
725
|
limit: z.ZodNumber;
|
|
725
726
|
windowSeconds: z.ZodNumber;
|
|
726
727
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
728
|
+
normalize: z.ZodOptional<z.ZodLiteral<"email">>;
|
|
727
729
|
}, z.core.$strict>>, z.ZodObject<{
|
|
728
730
|
__raw: z.ZodString;
|
|
729
731
|
}, z.core.$strict>]>>;
|
|
@@ -240,18 +240,26 @@ function renderMetric(p) {
|
|
|
240
240
|
const merged = { name: p.shortName, ...p.options };
|
|
241
241
|
return `r.metric(${renderValue(merged)});`;
|
|
242
242
|
}
|
|
243
|
+
// r.secret / r.secretNamespace throw on an empty writeRoles list; the designer's
|
|
244
|
+
// string-list input emits [] when cleared, which means "rely on handler access alone".
|
|
245
|
+
function dropEmptyWriteRoles(options) {
|
|
246
|
+
if (options.writeRoles?.length !== 0)
|
|
247
|
+
return options;
|
|
248
|
+
const { writeRoles: _emptyWriteRoles, ...rest } = options;
|
|
249
|
+
return rest;
|
|
250
|
+
}
|
|
243
251
|
function renderSecret(p) {
|
|
244
252
|
if (isRawRefSentinel(p.options)) {
|
|
245
253
|
return `r.secret(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
|
|
246
254
|
}
|
|
247
|
-
const merged = { name: p.shortName, ...p.options };
|
|
255
|
+
const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
|
|
248
256
|
return `r.secret(${renderValue(merged)});`;
|
|
249
257
|
}
|
|
250
258
|
function renderSecretNamespace(p) {
|
|
251
259
|
if (isRawRefSentinel(p.options)) {
|
|
252
260
|
return `r.secretNamespace(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
|
|
253
261
|
}
|
|
254
|
-
const merged = { name: p.shortName, ...p.options };
|
|
262
|
+
const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
|
|
255
263
|
return `r.secretNamespace(${renderValue(merged)});`;
|
|
256
264
|
}
|
|
257
265
|
function renderClaimKey(p) {
|
|
@@ -3,7 +3,7 @@ import type { ZodType } from "zod";
|
|
|
3
3
|
import { type QueryHandlerDefinition, type StreamHandlerDefinition, type WriteHandlerDefinition } from "./define-handler.js";
|
|
4
4
|
import type { RegisterEntityCrudOptions } from "./entity-handlers.js";
|
|
5
5
|
import type { FeatureBuilderState } from "./feature-builder-state.js";
|
|
6
|
-
import type { AccessRule, AgentHandlerHints, EntityDefinition, EntityRef, EscapeHatchDeclaration, HandlerRef, NameOrRef, PayloadRateLimitOption, QueryHandlerFn, RateLimitDeclaration, RelationDefinition, StreamHandlerFn, WriteHandlerFn } from "./types/index.js";
|
|
6
|
+
import type { AccessRule, AgentHandlerHints, EntityDefinition, EntityRef, EscapeHatchDeclaration, HandlerRef, KumikoEventTypeMap, NameOrRef, PayloadRateLimitOption, QueryHandlerFn, RateLimitDeclaration, RelationDefinition, StreamHandlerFn, WriteHandlerFn } from "./types/index.js";
|
|
7
7
|
export declare function buildEntityHandlerMethods<TName extends string>(state: FeatureBuilderState, name: TName): {
|
|
8
8
|
entity(nameOrDefinition: string | ({
|
|
9
9
|
readonly name: string;
|
|
@@ -14,7 +14,7 @@ export declare function buildEntityHandlerMethods<TName extends string>(state: F
|
|
|
14
14
|
readonly entity: NameOrRef;
|
|
15
15
|
readonly name: string;
|
|
16
16
|
} & RelationDefinition), relationName?: string, definition?: RelationDefinition): void;
|
|
17
|
-
writeHandler<TName_1 extends string, TSchema extends ZodType>(nameOrDef: string | WriteHandlerDefinition<TName_1, TSchema>, schema?: TSchema, handler?: WriteHandlerFn<z.infer<TSchema>>, options?: {
|
|
17
|
+
writeHandler<TName_1 extends string, TSchema extends ZodType, TPrepared = undefined>(nameOrDef: string | WriteHandlerDefinition<TName_1, TSchema, unknown, KumikoEventTypeMap, TPrepared>, schema?: TSchema, handler?: WriteHandlerFn<z.infer<TSchema>>, options?: {
|
|
18
18
|
access: AccessRule;
|
|
19
19
|
rateLimit?: RateLimitDeclaration;
|
|
20
20
|
additionalRateLimits?: readonly PayloadRateLimitOption[];
|
|
@@ -54,6 +54,17 @@ function agentSlots(source) {
|
|
|
54
54
|
...(source.agent && { agent: source.agent }),
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
function transactionPhaseSlots(def) {
|
|
58
|
+
return {
|
|
59
|
+
...(def.reserveBeforeTransaction && {
|
|
60
|
+
reserveBeforeTransaction: def.reserveBeforeTransaction,
|
|
61
|
+
}),
|
|
62
|
+
...(def.beforeTransaction && {
|
|
63
|
+
beforeTransaction: def.beforeTransaction, // @cast-boundary engine-bridge
|
|
64
|
+
}),
|
|
65
|
+
...(def.transaction && { transaction: def.transaction }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
57
68
|
function requireInlineHandlerArgs(kind, schema, handler, options) {
|
|
58
69
|
if (!schema || !handler || !options?.access) {
|
|
59
70
|
throw new Error(`${kind} inline form requires schema + handler + options.access`);
|
|
@@ -108,9 +119,7 @@ export function buildEntityHandlerMethods(state, name) {
|
|
|
108
119
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
109
120
|
...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
|
|
110
121
|
...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
|
|
111
|
-
...(def
|
|
112
|
-
reserveBeforeTransaction: def.reserveBeforeTransaction,
|
|
113
|
-
}),
|
|
122
|
+
...transactionPhaseSlots(def),
|
|
114
123
|
...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
|
|
115
124
|
// Forward the pipeline-build closure so boot-validators and
|
|
116
125
|
// Designer/AI tooling can inspect the step list. Absent on
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as z from "zod";
|
|
2
2
|
export declare const HANDLER_FIELD_REFERENCES_META_KEY = "references";
|
|
3
|
-
export
|
|
3
|
+
export type HandlerFieldReferenceMeta = string | {
|
|
4
|
+
readonly entity: string;
|
|
5
|
+
readonly by?: string;
|
|
6
|
+
};
|
|
7
|
+
export type HandlerFieldReference = {
|
|
8
|
+
readonly entity: string;
|
|
9
|
+
readonly by: string | undefined;
|
|
10
|
+
readonly multiple: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type HandlerFieldReferenceReading = {
|
|
13
|
+
readonly kind: "none";
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "invalid";
|
|
16
|
+
readonly raw: unknown;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "ok";
|
|
19
|
+
readonly reference: HandlerFieldReference;
|
|
20
|
+
};
|
|
21
|
+
export declare function readHandlerFieldReference(schema: z.ZodType, field: string): HandlerFieldReferenceReading;
|
|
22
|
+
export declare function handlerFieldReference(schema: z.ZodType, field: string): HandlerFieldReference | undefined;
|
|
@@ -1,31 +1,69 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as z from "zod";
|
|
2
2
|
import { zodDef, zodMeta, zodShapeField } from "../env/_zod-introspect.js";
|
|
3
3
|
export const HANDLER_FIELD_REFERENCES_META_KEY = "references";
|
|
4
4
|
const MAX_WRAPPER_DEPTH = 8;
|
|
5
|
-
|
|
5
|
+
const referenceMetaSchema = z.union([
|
|
6
|
+
z.string().min(1),
|
|
7
|
+
z.object({ entity: z.string().min(1), by: z.string().min(1).optional() }),
|
|
8
|
+
]);
|
|
9
|
+
function rawReferenceMeta(field) {
|
|
6
10
|
const meta = zodMeta(field);
|
|
7
11
|
if (typeof meta !== "object" || meta === null)
|
|
8
12
|
return undefined;
|
|
9
|
-
|
|
10
|
-
return typeof value === "string" ? value : undefined;
|
|
13
|
+
return Reflect.get(meta, HANDLER_FIELD_REFERENCES_META_KEY);
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
// from the outer type, so every wrapper layer is probed on the way in.
|
|
14
|
-
export function handlerFieldReference(schema, field) {
|
|
15
|
+
function unwrapObjectSchema(schema) {
|
|
15
16
|
let current = schema;
|
|
16
17
|
for (let depth = 0; depth < MAX_WRAPPER_DEPTH && current !== undefined; depth++) {
|
|
17
|
-
if (current instanceof ZodObject)
|
|
18
|
-
|
|
18
|
+
if (current instanceof z.ZodObject)
|
|
19
|
+
return current;
|
|
19
20
|
current = zodDef(current)?.innerType;
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
// `.optional()` / `.nullable()` / `.default()` after `.meta()` hide the meta from the outer
|
|
25
|
+
// type, so every wrapper layer is probed on the way in; array elements are probed too.
|
|
26
|
+
function findRawReferenceMeta(field) {
|
|
27
|
+
let current = field;
|
|
28
|
+
for (let depth = 0; depth < MAX_WRAPPER_DEPTH && current !== undefined; depth++) {
|
|
29
|
+
const raw = rawReferenceMeta(current);
|
|
30
|
+
if (raw !== undefined)
|
|
31
|
+
return raw;
|
|
32
|
+
current = current instanceof z.ZodArray ? zodDef(current)?.element : zodDef(current)?.innerType;
|
|
29
33
|
}
|
|
30
34
|
return undefined;
|
|
31
35
|
}
|
|
36
|
+
function isArrayField(field) {
|
|
37
|
+
let current = field;
|
|
38
|
+
for (let depth = 0; depth < MAX_WRAPPER_DEPTH && current !== undefined; depth++) {
|
|
39
|
+
if (current instanceof z.ZodArray)
|
|
40
|
+
return true;
|
|
41
|
+
current = zodDef(current)?.innerType;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
export function readHandlerFieldReference(schema, field) {
|
|
46
|
+
const objectSchema = unwrapObjectSchema(schema);
|
|
47
|
+
const fieldSchema = objectSchema && zodShapeField(objectSchema, field);
|
|
48
|
+
if (fieldSchema === undefined)
|
|
49
|
+
return { kind: "none" };
|
|
50
|
+
const raw = findRawReferenceMeta(fieldSchema);
|
|
51
|
+
if (raw === undefined)
|
|
52
|
+
return { kind: "none" };
|
|
53
|
+
const parsed = referenceMetaSchema.safeParse(raw);
|
|
54
|
+
if (!parsed.success)
|
|
55
|
+
return { kind: "invalid", raw };
|
|
56
|
+
const meta = parsed.data;
|
|
57
|
+
return {
|
|
58
|
+
kind: "ok",
|
|
59
|
+
reference: {
|
|
60
|
+
entity: typeof meta === "string" ? meta : meta.entity,
|
|
61
|
+
by: typeof meta === "string" ? undefined : meta.by,
|
|
62
|
+
multiple: isArrayField(fieldSchema),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function handlerFieldReference(schema, field) {
|
|
67
|
+
const reading = readHandlerFieldReference(schema, field);
|
|
68
|
+
return reading.kind === "ok" ? reading.reference : undefined;
|
|
69
|
+
}
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
|
|
|
43
43
|
export { type ChangelogEntry, type ChangelogType, compareVersions, type FeatureChangelog, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
|
|
44
44
|
export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, type ManifestSystemEvent, serializeManifest, } from "./feature-manifest.js";
|
|
45
45
|
export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
|
|
46
|
-
export { HANDLER_FIELD_REFERENCES_META_KEY, handlerFieldReference, } from "./handler-field-references.js";
|
|
46
|
+
export { HANDLER_FIELD_REFERENCES_META_KEY, type HandlerFieldReference, type HandlerFieldReferenceMeta, handlerFieldReference, } from "./handler-field-references.js";
|
|
47
47
|
export { resolveName, withResponseData } from "./handler-helpers.js";
|
|
48
48
|
export { i18nKey } from "./i18n-key.js";
|
|
49
49
|
export { buildSessionRoles, findForbiddenMembershipRole, isForbiddenMembershipRole, stripForbiddenMembershipRoles, } from "./membership-roles.js";
|
|
@@ -78,10 +78,10 @@ export { unwrapWorkflowStepError, WorkflowStepError } from "./steps/workflow-ste
|
|
|
78
78
|
export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
79
79
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
80
80
|
export { assertNotificationVariables, templateVariable, templateVariablesToZod, } from "./template-variables.js";
|
|
81
|
-
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
81
|
+
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, requireTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
82
82
|
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.js";
|
|
83
83
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
84
|
-
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
84
|
+
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
85
85
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
86
86
|
export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
|
|
87
87
|
export { runValidation } from "./validation.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -71,7 +71,7 @@ export { unwrapWorkflowStepError, WorkflowStepError } from "./steps/workflow-ste
|
|
|
71
71
|
export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
72
72
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
73
73
|
export { assertNotificationVariables, templateVariable, templateVariablesToZod, } from "./template-variables.js";
|
|
74
|
-
export { findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, } from "./tier-resolver-extension.js";
|
|
74
|
+
export { findTierResolverUsage, isTierResolverPlugin, requireTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, } from "./tier-resolver-extension.js";
|
|
75
75
|
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, } from "./transfer-adjacency.js";
|
|
76
76
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
77
77
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
@@ -288,7 +288,8 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
288
288
|
// Multi file: array of fileRef UUIDs. Same story as the singular
|
|
289
289
|
// variant — the element type has to match the UUID column on
|
|
290
290
|
// fileRefsTable.id.
|
|
291
|
-
|
|
291
|
+
const ids = z.array(z.uuid());
|
|
292
|
+
return field.maxCount === undefined ? ids : ids.max(field.maxCount);
|
|
292
293
|
}
|
|
293
294
|
case "reference":
|
|
294
295
|
// Tier 2.7e-3: Validiert UUID-shape. Existenz-Check der Reference
|
|
@@ -34,6 +34,7 @@ export type UnavailableSelectOption = {
|
|
|
34
34
|
};
|
|
35
35
|
/** Options of a select field that are offered for the given row values, in declaration order. */
|
|
36
36
|
export declare function availableSelectOptions(field: SelectOptionRules, values: Record<string, unknown>): readonly string[];
|
|
37
|
+
export declare function isUnchangedValue(a: unknown, b: unknown): boolean;
|
|
37
38
|
/** Select values that violate a `conditionalOptions` rule. With `previous`, a field is only
|
|
38
39
|
* checked when it or one of its condition fields changed, so legacy rows stay editable. */
|
|
39
40
|
export declare function findUnavailableSelectOptions(fields: Readonly<Record<string, FieldDefinition>>, values: Record<string, unknown>, previous?: Record<string, unknown>): readonly UnavailableSelectOption[];
|
|
@@ -126,7 +126,7 @@ export function availableSelectOptions(field, values) {
|
|
|
126
126
|
return field.options;
|
|
127
127
|
return field.options.filter((option) => rules.every((rule) => !rule.options.includes(option) || evalFieldCondition(rule.when, values)));
|
|
128
128
|
}
|
|
129
|
-
function
|
|
129
|
+
export function isUnchangedValue(a, b) {
|
|
130
130
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
131
131
|
}
|
|
132
132
|
/** Select values that violate a `conditionalOptions` rule. With `previous`, a field is only
|
|
@@ -141,7 +141,7 @@ export function findUnavailableSelectOptions(fields, values, previous) {
|
|
|
141
141
|
continue;
|
|
142
142
|
if (previous) {
|
|
143
143
|
const watched = [name, ...def.conditionalOptions.map((rule) => rule.when.field)];
|
|
144
|
-
if (watched.every((key) =>
|
|
144
|
+
if (watched.every((key) => isUnchangedValue(values[key], previous[key])))
|
|
145
145
|
continue;
|
|
146
146
|
}
|
|
147
147
|
const allowed = availableSelectOptions(def, values);
|