@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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { SELECT_OPTION_TONES } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
import { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
3
|
+
import { assertWriteHandlerRegistered, resolveRowActionNavigateTarget, validateActionHasIcon, validatePrimaryActionTarget, } from "./screens-actions.js";
|
|
4
|
+
import { rejectWriteOnlyFormField, validateFieldsXorGroups, validateFormSelectOptions, validateTabSections, } from "./screens-forms.js";
|
|
5
|
+
import { assertExtensionSectionHasComponent, editSectionLabel, listActionContext, navigateActionFromLink, rejectEmptySections, rejectMissingQuery, validateRecordHeaderAction, validateRecordSectionActions, } from "./screens-kind-shared.js";
|
|
6
|
+
import { assertRefTargetRegistered, validateListActionTargets, validateRelatedListSection, } from "./screens-lists.js";
|
|
7
|
+
import { isSelectOptionTone } from "./select-option-tone.js";
|
|
8
|
+
function validateProjectionDetailFlags(featureName, screenId, screen) {
|
|
9
|
+
if (screen.singleton === true && screen.idParam !== undefined) {
|
|
10
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
|
|
11
|
+
`and idParam — singleton means the server picks the row from the caller's context, so there ` +
|
|
12
|
+
`is no row id to send under idParam. Remove idParam.`);
|
|
13
|
+
}
|
|
14
|
+
// singleton has no row id in the path, but the auto-generated "Edit"
|
|
15
|
+
// action (defaultEditAction in kumiko-screen.tsx) navigates
|
|
16
|
+
// to the detailFor entity's entityEdit screen using that path id —
|
|
17
|
+
// under singleton it would silently open that screen in CREATE mode
|
|
18
|
+
// instead of editing the singleton row. Reject rather than let that
|
|
19
|
+
// win silently, same as the idParam check above.
|
|
20
|
+
if (screen.singleton === true && screen.detailFor !== undefined) {
|
|
21
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
|
|
22
|
+
`and detailFor — the auto-generated "Edit" action navigates using the path's row id, which a ` +
|
|
23
|
+
`singleton screen never has. Remove detailFor, or declare an explicit "edit" action instead.`);
|
|
24
|
+
}
|
|
25
|
+
if (screen.recordTitleField !== undefined && screen.header !== undefined) {
|
|
26
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets both header and ` +
|
|
27
|
+
`recordTitleField — header.title already names the record, so drop recordTitleField.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validateProjectionDetailTabs(featureName, screenId, screen) {
|
|
31
|
+
if (screen.layout.mode === "tabs") {
|
|
32
|
+
validateTabSections(featureName, screenId, "projectionDetail", screen.layout.sections);
|
|
33
|
+
for (const section of screen.layout.sections) {
|
|
34
|
+
if (isFieldsEditSection(section) && "groups" in section && section.actions !== undefined) {
|
|
35
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.id}" ` +
|
|
36
|
+
`declares both groups and actions in tabs mode — actions has no single group's Card to sit ` +
|
|
37
|
+
`in, and an extra title-less Card around the group cards would nest a Card inside a Card. ` +
|
|
38
|
+
`Move the actions onto one group's title, or drop groups for a flat fields grid.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function validateProjectionDetailMetricNavigate(ctx, screenId, screen, field, navigate) {
|
|
44
|
+
const featureName = ctx.feature.name;
|
|
45
|
+
if (navigate.screen === undefined && navigate.entity === undefined) {
|
|
46
|
+
if (navigate.tab !== undefined &&
|
|
47
|
+
!screen.layout.sections.some((section) => section.id === navigate.tab)) {
|
|
48
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) metric "${field}" ` +
|
|
49
|
+
`navigates to tab "${navigate.tab}", which is not a section id on this screen. Navigate to an existing section id or remove navigate.tab.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Also runs without `tab`: a typo'd screen/entity or an entity
|
|
54
|
+
// target without entityId would otherwise render a clickable
|
|
55
|
+
// metric that silently does nothing.
|
|
56
|
+
resolveRowActionNavigateTarget(featureName, screenId, "projectionDetail", "metric", navigateActionFromLink(field, navigate), ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function validateProjectionDetailMetric(ctx, screenId, screen, metric) {
|
|
60
|
+
if (typeof metric.label !== "string" || metric.label.length === 0) {
|
|
61
|
+
throw new Error(`[Feature ${ctx.feature.name}] Screen "${screenId}" (projectionDetail) metric "${metric.field}" has ` +
|
|
62
|
+
`no "label" — every metric needs an i18n label key, ` +
|
|
63
|
+
`there is no fallback to the raw column name.`);
|
|
64
|
+
}
|
|
65
|
+
if (metric.navigate !== undefined) {
|
|
66
|
+
validateProjectionDetailMetricNavigate(ctx, screenId, screen, metric.field, metric.navigate);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function validateProjectionDetailStatusTones(featureName, screenId, screen) {
|
|
70
|
+
for (const [statusValue, tone] of Object.entries(screen.header?.statusTones ?? {})) {
|
|
71
|
+
if (!isSelectOptionTone(tone)) {
|
|
72
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) header.statusTones["${statusValue}"] ` +
|
|
73
|
+
`is "${String(tone)}" — expected one of ${SELECT_OPTION_TONES.join(", ")}.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function validateProjectionDetailSubtitleParts(ctx, screenId, subtitleHref, parts) {
|
|
78
|
+
const featureName = ctx.feature.name;
|
|
79
|
+
if (subtitleHref !== undefined) {
|
|
80
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets header.subtitleHref ` +
|
|
81
|
+
`together with a multi-part header.subtitle — subtitleHref only works with the string form. Use a single-field subtitle or remove subtitleHref.`);
|
|
82
|
+
}
|
|
83
|
+
const seenPartFields = new Set();
|
|
84
|
+
for (const part of parts) {
|
|
85
|
+
const partField = typeof part === "string" ? part : part.field;
|
|
86
|
+
if (seenPartFields.has(partField)) {
|
|
87
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) header.subtitle ` +
|
|
88
|
+
`lists field "${partField}" twice. List each field once.`);
|
|
89
|
+
}
|
|
90
|
+
seenPartFields.add(partField);
|
|
91
|
+
const navigate = typeof part === "string" ? undefined : part.navigate;
|
|
92
|
+
if (navigate === undefined)
|
|
93
|
+
continue;
|
|
94
|
+
if (navigate.screen === undefined && navigate.entity === undefined) {
|
|
95
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) header.subtitle part ` +
|
|
96
|
+
`"${partField}" navigates with neither "screen" nor "entity" — a tab-only navigate has no link target.`);
|
|
97
|
+
}
|
|
98
|
+
resolveRowActionNavigateTarget(featureName, screenId, "projectionDetail", "subtitle part", navigateActionFromLink(partField, navigate), ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function validateProjectionDetailSubtitle(ctx, screenId, screen) {
|
|
102
|
+
const header = screen.header;
|
|
103
|
+
if (header !== undefined && Array.isArray(header.subtitle)) {
|
|
104
|
+
validateProjectionDetailSubtitleParts(ctx, screenId, header.subtitleHref, header.subtitle);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function validateProjectionDetailExtensionSection(featureName, screenId, section) {
|
|
108
|
+
// projectionDetail is read-only (no composed form submit) — an
|
|
109
|
+
// extension that persists through the host's Save button has
|
|
110
|
+
// nothing to save against. Extensions that persist themselves
|
|
111
|
+
// (e.g. notes-history NotesSection, via their own dispatcher
|
|
112
|
+
// writes) are fine.
|
|
113
|
+
if (section.contributesToFormSubmit === true) {
|
|
114
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) extension section ` +
|
|
115
|
+
`"${section.title}" sets contributesToFormSubmit: true, but projectionDetail has no ` +
|
|
116
|
+
`form submit to contribute to (read-only screen). Omit contributesToFormSubmit and ` +
|
|
117
|
+
`persist through the extension's own dispatcher writes instead.`);
|
|
118
|
+
}
|
|
119
|
+
assertExtensionSectionHasComponent(featureName, screenId, "projectionDetail", section);
|
|
120
|
+
// The host-derived entity name on projectionDetail is an internal
|
|
121
|
+
// placeholder (no real entity backs this screen) — a self-
|
|
122
|
+
// persisting extension needs the real domain entity name declared
|
|
123
|
+
// explicitly, or it silently reads/writes against the wrong type.
|
|
124
|
+
if (section.entityName === undefined || section.entityName.trim().length === 0) {
|
|
125
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) extension section ` +
|
|
126
|
+
`"${section.title}" has no entityName — projectionDetail has no real entity, so the ` +
|
|
127
|
+
`extension must declare entityName explicitly (the domain entity it persists against).`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function validateProjectionDetailRelatedList(ctx, screenId, screen, section) {
|
|
131
|
+
if (screen.layout.mode === "wizard") {
|
|
132
|
+
throw new Error(`[Feature ${ctx.feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
133
|
+
`is kind "relatedList" in a wizard layout — a read-only list is not supported in a ` +
|
|
134
|
+
`stepped form. Remove mode: "wizard" or drop the relatedList section.`);
|
|
135
|
+
}
|
|
136
|
+
validateRelatedListSection({
|
|
137
|
+
featureName: ctx.feature.name,
|
|
138
|
+
featureMap: ctx.featureMap,
|
|
139
|
+
screenId,
|
|
140
|
+
screenType: "projectionDetail",
|
|
141
|
+
where: `Screen "${screenId}" (projectionDetail) section "${section.title}" (relatedList)`,
|
|
142
|
+
section,
|
|
143
|
+
screens: ctx.feature.screens,
|
|
144
|
+
allWriteHandlerQns: ctx.allWriteHandlerQns,
|
|
145
|
+
allScreenQns: ctx.allScreenQns,
|
|
146
|
+
navTargetShortIds: ctx.navTargetShortIds,
|
|
147
|
+
screensByShortId: ctx.screensByShortId,
|
|
148
|
+
detailForScreens: ctx.detailForScreens,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function validateProjectionDetailWriteForm(ctx, screenId, screen, section) {
|
|
152
|
+
const featureName = ctx.feature.name;
|
|
153
|
+
if (Object.keys(section.fieldDefs).length === 0) {
|
|
154
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
155
|
+
`(writeForm) has an empty fieldDefs map — declare at least one field type.`);
|
|
156
|
+
}
|
|
157
|
+
if (section.fields.length === 0) {
|
|
158
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
159
|
+
`(writeForm) has zero fields — drop the section or add fields to it.`);
|
|
160
|
+
}
|
|
161
|
+
// A ref to a fieldDef the form never renders would never resolve.
|
|
162
|
+
const writeFormFieldNames = new Set(section.fields.map((f) => normalizeEditField(f).field));
|
|
163
|
+
for (const [defName, fdef] of Object.entries(section.fieldDefs)) {
|
|
164
|
+
validateFormSelectOptions(featureName, screenId, `projectionDetail section "${section.title}" writeForm`, defName, fdef, writeFormFieldNames);
|
|
165
|
+
rejectWriteOnlyFormField(featureName, screenId, `projectionDetail section "${section.title}" writeForm`, defName, fdef);
|
|
166
|
+
}
|
|
167
|
+
for (const f of section.fields) {
|
|
168
|
+
const fieldName = normalizeEditField(f).field;
|
|
169
|
+
if (section.fieldDefs[fieldName] === undefined) {
|
|
170
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
171
|
+
`(writeForm) field "${fieldName}" has no entry in fieldDefs — every rendered field needs ` +
|
|
172
|
+
`a type declared there. Add the field to fieldDefs.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (screen.layout.mode === "wizard") {
|
|
176
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
177
|
+
`is kind "writeForm" in a wizard layout — a self-persisting form is not supported in a ` +
|
|
178
|
+
`stepped form. Remove mode: "wizard" or drop the writeForm section.`);
|
|
179
|
+
}
|
|
180
|
+
assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (projectionDetail) section "${section.title}" (writeForm)`, section.handler, ctx.allWriteHandlerQns);
|
|
181
|
+
}
|
|
182
|
+
function validateProjectionDetailFieldsSection(ctx, screenId, section) {
|
|
183
|
+
const featureName = ctx.feature.name;
|
|
184
|
+
validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail)`, section);
|
|
185
|
+
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
186
|
+
const normalizedField = normalizeEditField(fieldSpec);
|
|
187
|
+
if (normalizedField.refEntity !== undefined) {
|
|
188
|
+
assertRefTargetRegistered(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail)`, `field "${normalizedField.field}" (refEntity)`, normalizedField.refEntity, featureName, ctx.featureMap);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function validateProjectionDetailSection(ctx, screenId, screen, section) {
|
|
193
|
+
const featureName = ctx.feature.name;
|
|
194
|
+
validateRecordSectionActions(ctx, screenId, "projectionDetail", section);
|
|
195
|
+
if (section.kind === "relatedList" && section.emptyState?.action !== undefined) {
|
|
196
|
+
const sectionLabel = editSectionLabel(section);
|
|
197
|
+
validateActionHasIcon(featureName, screenId, "projectionDetail", `section "${sectionLabel}" emptyState action`, section.emptyState.action);
|
|
198
|
+
validateListActionTargets(listActionContext(ctx, screenId, "projectionDetail", undefined, `Screen "${screenId}" (projectionDetail) section "${sectionLabel}"`), "emptyState action", section.emptyState.action);
|
|
199
|
+
}
|
|
200
|
+
if (isExtensionEditSection(section)) {
|
|
201
|
+
validateProjectionDetailExtensionSection(featureName, screenId, section);
|
|
202
|
+
}
|
|
203
|
+
else if (section.kind === "relatedList") {
|
|
204
|
+
validateProjectionDetailRelatedList(ctx, screenId, screen, section);
|
|
205
|
+
}
|
|
206
|
+
else if (isWriteFormEditSection(section)) {
|
|
207
|
+
validateProjectionDetailWriteForm(ctx, screenId, screen, section);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
validateProjectionDetailFieldsSection(ctx, screenId, section);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Query-driven like projectionList, but a single row plus layout instead of
|
|
214
|
+
// columns. No entity check is possible: fields resolve against the query
|
|
215
|
+
// response at render time, not against an entity.
|
|
216
|
+
export function validateProjectionDetailScreen(ctx, screenId, screen) {
|
|
217
|
+
const featureName = ctx.feature.name;
|
|
218
|
+
rejectMissingQuery(featureName, screenId, "projectionDetail", screen.query);
|
|
219
|
+
validateProjectionDetailFlags(featureName, screenId, screen);
|
|
220
|
+
rejectEmptySections(featureName, screenId, "projectionDetail", screen.layout.sections.length);
|
|
221
|
+
validateProjectionDetailTabs(featureName, screenId, screen);
|
|
222
|
+
for (const metric of screen.metrics ?? []) {
|
|
223
|
+
validateProjectionDetailMetric(ctx, screenId, screen, metric);
|
|
224
|
+
}
|
|
225
|
+
validateProjectionDetailStatusTones(featureName, screenId, screen);
|
|
226
|
+
validateProjectionDetailSubtitle(ctx, screenId, screen);
|
|
227
|
+
for (const section of screen.layout.sections) {
|
|
228
|
+
validateProjectionDetailSection(ctx, screenId, screen, section);
|
|
229
|
+
}
|
|
230
|
+
validatePrimaryActionTarget(featureName, screenId, screen);
|
|
231
|
+
for (const action of screen.actions ?? []) {
|
|
232
|
+
validateActionHasIcon(featureName, screenId, "projectionDetail", "action", action);
|
|
233
|
+
validateRecordHeaderAction(ctx, screenId, "projectionDetail", screen.detailFor, action);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ProjectionListScreenDefinition } from "../types/screen.ts";
|
|
2
|
+
import { type ScreenValidationContext } from "./screens-kind-shared.ts";
|
|
3
|
+
export declare function validateProjectionListScreen(ctx: ScreenValidationContext, screenId: string, screen: ProjectionListScreenDefinition): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { normalizeListColumn } from "../screen-helpers.js";
|
|
2
|
+
import { assertWriteHandlerRegistered, resolveRowActionNavigateTarget, validateAtMostOneRowClick, validateDrawerTargetAction, } from "./screens-actions.js";
|
|
3
|
+
import { listActionContext, rejectEmptyColumns, rejectMissingQuery, rejectNonArrayInFilter, validateListColumnRendering, } from "./screens-kind-shared.js";
|
|
4
|
+
import { validateListActionTargets, validateListFacets } from "./screens-lists.js";
|
|
5
|
+
function validateProjectionListRowActions(ctx, screenId, rowActions) {
|
|
6
|
+
const listContext = listActionContext(ctx, screenId, "projectionList", undefined, `Screen "${screenId}" (projectionList)`);
|
|
7
|
+
for (const action of rowActions) {
|
|
8
|
+
validateListActionTargets(listContext, "rowAction", action);
|
|
9
|
+
}
|
|
10
|
+
validateAtMostOneRowClick(ctx.feature.name, screenId, "projectionList", rowActions);
|
|
11
|
+
}
|
|
12
|
+
// Plain navigate toolbarActions (no tab) have no boot check yet.
|
|
13
|
+
function validateProjectionListToolbarActions(ctx, screenId, toolbarActions) {
|
|
14
|
+
const featureName = ctx.feature.name;
|
|
15
|
+
for (const action of toolbarActions) {
|
|
16
|
+
if (action.kind === "drawer") {
|
|
17
|
+
validateDrawerTargetAction(featureName, screenId, "projectionList", "toolbarAction", action, ctx.feature.screens);
|
|
18
|
+
}
|
|
19
|
+
else if (action.kind === "writeHandler") {
|
|
20
|
+
assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (projectionList) toolbarAction "${action.id}"`, action.handler, ctx.allWriteHandlerQns);
|
|
21
|
+
}
|
|
22
|
+
if (action.kind === "navigate" && action.tab !== undefined) {
|
|
23
|
+
resolveRowActionNavigateTarget(featureName, screenId, "projectionList", "toolbarAction", action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Query-driven, no entity, so only query + columns are checked (the column
|
|
28
|
+
// fields cannot be checked against an entity; they resolve against the
|
|
29
|
+
// projection rows at render time).
|
|
30
|
+
export function validateProjectionListScreen(ctx, screenId, screen) {
|
|
31
|
+
const featureName = ctx.feature.name;
|
|
32
|
+
rejectMissingQuery(featureName, screenId, "projectionList", screen.query);
|
|
33
|
+
rejectEmptyColumns(featureName, screenId, "projectionList", screen.columns.length);
|
|
34
|
+
for (const col of screen.columns) {
|
|
35
|
+
validateListColumnRendering(ctx, screenId, "projectionList", normalizeListColumn(col));
|
|
36
|
+
}
|
|
37
|
+
// Screen filter: field existence can't be checked without an entity (columns
|
|
38
|
+
// aren't a complete field inventory of the underlying query), so only the
|
|
39
|
+
// structure is pinned: "in" requires an array.
|
|
40
|
+
if (screen.filter !== undefined) {
|
|
41
|
+
rejectNonArrayInFilter(featureName, screenId, "projectionList", screen.filter);
|
|
42
|
+
}
|
|
43
|
+
if (screen.facets !== undefined) {
|
|
44
|
+
validateListFacets(`[Feature ${featureName}] Screen "${screenId}" (projectionList)`, screen.facets, screen.columns, featureName, ctx.featureMap);
|
|
45
|
+
}
|
|
46
|
+
if (screen.rowActions !== undefined) {
|
|
47
|
+
validateProjectionListRowActions(ctx, screenId, screen.rowActions);
|
|
48
|
+
}
|
|
49
|
+
if (screen.toolbarActions !== undefined) {
|
|
50
|
+
validateProjectionListToolbarActions(ctx, screenId, screen.toolbarActions);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SecretMintScreenDefinition } from "../types/screen.ts";
|
|
2
|
+
import type { ScreenValidationContext } from "./screens-kind-shared.ts";
|
|
3
|
+
export declare function validateSecretMintScreen(ctx: ScreenValidationContext, screenId: string, screen: SecretMintScreenDefinition): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { validateInlineFormScreen, validateSecretMintConfirm } from "./screens-forms.js";
|
|
2
|
+
function validateSecretMintRevealFields(featureName, screenId, screen) {
|
|
3
|
+
if (screen.reveal.fields.length === 0) {
|
|
4
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (secretMint) has an empty reveal.fields ` +
|
|
5
|
+
`list — declare at least one field to reveal.`);
|
|
6
|
+
}
|
|
7
|
+
const revealFieldNames = new Set();
|
|
8
|
+
for (const revealField of screen.reveal.fields) {
|
|
9
|
+
if (typeof revealField.field !== "string" || revealField.field.trim() === "") {
|
|
10
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (secretMint) has a reveal.fields entry ` +
|
|
11
|
+
`with an empty or non-string "field". Set field to a non-empty string.`);
|
|
12
|
+
}
|
|
13
|
+
if (revealFieldNames.has(revealField.field)) {
|
|
14
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (secretMint) reveal.fields has a ` +
|
|
15
|
+
`duplicate field "${revealField.field}" — each revealed field must be unique.`);
|
|
16
|
+
}
|
|
17
|
+
revealFieldNames.add(revealField.field);
|
|
18
|
+
if (typeof revealField.label !== "string" || revealField.label.trim() === "") {
|
|
19
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (secretMint) reveal.fields entry ` +
|
|
20
|
+
`"${revealField.field}" has an empty or non-string "label". Set label to a non-empty string.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function validateSecretMintScreen(ctx, screenId, screen) {
|
|
25
|
+
validateInlineFormScreen(ctx.feature, screenId, screen, "secretMint", ctx.allWriteHandlerQns, ctx, ctx.featureMap);
|
|
26
|
+
validateSecretMintRevealFields(ctx.feature.name, screenId, screen);
|
|
27
|
+
validateSecretMintConfirm(ctx.feature, screenId, screen, ctx.allWriteHandlerQns);
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type normalizeListColumn } from "../screen-helpers.ts";
|
|
2
|
+
import type { ConfigKeyDefinition, EntityDefinition, FeatureDefinition } from "../types/index.ts";
|
|
3
|
+
import type { EditExtensionSection, EditFieldsSection, EditSectionSpec, RowAction } from "../types/screen.ts";
|
|
4
|
+
import type { NavigateSourceScreenType, NavigateTargetLookups } from "./screens-actions.ts";
|
|
5
|
+
import { type resolveRowActionNavigateTarget } from "./screens-actions.ts";
|
|
6
|
+
import type { ListActionTargetContext } from "./screens-lists.ts";
|
|
7
|
+
export type ScreenValidationContext = NavigateTargetLookups & {
|
|
8
|
+
readonly feature: FeatureDefinition;
|
|
9
|
+
readonly featureMap: ReadonlyMap<string, FeatureDefinition>;
|
|
10
|
+
readonly allWriteHandlerQns: ReadonlySet<string>;
|
|
11
|
+
readonly configKeyDefsByQn: ReadonlyMap<string, ConfigKeyDefinition>;
|
|
12
|
+
};
|
|
13
|
+
export declare function findEntityFeature(entityName: string, featureMap: ReadonlyMap<string, FeatureDefinition>): string | undefined;
|
|
14
|
+
export declare function buildUnknownFieldMessage(featureName: string, screenId: string, fieldName: string, entityName: string, knownFields: ReadonlySet<string>): string;
|
|
15
|
+
export declare function listActionContext(ctx: ScreenValidationContext, screenId: string, screenType: NavigateSourceScreenType, screenEntity: string | undefined, where: string): ListActionTargetContext;
|
|
16
|
+
export declare function resolveScreenEntity(ctx: ScreenValidationContext, screenId: string, entityName: string): EntityDefinition;
|
|
17
|
+
export declare function rejectMissingQuery(featureName: string, screenId: string, kind: "projectionList" | "projectionDetail", query: unknown): void;
|
|
18
|
+
export declare function rejectEmptyColumns(featureName: string, screenId: string, kind: "projectionList" | "entityList", columnCount: number): void;
|
|
19
|
+
export declare function validateListColumnRendering(ctx: ScreenValidationContext, screenId: string, kind: "projectionList" | "entityList", column: ReturnType<typeof normalizeListColumn>): void;
|
|
20
|
+
export declare function rejectNonArrayInFilter(featureName: string, screenId: string, kind: "projectionList" | "entityList", filter: {
|
|
21
|
+
readonly op: string;
|
|
22
|
+
readonly value?: unknown;
|
|
23
|
+
}): void;
|
|
24
|
+
export declare function rejectEmptySections(featureName: string, screenId: string, kind: "projectionDetail" | "configEdit" | "entityEdit", sectionCount: number): void;
|
|
25
|
+
export declare function assertExtensionSectionHasComponent(featureName: string, screenId: string, kind: "projectionDetail" | "configEdit" | "entityEdit", section: EditExtensionSection): void;
|
|
26
|
+
export declare function rejectProjectionDetailOnlySection(featureName: string, screenId: string, kind: "configEdit" | "entityEdit", section: Exclude<EditSectionSpec, EditExtensionSection>): asserts section is EditFieldsSection;
|
|
27
|
+
export declare function editSectionLabel(section: EditSectionSpec): string;
|
|
28
|
+
export declare function validateRecordSectionActions(ctx: ScreenValidationContext, screenId: string, kind: "projectionDetail" | "entityEdit", section: EditSectionSpec): void;
|
|
29
|
+
export declare function validateRecordHeaderAction(ctx: ScreenValidationContext, screenId: string, kind: "projectionDetail" | "entityEdit", screenEntity: string | undefined, action: RowAction): void;
|
|
30
|
+
type NavigateLink = {
|
|
31
|
+
readonly screen?: string;
|
|
32
|
+
readonly entity?: string;
|
|
33
|
+
readonly entityId?: string;
|
|
34
|
+
readonly tab?: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function navigateActionFromLink(id: string, navigate: NavigateLink): Parameters<typeof resolveRowActionNavigateTarget>[4];
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Checks shared by several screen kinds; each helper takes the kind it reports under.
|
|
2
|
+
// Dependencies point one way: screens-kind-*.ts -> here ->
|
|
3
|
+
// screens-dashboard/forms/lists -> screens-actions.
|
|
4
|
+
import { qualifyEntityName } from "../qualified-name.js";
|
|
5
|
+
import { isWriteFormEditSection } from "../screen-helpers.js";
|
|
6
|
+
import { isRowClickAction, validateActionHasIcon, validateNavigateActionTab, validateRecordActionRedirect, } from "./screens-actions.js";
|
|
7
|
+
import { assertRefTargetRegistered, rejectInertColumnSortable, validateColumnRendererForm, validateRowActionTarget, } from "./screens-lists.js";
|
|
8
|
+
export function findEntityFeature(entityName, featureMap) {
|
|
9
|
+
for (const [name, feature] of featureMap) {
|
|
10
|
+
if (feature.entities?.[entityName])
|
|
11
|
+
return name;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
export function buildUnknownFieldMessage(featureName, screenId, fieldName, entityName, knownFields) {
|
|
16
|
+
const known = [...knownFields].sort().join(", ");
|
|
17
|
+
return (`[Feature ${featureName}] Screen "${screenId}" references field "${fieldName}" ` +
|
|
18
|
+
`which does not exist on entity "${entityName}" (known: ${known}).`);
|
|
19
|
+
}
|
|
20
|
+
export function listActionContext(ctx, screenId, screenType, screenEntity, where) {
|
|
21
|
+
return {
|
|
22
|
+
featureName: ctx.feature.name,
|
|
23
|
+
screenId,
|
|
24
|
+
screenType,
|
|
25
|
+
screenEntity,
|
|
26
|
+
where,
|
|
27
|
+
screens: ctx.feature.screens,
|
|
28
|
+
allWriteHandlerQns: ctx.allWriteHandlerQns,
|
|
29
|
+
allScreenQns: ctx.allScreenQns,
|
|
30
|
+
navTargetShortIds: ctx.navTargetShortIds,
|
|
31
|
+
screensByShortId: ctx.screensByShortId,
|
|
32
|
+
detailForScreens: ctx.detailForScreens,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// entityList / entityEdit: entity-refs are feature-local.
|
|
36
|
+
export function resolveScreenEntity(ctx, screenId, entityName) {
|
|
37
|
+
const entityDef = ctx.feature.entities?.[entityName];
|
|
38
|
+
if (entityDef)
|
|
39
|
+
return entityDef;
|
|
40
|
+
const known = Object.keys(ctx.feature.entities ?? {})
|
|
41
|
+
.sort()
|
|
42
|
+
.join(", ") || "(none)";
|
|
43
|
+
const crossFeature = findEntityFeature(entityName, ctx.featureMap);
|
|
44
|
+
const hint = crossFeature
|
|
45
|
+
? ` Entity "${entityName}" is owned by feature "${crossFeature}" — cross-feature screen ownership is not supported.`
|
|
46
|
+
: "";
|
|
47
|
+
throw new Error(`[Feature ${ctx.feature.name}] Screen "${screenId}" references entity "${entityName}" ` +
|
|
48
|
+
`which is not declared in this feature (known: ${known}).${hint}`);
|
|
49
|
+
}
|
|
50
|
+
export function rejectMissingQuery(featureName, screenId, kind, query) {
|
|
51
|
+
if (!query || typeof query !== "string") {
|
|
52
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) has empty or non-string query. Set query to a registered query name.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// An empty column list would render a blank table: almost always an
|
|
56
|
+
// in-progress screen the author forgot to fill in.
|
|
57
|
+
export function rejectEmptyColumns(featureName, screenId, kind, columnCount) {
|
|
58
|
+
if (columnCount === 0) {
|
|
59
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) has an empty columns list — ` +
|
|
60
|
+
`declare at least one column.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function validateListColumnRendering(ctx, screenId, kind, column) {
|
|
64
|
+
const featureName = ctx.feature.name;
|
|
65
|
+
validateColumnRendererForm(featureName, screenId, column);
|
|
66
|
+
rejectInertColumnSortable(featureName, screenId, kind, column);
|
|
67
|
+
if (column.refEntity !== undefined) {
|
|
68
|
+
assertRefTargetRegistered(`[Feature ${featureName}] Screen "${screenId}" (${kind})`, `column "${column.field}" (refEntity)`, column.refEntity, featureName, ctx.featureMap);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function rejectNonArrayInFilter(featureName, screenId, kind, filter) {
|
|
72
|
+
if (filter.op === "in" && !Array.isArray(filter.value)) {
|
|
73
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) filter.op "in" requires ` +
|
|
74
|
+
`filter.value to be a readonly array. Pass an array as filter.value.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// A layout with zero sections renders as nothing: reject at boot so the
|
|
78
|
+
// author sees it before the blank form surprises them.
|
|
79
|
+
export function rejectEmptySections(featureName, screenId, kind, sectionCount) {
|
|
80
|
+
if (sectionCount === 0) {
|
|
81
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) has an empty sections list — ` +
|
|
82
|
+
`declare at least one section.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function assertExtensionSectionHasComponent(featureName, screenId, kind, section) {
|
|
86
|
+
if (section.component?.react === undefined && section.component?.native === undefined) {
|
|
87
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) extension section ` +
|
|
88
|
+
`"${section.title}" has no component — declare a react/native component marker.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// relatedList and writeForm sections exist on projectionDetail only; what
|
|
92
|
+
// remains afterwards is a plain fields/groups section.
|
|
93
|
+
export function rejectProjectionDetailOnlySection(featureName, screenId, kind, section) {
|
|
94
|
+
if (section.kind === "relatedList") {
|
|
95
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) relatedList section ` +
|
|
96
|
+
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
97
|
+
`primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
|
|
98
|
+
}
|
|
99
|
+
if (isWriteFormEditSection(section)) {
|
|
100
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) writeForm section ` +
|
|
101
|
+
`"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function editSectionLabel(section) {
|
|
105
|
+
return section.title ?? section.id ?? "(untitled)";
|
|
106
|
+
}
|
|
107
|
+
export function validateRecordSectionActions(ctx, screenId, kind, section) {
|
|
108
|
+
const actionContext = `section "${editSectionLabel(section)}" action`;
|
|
109
|
+
for (const action of section.actions ?? []) {
|
|
110
|
+
validateActionHasIcon(ctx.feature.name, screenId, kind, actionContext, action);
|
|
111
|
+
validateRecordActionRedirect(ctx.feature, screenId, kind, action, ctx);
|
|
112
|
+
validateNavigateActionTab(ctx.feature.name, screenId, kind, actionContext, action, ctx);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const NO_ROW_SUBJECT = {
|
|
116
|
+
projectionDetail: "a detail screen",
|
|
117
|
+
entityEdit: "an edit screen",
|
|
118
|
+
};
|
|
119
|
+
// Header actions reuse RowAction (the displayed record stands in for the row),
|
|
120
|
+
// so the same navigate/writeHandler existence checks as list rowActions apply.
|
|
121
|
+
// `rowClick` is rejected outright: there is no row to click.
|
|
122
|
+
export function validateRecordHeaderAction(ctx, screenId, kind, screenEntity, action) {
|
|
123
|
+
const featureName = ctx.feature.name;
|
|
124
|
+
validateRecordActionRedirect(ctx.feature, screenId, kind, action, ctx);
|
|
125
|
+
if (isRowClickAction(action)) {
|
|
126
|
+
throw new Error(`[Feature ${featureName}] Screen "${qualifyEntityName(featureName, "screen", screenId)}" ` +
|
|
127
|
+
`(${kind}) action "${action.id}" sets rowClick: true — there is no row to ` +
|
|
128
|
+
`click on ${NO_ROW_SUBJECT[kind]}. Remove rowClick.`);
|
|
129
|
+
}
|
|
130
|
+
validateRowActionTarget(listActionContext(ctx, screenId, kind, screenEntity, `Screen "${screenId}" (${kind})`), "action", "rowAction", action);
|
|
131
|
+
}
|
|
132
|
+
export function navigateActionFromLink(id, navigate) {
|
|
133
|
+
return {
|
|
134
|
+
kind: "navigate",
|
|
135
|
+
id,
|
|
136
|
+
label: id,
|
|
137
|
+
...(navigate.screen !== undefined ? { screen: navigate.screen } : {}),
|
|
138
|
+
...(navigate.entity !== undefined ? { entity: navigate.entity } : {}),
|
|
139
|
+
...(navigate.entityId !== undefined ? { entityId: navigate.entityId } : {}),
|
|
140
|
+
...(navigate.tab !== undefined ? { tab: navigate.tab } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -24,7 +24,7 @@ type RelatedListSectionValidation = {
|
|
|
24
24
|
}>;
|
|
25
25
|
};
|
|
26
26
|
export declare function validateRelatedListSection(args: RelatedListSectionValidation): void;
|
|
27
|
-
type ListActionTargetContext = NavigateTargetLookups & {
|
|
27
|
+
export type ListActionTargetContext = NavigateTargetLookups & {
|
|
28
28
|
readonly featureName: string;
|
|
29
29
|
readonly screenId: string;
|
|
30
30
|
readonly screenType: NavigateSourceScreenType;
|
|
@@ -34,6 +34,7 @@ type ListActionTargetContext = NavigateTargetLookups & {
|
|
|
34
34
|
readonly allWriteHandlerQns: ReadonlySet<string>;
|
|
35
35
|
};
|
|
36
36
|
export declare function validateListActionTargets(ctx: ListActionTargetContext, actionLabel: "rowAction" | "emptyState action", action: RowAction): void;
|
|
37
|
+
export declare function validateRowActionTarget(ctx: ListActionTargetContext, actionLabel: "rowAction" | "emptyState action" | "action", paramsLabel: "rowAction" | "emptyState action", action: RowAction): void;
|
|
37
38
|
export declare function rejectInertColumnSortable(featureName: string, screenId: string, screenType: "entityList" | "projectionList", column: {
|
|
38
39
|
readonly field: string;
|
|
39
40
|
readonly sortable?: boolean;
|
|
@@ -2,7 +2,7 @@ import { REFERENCE_LOOKUP_SOURCES } from "../../ui-types/list-row-meta.js";
|
|
|
2
2
|
import { parseRefTarget } from "../parse-ref-target.js";
|
|
3
3
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
4
4
|
import { explicitListScreenId, normalizeListColumn, resolveNavParentScreen, } from "../screen-helpers.js";
|
|
5
|
-
import { isRowClickAction, rejectRedirectOnListRowAction, resolveRowActionNavigateTarget, validateClientActionName, validateDrawerTargetAction, validateRowActionNavigateParams, } from "./screens-actions.js";
|
|
5
|
+
import { assertWriteHandlerRegistered, isRowClickAction, rejectRedirectOnListRowAction, resolveRowActionNavigateTarget, validateClientActionName, validateDrawerTargetAction, validateRowActionNavigateParams, } from "./screens-actions.js";
|
|
6
6
|
// validateScreenHasNavArea runs once per screen; the index is built once per
|
|
7
7
|
// featureMap (immutable during boot) instead of re-scanning every feature each call.
|
|
8
8
|
const navAreaIndexByFeatureMap = new WeakMap();
|
|
@@ -115,13 +115,19 @@ function rejectEmptyStateActionParams(ctx, actionLabel, action) {
|
|
|
115
115
|
// Target checks shared by list/related-list rowActions and emptyState.action:
|
|
116
116
|
// navigate target + params, drawer target, write-handler registration.
|
|
117
117
|
export function validateListActionTargets(ctx, actionLabel, action) {
|
|
118
|
-
|
|
119
|
-
rejectRedirectOnListRowAction(featureName, screenId, screenType, action);
|
|
118
|
+
rejectRedirectOnListRowAction(ctx.featureName, ctx.screenId, ctx.screenType, action);
|
|
120
119
|
if (actionLabel === "emptyState action")
|
|
121
120
|
rejectEmptyStateActionParams(ctx, actionLabel, action);
|
|
121
|
+
validateRowActionTarget(ctx, actionLabel, actionLabel, action);
|
|
122
|
+
}
|
|
123
|
+
// Header actions have always reported navigate-params errors under the
|
|
124
|
+
// "rowAction" label, which validateRowActionNavigateParams only knows in list
|
|
125
|
+
// labels — hence paramsLabel apart from actionLabel.
|
|
126
|
+
export function validateRowActionTarget(ctx, actionLabel, paramsLabel, action) {
|
|
127
|
+
const { featureName, screenId, screenType } = ctx;
|
|
122
128
|
if (action.kind === "navigate") {
|
|
123
129
|
const target = resolveRowActionNavigateTarget(featureName, screenId, screenType, actionLabel, action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
|
|
124
|
-
validateRowActionNavigateParams(featureName, screenId, screenType, ctx.screenEntity, action, target,
|
|
130
|
+
validateRowActionNavigateParams(featureName, screenId, screenType, ctx.screenEntity, action, target, paramsLabel);
|
|
125
131
|
}
|
|
126
132
|
else if (action.kind === "drawer") {
|
|
127
133
|
validateDrawerTargetAction(featureName, screenId, screenType, actionLabel, action, ctx.screens);
|
|
@@ -129,11 +135,8 @@ export function validateListActionTargets(ctx, actionLabel, action) {
|
|
|
129
135
|
else if (action.kind === "clientAction") {
|
|
130
136
|
validateClientActionName(featureName, screenId, screenType, actionLabel, action);
|
|
131
137
|
}
|
|
132
|
-
else
|
|
133
|
-
|
|
134
|
-
`${actionLabel} "${action.id}" handler "${action.handler}" ` +
|
|
135
|
-
`is not a registered write-handler. Check the QN spelling (expected ` +
|
|
136
|
-
`"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
138
|
+
else {
|
|
139
|
+
assertWriteHandlerRegistered(featureName, `${ctx.where} ${actionLabel} "${action.id}"`, action.handler, ctx.allWriteHandlerQns);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
function validateRelatedListRowActions(args, rowActions) {
|
|
@@ -258,24 +261,23 @@ export function assertRefTargetRegistered(prefix, subject, refTarget, currentFea
|
|
|
258
261
|
.join(", ") || "(none)"}.`);
|
|
259
262
|
}
|
|
260
263
|
}
|
|
261
|
-
// Facets
|
|
264
|
+
// Facets — unlike filter, a field inventory IS available here: the
|
|
262
265
|
// declared columns. A facet on a field with no column is almost always a
|
|
263
266
|
// typo (the user never sees the field anywhere), so this is hard-checked
|
|
264
|
-
// rather than just documented. Shared by projectionList and relatedList
|
|
265
|
-
//
|
|
267
|
+
// rather than just documented. Shared by projectionList and relatedList —
|
|
268
|
+
// `prefix` carries the caller's own screen/section message lead-in.
|
|
266
269
|
//
|
|
267
|
-
// `type: "reference"` is exempt from the same-name-column rule
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
// matching column is still almost always a typo.
|
|
270
|
+
// `type: "reference"` is exempt from the same-name-column rule: it filters
|
|
271
|
+
// by an id field (e.g. "propertyId") but displays a different,
|
|
272
|
+
// human-readable column (e.g. "propertyLabel") — a column named after the
|
|
273
|
+
// filter field would almost never exist, and requiring one anyway would
|
|
274
|
+
// force apps to add a column no design calls for just to satisfy this check.
|
|
275
|
+
// Its own `entity` is mandatory on the type (TS, not just this validator) and
|
|
276
|
+
// is resolved below — that explicit, checked declaration is this facet type's
|
|
277
|
+
// field inventory, filling the same "not just a typo" role the column-name
|
|
278
|
+
// check plays for select/boolean facets. select/boolean facets keep the
|
|
279
|
+
// column requirement unchanged: both filter and display the same field, so a
|
|
280
|
+
// facet with no matching column is still almost always a typo.
|
|
279
281
|
export function validateListFacets(prefix, facets, columns, currentFeatureName, featureMap) {
|
|
280
282
|
const columnFieldNames = new Set(columns.map((col) => normalizeListColumn(col).field));
|
|
281
283
|
const seenFacetFields = new Set();
|
|
@@ -10,8 +10,6 @@ export declare function validateScreens(feature: FeatureDefinition, featureMap:
|
|
|
10
10
|
readonly featureName: string;
|
|
11
11
|
readonly screen: ScreenDefinition;
|
|
12
12
|
}>): void;
|
|
13
|
-
export declare function findEntityFeature(entityName: string, featureMap: ReadonlyMap<string, FeatureDefinition>): string | undefined;
|
|
14
|
-
export declare function buildUnknownFieldMessage(featureName: string, screenId: string, fieldName: string, entityName: string, knownFields: ReadonlySet<string>): string;
|
|
15
13
|
export declare function collectScreenQns(features: readonly FeatureDefinition[]): Set<string>;
|
|
16
14
|
export declare function screenShortIdsFrom(allScreenQns: ReadonlySet<string>): Set<string>;
|
|
17
15
|
export declare function collectScreensByShortId(features: readonly FeatureDefinition[]): Map<string, ReadonlyArray<{
|