@cosmicdrift/kumiko-framework 0.385.0 → 0.387.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/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/widgets.md +39 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +2 -0
- package/dist/api/request-context.js +16 -7
- package/dist/api/routes.js +2 -2
- package/dist/api/server.js +34 -33
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +15 -15
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.js +4 -4
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cross-tenant-reads.d.ts +8 -0
- package/dist/db/cross-tenant-reads.js +23 -0
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +2 -2
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +4 -4
- package/dist/db/event-store-executor-context.d.ts +1 -1
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read.js +27 -28
- package/dist/db/event-store-executor-write.js +65 -70
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +26 -19
- package/dist/db/migrate-runner.js +25 -25
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/queries/test-stack.d.ts +4 -1
- package/dist/db/queries/test-stack.js +15 -2
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +6 -6
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +82 -82
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +9 -2
- package/dist/derivatives/delete-stored-file.js +5 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +3 -3
- package/dist/engine/boot-validator/entity-handler.js +60 -61
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +1 -1
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +20 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +50 -57
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.js +15 -16
- package/dist/engine/boot-validator/screens-forms.js +10 -11
- package/dist/engine/boot-validator/screens-lists.js +14 -15
- package/dist/engine/boot-validator/screens.js +34 -40
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +75 -75
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +12 -11
- package/dist/engine/define-handler.js +12 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.js +13 -14
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +1 -1
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
- package/dist/engine/feature-ast/patterns.d.ts +2 -2
- package/dist/engine/feature-ast/render.js +2 -2
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +9 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.js +1 -1
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +12 -12
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +40 -38
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +7 -27
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.js +3 -3
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +28 -28
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +6 -7
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +2 -2
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +5 -6
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +11 -5
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +8 -8
- package/dist/internal/stack-support.d.ts +1 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-helpers.d.ts +1 -2
- package/dist/jobs/job-runner-helpers.js +8 -18
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner.js +17 -18
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +10 -10
- package/dist/pipeline/dispatch-shared.d.ts +1 -0
- package/dist/pipeline/dispatch-shared.js +42 -19
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +3 -3
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-delivery.js +1 -1
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/projection-rebuild.js +13 -12
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli.js +116 -22
- package/dist/search/index.js +4 -4
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +10 -9
- package/dist/utils/compare.js +1 -1
- package/package.json +4 -4
- package/src/changes.json +54 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/pii-personal-migration.ts +17 -8
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -23,7 +23,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
23
23
|
// features (a declarative list in the entity's owning feature navigates to
|
|
24
24
|
// the consumer app's custom editors this way). The validator mirrors that:
|
|
25
25
|
// same feature OR any feature. redirect/cancelTarget additionally accept a
|
|
26
|
-
// fully qualified cross-feature QN (resolveScreenTargetQn
|
|
26
|
+
// fully qualified cross-feature QN (resolveScreenTargetQn); short IDs
|
|
27
27
|
// stay same-feature as before.
|
|
28
28
|
const navTargetShortIds = screenShortIdsFrom(allScreenQns);
|
|
29
29
|
const navigateLookups = {
|
|
@@ -91,7 +91,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
91
91
|
assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, `column "${normalizedCol.field}" (refEntity)`, normalizedCol.refEntity, feature.name, featureMap);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
// Screen filter
|
|
94
|
+
// Screen filter — field existence can't be checked without
|
|
95
95
|
// an entity (columns aren't a complete field inventory of the
|
|
96
96
|
// underlying query), so only pin the structure: "in" requires an
|
|
97
97
|
// array. Field validity is documented in the PR body.
|
|
@@ -156,7 +156,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
156
156
|
`is no row id to send under idParam. Remove idParam.`);
|
|
157
157
|
}
|
|
158
158
|
// singleton has no row id in the path, but the auto-generated "Edit"
|
|
159
|
-
// action (
|
|
159
|
+
// action (defaultEditAction in kumiko-screen.tsx) navigates
|
|
160
160
|
// to the detailFor entity's entityEdit screen using that path id —
|
|
161
161
|
// under singleton it would silently open that screen in CREATE mode
|
|
162
162
|
// instead of editing the singleton row. Reject rather than let that
|
|
@@ -285,7 +285,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
285
285
|
// extension that persists through the host's Save button has
|
|
286
286
|
// nothing to save against. Extensions that persist themselves
|
|
287
287
|
// (e.g. notes-history NotesSection, via their own dispatcher
|
|
288
|
-
// writes) are fine
|
|
288
|
+
// writes) are fine.
|
|
289
289
|
if (section.contributesToFormSubmit === true) {
|
|
290
290
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) extension section ` +
|
|
291
291
|
`"${section.title}" sets contributesToFormSubmit: true, but projectionDetail has no ` +
|
|
@@ -420,9 +420,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
420
420
|
continue;
|
|
421
421
|
}
|
|
422
422
|
if (screen.type === "configEdit") {
|
|
423
|
-
// configEdit
|
|
424
|
-
//
|
|
425
|
-
// ist und der scope mit dem Key matcht.
|
|
423
|
+
// Validate configEdit layout and fields like actionForm, then verify that
|
|
424
|
+
// every qualified config key is registered and its scope matches the key.
|
|
426
425
|
const fieldNames = new Set(Object.keys(screen.fields));
|
|
427
426
|
if (fieldNames.size === 0) {
|
|
428
427
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) has empty fields map — ` +
|
|
@@ -473,8 +472,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
473
472
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) secretKeys names unknown field "${fname}".`);
|
|
474
473
|
}
|
|
475
474
|
}
|
|
476
|
-
// configKeys:
|
|
477
|
-
//
|
|
475
|
+
// configKeys: every fieldName must have a mapping entry, and every
|
|
476
|
+
// qualified key must exist in the registry.
|
|
478
477
|
for (const fname of fieldNames) {
|
|
479
478
|
const secretQn = screen.secretKeys?.[fname];
|
|
480
479
|
if (secretQn !== undefined) {
|
|
@@ -593,9 +592,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
593
592
|
assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (entityList)`, `column "${normalized.field}" (refEntity)`, normalized.refEntity, feature.name, featureMap);
|
|
594
593
|
}
|
|
595
594
|
}
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
// sortiert"-Debug-Cycle zur Laufzeit losgeht.
|
|
595
|
+
// Validate pagination, sorting, and search at boot so authoring errors
|
|
596
|
+
// fail before runtime debugging of empty or incorrectly sorted lists.
|
|
599
597
|
if (screen.pageSize !== undefined && screen.pageSize <= 0) {
|
|
600
598
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) has pageSize=${screen.pageSize} — ` +
|
|
601
599
|
`must be a positive integer.`);
|
|
@@ -606,15 +604,13 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
606
604
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) defaultSort references unknown ` +
|
|
607
605
|
`field "${sortField}". Known fields: ${[...fieldNames].sort().join(", ")}`);
|
|
608
606
|
}
|
|
609
|
-
// sortable: true
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
// im Author-Code als sortable markieren wenn das Field-Type
|
|
617
|
-
// es trägt (Erweiterung folgt).
|
|
607
|
+
// `sortable: true` is required so the UI never sorts on a column that has
|
|
608
|
+
// no server-side DB index or that the schema deliberately keeps unsortable
|
|
609
|
+
// (audit fields, computed values). `sortable` currently
|
|
610
|
+
// exists only on TextFieldDef; the `in` narrowing also permits other
|
|
611
|
+
// field types without an explicit flag, which is acceptable because
|
|
612
|
+
// Number/Date fields are naturally sortable. Authors can mark them
|
|
613
|
+
// sortable when their field type supports it.
|
|
618
614
|
const fieldDef = entityDef.fields[sortField];
|
|
619
615
|
const isSortable = fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
|
|
620
616
|
if (!isSortable) {
|
|
@@ -628,15 +624,14 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
628
624
|
`"${screen.createScreen}" does not resolve to a registered screen in this feature. ` +
|
|
629
625
|
`Known screens in this feature: ${Object.keys(feature.screens).sort().join(", ") || "(none)"}.`);
|
|
630
626
|
}
|
|
631
|
-
// Screen
|
|
632
|
-
// 1)
|
|
633
|
-
//
|
|
634
|
-
// 2)
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
// Außerdem: "in" verlangt readonly Array.
|
|
627
|
+
// Screen filters are checked in three layers:
|
|
628
|
+
// 1) The field exists on the entity; a typo would otherwise produce an
|
|
629
|
+
// empty list instead of a clear boot failure.
|
|
630
|
+
// 2) The field has `filterable: true`, preventing accidental filtering
|
|
631
|
+
// of audit, computed, or encrypted fields.
|
|
632
|
+
// 3) The operation matches the field type, so lt/gt on text fields
|
|
633
|
+
// fails at boot instead of causing surprising runtime behavior.
|
|
634
|
+
// Additionally, "in" requires a readonly array.
|
|
640
635
|
if (screen.filter !== undefined) {
|
|
641
636
|
const filterField = screen.filter.field;
|
|
642
637
|
if (!fieldNames.has(filterField)) {
|
|
@@ -668,9 +663,9 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
668
663
|
`filter.value to be a readonly array. Pass an array as filter.value.`);
|
|
669
664
|
}
|
|
670
665
|
}
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
666
|
+
// Pin rowActions at boot: a navigate target must exist in the same feature
|
|
667
|
+
// and a writeHandler QN must be registered. Typos would otherwise only
|
|
668
|
+
// surface on the first click as "Screen not found" or a 404.
|
|
674
669
|
if (screen.rowActions !== undefined) {
|
|
675
670
|
for (const action of screen.rowActions) {
|
|
676
671
|
rejectRedirectOnListRowAction(feature.name, screenId, "entityList", action);
|
|
@@ -681,7 +676,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
681
676
|
// the id for a screen owned by a different feature. Cross-feature
|
|
682
677
|
// + entityEdit therefore MUST set an explicit entityId, or the
|
|
683
678
|
// edit screen silently opens with no entity context at runtime.
|
|
684
|
-
// Entity-targets
|
|
679
|
+
// Entity-targets are exempt: the renderer always
|
|
685
680
|
// supplies an id for them (explicit entityId, else row["id"]),
|
|
686
681
|
// regardless of which feature the resolved detailFor screen
|
|
687
682
|
// belongs to.
|
|
@@ -730,9 +725,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
730
725
|
}
|
|
731
726
|
validateAtMostOneRowClick(feature.name, screenId, "entityList", screen.rowActions);
|
|
732
727
|
}
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
// writeHandler-QNs fallen bis hierhin erst beim Klick auf.
|
|
728
|
+
// toolbarActions, analogous to rowActions: misspelled navigate targets and
|
|
729
|
+
// unregistered writeHandler QNs fail at boot instead of on the first click.
|
|
736
730
|
if (screen.toolbarActions !== undefined) {
|
|
737
731
|
for (const action of screen.toolbarActions) {
|
|
738
732
|
if (action.kind === "navigate") {
|
|
@@ -895,9 +889,9 @@ export function collectScreenQns(features) {
|
|
|
895
889
|
}
|
|
896
890
|
return set;
|
|
897
891
|
}
|
|
898
|
-
//
|
|
899
|
-
// QNs
|
|
900
|
-
//
|
|
892
|
+
// Extract bare screen IDs (without the `<feature>:screen:` prefix) from
|
|
893
|
+
// qualified QNs for app-wide navigate-target resolution (see validateScreens).
|
|
894
|
+
// This mirrors the runtime router, which matches bare IDs across features.
|
|
901
895
|
export function screenShortIdsFrom(allScreenQns) {
|
|
902
896
|
const marker = ":screen:";
|
|
903
897
|
const set = new Set();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
1
3
|
export const SECURITY_BASELINE_FEATURE_NAMES = [
|
|
2
4
|
"sessions",
|
|
3
5
|
"crypto-shredding",
|
|
@@ -13,6 +15,5 @@ export function warnOnMissingSecurityBaseline(features, nodeEnv = process.env["N
|
|
|
13
15
|
// skip: every baseline feature is mounted
|
|
14
16
|
if (missing.length === 0)
|
|
15
17
|
return;
|
|
16
|
-
|
|
17
|
-
console.warn(`[kumiko:boot] NODE_ENV=production but the security baseline is incomplete — missing feature(s): ${missing.join(", ")}. Mount securityBaselineFeatures() from @cosmicdrift/kumiko-bundled-features/presets (or the missing features individually).`);
|
|
18
|
+
log.warn(`NODE_ENV=production but the security baseline is incomplete — missing feature(s): ${missing.join(", ")}. Mount securityBaselineFeatures() from @cosmicdrift/kumiko-bundled-features/presets (or the missing features individually).`);
|
|
18
19
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { parseRefTargetEntityName } from "../parse-ref-target.js";
|
|
2
2
|
import { findOverDeepTransferChain, MAX_TRANSFER_DEPTH, resolveTransferAdjacency, } from "../transfer-adjacency.js";
|
|
3
|
-
// --- Transfer-graph boot validation
|
|
3
|
+
// --- Transfer-graph boot validation ---
|
|
4
4
|
//
|
|
5
5
|
// tenant-handover walks an entity graph across the tenant boundary along two
|
|
6
6
|
// declared edge kinds: `parentRef` and plain `reference` fields. Two shapes
|
|
7
7
|
// cannot be walked, and both have to fail loudly here rather than silently
|
|
8
8
|
// leaving rows behind in the source tenant — the partial move is exactly what
|
|
9
|
-
//
|
|
9
|
+
// this validation exists to end.
|
|
10
10
|
//
|
|
11
11
|
// Scoped to entities declaring `transferable: true`, so a consumer that has
|
|
12
12
|
// one of these shapes but never hands that entity over is unaffected.
|
|
@@ -33,9 +33,9 @@ export function validateWorkspaces(feature, allNavQns) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
const SETTINGS_HUB_AUDIENCE_NAV_QN_SET = new Set(SETTINGS_HUB_AUDIENCE_NAV_QNS);
|
|
36
|
-
//
|
|
37
|
-
// or `r.nav({ workspaces })` never pass an explicit navAllowlist
|
|
38
|
-
//
|
|
36
|
+
// Apps that assign navs to workspaces via `r.workspace({ nav })`
|
|
37
|
+
// or `r.nav({ workspaces })` never pass an explicit navAllowlist — without
|
|
38
|
+
// this, warnOnUnreachableNavScreens never runs for them. Both
|
|
39
39
|
// `WorkspaceDefinition.nav` and `NavDefinition.workspaces` already hold
|
|
40
40
|
// fully-qualified QNs (validateWorkspaces/validateNavs compare them
|
|
41
41
|
// directly against allNavQns/allWorkspaceQns), so no re-qualification is
|
|
@@ -12,7 +12,7 @@ import { createAnonymousUser } from "./system-user.js";
|
|
|
12
12
|
// anything downstream accidentally reaches for them.
|
|
13
13
|
//
|
|
14
14
|
// Use: `{ db, registry, ...bridgeStub() }`
|
|
15
|
-
const notAvailable = (what) => async () => {
|
|
15
|
+
const notAvailable = (what) => async (..._args) => {
|
|
16
16
|
throw new Error(`ctx.${what} not available in this context — use the dispatcher, not a stubbed handler context`);
|
|
17
17
|
};
|
|
18
18
|
// Noop observability — hand back the shared fallback tracer so ctx.tracer has
|
|
@@ -24,11 +24,10 @@ export function bridgeStub(opts) {
|
|
|
24
24
|
// anonymous default values when no user is passed explicitly. Test code
|
|
25
25
|
// that cares about identity passes its own SessionUser here and gets it
|
|
26
26
|
// back on ctx.
|
|
27
|
-
const stubUser = opts?.user ??
|
|
28
|
-
createAnonymousUser("00000000-0000-0000-0000-000000000000"); // @cast-boundary engine-bridge
|
|
27
|
+
const stubUser = opts?.user ?? createAnonymousUser("00000000-0000-0000-0000-000000000000");
|
|
29
28
|
return {
|
|
30
29
|
user: stubUser,
|
|
31
|
-
query: notAvailable("query"),
|
|
30
|
+
query: notAvailable("query"),
|
|
32
31
|
queryAs: notAvailable("queryAs"),
|
|
33
32
|
write: notAvailable("write"),
|
|
34
33
|
writeAs: notAvailable("writeAs"),
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Builds the client-safe AppSchema from a server registry for the
|
|
2
|
+
// browser renderer pipeline. The dev server can therefore drop its entire
|
|
3
|
+
// hand-written `clientSchema` mirror and simply send the resolved AppSchema
|
|
4
|
+
// during boot.
|
|
5
5
|
//
|
|
6
|
-
// JSON
|
|
7
|
-
// JSON.stringify
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// JSON safety: project explicitly onto a whitelist instead of round-tripping
|
|
7
|
+
// through JSON.stringify. Functions would be silently dropped, and Zod schemas
|
|
8
|
+
// (v4 has .toJSON, but emits a _zod envelope) would appear as strange ghost
|
|
9
|
+
// properties. This is an intentional contract: everything read by the browser
|
|
10
|
+
// renderer pipeline must appear here. New fields needed by the browser must be
|
|
11
|
+
// added explicitly.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
13
|
+
// Currently projected:
|
|
14
14
|
// Entity: { table?, fields: { type, required?, sortable?, default? } }
|
|
15
|
-
// Screen: verbatim (ScreenDefinition
|
|
16
|
-
// custom
|
|
17
|
-
//
|
|
18
|
-
// Nav: verbatim (NavDefinition
|
|
19
|
-
// Workspace: verbatim definition + getWorkspaceNavs()
|
|
15
|
+
// Screen: verbatim (ScreenDefinition is JSON-safe by design —
|
|
16
|
+
// custom screens have no functions; layout/columns/etc. are
|
|
17
|
+
// plain literals)
|
|
18
|
+
// Nav: verbatim (NavDefinition contains only strings + literals)
|
|
19
|
+
// Workspace: verbatim definition + getWorkspaceNavs() from the registry
|
|
20
20
|
// Translations: verbatim feature.translations (raw r.translations keys,
|
|
21
21
|
// unfiltered — see FeatureSchema.translations doc)
|
|
22
22
|
//
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
// The schema route strips features the caller's tenant has switched off
|
|
25
25
|
// (r.toggleable) via stripDisabledFeaturesFromAppSchema.
|
|
26
26
|
import { ZodObject } from "zod";
|
|
27
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
27
28
|
import { collectUrlPrefillFieldsByScreenQn } from "./boot-validator/url-prefill-fields.js";
|
|
28
29
|
import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, } from "./build-config-feature-schema.js";
|
|
29
30
|
import { isEncryptedAtRest } from "./config-helpers.js";
|
|
30
31
|
import { isListQueryWithoutSearchableFields } from "./entity-list-searchability.js";
|
|
31
32
|
import { QnTypes, qualifyEntityName } from "./qualified-name.js";
|
|
33
|
+
const log = createFallbackLogger("kumiko");
|
|
32
34
|
export function buildAppSchema(registry, options = {}) {
|
|
33
35
|
const features = [];
|
|
34
36
|
const urlPrefillFieldsByScreenQn = collectUrlPrefillFieldsByScreenQn([
|
|
@@ -52,7 +54,7 @@ export function buildAppSchema(registry, options = {}) {
|
|
|
52
54
|
screens: projectScreens(featureName, feature.screens, registry, urlPrefillFieldsByScreenQn, encryptedConfigKeyQns),
|
|
53
55
|
...(navs.length > 0 && { navs }),
|
|
54
56
|
...(contentCollections.length > 0 && { contentCollections }),
|
|
55
|
-
//
|
|
57
|
+
// Verbatim r.translations({keys}) — see FeatureSchema.translations
|
|
56
58
|
// doc for why this must NOT go through registry.getAllTranslations()
|
|
57
59
|
// (double-prefixes features that already qualify their own keys).
|
|
58
60
|
...(Object.keys(feature.translations ?? {}).length > 0 && {
|
|
@@ -62,12 +64,12 @@ export function buildAppSchema(registry, options = {}) {
|
|
|
62
64
|
};
|
|
63
65
|
features.push(featureSchema);
|
|
64
66
|
}
|
|
65
|
-
// Workspaces: getAllWorkspaces()
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
67
|
+
// Workspaces: getAllWorkspaces() returns QUALIFIED ids (the Registry writes
|
|
68
|
+
// them during store overwrite). The browser renderer expects short ids
|
|
69
|
+
// (matching the URL segment and navigate({ workspaceId })). We therefore
|
|
70
|
+
// read directly from `feature.workspaces`, where the ids are still in the
|
|
71
|
+
// author's short form, and obtain the pre-resolved navMembers from the
|
|
72
|
+
// registry.
|
|
71
73
|
let workspaces = [];
|
|
72
74
|
for (const [featureName, feature] of registry.features) {
|
|
73
75
|
for (const [shortId, definition] of Object.entries(feature.workspaces)) {
|
|
@@ -78,16 +80,16 @@ export function buildAppSchema(registry, options = {}) {
|
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
|
-
// Self-
|
|
82
|
-
//
|
|
83
|
-
//
|
|
83
|
+
// Self-populating Settings Hub: screens/navs (plus one workspace) are
|
|
84
|
+
// derived from the declared config keys with `mask` and attached here,
|
|
85
|
+
// so the app author does not need manual r.screen/r.nav declarations.
|
|
84
86
|
const appHadWorkspaces = workspaces.length > 0;
|
|
85
87
|
const generated = buildConfigFeatureSchema(registry);
|
|
86
88
|
if (generated.screens.length > 0) {
|
|
87
89
|
mergeSettingsHubIntoConfigFeature(features, generated);
|
|
88
|
-
// Flip
|
|
89
|
-
//
|
|
90
|
-
//
|
|
90
|
+
// Flip protection: applies only to apps that already use workspaces. For an
|
|
91
|
+
// app without workspaces, app.workspaces remains undefined, so the renderer
|
|
92
|
+
// shows all navs without filtering, including the Hub navs.
|
|
91
93
|
if (appHadWorkspaces) {
|
|
92
94
|
const placed = placeSettingsHub(workspaces, generated);
|
|
93
95
|
workspaces = placed.workspaces;
|
|
@@ -109,16 +111,16 @@ export function buildAppSchema(registry, options = {}) {
|
|
|
109
111
|
// instead so a leaked function renderer is actually caught.
|
|
110
112
|
const offender = findNonJsonSafePath(schema, "schema");
|
|
111
113
|
if (offender !== null) {
|
|
112
|
-
|
|
113
|
-
console.error(`[kumiko] buildAppSchema: Output ist nicht JSON-safe — nicht-serialisierbarer Wert bei "${offender}" (Funktions-Renderer oder Klassen-Instanz im Schema?).`, schema);
|
|
114
|
+
log.error(`buildAppSchema: Output ist nicht JSON-safe — nicht-serialisierbarer Wert bei "${offender}" (Funktions-Renderer oder Klassen-Instanz im Schema?).`, { offender });
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
return schema;
|
|
117
118
|
}
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// bundled
|
|
121
|
-
//
|
|
119
|
+
// Attaches the generated Hub screens/navs to the config FeatureSchema
|
|
120
|
+
// (qualified as config:screen:* / config:nav:*). If it does not yet exist
|
|
121
|
+
// (because the config bundled feature is not mounted), it is created. Using
|
|
122
|
+
// find-or-create instead of an unconditional push prevents a second
|
|
123
|
+
// FeatureSchema with the same featureName.
|
|
122
124
|
function mergeSettingsHubIntoConfigFeature(features, generated) {
|
|
123
125
|
const existing = features.find((f) => f.featureName === SETTINGS_HUB_FEATURE);
|
|
124
126
|
if (existing === undefined) {
|
|
@@ -202,8 +204,7 @@ function warnUnplacedAudiences(unplaced) {
|
|
|
202
204
|
// skip: every audience placed — nothing to warn about
|
|
203
205
|
if (unplaced.length === 0)
|
|
204
206
|
return;
|
|
205
|
-
|
|
206
|
-
console.warn(`[kumiko] Settings-Hub: ${unplaced.join(", ")} nicht in einer App-Workspace platziert — ` +
|
|
207
|
+
log.warn(`Settings-Hub: ${unplaced.join(", ")} nicht in einer App-Workspace platziert — ` +
|
|
207
208
|
`erscheint im Standalone-"Einstellungen"-Tab. Referenziere ` +
|
|
208
209
|
`${unplaced.map((id) => `${SETTINGS_HUB_FEATURE}:nav:${id}`).join(", ")} ` +
|
|
209
210
|
`in einer r.workspace.nav, um die Gruppe inline zu zeigen.`);
|
|
@@ -212,8 +213,7 @@ function warnDanglingAudienceRefs(dangling) {
|
|
|
212
213
|
// skip: no dangling refs — nothing to warn about
|
|
213
214
|
if (dangling.length === 0)
|
|
214
215
|
return;
|
|
215
|
-
|
|
216
|
-
console.warn(`[kumiko] Settings-Hub: ${dangling
|
|
216
|
+
log.warn(`Settings-Hub: ${dangling
|
|
217
217
|
.map((id) => `${SETTINGS_HUB_FEATURE}:nav:${id}`)
|
|
218
218
|
.join(", ")} in einer Workspace referenziert, aber nie generiert — ` +
|
|
219
219
|
`keine Config-Keys für diesen Scope registriert. Tippfehler oder ` +
|
|
@@ -260,8 +260,8 @@ export function findNonJsonSafePath(value, path) {
|
|
|
260
260
|
}
|
|
261
261
|
// projectionList screens don't declare searchable/sortable/paginated as
|
|
262
262
|
// authoring intent — they're derived here from the bound query handler's
|
|
263
|
-
// Zod schema, the source of truth for what parameters it actually accepts
|
|
264
|
-
//
|
|
263
|
+
// Zod schema, the source of truth for what parameters it actually accepts.
|
|
264
|
+
// A hand-written `searchable: true` still wins over the derived
|
|
265
265
|
// default (screen.searchable ?? derived); the boot-validator (3a) rejects
|
|
266
266
|
// one that contradicts the schema.
|
|
267
267
|
function projectScreens(featureName, screens, registry, urlPrefillFieldsByScreenQn, encryptedConfigKeyQns) {
|
|
@@ -372,30 +372,30 @@ function projectEntities(entities) {
|
|
|
372
372
|
}
|
|
373
373
|
return out;
|
|
374
374
|
}
|
|
375
|
-
// EntityDefinition
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
375
|
+
// EntityDefinition is a discriminated union of field types, and we know the
|
|
376
|
+
// JSON-safe properties for each type. Rather than handling every variant
|
|
377
|
+
// separately, walk the field map and filter it through the whitelist. Values
|
|
378
|
+
// that do not pass through include server-only runtime values such as
|
|
379
|
+
// ZodValidate functions, computed functions, and default functions.
|
|
380
380
|
function projectEntity(entityName, entity) {
|
|
381
381
|
const fieldsOut = {};
|
|
382
382
|
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
383
383
|
assertValidMultilineRows(`Entity "${entityName}"`, fieldName, fieldDef);
|
|
384
384
|
fieldsOut[fieldName] = projectField(fieldDef);
|
|
385
385
|
}
|
|
386
|
-
// derivedFields
|
|
387
|
-
// derive
|
|
388
|
-
// `entity.derivedFields[field].valueType
|
|
389
|
-
// "references unknown field".
|
|
390
|
-
//
|
|
386
|
+
// derivedFields MUST be included in the client schema (metadata only, not the
|
|
387
|
+
// derive function): computeListViewModel resolves an entityList column via
|
|
388
|
+
// `entity.derivedFields[field].valueType`; without the entry it throws
|
|
389
|
+
// "references unknown field". The executor has already attached the value
|
|
390
|
+
// server-side to the row; the client only needs valueType for rendering.
|
|
391
391
|
const derivedOut = {};
|
|
392
392
|
for (const [name, derivedDef] of Object.entries(entity.derivedFields ?? {})) {
|
|
393
393
|
derivedOut[name] = projectDerivedField(derivedDef);
|
|
394
394
|
}
|
|
395
|
-
// EntityDefinition
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
395
|
+
// EntityDefinition accepts idType/access/searchWeight as optional, but we omit
|
|
396
|
+
// them because the browser renderer does not read them. `table` is included
|
|
397
|
+
// in case apps reference `entity.table` directly.
|
|
398
|
+
// No cast is needed: all omitted fields are optional (`?`).
|
|
399
399
|
return {
|
|
400
400
|
fields: fieldsOut,
|
|
401
401
|
// @cast-boundary schema-walk: EntityDefinition.derivedFields is typed for
|
|
@@ -413,10 +413,10 @@ function projectEntity(entityName, entity) {
|
|
|
413
413
|
...(entity.lockedWhen !== undefined && { lockedWhen: entity.lockedWhen }),
|
|
414
414
|
};
|
|
415
415
|
}
|
|
416
|
-
//
|
|
417
|
-
// (
|
|
418
|
-
// ClientDerivedFieldDef
|
|
419
|
-
//
|
|
416
|
+
// Only valueType is forwarded — the derive function is server-only and is not
|
|
417
|
+
// JSON-safe (it would otherwise trigger the output-walk guard). Return
|
|
418
|
+
// ClientDerivedFieldDef honestly instead of pretending to return a
|
|
419
|
+
// DerivedFieldDef without its derive function.
|
|
420
420
|
function projectDerivedField(derivedDef) {
|
|
421
421
|
return { valueType: derivedDef.valueType };
|
|
422
422
|
}
|
|
@@ -470,28 +470,28 @@ function projectField(fieldDef) {
|
|
|
470
470
|
out["required"] = def["required"];
|
|
471
471
|
if (typeof def["sortable"] === "boolean")
|
|
472
472
|
out["sortable"] = def["sortable"];
|
|
473
|
-
// filterable
|
|
474
|
-
// boolean)
|
|
473
|
+
// filterable controls the faceted filter dropdowns in the renderer (select/
|
|
474
|
+
// boolean), so it must be included in the client schema.
|
|
475
475
|
if (typeof def["filterable"] === "boolean")
|
|
476
476
|
out["filterable"] = def["filterable"];
|
|
477
477
|
if (typeof def["searchable"] === "boolean")
|
|
478
478
|
out["searchable"] = def["searchable"];
|
|
479
479
|
if (isJsonSafeValue(def["default"]))
|
|
480
480
|
out["default"] = def["default"];
|
|
481
|
-
// Select: options
|
|
481
|
+
// Select: the options list is plain JSON, so forward it.
|
|
482
482
|
if (Array.isArray(def["options"]))
|
|
483
483
|
out["options"] = def["options"];
|
|
484
484
|
if (isJsonSafeObject(def["optionTones"]))
|
|
485
485
|
out["optionTones"] = def["optionTones"];
|
|
486
|
-
// Reference: entity
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
486
|
+
// Reference: entity target, labelField, and multiple are required by the
|
|
487
|
+
// renderer. ReferenceInput builds the options query from refEntity/refFeature
|
|
488
|
+
// and resolves the label through labelField; without these properties, the
|
|
489
|
+
// dropdown is empty (the QN becomes `<feature>:query::list` → 404).
|
|
490
490
|
if (typeof def["entity"] === "string")
|
|
491
491
|
out["entity"] = def["entity"];
|
|
492
492
|
if (typeof def["labelField"] === "string")
|
|
493
493
|
out["labelField"] = def["labelField"];
|
|
494
|
-
//
|
|
494
|
+
// Author-supplied picker source — without it the client falls back to the list handler.
|
|
495
495
|
if (typeof def["optionsQuery"] === "string")
|
|
496
496
|
out["optionsQuery"] = def["optionsQuery"];
|
|
497
497
|
// Select: tier/tenant availability of the static options, loaded by the renderer.
|
|
@@ -504,7 +504,7 @@ function projectField(fieldDef) {
|
|
|
504
504
|
out["multiple"] = def["multiple"];
|
|
505
505
|
// MultiSelect: display picks checkboxes vs. combobox in the renderer,
|
|
506
506
|
// columns/maxRows size the checkbox grid — without these the renderer
|
|
507
|
-
// always falls back to the combobox
|
|
507
|
+
// always falls back to the combobox. Select: display picks
|
|
508
508
|
// "radio" | "dropdown" the same way.
|
|
509
509
|
if (typeof def["display"] === "string")
|
|
510
510
|
out["display"] = def["display"];
|
|
@@ -513,19 +513,19 @@ function projectField(fieldDef) {
|
|
|
513
513
|
if (typeof def["maxRows"] === "number")
|
|
514
514
|
out["maxRows"] = def["maxRows"];
|
|
515
515
|
// text/longText: textarea row count — DefaultInput renders a single-line
|
|
516
|
-
// input otherwise
|
|
516
|
+
// input otherwise.
|
|
517
517
|
if (typeof def["multiline"] === "boolean" || isJsonSafeObject(def["multiline"]))
|
|
518
518
|
out["multiline"] = def["multiline"];
|
|
519
519
|
// number: Zod write-boundary bounds. date/timestamp/locatedTimestamp:
|
|
520
|
-
// ISO-string picker bounds — same keys, different literal type
|
|
520
|
+
// ISO-string picker bounds — same keys, different literal type.
|
|
521
521
|
if (typeof def["min"] === "number" || typeof def["min"] === "string")
|
|
522
522
|
out["min"] = def["min"];
|
|
523
523
|
if (typeof def["max"] === "number" || typeof def["max"] === "string")
|
|
524
524
|
out["max"] = def["max"];
|
|
525
|
-
// date/timestamp/locatedTimestamp: display/parsing locale override
|
|
525
|
+
// date/timestamp/locatedTimestamp: display/parsing locale override.
|
|
526
526
|
if (typeof def["locale"] === "string")
|
|
527
527
|
out["locale"] = def["locale"];
|
|
528
|
-
// image/images: which camera a mobile capture opens
|
|
528
|
+
// image/images: which camera a mobile capture opens.
|
|
529
529
|
if (typeof def["capture"] === "string")
|
|
530
530
|
out["capture"] = def["capture"];
|
|
531
531
|
// number: display-only suffix, static or read from a sibling field of the row.
|
|
@@ -559,13 +559,13 @@ function projectField(fieldDef) {
|
|
|
559
559
|
// decimal, incl. embedded sub-fields: rounding of derived embedded-list cells.
|
|
560
560
|
if (typeof def["scale"] === "number")
|
|
561
561
|
out["scale"] = def["scale"];
|
|
562
|
-
// money: declared currency source (MoneyCurrencySource
|
|
562
|
+
// money: declared currency source (MoneyCurrencySource) — without
|
|
563
563
|
// it the renderer can't tell a "currency: { kind: 'tenant' }" field apart
|
|
564
564
|
// from an undeclared one and always falls back to entity.defaultCurrency.
|
|
565
565
|
if (isJsonSafeObject(def["currency"]))
|
|
566
566
|
out["currency"] = def["currency"];
|
|
567
567
|
// embedded lists: row-count bounds, computed cells, totals row, and the
|
|
568
|
-
// sibling-money-field totals check
|
|
568
|
+
// sibling-money-field totals check.
|
|
569
569
|
if (typeof def["minItems"] === "number")
|
|
570
570
|
out["minItems"] = def["minItems"];
|
|
571
571
|
if (typeof def["maxItems"] === "number")
|
|
@@ -577,7 +577,7 @@ function projectField(fieldDef) {
|
|
|
577
577
|
if (isJsonSafeObject(def["totalsMatch"]))
|
|
578
578
|
out["totalsMatch"] = def["totalsMatch"];
|
|
579
579
|
// embedded lists: the sub-field map itself — without it the client-side
|
|
580
|
-
// edit view-model iterates `undefined` and the screen crashes
|
|
580
|
+
// edit view-model iterates `undefined` and the screen crashes.
|
|
581
581
|
// Recursive per sub-field, NOT a blanket isJsonSafeValue gate: a single
|
|
582
582
|
// sub-field carrying a validator fn would otherwise drop the whole map,
|
|
583
583
|
// and a blanket copy would ship server internals.
|