@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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 +6 -1
- package/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +40 -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-error-route.js +8 -2
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -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 +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1250
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +31 -27
- 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/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +7 -5
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- 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/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- 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/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +3 -3
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +105 -112
- package/dist/db/event-store-executor-context.d.ts +3 -2
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +50 -704
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +401 -510
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- 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 +97 -105
- package/dist/db/migrate-runner.js +29 -26
- 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/query.d.ts +3 -16
- package/dist/db/query.js +8 -761
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/reference-data.js +6 -1
- 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 +137 -190
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +48 -11
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +6 -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/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +4 -4
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +77 -61
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +6 -4
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +22 -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 +258 -187
- 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.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +23 -17
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +12 -17
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +26 -24
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +102 -869
- 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 +130 -170
- 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 +21 -13
- package/dist/engine/define-handler.js +13 -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.d.ts +13 -0
- package/dist/engine/entity-handlers.js +22 -34
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- 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 +2 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- 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 +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +11 -2
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +50 -89
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +12 -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/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- 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.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +7 -5
- 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 +13 -13
- 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 +251 -233
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- 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/strip-disabled-features-from-app-schema.js +51 -14
- 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 +9 -28
- 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.d.ts +4 -1
- package/dist/entrypoint/index.js +10 -4
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- 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 +29 -29
- 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 +7 -8
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +12 -6
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +7 -9
- 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 +22 -13
- 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/resolve-content-type.js +43 -0
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/upload-ledger.js +7 -0
- 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 +188 -212
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +69 -4
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +57 -669
- 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/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- 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/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +20 -13
- package/dist/pipeline/dispatch-shared.d.ts +14 -3
- package/dist/pipeline/dispatch-shared.js +67 -721
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +297 -190
- 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-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +4 -6
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- 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/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +15 -13
- 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/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -510
- package/dist/search/index.js +4 -4
- package/dist/search/meilisearch-adapter.js +2 -1
- 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 +159 -130
- package/dist/utils/compare.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +358 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/feature-layout.ts +10 -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/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +192 -153
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -7,7 +7,7 @@ export function isRowClickAction(action) {
|
|
|
7
7
|
return (action.kind === "navigate" || action.kind === "drawer") && action.rowClick === true;
|
|
8
8
|
}
|
|
9
9
|
// entityList and projectionList both allow a rowAction to double as the
|
|
10
|
-
// row-body click target (rowClick: true
|
|
10
|
+
// row-body click target (rowClick: true) — at most one per
|
|
11
11
|
// screen, or the renderer can't tell which one should fire.
|
|
12
12
|
export function validateAtMostOneRowClick(featureName, screenId, screenType, rowActions) {
|
|
13
13
|
const rowClickActions = rowActions.filter(isRowClickAction);
|
|
@@ -30,18 +30,17 @@ export function readsNavigateParamsAsFormPrefill(target, explicitEntityId, sourc
|
|
|
30
30
|
return false;
|
|
31
31
|
return sourceScreenEntity === undefined || target.entity !== sourceScreenEntity;
|
|
32
32
|
}
|
|
33
|
-
//
|
|
34
|
-
// entityList and projectionList
|
|
33
|
+
// Navigate rowAction → target-screen params validity. Shared by
|
|
34
|
+
// entityList and projectionList — projectionList has no
|
|
35
35
|
// `screen.entity`, so there's no same-entity row["id"] auto-fill case: any
|
|
36
36
|
// entityEdit target without an explicit entityId reaches create there.
|
|
37
37
|
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// `
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// a silently-empty dashboard.
|
|
38
|
+
// Dashboard targets also read URL search params — but only for the single
|
|
39
|
+
// `filter.id` a dashboard declares (useFilterParams in dashboard-body.tsx
|
|
40
|
+
// seeds its value from `nav.searchParams[filter.id]`). A dashboard with no
|
|
41
|
+
// `filter` has nowhere for the value to land, and a params extractor whose
|
|
42
|
+
// keys don't include the filter's id would silently miss it — both are boot
|
|
43
|
+
// errors instead of a silently-empty dashboard.
|
|
45
44
|
export function validateRowActionNavigateParams(featureName, screenId, screenType, screenEntity, action, target, actionLabel = "rowAction") {
|
|
46
45
|
// skip: not a navigate-with-params action — nothing to validate here.
|
|
47
46
|
if (action.kind !== "navigate" || action.params === undefined)
|
|
@@ -122,7 +121,7 @@ export function validateActionHasIcon(featureName, screenId, screenType, context
|
|
|
122
121
|
`its kebab segments via the default icon map (resolveActionIcon).`);
|
|
123
122
|
}
|
|
124
123
|
}
|
|
125
|
-
//
|
|
124
|
+
// Pin an action's declarative field references against the entity
|
|
126
125
|
// fields — a typo in pick/map source fields or visible.field would otherwise
|
|
127
126
|
// silently yield `undefined` in the payload or permanently wrong visibility
|
|
128
127
|
// (same "typo only surfaces on click" class as navigate/handler). Exported for
|
|
@@ -203,12 +202,12 @@ export function validateScreenNavTarget(featureName, screenId, screenKind, field
|
|
|
203
202
|
}
|
|
204
203
|
// kind:"drawer" resolves same-feature only — unlike navigate/redirect,
|
|
205
204
|
// which the runtime router resolves app-wide (see the comment on
|
|
206
|
-
//
|
|
205
|
+
// buildScreenValidationContext in screens.ts), the drawer mounts the target inline using this
|
|
207
206
|
// feature's schema, so a cross-feature reference could never actually
|
|
208
207
|
// render. Two distinct failure messages: dangling reference vs. wrong
|
|
209
|
-
// screen type. Shared by ToolbarAction
|
|
210
|
-
//
|
|
211
|
-
//
|
|
208
|
+
// screen type. Shared by ToolbarAction and RowAction drawer variants —
|
|
209
|
+
// same target-resolution rule, only the label in the error message
|
|
210
|
+
// ("toolbarAction" vs. "rowAction" vs. "action") differs.
|
|
212
211
|
export function validateDrawerTargetAction(featureName, screenId, screenKind, actionLabel, action, screens) {
|
|
213
212
|
const target = screens[action.screen];
|
|
214
213
|
if (target === undefined) {
|
|
@@ -337,8 +336,8 @@ export function resolveRowActionNavigateTarget(featureName, screenId, screenType
|
|
|
337
336
|
// redirect is either a short screen id (same-feature, e.g. "item-list") or
|
|
338
337
|
// a fully-qualified cross-feature QN (`<feature>:screen:<id>`) — the
|
|
339
338
|
// renderer strips the latter to the short id (lastSegment) when
|
|
340
|
-
// navigating, which the nav-router resolves app-wide
|
|
341
|
-
// form
|
|
339
|
+
// navigating, which the nav-router resolves app-wide. The object
|
|
340
|
+
// form carries the same target under `screen` plus the payload
|
|
342
341
|
// field `idFrom`. Shared by actionForm/secretMint (validateInlineFormNavTargets)
|
|
343
342
|
// and entityEdit — same rule, same error message, one place to keep them in sync.
|
|
344
343
|
export function validateRedirectTarget(feature, screenId, screenKind, redirect, lookups,
|
|
@@ -486,3 +485,10 @@ export function validateNavigateActionTab(featureName, screenId, screenType, act
|
|
|
486
485
|
return;
|
|
487
486
|
resolveRowActionNavigateTarget(featureName, screenId, screenType, actionLabel, action, lookups.allScreenQns, lookups.navTargetShortIds, lookups.screensByShortId, lookups.detailForScreens);
|
|
488
487
|
}
|
|
488
|
+
export function assertWriteHandlerRegistered(featureName, subject, handler, allWriteHandlerQns) {
|
|
489
|
+
if (!allWriteHandlerQns.has(handler)) {
|
|
490
|
+
throw new Error(`[Feature ${featureName}] ${subject} handler "${handler}" ` +
|
|
491
|
+
`is not a registered write-handler. Check the QN spelling (expected ` +
|
|
492
|
+
`"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DASHBOARD_MIN_REFRESH_SECONDS } from "@cosmicdrift/kumiko-types/screen";
|
|
2
2
|
import { normalizeListColumn } from "../screen-helpers.js";
|
|
3
|
-
import { isRowClickAction, resolveRowActionNavigateTarget, resolveScreenTargetQn, validateActionHasIcon, validateClientActionName, validateRowActionNavigateParams, } from "./screens-actions.js";
|
|
3
|
+
import { assertWriteHandlerRegistered, isRowClickAction, resolveRowActionNavigateTarget, resolveScreenTargetQn, validateActionHasIcon, validateClientActionName, validateRowActionNavigateParams, } from "./screens-actions.js";
|
|
4
4
|
import { assertRefTargetRegistered, validateColumnRendererForm } from "./screens-lists.js";
|
|
5
5
|
// Panel-driven, no entity — structural checks (unique panel ids, non-empty
|
|
6
6
|
// queries/columns/valueField); the query contracts (stat record, points
|
|
@@ -68,10 +68,7 @@ export function validateDashboardActions(featureName, screenId, screen, allWrite
|
|
|
68
68
|
throw new Error(`${where} sets redirect — a dashboard action stays on the dashboard and refetches ` +
|
|
69
69
|
`after success. Remove redirect.`);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
throw new Error(`${where} handler "${action.handler}" is not a registered write-handler. Check the QN ` +
|
|
73
|
-
`spelling (expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
74
|
-
}
|
|
71
|
+
assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (dashboard) action "${action.id}"`, action.handler, allWriteHandlerQns);
|
|
75
72
|
}
|
|
76
73
|
}
|
|
77
74
|
}
|
|
@@ -2,9 +2,9 @@ import { NO_WIDGET_FIELD_TYPES, optionsQueryFieldRefs } from "@cosmicdrift/kumik
|
|
|
2
2
|
import { isKebabSegment } from "../qualified-name.js";
|
|
3
3
|
import { isExtensionEditSection, isWriteFormEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
4
4
|
import { validateFormDerivations, validateFormDerivationsEnforced } from "./form-derivations.js";
|
|
5
|
-
import { isSensitiveOrPasswordField, validateIdlessSuccessLink, validateRedirectTarget, validateScreenNavTarget, } from "./screens-actions.js";
|
|
5
|
+
import { assertWriteHandlerRegistered, isSensitiveOrPasswordField, validateIdlessSuccessLink, validateRedirectTarget, validateScreenNavTarget, } from "./screens-actions.js";
|
|
6
6
|
// A field type in NO_WIDGET_FIELD_TYPES renders read-only on the auto-wired
|
|
7
|
-
// entityEdit path
|
|
7
|
+
// entityEdit path — a required field the user can never fill would
|
|
8
8
|
// block every save. Only the statically-resolvable case is caught here: a
|
|
9
9
|
// literal `required: true` (screen-spec override or entity-level default).
|
|
10
10
|
// A dynamic FieldCondition depends on runtime form values and can't be
|
|
@@ -95,7 +95,7 @@ export function validateNoWidgetRequiredField(featureName, screenId, entityDef,
|
|
|
95
95
|
if (fieldDef === undefined || !NO_WIDGET_FIELD_TYPES.includes(fieldDef.type))
|
|
96
96
|
return;
|
|
97
97
|
// Embedded LIST fields (`multiple: true`) get their own EmbeddedListField
|
|
98
|
-
// grid widget
|
|
98
|
+
// grid widget — only plain (non-list) embedded has no widget.
|
|
99
99
|
const isEmbeddedList = fieldDef.type === "embedded" && fieldDef.multiple === true;
|
|
100
100
|
// skip: list variant has a widget — not the no-widget case this guard targets.
|
|
101
101
|
if (isEmbeddedList)
|
|
@@ -115,8 +115,7 @@ export function validateNoWidgetRequiredField(featureName, screenId, entityDef,
|
|
|
115
115
|
}
|
|
116
116
|
// Shared by projectionDetail and entityEdit: a tab strip needs something to
|
|
117
117
|
// label each tab with and a stable id per tab — the id anchors the ?tab=
|
|
118
|
-
// param on projectionDetail and the jump-to-erroring-tab logic on entityEdit
|
|
119
|
-
// (fw#3134).
|
|
118
|
+
// param on projectionDetail and the jump-to-erroring-tab logic on entityEdit.
|
|
120
119
|
export function validateTabSections(featureName, screenId, screenType, sections) {
|
|
121
120
|
if (sections.length < 2) {
|
|
122
121
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has mode: "tabs" but only ` +
|
|
@@ -149,7 +148,7 @@ export function validateTabSections(featureName, screenId, screenType, sections)
|
|
|
149
148
|
export function validateWizardLayout(featureName, screenId, screenType, layout, featureMap) {
|
|
150
149
|
// entityEdit renders tabs like wizard steps: every section stays mounted,
|
|
151
150
|
// submit validates across all of them, and a field error jumps to the tab
|
|
152
|
-
// holding it
|
|
151
|
+
// holding it. The other three have no such jump — actionForm and
|
|
153
152
|
// secretMint are one-shot forms, configEdit fires a write per field — so a
|
|
154
153
|
// required field on a hidden tab would still block their submit silently.
|
|
155
154
|
if (layout.mode === "tabs") {
|
|
@@ -201,7 +200,7 @@ export function validateWizardLayout(featureName, screenId, screenType, layout,
|
|
|
201
200
|
//
|
|
202
201
|
// Field-level renderer QN strings (cross-feature `component:` references)
|
|
203
202
|
// are NOT validated here — the r.uiComponent registry that would resolve
|
|
204
|
-
// them ships
|
|
203
|
+
// them ships later. Until then those are kept opaque on purpose.
|
|
205
204
|
// Shared by the mint step's own handler (screen.handler) and its optional
|
|
206
205
|
// confirm step (confirm.handler) — both are a bare write-handler QN with the
|
|
207
206
|
// same registration requirement.
|
|
@@ -209,11 +208,7 @@ function validateWriteHandlerRegistered(featureName, screenId, context, handler,
|
|
|
209
208
|
if (!handler || typeof handler !== "string") {
|
|
210
209
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) has empty or non-string handler. Set handler to a non-empty handler name.`);
|
|
211
210
|
}
|
|
212
|
-
|
|
213
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) handler "${handler}" ` +
|
|
214
|
-
`is not a registered write-handler. Check the QN spelling (expected ` +
|
|
215
|
-
`"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
216
|
-
}
|
|
211
|
+
assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (${context})`, handler, allWriteHandlerQns);
|
|
217
212
|
}
|
|
218
213
|
// Every field entry must carry a `type` discriminator. An author typo
|
|
219
214
|
// (`title: { required: true }` without a type) would otherwise let
|
|
@@ -226,7 +221,7 @@ function validateWriteHandlerRegistered(featureName, screenId, context, handler,
|
|
|
226
221
|
// secretMint's own `confirm` step — a second, independent inline form on the
|
|
227
222
|
// same screen — can reuse the same checks as its `fields`/`layout` pair.
|
|
228
223
|
// `allowEmpty` is only ever true for a secretMint's own mint step (an
|
|
229
|
-
// input-less mint
|
|
224
|
+
// input-less mint): the secret is server-generated and the mint
|
|
230
225
|
// step is just its submit button — actionForm and the confirm step always
|
|
231
226
|
// require at least one field.
|
|
232
227
|
function validateFormFieldsMap(featureName, screenId, context, fields, allowEmpty) {
|
|
@@ -330,7 +325,7 @@ export function validateFormSelectOptions(featureName, screenId, context, fieldN
|
|
|
330
325
|
}
|
|
331
326
|
}
|
|
332
327
|
}
|
|
333
|
-
// Fail-closed currency-source gate
|
|
328
|
+
// Fail-closed currency-source gate, the runtime half of the
|
|
334
329
|
// narrowed `FormFieldDefinition` — an untyped JS consumer has no compiler to
|
|
335
330
|
// stop it. An inline form screen has no entity, so a money field there can't
|
|
336
331
|
// borrow `entity.defaultCurrency`: without a declared source the renderer
|
|
@@ -454,7 +449,7 @@ function validateInlineFormNavTargets(feature, screenId, screen, kind, lookups)
|
|
|
454
449
|
export function validateInlineFormScreen(feature, screenId, screen, kind, allWriteHandlerQns, lookups, featureMap) {
|
|
455
450
|
validateWriteHandlerRegistered(feature.name, screenId, kind, screen.handler, allWriteHandlerQns);
|
|
456
451
|
// Only a secretMint's own mint step may skip fields/sections entirely
|
|
457
|
-
// (input-less mint
|
|
452
|
+
// (input-less mint) — actionForm always needs at least one field.
|
|
458
453
|
const allowEmptyMintForm = kind === "secretMint";
|
|
459
454
|
const fieldNames = validateFormFieldsMap(feature.name, screenId, kind, screen.fields, allowEmptyMintForm);
|
|
460
455
|
validateFormLayoutSections(feature.name, screenId, kind, screen.layout, fieldNames, screen.fields, allowEmptyMintForm && fieldNames.size === 0);
|
|
@@ -472,14 +467,14 @@ export function validateInlineFormScreen(feature, screenId, screen, kind, allWri
|
|
|
472
467
|
}
|
|
473
468
|
validateInlineFormNavTargets(feature, screenId, screen, kind, lookups);
|
|
474
469
|
}
|
|
475
|
-
// SecretMintConfirmStep
|
|
470
|
+
// SecretMintConfirmStep: an optional proof-of-receipt form rendered
|
|
476
471
|
// on the reveal card in place of the bare acknowledge button (TOTP enrollment:
|
|
477
472
|
// scan the code, then enter one). Unlike the mint step, confirm always needs
|
|
478
473
|
// at least one field — a step whose only job is proving receipt with no input
|
|
479
474
|
// would be meaningless — and its layout is a single plain form, never a
|
|
480
475
|
// wizard/tabs flow and never a persisted draft: a one-time reveal-confirm is
|
|
481
476
|
// not a multi-step flow, and persisting a draft of it would re-open the leak
|
|
482
|
-
//
|
|
477
|
+
// (a lingering copy of the just-revealed secret in storage).
|
|
483
478
|
export function validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns) {
|
|
484
479
|
const confirm = screen.confirm;
|
|
485
480
|
// skip: the screen declares no confirm step — nothing to validate.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ConfigEditScreenDefinition } from "../types/screen.ts";
|
|
2
|
+
import { type ScreenValidationContext } from "./screens-kind-shared.ts";
|
|
3
|
+
export declare function validateConfigEditScreen(ctx: ScreenValidationContext, screenId: string, screen: ConfigEditScreenDefinition): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isEncryptedAtRest } from "../config-helpers.js";
|
|
2
|
+
import { isExtensionEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
3
|
+
import { isWriteOnlyField, validateConfigEditSecretField, validateFieldsXorGroups, validateFormSelectOptions, validateWizardLayout, } from "./screens-forms.js";
|
|
4
|
+
import { assertExtensionSectionHasComponent, rejectEmptySections, rejectProjectionDetailOnlySection, } from "./screens-kind-shared.js";
|
|
5
|
+
function validateConfigEditFields(featureName, screenId, screen, fieldNames) {
|
|
6
|
+
if (fieldNames.size === 0) {
|
|
7
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) has empty fields map — ` +
|
|
8
|
+
`declare at least one field.`);
|
|
9
|
+
}
|
|
10
|
+
for (const [fname, fdef] of Object.entries(screen.fields)) {
|
|
11
|
+
const fieldType = fdef.type;
|
|
12
|
+
if (typeof fieldType !== "string" || fieldType.length === 0) {
|
|
13
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" has no ` +
|
|
14
|
+
`\`type\` set. Each field must declare a type (e.g. "text", "number", "select").`);
|
|
15
|
+
}
|
|
16
|
+
validateFormSelectOptions(featureName, screenId, "configEdit", fname, fdef, fieldNames);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function validateConfigEditFieldsSection(featureName, screenId, fieldNames, section) {
|
|
20
|
+
rejectProjectionDetailOnlySection(featureName, screenId, "configEdit", section);
|
|
21
|
+
validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (configEdit)`, section);
|
|
22
|
+
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
23
|
+
const normalized = normalizeEditField(fieldSpec);
|
|
24
|
+
if (!fieldNames.has(normalized.field)) {
|
|
25
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) layout references unknown ` +
|
|
26
|
+
`field "${normalized.field}". Known fields: ${[...fieldNames].sort().join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validateConfigEditSection(featureName, screenId, fieldNames, section) {
|
|
31
|
+
if (isExtensionEditSection(section)) {
|
|
32
|
+
assertExtensionSectionHasComponent(featureName, screenId, "configEdit", section);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
validateConfigEditFieldsSection(featureName, screenId, fieldNames, section);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// configKeys: every fieldName must have a mapping entry, and every qualified
|
|
39
|
+
// key must exist in the registry.
|
|
40
|
+
function validateConfigEditFieldKey(ctx, screenId, screen, fname) {
|
|
41
|
+
const secretQn = screen.secretKeys?.[fname];
|
|
42
|
+
if (secretQn !== undefined) {
|
|
43
|
+
validateConfigEditSecretField(ctx.feature.name, screen, fname, secretQn, ctx.featureMap);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
validateConfigKeyMapping(ctx, screenId, screen, fname);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function validateConfigKeyMapping(ctx, screenId, screen, fname) {
|
|
50
|
+
const featureName = ctx.feature.name;
|
|
51
|
+
const qualified = screen.configKeys[fname];
|
|
52
|
+
if (qualified === undefined) {
|
|
53
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" has ` +
|
|
54
|
+
`no entry in the configKeys map. Every declared field needs a mapping to a ` +
|
|
55
|
+
`qualified config key (\`<feature>:config:<short>\`). Add one to configKeys.`);
|
|
56
|
+
}
|
|
57
|
+
const configKeyDef = ctx.configKeyDefsByQn.get(qualified);
|
|
58
|
+
if (configKeyDef === undefined) {
|
|
59
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" → ` +
|
|
60
|
+
`Config key "${qualified}" is not declared in any feature registry. Typo? ` +
|
|
61
|
+
`Expected format: "<feature>:config:<short>". Known keys: ${[...ctx.configKeyDefsByQn.keys()].sort().join(", ") || "(none)"}`);
|
|
62
|
+
}
|
|
63
|
+
if (isWriteOnlyField(screen.fields[fname]) && !isEncryptedAtRest(configKeyDef)) {
|
|
64
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" declares writeOnly ` +
|
|
65
|
+
`but config key "${qualified}" is not encrypted at rest — writeOnly on a configEdit field ` +
|
|
66
|
+
`only hides a stored secret (declare the key with encrypted: true or backing: "secrets").`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Validate configEdit layout and fields like actionForm, then verify that
|
|
70
|
+
// every qualified config key is registered and its scope matches the key.
|
|
71
|
+
export function validateConfigEditScreen(ctx, screenId, screen) {
|
|
72
|
+
const featureName = ctx.feature.name;
|
|
73
|
+
const fieldNames = new Set(Object.keys(screen.fields));
|
|
74
|
+
validateConfigEditFields(featureName, screenId, screen, fieldNames);
|
|
75
|
+
rejectEmptySections(featureName, screenId, "configEdit", screen.layout.sections.length);
|
|
76
|
+
for (const section of screen.layout.sections) {
|
|
77
|
+
validateConfigEditSection(featureName, screenId, fieldNames, section);
|
|
78
|
+
}
|
|
79
|
+
validateWizardLayout(featureName, screenId, "configEdit", screen.layout, ctx.featureMap);
|
|
80
|
+
for (const fname of Object.keys(screen.secretKeys ?? {})) {
|
|
81
|
+
if (!fieldNames.has(fname)) {
|
|
82
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) secretKeys names unknown field "${fname}".`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const fname of fieldNames) {
|
|
86
|
+
validateConfigEditFieldKey(ctx, screenId, screen, fname);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EntityEditScreenDefinition } from "../types/screen.ts";
|
|
2
|
+
import { type ScreenValidationContext } from "./screens-kind-shared.ts";
|
|
3
|
+
export declare function validateEntityEditScreen(ctx: ScreenValidationContext, screenId: string, screen: EntityEditScreenDefinition): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isExtensionEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
2
|
+
import { validateRedirectTarget } from "./screens-actions.js";
|
|
3
|
+
import { validateEditFieldExtras, validateFieldFormRefs, validateFieldsXorGroups, validateFillActionTarget, validateNoWidgetRequiredField, validateWizardLayout, } from "./screens-forms.js";
|
|
4
|
+
import { assertExtensionSectionHasComponent, buildUnknownFieldMessage, rejectEmptySections, rejectProjectionDetailOnlySection, resolveScreenEntity, validateRecordHeaderAction, validateRecordSectionActions, } from "./screens-kind-shared.js";
|
|
5
|
+
function validateEntityEditField(featureName, screenId, entityName, entityDef, fieldNames, fieldSpec) {
|
|
6
|
+
const normalized = normalizeEditField(fieldSpec);
|
|
7
|
+
if (!fieldNames.has(normalized.field)) {
|
|
8
|
+
throw new Error(buildUnknownFieldMessage(featureName, screenId, normalized.field, entityName, fieldNames));
|
|
9
|
+
}
|
|
10
|
+
validateNoWidgetRequiredField(featureName, screenId, entityDef, normalized);
|
|
11
|
+
validateEditFieldExtras(featureName, screenId, entityDef, normalized);
|
|
12
|
+
const fieldWhere = `[Feature ${featureName}] Screen "${screenId}" (entityEdit) field "${normalized.field}"`;
|
|
13
|
+
validateFieldFormRefs(fieldWhere, normalized, fieldNames);
|
|
14
|
+
validateFillActionTarget(fieldWhere, normalized, entityDef.fields[normalized.field]);
|
|
15
|
+
}
|
|
16
|
+
function validateEntityEditSection(ctx, screenId, screen, entityDef, fieldNames, section) {
|
|
17
|
+
const featureName = ctx.feature.name;
|
|
18
|
+
validateRecordSectionActions(ctx, screenId, "entityEdit", section);
|
|
19
|
+
if (isExtensionEditSection(section)) {
|
|
20
|
+
assertExtensionSectionHasComponent(featureName, screenId, "entityEdit", section);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
rejectProjectionDetailOnlySection(featureName, screenId, "entityEdit", section);
|
|
24
|
+
validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (entityEdit)`, section);
|
|
25
|
+
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
26
|
+
validateEntityEditField(featureName, screenId, screen.entity, entityDef, fieldNames, fieldSpec);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function validateEntityEditScreen(ctx, screenId, screen) {
|
|
31
|
+
const featureName = ctx.feature.name;
|
|
32
|
+
const entityDef = resolveScreenEntity(ctx, screenId, screen.entity);
|
|
33
|
+
const fieldNames = new Set(Object.keys(entityDef.fields));
|
|
34
|
+
rejectEmptySections(featureName, screenId, "entityEdit", screen.layout.sections.length);
|
|
35
|
+
for (const section of screen.layout.sections) {
|
|
36
|
+
validateEntityEditSection(ctx, screenId, screen, entityDef, fieldNames, section);
|
|
37
|
+
}
|
|
38
|
+
validateWizardLayout(featureName, screenId, "entityEdit", screen.layout, ctx.featureMap);
|
|
39
|
+
for (const action of screen.actions ?? []) {
|
|
40
|
+
validateRecordHeaderAction(ctx, screenId, "entityEdit", screen.entity, action);
|
|
41
|
+
}
|
|
42
|
+
if (screen.recordTitleField !== undefined && !fieldNames.has(screen.recordTitleField)) {
|
|
43
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityEdit) recordTitleField ` +
|
|
44
|
+
`"${screen.recordTitleField}" is not a field of entity "${screen.entity}" ` +
|
|
45
|
+
`(known: ${[...fieldNames].sort().join(", ")}).`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof screen.readOnly === "object" && !fieldNames.has(screen.readOnly.field)) {
|
|
48
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityEdit) readOnly condition field ` +
|
|
49
|
+
`"${screen.readOnly.field}" is not a field of entity "${screen.entity}" ` +
|
|
50
|
+
`(known: ${[...fieldNames].sort().join(", ")}).`);
|
|
51
|
+
}
|
|
52
|
+
if (screen.redirect !== undefined) {
|
|
53
|
+
validateRedirectTarget(ctx.feature, screenId, "entityEdit", screen.redirect, ctx);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EntityListScreenDefinition } from "../types/screen.ts";
|
|
2
|
+
import { type ScreenValidationContext } from "./screens-kind-shared.ts";
|
|
3
|
+
export declare function validateEntityListScreen(ctx: ScreenValidationContext, screenId: string, screen: EntityListScreenDefinition): void;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { isListFilterAllowed } from "../../db/entity-field-encryption.js";
|
|
2
|
+
import { rowMetaFieldNames } from "../../db/table-builder.js";
|
|
3
|
+
import { qualifyEntityName } from "../qualified-name.js";
|
|
4
|
+
import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops.js";
|
|
5
|
+
import { normalizeListColumn } from "../screen-helpers.js";
|
|
6
|
+
import { LIST_ROW_META_COLUMN_NAMES } from "./entity-list-screens.js";
|
|
7
|
+
import { assertWriteHandlerRegistered, rejectRedirectOnListRowAction, resolveRowActionNavigateTarget, validateActionFieldRefs, validateAtMostOneRowClick, validateDrawerTargetAction, validateRowActionNavigateParams, } from "./screens-actions.js";
|
|
8
|
+
import { buildUnknownFieldMessage, listActionContext, rejectEmptyColumns, rejectNonArrayInFilter, resolveScreenEntity, validateListColumnRendering, } from "./screens-kind-shared.js";
|
|
9
|
+
import { validateRelatedListSection, validateRowActionTarget } from "./screens-lists.js";
|
|
10
|
+
function resolveEntityListFields(ctx, screenId, entityName) {
|
|
11
|
+
const entityDef = resolveScreenEntity(ctx, screenId, entityName);
|
|
12
|
+
const fieldNames = new Set(Object.keys(entityDef.fields));
|
|
13
|
+
// List columns may also name a read-time derived field (not a stored
|
|
14
|
+
// column). Allowed for display; deliberately NOT added to `fieldNames`, so
|
|
15
|
+
// defaultSort/filter on a derived field still fails — server-side sort over
|
|
16
|
+
// a non-column is a silent no-op (see DerivedFieldDef).
|
|
17
|
+
const columnFieldNames = entityDef.derivedFields !== undefined
|
|
18
|
+
? new Set([...fieldNames, ...Object.keys(entityDef.derivedFields)])
|
|
19
|
+
: fieldNames;
|
|
20
|
+
const rowMeta = rowMetaFieldNames(entityDef.softDelete ?? false);
|
|
21
|
+
return { entityDef, fieldNames, columnFieldNames, rowMeta };
|
|
22
|
+
}
|
|
23
|
+
function validateEntityListColumns(ctx, screenId, screen, fields) {
|
|
24
|
+
const featureName = ctx.feature.name;
|
|
25
|
+
rejectEmptyColumns(featureName, screenId, "entityList", screen.columns.length);
|
|
26
|
+
for (const col of screen.columns) {
|
|
27
|
+
const normalized = normalizeListColumn(col);
|
|
28
|
+
// A row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) is a
|
|
29
|
+
// real DB column without being a declared entity field — e.g. a
|
|
30
|
+
// SystemAdmin cross-tenant list exposing tenantId. Accept it like a
|
|
31
|
+
// regular field name — but only the columns computeListViewModel can
|
|
32
|
+
// actually render (LIST_ROW_META_COLUMN_NAMES), not the wider
|
|
33
|
+
// softDelete-aware rowMeta set used for payload/visible refs.
|
|
34
|
+
//
|
|
35
|
+
// A virtual presentational column (drawn by a columnRenderer component
|
|
36
|
+
// from the row, e.g. tag chips) needs BOTH a label AND a renderer —
|
|
37
|
+
// renderer is what actually makes it "virtual" (label alone still
|
|
38
|
+
// needs list.ts's virtual-branch to have something to draw). A column
|
|
39
|
+
// with neither matching a real field/row-meta column NOR a renderer is
|
|
40
|
+
// a typo worth failing the boot.
|
|
41
|
+
if (!fields.columnFieldNames.has(normalized.field) &&
|
|
42
|
+
!LIST_ROW_META_COLUMN_NAMES.has(normalized.field) &&
|
|
43
|
+
!(normalized.label !== undefined && normalized.renderer !== undefined)) {
|
|
44
|
+
throw new Error(buildUnknownFieldMessage(featureName, screenId, normalized.field, screen.entity, fields.columnFieldNames));
|
|
45
|
+
}
|
|
46
|
+
validateListColumnRendering(ctx, screenId, "entityList", normalized);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function validateEntityListDefaultSort(featureName, screenId, defaultSort, fields) {
|
|
50
|
+
const sortField = defaultSort.field;
|
|
51
|
+
if (!fields.fieldNames.has(sortField)) {
|
|
52
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) defaultSort references unknown ` +
|
|
53
|
+
`field "${sortField}". Known fields: ${[...fields.fieldNames].sort().join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
// `sortable: true` is required so the UI never sorts on a column that has
|
|
56
|
+
// no server-side DB index or that the schema deliberately keeps unsortable
|
|
57
|
+
// (audit fields, computed values). `sortable` currently
|
|
58
|
+
// exists only on TextFieldDef; the `in` narrowing also permits other
|
|
59
|
+
// field types without an explicit flag, which is acceptable because
|
|
60
|
+
// Number/Date fields are naturally sortable. Authors can mark them
|
|
61
|
+
// sortable when their field type supports it.
|
|
62
|
+
const fieldDef = fields.entityDef.fields[sortField];
|
|
63
|
+
const isSortable = fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
|
|
64
|
+
if (!isSortable) {
|
|
65
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) defaultSort.field "${sortField}" ` +
|
|
66
|
+
`is not sortable. Set sortable: true on the field definition or pick another field.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Screen filters are checked in three layers:
|
|
70
|
+
// 1) The field exists on the entity; a typo would otherwise produce an
|
|
71
|
+
// empty list instead of a clear boot failure.
|
|
72
|
+
// 2) The field has `filterable: true`, preventing accidental filtering
|
|
73
|
+
// of audit, computed, or encrypted fields.
|
|
74
|
+
// 3) The operation matches the field type, so lt/gt on text fields
|
|
75
|
+
// fails at boot instead of causing surprising runtime behavior.
|
|
76
|
+
// Additionally, "in" requires a readonly array.
|
|
77
|
+
function validateEntityListFilter(featureName, screenId, filter, fields) {
|
|
78
|
+
const filterField = filter.field;
|
|
79
|
+
if (!fields.fieldNames.has(filterField)) {
|
|
80
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter references unknown ` +
|
|
81
|
+
`field "${filterField}". Known fields: ${[...fields.fieldNames].sort().join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
if (!isListFilterAllowed(fields.entityDef, filterField, filter)) {
|
|
84
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter references field ` +
|
|
85
|
+
`"${filterField}" which is encrypted, personal or secret, so the list handler rejects ` +
|
|
86
|
+
`this filter at runtime. Only op "eq" works on a personal field with find: "exact" or ` +
|
|
87
|
+
`"fuzzy"; otherwise drop the filter or filter on another field.`);
|
|
88
|
+
}
|
|
89
|
+
const fieldDef = fields.entityDef.fields[filterField];
|
|
90
|
+
if (fieldDef !== undefined && !isFieldFilterable(fieldDef)) {
|
|
91
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter references field ` +
|
|
92
|
+
`"${filterField}" which is not filterable. Set filterable: true on the field ` +
|
|
93
|
+
`definition or pick another field.`);
|
|
94
|
+
}
|
|
95
|
+
if (fieldDef !== undefined) {
|
|
96
|
+
const allowedOps = getAllowedFilterOps(fieldDef);
|
|
97
|
+
if (!allowedOps.includes(filter.op)) {
|
|
98
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter.op ` +
|
|
99
|
+
`"${filter.op}" is not allowed on field "${filterField}" ` +
|
|
100
|
+
`(type "${fieldDef.type}"). Allowed ops: ${allowedOps.join(", ") || "(none)"}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
rejectNonArrayInFilter(featureName, screenId, "entityList", filter);
|
|
104
|
+
}
|
|
105
|
+
// The renderer's default-entityId fallback (row["id"]) only fires for a
|
|
106
|
+
// same-feature entityEdit target — it can't safely guess the id for a screen
|
|
107
|
+
// owned by a different feature. Cross-feature + entityEdit therefore MUST set
|
|
108
|
+
// an explicit entityId, or the edit screen silently opens with no entity
|
|
109
|
+
// context at runtime. Entity-targets are exempt: the renderer always supplies
|
|
110
|
+
// an id for them (explicit entityId, else row["id"]), regardless of which
|
|
111
|
+
// feature the resolved detailFor screen belongs to.
|
|
112
|
+
function rejectCrossFeatureEditTargetWithoutEntityId(featureName, screenId, action, target) {
|
|
113
|
+
if (action.screen !== undefined &&
|
|
114
|
+
target !== undefined &&
|
|
115
|
+
target.featureName !== featureName &&
|
|
116
|
+
target.screen.type === "entityEdit" &&
|
|
117
|
+
action.entityId === undefined) {
|
|
118
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) rowAction "${action.id}" ` +
|
|
119
|
+
`navigates cross-feature to entityEdit screen "${action.screen}" (feature ` +
|
|
120
|
+
`"${target.featureName}") without an explicit entityId field — the renderer's ` +
|
|
121
|
+
`same-feature row["id"] fallback does not apply across features. Set entityId to ` +
|
|
122
|
+
`the row field that names the target entity's id.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Pin rowActions at boot: a navigate target must exist and a writeHandler QN
|
|
126
|
+
// must be registered. Typos would otherwise only surface on the first click as
|
|
127
|
+
// "Screen not found" or a 404.
|
|
128
|
+
function validateEntityListRowAction(ctx, screenId, screen, fields, action) {
|
|
129
|
+
const featureName = ctx.feature.name;
|
|
130
|
+
rejectRedirectOnListRowAction(featureName, screenId, "entityList", action);
|
|
131
|
+
if (action.kind === "navigate") {
|
|
132
|
+
const target = resolveRowActionNavigateTarget(featureName, screenId, "entityList", "rowAction", action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
|
|
133
|
+
rejectCrossFeatureEditTargetWithoutEntityId(featureName, screenId, action, target);
|
|
134
|
+
// params only have a reader in actionForm and entityEdit-CREATE; on
|
|
135
|
+
// projectionDetail/dashboard/configEdit/entityEdit-update they are
|
|
136
|
+
// silently ignored at runtime. `custom` is deliberately exempt: it renders
|
|
137
|
+
// an app-registered component the framework has no visibility into, so
|
|
138
|
+
// flagging it would be a false positive on working code.
|
|
139
|
+
//
|
|
140
|
+
// Whether an entityEdit target lands in create or update mode is decided
|
|
141
|
+
// by the same rule the renderer's runNavigate() uses: an explicit
|
|
142
|
+
// entityId always forces update mode, and (absent an explicit entityId) a
|
|
143
|
+
// same-entity target gets row["id"] auto-injected — only a cross-entity
|
|
144
|
+
// target with no explicit entityId reaches create.
|
|
145
|
+
validateRowActionNavigateParams(featureName, screenId, "entityList", screen.entity, action, target);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
validateRowActionTarget(listActionContext(ctx, screenId, "entityList", screen.entity, `Screen "${screenId}" (entityList)`), "rowAction", "rowAction", action);
|
|
149
|
+
}
|
|
150
|
+
validateActionFieldRefs(featureName, screenId, "rowAction", action.id, action, fields.fieldNames, fields.rowMeta);
|
|
151
|
+
}
|
|
152
|
+
// toolbarActions, analogous to rowActions: misspelled navigate targets and
|
|
153
|
+
// unregistered writeHandler QNs fail at boot instead of on the first click.
|
|
154
|
+
function validateEntityListToolbarAction(ctx, screenId, fields, action) {
|
|
155
|
+
const featureName = ctx.feature.name;
|
|
156
|
+
if (action.kind === "navigate") {
|
|
157
|
+
const candidateQn = qualifyEntityName(featureName, "screen", action.screen);
|
|
158
|
+
if (!ctx.allScreenQns.has(candidateQn) && !ctx.navTargetShortIds.has(action.screen)) {
|
|
159
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
|
|
160
|
+
`navigate-target "${action.screen}" does not resolve to a registered screen in any feature. Use "<feature>:screen:<id>" of an existing screen.`);
|
|
161
|
+
}
|
|
162
|
+
if (action.tab !== undefined) {
|
|
163
|
+
resolveRowActionNavigateTarget(featureName, screenId, "entityList", "toolbarAction", action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (action.kind === "drawer") {
|
|
167
|
+
validateDrawerTargetAction(featureName, screenId, "entityList", "toolbarAction", action, ctx.feature.screens);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (entityList) toolbarAction "${action.id}"`, action.handler, ctx.allWriteHandlerQns);
|
|
171
|
+
}
|
|
172
|
+
validateActionFieldRefs(featureName, screenId, "toolbarAction", action.id, action, fields.fieldNames, fields.rowMeta);
|
|
173
|
+
}
|
|
174
|
+
function validateEntityListExpandableRow(ctx, screenId, expandableRow, fields) {
|
|
175
|
+
validateRelatedListSection({
|
|
176
|
+
featureName: ctx.feature.name,
|
|
177
|
+
featureMap: ctx.featureMap,
|
|
178
|
+
screenId,
|
|
179
|
+
screenType: "entityList expandableRow",
|
|
180
|
+
where: `Screen "${screenId}" (entityList) expandableRow "${expandableRow.title}"`,
|
|
181
|
+
section: expandableRow,
|
|
182
|
+
screens: ctx.feature.screens,
|
|
183
|
+
allWriteHandlerQns: ctx.allWriteHandlerQns,
|
|
184
|
+
allScreenQns: ctx.allScreenQns,
|
|
185
|
+
navTargetShortIds: ctx.navTargetShortIds,
|
|
186
|
+
screensByShortId: ctx.screensByShortId,
|
|
187
|
+
detailForScreens: ctx.detailForScreens,
|
|
188
|
+
});
|
|
189
|
+
// Toolbar actions of the expansion evaluate against the host row.
|
|
190
|
+
for (const action of expandableRow.toolbarActions ?? []) {
|
|
191
|
+
validateActionFieldRefs(ctx.feature.name, screenId, "toolbarAction", action.id, action, fields.fieldNames, fields.rowMeta);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export function validateEntityListScreen(ctx, screenId, screen) {
|
|
195
|
+
const featureName = ctx.feature.name;
|
|
196
|
+
const fields = resolveEntityListFields(ctx, screenId, screen.entity);
|
|
197
|
+
validateEntityListColumns(ctx, screenId, screen, fields);
|
|
198
|
+
// Validate pagination, sorting, and search at boot so authoring errors
|
|
199
|
+
// fail before runtime debugging of empty or incorrectly sorted lists.
|
|
200
|
+
if (screen.pageSize !== undefined && screen.pageSize <= 0) {
|
|
201
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) has pageSize=${screen.pageSize} — ` +
|
|
202
|
+
`must be a positive integer.`);
|
|
203
|
+
}
|
|
204
|
+
if (screen.defaultSort !== undefined) {
|
|
205
|
+
validateEntityListDefaultSort(featureName, screenId, screen.defaultSort, fields);
|
|
206
|
+
}
|
|
207
|
+
if (screen.createScreen !== undefined &&
|
|
208
|
+
!Object.hasOwn(ctx.feature.screens, screen.createScreen)) {
|
|
209
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) createScreen ` +
|
|
210
|
+
`"${screen.createScreen}" does not resolve to a registered screen in this feature. ` +
|
|
211
|
+
`Known screens in this feature: ${Object.keys(ctx.feature.screens).sort().join(", ") || "(none)"}.`);
|
|
212
|
+
}
|
|
213
|
+
if (screen.filter !== undefined) {
|
|
214
|
+
validateEntityListFilter(featureName, screenId, screen.filter, fields);
|
|
215
|
+
}
|
|
216
|
+
if (screen.rowActions !== undefined) {
|
|
217
|
+
for (const action of screen.rowActions) {
|
|
218
|
+
validateEntityListRowAction(ctx, screenId, screen, fields, action);
|
|
219
|
+
}
|
|
220
|
+
validateAtMostOneRowClick(featureName, screenId, "entityList", screen.rowActions);
|
|
221
|
+
}
|
|
222
|
+
for (const action of screen.toolbarActions ?? []) {
|
|
223
|
+
validateEntityListToolbarAction(ctx, screenId, fields, action);
|
|
224
|
+
}
|
|
225
|
+
if (screen.expandableRow !== undefined) {
|
|
226
|
+
validateEntityListExpandableRow(ctx, screenId, screen.expandableRow, fields);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ProjectionDetailScreenDefinition } from "../types/screen.ts";
|
|
2
|
+
import { type ScreenValidationContext } from "./screens-kind-shared.ts";
|
|
3
|
+
export declare function validateProjectionDetailScreen(ctx: ScreenValidationContext, screenId: string, screen: ProjectionDetailScreenDefinition): void;
|