@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 +9 -1
- 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/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- 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/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- 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 +206 -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 +299 -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 +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- 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.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- 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 +101 -0
- 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.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -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 +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- 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-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- 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 +235 -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 +56 -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 +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.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 +61 -2
- 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 +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- 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 +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -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/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 +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- 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 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- 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 +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
|
@@ -43,8 +43,9 @@ const WRITE_ONLY_FORBIDDEN_FLAGS = [
|
|
|
43
43
|
"filterable",
|
|
44
44
|
"lookupable",
|
|
45
45
|
];
|
|
46
|
-
function validateWriteOnlyField(
|
|
47
|
-
const
|
|
46
|
+
function validateWriteOnlyField(input) {
|
|
47
|
+
const { field } = input;
|
|
48
|
+
const where = fieldLocation(input);
|
|
48
49
|
if (field.type === "embedded") {
|
|
49
50
|
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
50
51
|
if ("writeOnly" in subField && subField.writeOnly === true) {
|
|
@@ -82,8 +83,12 @@ const JSONB_PERSONAL_FORBIDDEN_FLAGS = [
|
|
|
82
83
|
"lookupable",
|
|
83
84
|
];
|
|
84
85
|
const CUSTOM_FIELDS_COLUMN = "customFields";
|
|
85
|
-
function validatePersonalJsonbField(
|
|
86
|
-
const
|
|
86
|
+
function validatePersonalJsonbField(input) {
|
|
87
|
+
const { entity, fieldName, field } = input;
|
|
88
|
+
// skip: only personal jsonb fields are stored as a ciphertext scalar
|
|
89
|
+
if (field.type !== "jsonb" || input.subjectAnnotationCount === 0)
|
|
90
|
+
return;
|
|
91
|
+
const where = fieldLocation(input);
|
|
87
92
|
const reason = "a personal jsonb field is stored as a ciphertext string, so nothing inside it can be indexed, filtered, sorted, searched or merged";
|
|
88
93
|
const flags = field; // @cast-boundary schema-walk
|
|
89
94
|
for (const flag of JSONB_PERSONAL_FORBIDDEN_FLAGS) {
|
|
@@ -125,156 +130,229 @@ function validatePersonalJsonbField(featureName, entityName, entity, fieldName,
|
|
|
125
130
|
// so schema drift is detected during boot.
|
|
126
131
|
export function validatePiiAndRetention(feature) {
|
|
127
132
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
133
|
+
validateFieldAnnotations(feature.name, entityName, entity);
|
|
134
|
+
validateEntityRetention(feature.name, entityName, entity);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Order matters: the first failing rule decides the boot error, and warnings
|
|
138
|
+
// are emitted in this order.
|
|
139
|
+
const FIELD_RULES = [
|
|
140
|
+
rejectMultipleSubjectAnnotations,
|
|
141
|
+
rejectTextWithoutPersonalStance,
|
|
142
|
+
rejectInvalidLookupable,
|
|
143
|
+
rejectSensitiveWithoutCiphertextAtRest,
|
|
144
|
+
validateWriteOnlyField,
|
|
145
|
+
rejectCiphertextSortOrSensitiveSearch,
|
|
146
|
+
validatePersonalJsonbField,
|
|
147
|
+
validatePersonalOfOwner,
|
|
148
|
+
warnOnUnannotatedPersonalName,
|
|
149
|
+
];
|
|
150
|
+
function validateFieldAnnotations(featureName, entityName, entity) {
|
|
151
|
+
const personalOfOwnerFieldNames = ownerFieldNamesReferencedByPersonalOf(entity.fields);
|
|
152
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
153
|
+
const flags = field; // @cast-boundary schema-walk
|
|
154
|
+
const input = {
|
|
155
|
+
featureName,
|
|
156
|
+
entityName,
|
|
157
|
+
entity,
|
|
158
|
+
fieldName,
|
|
159
|
+
field,
|
|
160
|
+
flags,
|
|
161
|
+
subjectAnnotationCount: countSubjectAnnotations(flags),
|
|
162
|
+
personalOfOwnerFieldNames,
|
|
163
|
+
};
|
|
164
|
+
for (const rule of FIELD_RULES)
|
|
165
|
+
rule(input);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function countSubjectAnnotations(flags) {
|
|
169
|
+
return [flags.pii, flags.userOwned, flags.tenantOwned, flags.recordOwned].filter(Boolean).length;
|
|
170
|
+
}
|
|
171
|
+
function fieldLocation(input) {
|
|
172
|
+
return `[Feature ${input.featureName}] Field "${input.fieldName}" on entity "${input.entityName}"`;
|
|
173
|
+
}
|
|
174
|
+
function rejectMultipleSubjectAnnotations(input) {
|
|
175
|
+
if (input.subjectAnnotationCount > 1) {
|
|
176
|
+
throw new Error(`${fieldLocation(input)} has multiple subject-key annotations (personal: "self" / "tenant" / { of: "<field>" }). Pick one — each field belongs to exactly one subject.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function rejectTextWithoutPersonalStance(input) {
|
|
180
|
+
const { field, flags } = input;
|
|
181
|
+
if ((field.type === "text" || field.type === "longText") &&
|
|
182
|
+
input.subjectAnnotationCount === 0 &&
|
|
183
|
+
!flags.allowPlaintext &&
|
|
184
|
+
!flags.subjectRef) {
|
|
185
|
+
throw new Error(`${fieldLocation(input)} (type "${field.type}") declares no personal stance. Build it with createTextField / createLongTextField and { personal: "self" | "tenant" | "ref" | { of: "<ownerField>" } | false }; false additionally needs { reason: "..." } stating why the value is not personal data.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// lookupable: only text fields with a subject annotation are supported. Without
|
|
189
|
+
// a subject, the value remains plaintext, so a blind index would be a
|
|
190
|
+
// second deterministic copy without any benefit.
|
|
191
|
+
function rejectInvalidLookupable(input) {
|
|
192
|
+
// skip: only lookupable fields carry a blind index to validate
|
|
193
|
+
if (input.flags.lookupable !== true)
|
|
194
|
+
return;
|
|
195
|
+
if (input.field.type !== "text") {
|
|
196
|
+
throw new Error(`${fieldLocation(input)} declares find: "exact" (or "fuzzy") but has type "${input.field.type}" — blind-index equality lookups only apply to text fields. Use a text field or set find: "none".`);
|
|
197
|
+
}
|
|
198
|
+
if (input.subjectAnnotationCount === 0) {
|
|
199
|
+
throw new Error(`${fieldLocation(input)} declares find: "exact" (or "fuzzy") without a subject annotation (personal: "self" / "tenant" / { of: "<field>" }). Plaintext fields don't need a blind index — add personal or set find: "none".`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Sensitive fields are stored as table ciphertext in the event log; without
|
|
203
|
+
// ciphertext at rest, appending the event would write plaintext to the
|
|
204
|
+
// immutable history.
|
|
205
|
+
function rejectSensitiveWithoutCiphertextAtRest(input) {
|
|
206
|
+
const { flags } = input;
|
|
207
|
+
if (flags.sensitive === true && input.subjectAnnotationCount === 0 && flags.encrypted !== true) {
|
|
208
|
+
throw new Error(`${fieldLocation(input)} declares { sensitive: true } without ciphertext-at-rest. Since #967 the event log stores sensitive fields as table ciphertext — add a subject annotation (personal: "self" / "tenant" / { of: "<field>" }) or { encrypted: true }.`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Sorting reads the projection column — that stays ciphertext, so
|
|
212
|
+
// sortable + subject annotation stays a boot-fail. searchable is allowed:
|
|
213
|
+
// the search consumer decrypts into the derived index and forget purges
|
|
214
|
+
// those docs (see createSearchEventConsumer). sensitive + searchable stays
|
|
215
|
+
// forbidden (never searchable).
|
|
216
|
+
function rejectCiphertextSortOrSensitiveSearch(input) {
|
|
217
|
+
const { flags } = input;
|
|
218
|
+
if (input.subjectAnnotationCount > 0 && flags.sortable === true) {
|
|
219
|
+
throw new Error(`${fieldLocation(input)} combines a subject-key annotation with { sortable: true } — sorting reads the projection column, which is ciphertext at rest. For equality lookups use find: "exact"; drop sortable or keep the field plaintext (personal: false).`);
|
|
220
|
+
}
|
|
221
|
+
if (flags.sensitive === true && flags.searchable === true) {
|
|
222
|
+
throw new Error(`${fieldLocation(input)} combines { sensitive: true } with { searchable: true } — sensitive means ciphertext at rest, never searchable, stripped from event echoes — authorized readers can still read it back (passwords, tokens, tax IDs). Subject-annotated identity fields may be searchable (#1610); sensitive fields may not. To never return the value at all, add { writeOnly: true }.`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function validatePersonalOfOwner(input) {
|
|
226
|
+
const personalOf = input.flags.userOwned;
|
|
227
|
+
// skip: only personal: { of } fields reference an owner field
|
|
228
|
+
if (!personalOf)
|
|
229
|
+
return;
|
|
230
|
+
const ownerName = personalOf.ownerField;
|
|
231
|
+
if (!ownerName || typeof ownerName !== "string") {
|
|
232
|
+
throw new Error(`${fieldLocation(input)} has personal: { of: ... } without an owner field name. Pass the name of the owner field: personal: { of: "<ownerField>" }.`);
|
|
233
|
+
}
|
|
234
|
+
const ownerField = requireOwnerField(input, ownerName);
|
|
235
|
+
rejectOwnerWithoutRowFallback(input, ownerName, ownerField, personalOf);
|
|
236
|
+
warnOnNonUserOwnerReference(input, ownerName, ownerField);
|
|
237
|
+
}
|
|
238
|
+
function requireOwnerField(input, ownerName) {
|
|
239
|
+
const fieldsByName = input.entity.fields;
|
|
240
|
+
const ownerField = fieldsByName[ownerName];
|
|
241
|
+
if (!ownerField) {
|
|
242
|
+
const known = Object.keys(fieldsByName).sort().join(", ");
|
|
243
|
+
throw new Error(`${fieldLocation(input)} references personal.of "${ownerName}" but no such field exists. Known fields: ${known}`);
|
|
244
|
+
}
|
|
245
|
+
// Text is accepted alongside reference: the ES-framework carries
|
|
246
|
+
// user ids as plain text columns throughout the bundled features
|
|
247
|
+
// (user-session.userId, invitation.invitedBy) — there is no
|
|
248
|
+
// relational FK to point a reference at. Self-referencing
|
|
249
|
+
// ownerField (the field's own value IS the owner id) rides on this.
|
|
250
|
+
if (ownerField.type !== "reference" && ownerField.type !== "text") {
|
|
251
|
+
throw new Error(`[Feature ${input.featureName}] personal.of "${ownerName}" on entity "${input.entityName}" must be a reference or text (userId) field, got type "${ownerField.type}"`);
|
|
252
|
+
}
|
|
253
|
+
return ownerField;
|
|
254
|
+
}
|
|
255
|
+
function rejectOwnerWithoutRowFallback(input, ownerName, ownerField, personalOf) {
|
|
256
|
+
const { whenAbsent } = personalOf;
|
|
257
|
+
if (whenAbsent !== undefined && whenAbsent !== "tenant" && whenAbsent !== "self") {
|
|
258
|
+
throw new Error(`${fieldLocation(input)} declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" or "self" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant" (tenant key) or "self" (the row's own record key).`);
|
|
259
|
+
}
|
|
260
|
+
if (ownerField.required !== true && whenAbsent === undefined) {
|
|
261
|
+
throw new Error(`${fieldLocation(input)} is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, whenAbsent: "self" to encrypt them under the row's own record key (shreddable per record), or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Custom subject entities (such as HR employees or patients) are allowed but
|
|
265
|
+
// should be an intentional choice.
|
|
266
|
+
function warnOnNonUserOwnerReference(input, ownerName, ownerField) {
|
|
267
|
+
// skip: text owner fields carry a user id by convention, there is no target to check
|
|
268
|
+
if (ownerField.type !== "reference")
|
|
269
|
+
return;
|
|
270
|
+
const refTarget = ownerField.entity;
|
|
271
|
+
const targetEntity = refTarget.includes(":") ? refTarget.split(":")[1] : refTarget;
|
|
272
|
+
if (targetEntity !== "user") {
|
|
273
|
+
log.warn(`[Feature ${input.featureName}] personal.of "${ownerName}" on entity "${input.entityName}" targets reference "${refTarget}" — typically should be a user reference. If intentional (custom subject-entity like employee/patient), ignore.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// First match wins. Business data with person-like names (for example,
|
|
277
|
+
// company.legalName) can produce false positives; authors can suppress them
|
|
278
|
+
// with { personal: false, reason: "is_business_data" }.
|
|
279
|
+
const UNANNOTATED_NAME_WARNINGS = [
|
|
280
|
+
{
|
|
281
|
+
applies: (input) => FILE_FIELD_TYPES.has(input.field.type),
|
|
282
|
+
message: (input) => `[Feature ${input.featureName}] File field "${input.fieldName}" on entity "${input.entityName}" has no personal annotation. A forget with strategy "delete" then only severs the uploader link and keeps the binary. If the file holds personal data, mark it { personal: "self" } or { personal: { of: "<ownerField>" } }. If it is business data, set { personal: false, reason: "is_business_data" } to silence.`,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
applies: (_input, lowerName) => PII_DIRECT_NAME_HINTS.has(lowerName),
|
|
286
|
+
message: (input) => `${fieldLocation(input)} has a PII-typical name but no personal annotation. If this is PII, mark it { personal: "self", find: ... }. If business data, set { personal: false, reason: "is_business_data" } to silence.`,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
applies: (_input, lowerName) => PII_USER_OWNED_NAME_HINTS.has(lowerName),
|
|
290
|
+
message: (input) => `${fieldLocation(input)} has a user-content-typical name but no personal annotation. If this contains user-generated content, mark it { personal: { of: "<authorIdField>" }, find: ... }. If business data, set { personal: false, reason: "..." } to silence.`,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
applies: (input, lowerName) => PII_USER_REFERENCE_NAME_HINTS.has(lowerName) &&
|
|
294
|
+
!input.flags.subjectRef &&
|
|
295
|
+
!input.personalOfOwnerFieldNames.has(input.fieldName),
|
|
296
|
+
message: (input) => `${fieldLocation(input)} has a user-reference-typical name but no personal annotation — a foreign key into \`user\` carries Art.17 obligations even with no annotated content on the entity. Mark it { personal: "ref" } AND register r.useExtension(EXT_USER_DATA, "${input.entityName}", …) for Art.17 coverage — this warning is the only boot-time check for that, registering the hook is not enforced. Or { personal: { of: "${input.fieldName}" } } on the field it owns. If business data, set { personal: false, reason: "..." } to silence.`,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
function warnOnUnannotatedPersonalName(input) {
|
|
300
|
+
// skip: an annotation or an explicit plaintext reason already states the field's stance
|
|
301
|
+
if (input.subjectAnnotationCount > 0 || input.flags.allowPlaintext)
|
|
302
|
+
return;
|
|
303
|
+
const lowerName = input.fieldName.toLowerCase();
|
|
304
|
+
const warning = UNANNOTATED_NAME_WARNINGS.find((candidate) => candidate.applies(input, lowerName));
|
|
305
|
+
if (warning)
|
|
306
|
+
log.warn(warning.message(input));
|
|
307
|
+
}
|
|
308
|
+
const RETENTION_RULES = [
|
|
309
|
+
warnOnInvalidKeepFor,
|
|
310
|
+
rejectUnknownRetentionReference,
|
|
311
|
+
warnOnBlockDeleteWithoutAnonymize,
|
|
312
|
+
];
|
|
313
|
+
function validateEntityRetention(featureName, entityName, entity) {
|
|
314
|
+
const { retention } = entity;
|
|
315
|
+
// skip: entities without a retention policy have nothing to validate
|
|
316
|
+
if (!retention)
|
|
317
|
+
return;
|
|
318
|
+
const input = {
|
|
319
|
+
featureName,
|
|
320
|
+
entityName,
|
|
321
|
+
fieldsByName: entity.fields,
|
|
322
|
+
retention,
|
|
323
|
+
};
|
|
324
|
+
for (const rule of RETENTION_RULES)
|
|
325
|
+
rule(input);
|
|
326
|
+
}
|
|
327
|
+
function warnOnInvalidKeepFor({ featureName, entityName, retention }) {
|
|
328
|
+
if (!KEEP_FOR_PATTERN.test(retention.keepFor)) {
|
|
329
|
+
log.warn(`[Feature ${featureName}] Entity "${entityName}" retention.keepFor="${retention.keepFor}" has an invalid format. Expected: <number><h|d|w|m|y> (e.g. "30d", "10y", "6m"). The cleanup job cannot parse anything else; fix keepFor.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function rejectUnknownRetentionReference(input) {
|
|
333
|
+
const { featureName, entityName, fieldsByName } = input;
|
|
334
|
+
const refName = input.retention.reference;
|
|
335
|
+
// skip: no explicit retention.reference to resolve
|
|
336
|
+
if (refName === undefined)
|
|
337
|
+
return;
|
|
338
|
+
if (!fieldsByName[refName] && !FRAMEWORK_TIMESTAMP_FIELDS.has(refName)) {
|
|
339
|
+
const known = Object.keys(fieldsByName).sort().join(", ");
|
|
340
|
+
const framework = [...FRAMEWORK_TIMESTAMP_FIELDS].sort().join(", ");
|
|
341
|
+
throw new Error(`[Feature ${featureName}] Entity "${entityName}" retention.reference "${refName}" does not exist. Known fields: ${known} — framework-managed timestamps also accepted: ${framework}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// blockDelete with no field that can carry `anonymize` is fine;
|
|
345
|
+
// subjectRef-only entities are covered by the V3 EXT_USER_DATA guard instead.
|
|
346
|
+
function warnOnBlockDeleteWithoutAnonymize(input) {
|
|
347
|
+
const { featureName, entityName, fieldsByName } = input;
|
|
348
|
+
// skip: only blockDelete relies on anonymize once keepFor expires
|
|
349
|
+
if (input.retention.strategy !== "blockDelete")
|
|
350
|
+
return;
|
|
351
|
+
const hasAnonymize = Object.values(fieldsByName).some((f) => {
|
|
352
|
+
const a = f; // @cast-boundary schema-walk
|
|
353
|
+
return Boolean(a.anonymize);
|
|
354
|
+
});
|
|
355
|
+
if (entityHasAnonymizableSubjectField(fieldsByName) && !hasAnonymize) {
|
|
356
|
+
log.warn(`[Feature ${featureName}] Entity "${entityName}" retention.strategy="blockDelete" but no field has an anonymize-function. Once keepFor expires, the data-retention cron anonymizes nothing and the row keeps its PII. Add { anonymize: () => null } or () => "[ANONYMIZED]" to PII fields.`);
|
|
279
357
|
}
|
|
280
358
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FeatureDefinition, QueryHandlerDef } from "../types/index.ts";
|
|
2
2
|
export declare function buildQueryHandlerMap(features: readonly FeatureDefinition[]): ReadonlyMap<string, QueryHandlerDef>;
|
|
3
3
|
export declare function validateProjectionListScreens(features: readonly FeatureDefinition[]): void;
|
|
4
4
|
export declare function validateRelatedListSectionQueries(features: readonly FeatureDefinition[]): void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isListQueryWithoutSearchableFields, resolveEntityBehindListQuery, } from "../entity-list-searchability.js";
|
|
2
2
|
import { QnTypes, qualifyEntityName } from "../qualified-name.js";
|
|
3
3
|
import { isEditLayoutScreen } from "../screen-helpers.js";
|
|
4
|
-
import { relatedListGroupHeaderLabel, relatedListGroupKey, } from "../types/index.js";
|
|
5
4
|
import { SEARCHABLE_FALSE_WHITELIST } from "./entity-list-screens.js";
|
|
5
|
+
import { validateListTableAggregateQuery } from "./list-table-aggregate-query.js";
|
|
6
6
|
import { findEntity } from "./parent-ref.js";
|
|
7
7
|
import { getZodObjectShape } from "./zod-shape.js";
|
|
8
8
|
// Sibling to entity-list-screens.ts rather than an extension of it:
|
|
@@ -66,6 +66,7 @@ function validateOneProjectionListScreen(feature, screen, queryHandlers, feature
|
|
|
66
66
|
}
|
|
67
67
|
validateProjectionListFilterSchemaAcceptance(prefix, screen, schema);
|
|
68
68
|
validateProjectionListFacetsSchemaAcceptance(prefix, screen, schema);
|
|
69
|
+
validateListTableAggregateQuery({ prefix, ...screen, handler, featureMap });
|
|
69
70
|
}
|
|
70
71
|
// filter sends `payload.filter` — same 422 footgun as facets below
|
|
71
72
|
// if the handler's schema doesn't accept it.
|
|
@@ -190,17 +191,6 @@ function validateRelatedListParentFilter(prefix, section, schema, handler, featu
|
|
|
190
191
|
`entity behind query "${section.query}". Known fields: ${Object.keys(entity.fields).sort().join(", ")}.`);
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
|
-
// A collapsed group without a header has no toggle, so its rows could never be opened.
|
|
194
|
-
function validateRelatedListGroupBy(prefix, section) {
|
|
195
|
-
const groupBy = section.groupBy;
|
|
196
|
-
const collapsedKey = groupBy?.collapsedWhen === undefined ? undefined : relatedListGroupKey(groupBy.collapsedWhen);
|
|
197
|
-
if (groupBy !== undefined &&
|
|
198
|
-
collapsedKey !== undefined &&
|
|
199
|
-
relatedListGroupHeaderLabel(groupBy, collapsedKey) === undefined) {
|
|
200
|
-
throw new Error(`${prefix}: groupBy.collapsedWhen "${collapsedKey}" names a group without a header; ` +
|
|
201
|
-
`set groupBy.label or groupBy.labels["${collapsedKey}"], or drop collapsedWhen.`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
194
|
// relatedList sections declare search/facets the same way a projectionList
|
|
205
195
|
// screen does, and hit the same 422 footgun: definePagedQueryHandler doesn't
|
|
206
196
|
// auto-merge params into the handler's own Zod schema, so an
|
|
@@ -217,7 +207,7 @@ export function validateRelatedListSectionQueries(features) {
|
|
|
217
207
|
validateRelatedListSearchable(prefix, section, schema);
|
|
218
208
|
validateRelatedListFacets(prefix, section, schema);
|
|
219
209
|
validateRelatedListParentFilter(prefix, section, schema, handler, featureMap);
|
|
220
|
-
|
|
210
|
+
validateListTableAggregateQuery({ prefix, ...section, handler, featureMap });
|
|
221
211
|
};
|
|
222
212
|
for (const feature of features) {
|
|
223
213
|
for (const screen of Object.values(feature.screens)) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
const EXPANDABLE_ROW_TABLE_KEYS = new Set(["grouping"]);
|
|
3
|
+
const EXPANDABLE_ROW_REJECTED_GROUPING_KEYS = ["userChoice", "fields"];
|
|
4
|
+
// `groupBy` grouped the loaded page on the client; it is gone, and a screen
|
|
5
|
+
// still carrying it fails at boot instead of silently losing its groups.
|
|
6
|
+
function rejectRemovedGroupBy(prefix, section) {
|
|
7
|
+
if (Object.hasOwn(section, "groupBy")) {
|
|
8
|
+
throw new Error(`${prefix} uses groupBy, which was removed. Use table.grouping: { field, label?, labels?, collapsedWhen? }; ` +
|
|
9
|
+
`groups and counts come from the server, so the bound query must accept aggregate.`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function rejectExpandableRowTableOptions(prefix, table) {
|
|
13
|
+
if (isPlainObject(table)) {
|
|
14
|
+
rejectUnsupportedTableKeys(prefix, table);
|
|
15
|
+
rejectGroupingChoice(prefix, table["grouping"]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function rejectUnsupportedTableKeys(prefix, table) {
|
|
19
|
+
const unsupported = Object.keys(table).filter((key) => !EXPANDABLE_ROW_TABLE_KEYS.has(key));
|
|
20
|
+
if (unsupported.length > 0) {
|
|
21
|
+
throw new Error(`${prefix} supports only table.grouping, not table.${unsupported.join(", table.")}. ` +
|
|
22
|
+
`Put the other table options on the entityList screen itself.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function rejectGroupingChoice(prefix, grouping) {
|
|
26
|
+
const choiceKey = isPlainObject(grouping)
|
|
27
|
+
? EXPANDABLE_ROW_REJECTED_GROUPING_KEYS.find((key) => Object.hasOwn(grouping, key))
|
|
28
|
+
: undefined;
|
|
29
|
+
if (choiceKey !== undefined) {
|
|
30
|
+
throw new Error(`${prefix} does not support table.grouping.${choiceKey}: an expanded row has no view menu. ` +
|
|
31
|
+
`Set a fixed table.grouping.field.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Grouping rules of a relatedList section or entityList.expandableRow, before the shared table checks. */
|
|
35
|
+
export function validateRelatedListGroupingRules(prefix, section, isExpandableRow) {
|
|
36
|
+
rejectRemovedGroupBy(prefix, section);
|
|
37
|
+
if (isExpandableRow && Object.hasOwn(section, "bulkActions")) {
|
|
38
|
+
throw new Error(`${prefix} does not support bulkActions. Put bulkActions on the entityList screen itself.`);
|
|
39
|
+
}
|
|
40
|
+
if (isExpandableRow)
|
|
41
|
+
rejectExpandableRowTableOptions(prefix, section.table);
|
|
42
|
+
}
|
|
@@ -64,4 +64,5 @@ export type NavigateTargetLookups = {
|
|
|
64
64
|
}>;
|
|
65
65
|
};
|
|
66
66
|
export declare function validateNavigateActionTab(featureName: string, screenId: string, screenType: NavigateSourceScreenType, actionLabel: string, action: RowAction, lookups: NavigateTargetLookups): void;
|
|
67
|
+
export declare function assertWriteHandlerRegistered(featureName: string, subject: string, handler: string, allWriteHandlerQns: ReadonlySet<string>): void;
|
|
67
68
|
export {};
|
|
@@ -202,7 +202,7 @@ export function validateScreenNavTarget(featureName, screenId, screenKind, field
|
|
|
202
202
|
}
|
|
203
203
|
// kind:"drawer" resolves same-feature only — unlike navigate/redirect,
|
|
204
204
|
// which the runtime router resolves app-wide (see the comment on
|
|
205
|
-
//
|
|
205
|
+
// buildScreenValidationContext in screens.ts), the drawer mounts the target inline using this
|
|
206
206
|
// feature's schema, so a cross-feature reference could never actually
|
|
207
207
|
// render. Two distinct failure messages: dangling reference vs. wrong
|
|
208
208
|
// screen type. Shared by ToolbarAction and RowAction drawer variants —
|
|
@@ -485,3 +485,10 @@ export function validateNavigateActionTab(featureName, screenId, screenType, act
|
|
|
485
485
|
return;
|
|
486
486
|
resolveRowActionNavigateTarget(featureName, screenId, screenType, actionLabel, action, lookups.allScreenQns, lookups.navTargetShortIds, lookups.screensByShortId, lookups.detailForScreens);
|
|
487
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,7 +2,7 @@ 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
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
|
|
@@ -208,11 +208,7 @@ function validateWriteHandlerRegistered(featureName, screenId, context, handler,
|
|
|
208
208
|
if (!handler || typeof handler !== "string") {
|
|
209
209
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) has empty or non-string handler. Set handler to a non-empty handler name.`);
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) handler "${handler}" ` +
|
|
213
|
-
`is not a registered write-handler. Check the QN spelling (expected ` +
|
|
214
|
-
`"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
215
|
-
}
|
|
211
|
+
assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (${context})`, handler, allWriteHandlerQns);
|
|
216
212
|
}
|
|
217
213
|
// Every field entry must carry a `type` discriminator. An author typo
|
|
218
214
|
// (`title: { required: true }` without a type) would otherwise let
|
|
@@ -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;
|