@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
package/dist/db/eagerload.js
CHANGED
|
@@ -24,12 +24,15 @@ import { filterReadFields } from "../engine/field-access.js";
|
|
|
24
24
|
import { buildOwnershipClause, combineClauses, shiftParams, tableNameOf, } from "../engine/ownership.js";
|
|
25
25
|
import { parseRefTargetEntityName } from "../engine/parse-ref-target.js";
|
|
26
26
|
import { isUuid, SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
27
|
+
import { scrubLogValue } from "../logging/scrub.js";
|
|
28
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
27
29
|
import { collectEncryptedFieldNames, decryptEntityFieldValues, resolveEntityFieldEncryption, } from "./entity-field-encryption.js";
|
|
28
30
|
import { buildParentRefClause } from "./parent-ref-clause.js";
|
|
29
31
|
import { toPostgresArrayLiteral } from "./queries/postgres-array-literal.js";
|
|
30
32
|
import { executeRawQueryRead } from "./queries/raw-sql.js";
|
|
31
33
|
import { buildEntityTable, physicalColumnName } from "./table-builder.js";
|
|
32
34
|
import { tenantDbRunner } from "./tenant-db-runner.js";
|
|
35
|
+
const log = createFallbackLogger("eagerload");
|
|
33
36
|
function isReferenceField(field) {
|
|
34
37
|
return field.type === "reference";
|
|
35
38
|
}
|
|
@@ -105,7 +108,11 @@ async function buildRefLookupMap(rawRefRows, refEntity, refEntityName, viewer, p
|
|
|
105
108
|
decrypted = await decryptReferencedRow(r, piiFields, encryptedFields, kms);
|
|
106
109
|
}
|
|
107
110
|
catch (e) {
|
|
108
|
-
|
|
111
|
+
log.warn("failed to decrypt referenced row", {
|
|
112
|
+
entity: refEntityName,
|
|
113
|
+
id: String(r["id"]),
|
|
114
|
+
err: scrubLogValue(e),
|
|
115
|
+
});
|
|
109
116
|
continue;
|
|
110
117
|
}
|
|
111
118
|
const id = decrypted["id"];
|
|
@@ -126,26 +133,26 @@ function collectRefIds(rows, rf) {
|
|
|
126
133
|
}
|
|
127
134
|
return ids;
|
|
128
135
|
}
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
136
|
+
/** Eagerloads a list of rows without mutating them; returns shallow copies
|
|
137
|
+
* with an added `_refs` property. */
|
|
131
138
|
export async function enrichWithReferences(rows, entity, resolveEntity, db, viewer) {
|
|
132
139
|
const refFields = collectReferenceFields(entity);
|
|
133
140
|
if (refFields.length === 0 || rows.length === 0) {
|
|
134
141
|
return rows.map((r) => ({ ...r }));
|
|
135
142
|
}
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
143
|
+
// For each reference field, collect IDs in a deduplicated Set, then run one
|
|
144
|
+
// SELECT WHERE id IN (...). Build the maps in parallel so the lookups do
|
|
145
|
+
// not run serially (Promise.all).
|
|
139
146
|
const lookupMaps = await Promise.all(refFields.map(async (rf) => {
|
|
140
147
|
const ids = collectRefIds(rows, rf);
|
|
141
148
|
if (ids.size === 0)
|
|
142
149
|
return { fieldName: rf.fieldName, multiple: rf.multiple, map: new Map() };
|
|
143
150
|
const refEntity = resolveEntity(rf.refEntityName);
|
|
144
151
|
if (refEntity === undefined) {
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// UUID
|
|
152
|
+
// Authoring error or race condition (the entity was renamed without a
|
|
153
|
+
// registry reload). The boot validator normally catches this;
|
|
154
|
+
// as a runtime safeguard, an empty map makes the renderer fall back
|
|
155
|
+
// to the UUID instead of crashing.
|
|
149
156
|
return { fieldName: rf.fieldName, multiple: rf.multiple, map: new Map() };
|
|
150
157
|
}
|
|
151
158
|
const refTable = buildEntityTable(rf.refEntityName, refEntity);
|
|
@@ -159,7 +166,9 @@ export async function enrichWithReferences(rows, entity, resolveEntity, db, view
|
|
|
159
166
|
const scope = await resolveRefAccessScope(refEntity, refTable, refTableName, idArray, db, viewer);
|
|
160
167
|
if (scope.kind === "none")
|
|
161
168
|
return emptyLookup;
|
|
162
|
-
const lookedUpRows =
|
|
169
|
+
const lookedUpRows = await selectMany(db, refTable, {
|
|
170
|
+
id: idArray,
|
|
171
|
+
});
|
|
163
172
|
const rawRefRows = scope.kind === "restricted"
|
|
164
173
|
? lookedUpRows.filter((r) => scope.readableIds.has(String(r["id"])))
|
|
165
174
|
: lookedUpRows;
|
|
@@ -190,7 +199,7 @@ export async function enrichWithReferences(rows, entity, resolveEntity, db, view
|
|
|
190
199
|
return { ...row, _refs: refs };
|
|
191
200
|
});
|
|
192
201
|
}
|
|
193
|
-
/** Single-
|
|
202
|
+
/** Single-row variant for detail calls. */
|
|
194
203
|
export async function enrichRowWithReferences(row, entity, resolveEntity, db, viewer) {
|
|
195
204
|
const enriched = await enrichWithReferences([row], entity, resolveEntity, db, viewer);
|
|
196
205
|
return enriched[0] ?? { ...row, _refs: {} };
|
|
@@ -3,6 +3,7 @@ import { type Logger } from "../logging/index.ts";
|
|
|
3
3
|
import type { EnvelopeCipher } from "../secrets/envelope-cipher.ts";
|
|
4
4
|
export declare function collectEncryptedFieldNames(entity: EntityDefinition): ReadonlySet<string>;
|
|
5
5
|
export declare function isSensitiveLabelField(entity: EntityDefinition, field: string): boolean;
|
|
6
|
+
export declare function holdsCiphertextOrSecret(entity: EntityDefinition, field: string): boolean;
|
|
6
7
|
export declare function isListFilterAllowed(entity: EntityDefinition, field: string, filter: {
|
|
7
8
|
readonly op: string;
|
|
8
9
|
readonly value: unknown;
|
|
@@ -24,7 +24,7 @@ export function isSensitiveLabelField(entity, field) {
|
|
|
24
24
|
}
|
|
25
25
|
// A `sensitive` (find: "secret") field is never comparable either, even while its value is still
|
|
26
26
|
// legacy plaintext.
|
|
27
|
-
function holdsCiphertextOrSecret(entity, field) {
|
|
27
|
+
export function holdsCiphertextOrSecret(entity, field) {
|
|
28
28
|
const def = entity.fields[field];
|
|
29
29
|
if (def === undefined)
|
|
30
30
|
return false;
|
|
@@ -11,7 +11,7 @@ export declare function resolveTableName(entityName: string, entity: EntityDefin
|
|
|
11
11
|
* (convention: `store_*`) — or build columns by hand with `defineUnmanagedTable`.
|
|
12
12
|
*
|
|
13
13
|
* Named `derive*` (not `build*Meta`) so it is not mistaken for the unmanaged
|
|
14
|
-
* escape hatch
|
|
14
|
+
* escape hatch.
|
|
15
15
|
*/
|
|
16
16
|
export declare function deriveEntityTableMeta(entityName: string, entity: EntityDefinition, options: BuildEntityTableMetaOptions & {
|
|
17
17
|
readonly source: "unmanaged";
|
|
@@ -20,7 +20,7 @@ export declare function deriveEntityTableMeta(entityName: string, entity: Entity
|
|
|
20
20
|
export declare function assertBackingTableSuperset(entityName: string, fieldMeta: EntityTableMeta, tableMeta: EntityTableMeta): void;
|
|
21
21
|
/**
|
|
22
22
|
* Hand-built EntityTableMeta for direct-write stores (no entity base columns).
|
|
23
|
-
* Prefer a `store_*` table name; `read_` is reserved for managed projections
|
|
23
|
+
* Prefer a `store_*` table name; `read_` is reserved for managed projections.
|
|
24
24
|
*
|
|
25
25
|
* Escape hatch, not a shortcut: no audit trail, no automatic tenant_id index,
|
|
26
26
|
* no softDelete — the app author owns tenant-scoping and retention for this
|
|
@@ -173,7 +173,7 @@ function fieldToColumnMeta(name, field, entity) {
|
|
|
173
173
|
return [{ name: snake, pgType: "jsonb", notNull: true, defaultSql: "'{}'::jsonb" }];
|
|
174
174
|
case "date":
|
|
175
175
|
// Real PG `date` — Temporal.PlainDate, no time-of-day/timezone
|
|
176
|
-
// component. See plainDate() in dialect.ts
|
|
176
|
+
// component. See plainDate() in dialect.ts.
|
|
177
177
|
return [{ name: snake, pgType: "date", notNull: field.required === true }];
|
|
178
178
|
case "timestamp":
|
|
179
179
|
return [{ name: snake, pgType: "timestamptz", notNull: field.required === true }];
|
|
@@ -210,30 +210,48 @@ export function resolveTableName(entityName, entity, featureName) {
|
|
|
210
210
|
export function deriveEntityTableMeta(entityName, entity, options) {
|
|
211
211
|
const tableName = resolveTableName(entityName, entity, options?.featureName);
|
|
212
212
|
const source = options?.source ?? "managed";
|
|
213
|
-
if (source === "unmanaged") {
|
|
214
|
-
assertUnmanagedTableName(tableName, "deriveEntityTableMeta");
|
|
215
|
-
}
|
|
216
213
|
const idType = entity.idType ?? "uuid";
|
|
217
|
-
if (source === "unmanaged"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
214
|
+
if (source === "unmanaged")
|
|
215
|
+
assertUnmanagedRules(entityName, entity, tableName, idType);
|
|
216
|
+
const layout = collectEntityColumns(entity, idType);
|
|
217
|
+
const indexes = [
|
|
218
|
+
{ name: `${tableName}_tenant_id_idx`, columns: ["tenant_id"] },
|
|
219
|
+
...foreignKeyIndexes(tableName, entity, options?.relations, layout.fieldNameToSnake),
|
|
220
|
+
...blindIndexIndexes(tableName, layout.bidxSnakeByFieldSnake),
|
|
221
|
+
...declaredIndexes(tableName, entity, layout),
|
|
222
|
+
];
|
|
223
|
+
const piiSubjectFields = collectPiiEncryptedFields(entity);
|
|
224
|
+
const meta = {
|
|
225
|
+
tableName,
|
|
226
|
+
columns: layout.columns,
|
|
227
|
+
indexes,
|
|
228
|
+
source,
|
|
229
|
+
...(piiSubjectFields.length > 0 && { piiSubjectFields }),
|
|
230
|
+
...(entity.tenancy === "global" && { tenancy: "global" }),
|
|
231
|
+
};
|
|
232
|
+
return source === "unmanaged" ? brandWritable(meta) : meta;
|
|
233
|
+
}
|
|
234
|
+
function assertUnmanagedRules(entityName, entity, tableName, idType) {
|
|
235
|
+
assertUnmanagedTableName(tableName, "deriveEntityTableMeta");
|
|
236
|
+
if (idType === "serial")
|
|
237
|
+
assertRecordSubjectsShreddable(entityName, entity);
|
|
238
|
+
}
|
|
239
|
+
// Unmanaged direct-write stores bypass validateRecordOwnedSubjects (it only walks
|
|
240
|
+
// feature.entities), yet encryptForDirectWrite still keys recordOwned and
|
|
241
|
+
// whenAbsent: "self" fields as `record:<entity>:<id>`; forgetSubject requires a UUID,
|
|
242
|
+
// so the value could never be shredded.
|
|
243
|
+
function assertRecordSubjectsShreddable(entityName, entity) {
|
|
244
|
+
const recordOwnedField = Object.entries(entity.fields).find(([, field]) => isRecordKeyedField(field));
|
|
245
|
+
if (recordOwnedField) {
|
|
246
|
+
throw new Error(`deriveEntityTableMeta("${entityName}", { source: "unmanaged" }): field "${recordOwnedField[0]}" is recordOwned (or whenAbsent: "self") but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field could never be shredded. Use idType: "uuid", or give the field a different personal stance.`);
|
|
226
247
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// filterable marking. The DB column stays the same, only the application
|
|
233
|
-
// metadata on the field side changes.
|
|
234
|
-
const baseCols = fullBaseColumns(idType, entity.softDelete === true);
|
|
248
|
+
}
|
|
249
|
+
// A field column with a base column's pg name overrides it in place (last wins,
|
|
250
|
+
// like `{ ...base, ...fields }`): user-session declares `tenantId` for access
|
|
251
|
+
// control, fileRef `insertedAt` for sort/filter flags; the DB column is unchanged.
|
|
252
|
+
function collectEntityColumns(entity, idType) {
|
|
235
253
|
const colByName = new Map();
|
|
236
|
-
for (const c of
|
|
254
|
+
for (const c of fullBaseColumns(idType, entity.softDelete === true))
|
|
237
255
|
colByName.set(c.name, c);
|
|
238
256
|
const fieldNameToSnake = new Map();
|
|
239
257
|
const bidxSnakeByFieldSnake = new Map();
|
|
@@ -242,8 +260,7 @@ export function deriveEntityTableMeta(entityName, entity, options) {
|
|
|
242
260
|
for (const c of fieldCols)
|
|
243
261
|
colByName.set(c.name, c);
|
|
244
262
|
// Multi-column fields map to their primary column when its name IS the
|
|
245
|
-
// field's snake (text+bidx, money+currency)
|
|
246
|
-
// fallback below, so explicit indexes keep resolving.
|
|
263
|
+
// field's snake (text+bidx, money+currency), so explicit indexes keep resolving.
|
|
247
264
|
const primary = fieldCols[0];
|
|
248
265
|
if (primary && primary.name === toSnakeCase(name))
|
|
249
266
|
fieldNameToSnake.set(name, primary.name);
|
|
@@ -251,101 +268,77 @@ export function deriveEntityTableMeta(entityName, entity, options) {
|
|
|
251
268
|
if (primary && bidxCol)
|
|
252
269
|
bidxSnakeByFieldSnake.set(primary.name, bidxCol.name);
|
|
253
270
|
}
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const final = colByName.get(c.name);
|
|
259
|
-
if (final && !seen.has(final.name)) {
|
|
260
|
-
columns.push(final);
|
|
261
|
-
seen.add(final.name);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
for (const c of colByName.values()) {
|
|
265
|
-
if (!seen.has(c.name)) {
|
|
266
|
-
columns.push(c);
|
|
267
|
-
seen.add(c.name);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const indexes = [{ name: `${tableName}_tenant_id_idx`, columns: ["tenant_id"] }];
|
|
271
|
-
// FK-Indexes: file/image-Felder + belongsTo-Relations
|
|
271
|
+
// Map insertion order keeps base columns first (overrides stay in place), then new field columns.
|
|
272
|
+
return { columns: [...colByName.values()], fieldNameToSnake, bidxSnakeByFieldSnake };
|
|
273
|
+
}
|
|
274
|
+
function foreignKeyIndexes(tableName, entity, relations, fieldNameToSnake) {
|
|
272
275
|
const fkSnakeNames = new Set();
|
|
273
276
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
274
277
|
if (field.type === "file" || field.type === "image")
|
|
275
278
|
fkSnakeNames.add(toSnakeCase(name));
|
|
276
279
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const snake = fieldNameToSnake.get(rel.foreignKey) ?? toSnakeCase(rel.foreignKey);
|
|
281
|
-
fkSnakeNames.add(snake);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
for (const snake of fkSnakeNames) {
|
|
286
|
-
indexes.push({ name: `${tableName}_${snake}_idx`, columns: [snake] });
|
|
287
|
-
}
|
|
288
|
-
// lookupable fields: index on the bidx column — the query compiler's
|
|
289
|
-
// OR rewrite hits it on every equality lookup.
|
|
290
|
-
for (const bidxSnake of bidxSnakeByFieldSnake.values()) {
|
|
291
|
-
indexes.push({ name: `${tableName}_${bidxSnake}_idx`, columns: [bidxSnake] });
|
|
292
|
-
}
|
|
293
|
-
// Explicitly declared indexes (EntityIndexDef). `def.where` is a
|
|
294
|
-
// SqlExpression (`sql\`…\`` from @cosmicdrift/kumiko-framework/db) —
|
|
295
|
-
// renderable via `.text`. Unknown where-shapes stay needsManualWhere.
|
|
296
|
-
for (const def of entity.indexes ?? []) {
|
|
297
|
-
const cols = def.columns.map((fieldName) => fieldNameToSnake.get(fieldName) ?? toSnakeCase(fieldName));
|
|
298
|
-
const suffix = def.unique === true ? "unique" : "idx";
|
|
299
|
-
const indexName = def.name ?? `${tableName}_${cols.join("_")}_${suffix}`;
|
|
300
|
-
const explicitWhereSql = sqlExpressionText(def.where);
|
|
301
|
-
// Soft-deleted rows keep their value, so a full unique index would
|
|
302
|
-
// permanently block reuse; kept in lock-step with table-builder.ts (framework#2593).
|
|
303
|
-
const whereSql = explicitWhereSql ??
|
|
304
|
-
(def.where === undefined && def.unique === true && entity.softDelete === true
|
|
305
|
-
? SOFT_DELETE_LIVE_ROW_PREDICATE
|
|
306
|
-
: undefined);
|
|
307
|
-
indexes.push({
|
|
308
|
-
name: indexName,
|
|
309
|
-
columns: cols,
|
|
310
|
-
...(def.unique === true && { unique: true }),
|
|
311
|
-
...(whereSql !== undefined && { whereSql }),
|
|
312
|
-
...(def.where !== undefined && explicitWhereSql === undefined && { needsManualWhere: true }),
|
|
313
|
-
});
|
|
314
|
-
// Unique index over lookupable columns: partial bidx counterpart, so
|
|
315
|
-
// uniqueness also holds for encrypted rows. The original stays
|
|
316
|
-
// for plaintext legacy rows; partial (IS NOT NULL) because bidx is
|
|
317
|
-
// NULL for erased/key-less rows.
|
|
318
|
-
if (def.unique === true && def.where === undefined) {
|
|
319
|
-
const bidxCols = cols.map((c) => bidxSnakeByFieldSnake.get(c) ?? c);
|
|
320
|
-
if (bidxCols.some((c, i) => c !== cols[i])) {
|
|
321
|
-
const notNullParts = bidxCols
|
|
322
|
-
.filter((c, i) => c !== cols[i])
|
|
323
|
-
.map((c) => `"${c}" IS NOT NULL`);
|
|
324
|
-
// Soft-deleted rows keep their bidx hash — without this, a restored
|
|
325
|
-
// row can't reuse a value a soft-deleted sibling still holds
|
|
326
|
-
// (framework#2464). Kept in lock-step with table-builder.ts.
|
|
327
|
-
const whereParts = entity.softDelete === true
|
|
328
|
-
? [...notNullParts, SOFT_DELETE_LIVE_ROW_PREDICATE]
|
|
329
|
-
: notNullParts;
|
|
330
|
-
indexes.push({
|
|
331
|
-
name: `${indexName}_bidx`,
|
|
332
|
-
columns: bidxCols,
|
|
333
|
-
unique: true,
|
|
334
|
-
whereSql: whereParts.join(" AND "),
|
|
335
|
-
});
|
|
336
|
-
}
|
|
280
|
+
for (const rel of Object.values(relations ?? {})) {
|
|
281
|
+
if (rel.type === "belongsTo") {
|
|
282
|
+
fkSnakeNames.add(fieldNameToSnake.get(rel.foreignKey) ?? toSnakeCase(rel.foreignKey));
|
|
337
283
|
}
|
|
338
284
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
285
|
+
return [...fkSnakeNames].map((snake) => ({
|
|
286
|
+
name: `${tableName}_${snake}_idx`,
|
|
287
|
+
columns: [snake],
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
// The query compiler's OR rewrite hits the bidx column on every equality lookup.
|
|
291
|
+
function blindIndexIndexes(tableName, bidxSnakeByFieldSnake) {
|
|
292
|
+
return [...bidxSnakeByFieldSnake.values()].map((bidxSnake) => ({
|
|
293
|
+
name: `${tableName}_${bidxSnake}_idx`,
|
|
294
|
+
columns: [bidxSnake],
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
function declaredIndexes(tableName, entity, layout) {
|
|
298
|
+
return (entity.indexes ?? []).flatMap((def) => declaredIndexWithBidxCounterpart(tableName, entity, def, layout));
|
|
299
|
+
}
|
|
300
|
+
// `def.where` is a SqlExpression renderable via `.text`; unknown where-shapes
|
|
301
|
+
// stay needsManualWhere.
|
|
302
|
+
function declaredIndexWithBidxCounterpart(tableName, entity, def, layout) {
|
|
303
|
+
const cols = def.columns.map((fieldName) => layout.fieldNameToSnake.get(fieldName) ?? toSnakeCase(fieldName));
|
|
304
|
+
const suffix = def.unique === true ? "unique" : "idx";
|
|
305
|
+
const indexName = def.name ?? `${tableName}_${cols.join("_")}_${suffix}`;
|
|
306
|
+
const explicitWhereSql = sqlExpressionText(def.where);
|
|
307
|
+
// Soft-deleted rows keep their value, so a full unique index would
|
|
308
|
+
// permanently block reuse; kept in lock-step with table-builder.ts.
|
|
309
|
+
const whereSql = explicitWhereSql ??
|
|
310
|
+
(def.where === undefined && def.unique === true && entity.softDelete === true
|
|
311
|
+
? SOFT_DELETE_LIVE_ROW_PREDICATE
|
|
312
|
+
: undefined);
|
|
313
|
+
const declared = {
|
|
314
|
+
name: indexName,
|
|
315
|
+
columns: cols,
|
|
316
|
+
...(def.unique === true && { unique: true }),
|
|
317
|
+
...(whereSql !== undefined && { whereSql }),
|
|
318
|
+
...(def.where !== undefined && explicitWhereSql === undefined && { needsManualWhere: true }),
|
|
319
|
+
};
|
|
320
|
+
if (def.unique !== true || def.where !== undefined)
|
|
321
|
+
return [declared];
|
|
322
|
+
const counterpart = bidxUniqueCounterpart(indexName, cols, entity.softDelete === true, layout.bidxSnakeByFieldSnake);
|
|
323
|
+
return counterpart ? [declared, counterpart] : [declared];
|
|
324
|
+
}
|
|
325
|
+
// Unique index over lookupable columns gets a partial bidx counterpart so
|
|
326
|
+
// uniqueness also holds for encrypted rows; IS NOT NULL because bidx is NULL
|
|
327
|
+
// for erased/key-less rows.
|
|
328
|
+
function bidxUniqueCounterpart(indexName, cols, softDelete, bidxSnakeByFieldSnake) {
|
|
329
|
+
const bidxCols = cols.map((c) => bidxSnakeByFieldSnake.get(c) ?? c);
|
|
330
|
+
const notNullParts = bidxCols.filter((c, i) => c !== cols[i]).map((c) => `"${c}" IS NOT NULL`);
|
|
331
|
+
if (notNullParts.length === 0)
|
|
332
|
+
return undefined;
|
|
333
|
+
// Soft-deleted rows keep their bidx hash, so without the live-row predicate a
|
|
334
|
+
// restored row could not reuse a value a soft-deleted sibling still holds.
|
|
335
|
+
const whereParts = softDelete ? [...notNullParts, SOFT_DELETE_LIVE_ROW_PREDICATE] : notNullParts;
|
|
336
|
+
return {
|
|
337
|
+
name: `${indexName}_bidx`,
|
|
338
|
+
columns: bidxCols,
|
|
339
|
+
unique: true,
|
|
340
|
+
whereSql: whereParts.join(" AND "),
|
|
347
341
|
};
|
|
348
|
-
return source === "unmanaged" ? brandWritable(meta) : meta;
|
|
349
342
|
}
|
|
350
343
|
function sqlExpressionText(where) {
|
|
351
344
|
return isSqlExpression(where) ? where.text : undefined;
|
|
@@ -384,7 +377,7 @@ function columnsByNameMeta(meta) {
|
|
|
384
377
|
}
|
|
385
378
|
/**
|
|
386
379
|
* Hand-built EntityTableMeta for direct-write stores (no entity base columns).
|
|
387
|
-
* Prefer a `store_*` table name; `read_` is reserved for managed projections
|
|
380
|
+
* Prefer a `store_*` table name; `read_` is reserved for managed projections.
|
|
388
381
|
*
|
|
389
382
|
* Escape hatch, not a shortcut: no audit trail, no automatic tenant_id index,
|
|
390
383
|
* no softDelete — the app author owns tenant-scoping and retention for this
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ErasedSubjectHandling } from "../crypto/pii-field-encryption.ts";
|
|
1
2
|
import type { WhereObject } from "../db/query.ts";
|
|
2
3
|
import type { EntityDefinition, EntityId, SessionUser, TenantId } from "../engine/types/index.ts";
|
|
3
4
|
import { type WriteFailure } from "../errors/index.ts";
|
|
@@ -7,7 +8,7 @@ import type { SearchAdapter } from "../search/types.ts";
|
|
|
7
8
|
import type { TableColumns } from "./dialect.ts";
|
|
8
9
|
import type { EventStoreExecutorOptions } from "./event-store-executor.ts";
|
|
9
10
|
import type { TenantDb } from "./tenant-db.ts";
|
|
10
|
-
export type Table = TableColumns
|
|
11
|
+
export type Table = TableColumns;
|
|
11
12
|
export declare function buildFilterWhere(field: string, op: "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in", value: unknown): WhereObject | null;
|
|
12
13
|
export declare function ownershipSubject(user: SessionUser, streamTenantId: TenantId | undefined): SessionUser;
|
|
13
14
|
export declare function isForeignTenantOnGlobalEntity(entity: EntityDefinition, tenantIdValue: unknown): boolean;
|
|
@@ -62,7 +63,7 @@ export type ExecutorContext = {
|
|
|
62
63
|
subjectSource?: Record<string, unknown>;
|
|
63
64
|
keyTenantId?: TenantId | undefined;
|
|
64
65
|
ownerFallback?: Record<string, unknown>;
|
|
65
|
-
onErasedSubject?:
|
|
66
|
+
onErasedSubject?: ErasedSubjectHandling;
|
|
66
67
|
}) => Promise<Record<string, unknown>>;
|
|
67
68
|
readonly decryptForRead: (row: Record<string, unknown>, opts?: DecryptForReadOptions) => Promise<Record<string, unknown>>;
|
|
68
69
|
readonly ownerTransitionRekeys: (previous: Record<string, unknown>, mergedNew: Record<string, unknown>, submittedChanges: Record<string, unknown>) => Record<string, unknown>;
|
|
@@ -13,10 +13,10 @@ import { constraintOf, isUniqueViolation } from "./pg-error.js";
|
|
|
13
13
|
import { toSnakeCase } from "./table-builder.js";
|
|
14
14
|
import { columnHandleName, schemaTableName } from "./table-introspection.js";
|
|
15
15
|
import { tenantDbRunner } from "./tenant-db-runner.js";
|
|
16
|
-
// Screen
|
|
17
|
-
//
|
|
18
|
-
// `op`
|
|
19
|
-
// empty
|
|
16
|
+
// Screen filter: maps an operator to a WhereObject condition.
|
|
17
|
+
// The boot validator checks field existence, filterability, and operator/type compatibility.
|
|
18
|
+
// `op` is normalized to {eq,ne,lt,gt,lte,gte,in}; "in" with an
|
|
19
|
+
// empty array explicitly matches nothing.
|
|
20
20
|
export function buildFilterWhere(field, op, value) {
|
|
21
21
|
switch (op) {
|
|
22
22
|
case "eq":
|
|
@@ -70,11 +70,11 @@ function scalarDefault(field) {
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
//
|
|
74
|
-
// (create
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// applyEntityEvent
|
|
73
|
+
// Maps PG-23505 (unique-violation) caught from applyEntityEvent
|
|
74
|
+
// (create and update paths) to WriteFailure(UniqueViolationError 409).
|
|
75
|
+
// Other errors propagate via re-throw. Extracted because this pattern
|
|
76
|
+
// occurs in two places in the executor; the caller wraps the
|
|
77
|
+
// applyEntityEvent call in try-catch and delegates the mapping here.
|
|
78
78
|
//
|
|
79
79
|
// Returns WriteFailure on match, null otherwise (caller re-throws).
|
|
80
80
|
export function tryMapUniqueViolation(e, entityName) {
|
|
@@ -161,8 +161,8 @@ export function buildExecutorContext(table, entity, options) {
|
|
|
161
161
|
}
|
|
162
162
|
// Pre-compute the set of sensitive field names once. The event log stores
|
|
163
163
|
// these fields as table ciphertext (boot validates sensitive ⇒ pii |
|
|
164
|
-
// encrypted
|
|
165
|
-
// responses never carry the value
|
|
164
|
+
// encrypted) — the set only strips the caller-facing event echo so
|
|
165
|
+
// responses never carry the value.
|
|
166
166
|
const sensitiveFields = new Set();
|
|
167
167
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
168
168
|
if ("sensitive" in field && field.sensitive === true) {
|
|
@@ -184,8 +184,8 @@ export function buildExecutorContext(table, entity, options) {
|
|
|
184
184
|
return resolveEntityFieldEncryption();
|
|
185
185
|
}
|
|
186
186
|
// No adapter configured = crypto-shredding off; pii fields stay plaintext
|
|
187
|
-
// (
|
|
188
|
-
// PgKmsAdapter
|
|
187
|
+
// (previous behavior). The hard boot gate ships with the prod-grade
|
|
188
|
+
// PgKmsAdapter.
|
|
189
189
|
function piiKms() {
|
|
190
190
|
return options.kms ?? configuredPiiSubjectKms();
|
|
191
191
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ListAggregatePayload, ListAggregateResult } from "@cosmicdrift/kumiko-types/list-aggregate-types";
|
|
2
|
+
import type { SessionUser } from "../engine/types/index.ts";
|
|
3
|
+
import type { ExecutorContext } from "./event-store-executor-context.ts";
|
|
4
|
+
import { type ListRuntimeOptions, type ListWherePayload } from "./event-store-executor-read-where.ts";
|
|
5
|
+
import type { TenantDb } from "./tenant-db.ts";
|
|
6
|
+
export declare function aggregateList(ctx: ExecutorContext, payload: ListWherePayload & {
|
|
7
|
+
readonly aggregate: ListAggregatePayload;
|
|
8
|
+
}, user: SessionUser, db: TenantDb, runtimeOptions: ListRuntimeOptions | undefined): Promise<ListAggregateResult>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ValidationError } from "../errors/index.js";
|
|
2
|
+
import { aggregateKeyOf, decodeListAggregateValue, emptyListAggregateValue, groupedKeyOrderLimitSql, isListAggregateGroupable, isListAggregateMeasureSupported, listAggregateKeySql, listAggregateMeasureSql, } from "./aggregate-sql.js";
|
|
3
|
+
import { holdsCiphertextOrSecret } from "./entity-field-encryption.js";
|
|
4
|
+
import { appendShiftedClause, bindShiftedClause, } from "./event-store-executor-read-filter.js";
|
|
5
|
+
import { buildListWhere, createFieldReadClause, } from "./event-store-executor-read-where.js";
|
|
6
|
+
import { executeRawQueryRead } from "./queries/raw-sql.js";
|
|
7
|
+
import { schemaTableName } from "./table-introspection.js";
|
|
8
|
+
import { tenantDbRunner } from "./tenant-db-runner.js";
|
|
9
|
+
// One group more than the cap is fetched to tell "exactly 200" from "more".
|
|
10
|
+
const MAX_AGGREGATE_GROUPS = 200;
|
|
11
|
+
function rejectAggregate(path, field, hint) {
|
|
12
|
+
throw new ValidationError({
|
|
13
|
+
fields: [
|
|
14
|
+
{
|
|
15
|
+
path,
|
|
16
|
+
code: "field_not_aggregatable",
|
|
17
|
+
i18nKey: "errors.validation.invalid_value",
|
|
18
|
+
params: { field, hint },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Unknown and unreadable fields share one silent outcome so the response cannot
|
|
24
|
+
// confirm that a field exists; only then are the sensitive-field and type gates applied.
|
|
25
|
+
function gateField(ctx, fieldReadClause, field, path) {
|
|
26
|
+
const def = ctx.entity.fields[field];
|
|
27
|
+
if (def === undefined ||
|
|
28
|
+
!Object.hasOwn(ctx.entity.fields, field) ||
|
|
29
|
+
!Object.hasOwn(ctx.table, field))
|
|
30
|
+
return { kind: "silent" };
|
|
31
|
+
const clause = fieldReadClause(field);
|
|
32
|
+
if (clause.kind === "empty")
|
|
33
|
+
return { kind: "silent" };
|
|
34
|
+
if (holdsCiphertextOrSecret(ctx.entity, field)) {
|
|
35
|
+
rejectAggregate(path, field, "Encrypted, personal and secret fields cannot be aggregated.");
|
|
36
|
+
}
|
|
37
|
+
return { kind: "ok", def, clause };
|
|
38
|
+
}
|
|
39
|
+
function planAggregate(ctx, fieldReadClause, aggregate) {
|
|
40
|
+
const groupBy = aggregate.groupBy === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: {
|
|
43
|
+
field: aggregate.groupBy,
|
|
44
|
+
gate: gateField(ctx, fieldReadClause, aggregate.groupBy, "aggregate.groupBy"),
|
|
45
|
+
};
|
|
46
|
+
if (groupBy?.gate.kind === "ok") {
|
|
47
|
+
const { def } = groupBy.gate;
|
|
48
|
+
const multiple = def.type === "reference" && def.multiple === true;
|
|
49
|
+
if (!isListAggregateGroupable(def.type, multiple)) {
|
|
50
|
+
rejectAggregate("aggregate.groupBy", groupBy.field, "This field type cannot be grouped by.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const measures = aggregate.measures.map((m, i) => {
|
|
54
|
+
const path = `aggregate.measures.${i}`;
|
|
55
|
+
const gate = gateField(ctx, fieldReadClause, m.field, path);
|
|
56
|
+
if (gate.kind === "ok" && !isListAggregateMeasureSupported(m.kind, gate.def.type)) {
|
|
57
|
+
rejectAggregate(path, m.field, `"${m.kind}" is not supported on a ${gate.def.type} field.`);
|
|
58
|
+
}
|
|
59
|
+
return { kind: m.kind, field: m.field, gate };
|
|
60
|
+
});
|
|
61
|
+
return { groupBy, measures };
|
|
62
|
+
}
|
|
63
|
+
function emptyMeasureValues(plan) {
|
|
64
|
+
return plan.measures.map((m) => m.gate.kind === "silent" ? null : emptyListAggregateValue(m.kind));
|
|
65
|
+
}
|
|
66
|
+
function renderSelectList(plan, where) {
|
|
67
|
+
const parts = [];
|
|
68
|
+
if (plan.groupBy?.gate.kind === "ok") {
|
|
69
|
+
const { def, clause } = plan.groupBy.gate;
|
|
70
|
+
parts.push(listAggregateKeySql(def.type, where.colSql(plan.groupBy.field), "k0"));
|
|
71
|
+
// WHERE, not CASE: rows the viewer may not read must not collapse into the real NULL group.
|
|
72
|
+
if (clause.kind === "sql")
|
|
73
|
+
appendShiftedClause(where, clause);
|
|
74
|
+
}
|
|
75
|
+
parts.push("COUNT(*)::float8 AS c");
|
|
76
|
+
for (const [i, m] of plan.measures.entries()) {
|
|
77
|
+
if (m.gate.kind === "silent")
|
|
78
|
+
continue; // skip: an unreadable measure stays out of the SELECT and decodes to null
|
|
79
|
+
const filterSql = m.gate.clause.kind === "sql" ? bindShiftedClause(where.params, m.gate.clause) : undefined;
|
|
80
|
+
parts.push(`${listAggregateMeasureSql(m.kind, m.gate.def.type, where.colSql(m.field), filterSql)} AS m${i}`);
|
|
81
|
+
}
|
|
82
|
+
return parts.join(", ");
|
|
83
|
+
}
|
|
84
|
+
export async function aggregateList(ctx, payload, user, db, runtimeOptions) {
|
|
85
|
+
const plan = planAggregate(ctx, createFieldReadClause(ctx, user), payload.aggregate);
|
|
86
|
+
const grouped = plan.groupBy !== undefined;
|
|
87
|
+
if (plan.groupBy?.gate.kind === "silent")
|
|
88
|
+
return { groups: [] };
|
|
89
|
+
const built = await buildListWhere(ctx, payload, user, db, runtimeOptions);
|
|
90
|
+
if (built.kind === "empty") {
|
|
91
|
+
return {
|
|
92
|
+
groups: grouped ? [] : [{ key: null, count: 0, measures: emptyMeasureValues(plan) }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const { where, searchTruncated } = built;
|
|
96
|
+
const selectList = renderSelectList(plan, where);
|
|
97
|
+
const whereText = where.whereSql.length > 0 ? ` WHERE ${where.whereSql.join(" AND ")}` : "";
|
|
98
|
+
const groupText = grouped ? groupedKeyOrderLimitSql(MAX_AGGREGATE_GROUPS + 1) : "";
|
|
99
|
+
const tableName = String(schemaTableName(ctx.table));
|
|
100
|
+
const rows = await executeRawQueryRead(tenantDbRunner(db), `SELECT ${selectList} FROM "${tableName}"${whereText}${groupText}`, where.params);
|
|
101
|
+
const truncated = rows.length > MAX_AGGREGATE_GROUPS;
|
|
102
|
+
const groups = rows.slice(0, MAX_AGGREGATE_GROUPS).map((row) => ({
|
|
103
|
+
key: grouped ? aggregateKeyOf(row["k0"]) : null,
|
|
104
|
+
count: Number(row["c"]),
|
|
105
|
+
measures: plan.measures.map((m, i) => m.gate.kind === "silent" ? null : decodeListAggregateValue(row[`m${i}`])),
|
|
106
|
+
}));
|
|
107
|
+
return {
|
|
108
|
+
groups,
|
|
109
|
+
...(truncated && { truncated: true }),
|
|
110
|
+
...(searchTruncated && { searchTruncated: true }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type OwnershipClause } from "../engine/ownership.ts";
|
|
2
|
+
import type { EntityDefinition } from "../engine/types/index.ts";
|
|
3
|
+
import { type Table } from "./event-store-executor-context.ts";
|
|
4
|
+
export type ListWhere = {
|
|
5
|
+
readonly whereSql: string[];
|
|
6
|
+
readonly params: unknown[];
|
|
7
|
+
readonly colSql: (field: string) => string;
|
|
8
|
+
};
|
|
9
|
+
export declare function bindShiftedClause(params: unknown[], clause: {
|
|
10
|
+
readonly sqlText: string;
|
|
11
|
+
readonly params: readonly unknown[];
|
|
12
|
+
}): string;
|
|
13
|
+
export declare function appendShiftedClause(where: ListWhere, clause: {
|
|
14
|
+
readonly sqlText: string;
|
|
15
|
+
readonly params: readonly unknown[];
|
|
16
|
+
}): void;
|
|
17
|
+
export declare function bindPlaceholders(params: unknown[], values: readonly unknown[]): string;
|
|
18
|
+
export type ListFilter = {
|
|
19
|
+
readonly field: string;
|
|
20
|
+
readonly op: "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
|
|
21
|
+
readonly value: unknown;
|
|
22
|
+
};
|
|
23
|
+
export declare function deriveParentNarrowTypes(entity: EntityDefinition, payload: {
|
|
24
|
+
readonly filter?: ListFilter;
|
|
25
|
+
readonly filters?: readonly ListFilter[];
|
|
26
|
+
}): ReadonlySet<string> | undefined;
|
|
27
|
+
export declare function collectListFilters(payload: {
|
|
28
|
+
readonly filter?: ListFilter | undefined;
|
|
29
|
+
readonly filters?: readonly ListFilter[] | undefined;
|
|
30
|
+
}): readonly ListFilter[];
|
|
31
|
+
export declare function applyListFilter(where: ListWhere, f: ListFilter, table: Table, entity: EntityDefinition, fieldReadClause: (field: string) => OwnershipClause): void;
|