@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/query.js
CHANGED
|
@@ -12,33 +12,19 @@
|
|
|
12
12
|
// transaction<T>(db, fn) → T
|
|
13
13
|
//
|
|
14
14
|
// `table` is an EntityTableMeta; the legacy table objects carrying it under a Symbol are accepted too.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
18
|
-
import { isOneOf, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
19
|
-
import { requestContext } from "../api/request-context.js";
|
|
20
|
-
import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
|
|
21
|
-
import { isPiiCiphertext } from "../crypto/pii-ciphertext-format.js";
|
|
22
|
-
import { camelCase as envCamelCase } from "../env/index.js";
|
|
15
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
16
|
+
import { quoteIdent } from "../crypto/ciphertext-pattern.js";
|
|
23
17
|
import { InternalError } from "../errors/index.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { SQL_EXPR_BRAND } from "./dialect.js";
|
|
18
|
+
import { aggregateDimensionSql, aggregateGroupOrderLimitSql, aggregateKeyOf, aggregateMeasureSql, assertValidAggregateLimits, } from "./aggregate-sql.js";
|
|
19
|
+
import { isClosedConnectionError, isPooledClient, logClosedConnection, poolMaxOf, } from "./closed-connection.js";
|
|
27
20
|
import { buildDistinctOnClause } from "./distinct-on.js";
|
|
21
|
+
import { coerceDriverRow } from "./driver-row-coercion.js";
|
|
22
|
+
import { prepareValue } from "./driver-value-preparation.js";
|
|
28
23
|
import { executorManagedWriteDenied } from "./executor-managed-tables.js";
|
|
29
24
|
import { extractPgError } from "./pg-error.js";
|
|
30
|
-
import {
|
|
25
|
+
import { extractTableInfo } from "./table-info.js";
|
|
31
26
|
import { asEntityTableMeta, requireEntityTableMeta } from "./table-introspection.js";
|
|
32
|
-
|
|
33
|
-
// (designed for SHOUT_CASE input) — for already-camelCase keys (mock rows
|
|
34
|
-
// in tests, projection-aliased columns) it would silently produce "tenantid"
|
|
35
|
-
// instead of leaving "tenantId" alone. Guard with an underscore check so
|
|
36
|
-
// the conversion only fires when the key is actually snake-shaped.
|
|
37
|
-
function snakeToCamel(key) {
|
|
38
|
-
if (!key.includes("_"))
|
|
39
|
-
return key;
|
|
40
|
-
return envCamelCase(key);
|
|
41
|
-
}
|
|
27
|
+
import { buildWhereClause } from "./where-clause.js";
|
|
42
28
|
// table() (dialect) stores the canonical, shadow-proof EntityTableMeta under
|
|
43
29
|
// `Symbol.for("kumiko:schema:Meta")`. The column handles on the table object are
|
|
44
30
|
// enumerable props, so an entity field named `source`/`columns`/`tableName`/…
|
|
@@ -148,671 +134,13 @@ function assertNotTenantScoped(db, fnName) {
|
|
|
148
134
|
"explicit cross-tenant op, or use a scoped TenantDb method.");
|
|
149
135
|
}
|
|
150
136
|
}
|
|
151
|
-
const WHERE_OPERATOR_KEYS = [
|
|
152
|
-
"gt",
|
|
153
|
-
"gte",
|
|
154
|
-
"lt",
|
|
155
|
-
"lte",
|
|
156
|
-
"ne",
|
|
157
|
-
"in",
|
|
158
|
-
"notIn",
|
|
159
|
-
"like",
|
|
160
|
-
"jsonText",
|
|
161
|
-
];
|
|
162
|
-
const _whereOperatorKeysExhaustive = true;
|
|
163
|
-
void _whereOperatorKeysExhaustive;
|
|
164
|
-
function isJsonTextMatchList(value) {
|
|
165
|
-
return Array.isArray(value);
|
|
166
|
-
}
|
|
167
|
-
function isWhereOperator(v) {
|
|
168
|
-
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
169
|
-
return false;
|
|
170
|
-
// Don't false-positive on Date / Temporal / other domain-objects.
|
|
171
|
-
// WhereOperator is plain object literal with at most these keys.
|
|
172
|
-
const keys = Object.keys(v);
|
|
173
|
-
if (keys.length === 0)
|
|
174
|
-
return false;
|
|
175
|
-
return keys.every((k) => isOneOf(WHERE_OPERATOR_KEYS, k));
|
|
176
|
-
}
|
|
177
|
-
export function extractTableInfo(table) {
|
|
178
|
-
const meta = asEntityTableMeta(table);
|
|
179
|
-
if (!meta) {
|
|
180
|
-
throw new Error("extractTableInfo: table is not a kumiko EntityTableMeta — " +
|
|
181
|
-
"build it via buildEntityTable / deriveEntityTableMeta / table().");
|
|
182
|
-
}
|
|
183
|
-
const colByField = new Map();
|
|
184
|
-
const fieldByCol = new Map();
|
|
185
|
-
const typeByCol = new Map();
|
|
186
|
-
const bigintModeByCol = new Map();
|
|
187
|
-
for (const c of meta.columns) {
|
|
188
|
-
typeByCol.set(c.name, c.pgType);
|
|
189
|
-
if (c.bigintJsMode !== undefined)
|
|
190
|
-
bigintModeByCol.set(c.name, c.bigintJsMode);
|
|
191
|
-
// EntityTableMeta column names are snake_case. Map snake → snake AND
|
|
192
|
-
// derive a camelCase JS field-name so result rows can be renamed back
|
|
193
|
-
// to the API shape (`aggregate_id` → `aggregateId`).
|
|
194
|
-
colByField.set(c.name, c.name);
|
|
195
|
-
const camel = snakeToCamel(c.name);
|
|
196
|
-
if (camel !== c.name)
|
|
197
|
-
colByField.set(camel, c.name);
|
|
198
|
-
fieldByCol.set(c.name, camel === c.name ? c.name : camel);
|
|
199
|
-
}
|
|
200
|
-
return {
|
|
201
|
-
name: meta.tableName,
|
|
202
|
-
columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
|
|
203
|
-
pgTypeOf: (col) => typeByCol.get(col),
|
|
204
|
-
bigintJsModeOf: (col) => bigintModeByCol.get(col),
|
|
205
|
-
fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
|
|
206
|
-
hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
function quoteIdent(name) {
|
|
210
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
211
|
-
}
|
|
212
|
-
// --- Value coercion at the driver boundary ------------------------------
|
|
213
|
-
//
|
|
214
|
-
// postgres-js returns timestamptz as JS Date (or ISO string depending on
|
|
215
|
-
// driver config). Framework contract says timestamptz surfaces as
|
|
216
|
-
// Temporal.Instant — without coercion every read hands callers a Date and
|
|
217
|
-
// downstream code that does .epochMilliseconds / .add(...) crashes.
|
|
218
|
-
//
|
|
219
|
-
// Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
|
|
220
|
-
// params — convert to ISO string when the column pgType is timestamptz.
|
|
221
|
-
function isTemporalInstant(v) {
|
|
222
|
-
return propertyOf(v, Symbol.toStringTag) === "Temporal.Instant";
|
|
223
|
-
}
|
|
224
|
-
// Postgres bigint → JS: safe integers become `number` (matches createBigIntField /
|
|
225
|
-
// drizzle mode:"number"); values outside ±2^53 stay `bigint` (money cents, raw
|
|
226
|
-
// unmanaged columns). Uses BigInt equality check so 9007199254740993n never
|
|
227
|
-
// silently rounds to a safe integer.
|
|
228
|
-
function coerceBigIntFromDriver(value) {
|
|
229
|
-
let bi;
|
|
230
|
-
if (typeof value === "string") {
|
|
231
|
-
try {
|
|
232
|
-
bi = BigInt(value);
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
return value;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
bi = value;
|
|
240
|
-
}
|
|
241
|
-
const min = BigInt(Number.MIN_SAFE_INTEGER);
|
|
242
|
-
const max = BigInt(Number.MAX_SAFE_INTEGER);
|
|
243
|
-
if (bi >= min && bi <= max) {
|
|
244
|
-
const n = Number(bi);
|
|
245
|
-
if (BigInt(n) === bi)
|
|
246
|
-
return n;
|
|
247
|
-
}
|
|
248
|
-
return bi;
|
|
249
|
-
}
|
|
250
|
-
function instantFromDriver(value) {
|
|
251
|
-
if (value === null || value === undefined)
|
|
252
|
-
return null;
|
|
253
|
-
if (isTemporalInstant(value))
|
|
254
|
-
return value;
|
|
255
|
-
// Number coercion via +date — equivalent to .getTime() without a Date-API
|
|
256
|
-
// method (guard-no-date-api). Date → epoch milliseconds, then to Temporal.Instant.
|
|
257
|
-
if (value instanceof Date)
|
|
258
|
-
return Temporal.Instant.fromEpochMilliseconds(+value);
|
|
259
|
-
if (typeof value === "string") {
|
|
260
|
-
try {
|
|
261
|
-
return Temporal.Instant.from(value);
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (typeof value === "number")
|
|
268
|
-
return Temporal.Instant.fromEpochMilliseconds(value);
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
function isTemporalPlainDate(v) {
|
|
272
|
-
return propertyOf(v, Symbol.toStringTag) === "Temporal.PlainDate";
|
|
273
|
-
}
|
|
274
|
-
function isTemporalPlainDateTimeLike(v) {
|
|
275
|
-
const tag = propertyOf(v, Symbol.toStringTag);
|
|
276
|
-
return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
|
|
277
|
-
typeof v === "object" &&
|
|
278
|
-
v !== null &&
|
|
279
|
-
"toPlainDate" in v &&
|
|
280
|
-
typeof v.toPlainDate === "function");
|
|
281
|
-
}
|
|
282
|
-
// Verified empirically (kumiko-framework#1924): a `date`
|
|
283
|
-
// column comes back as a native JS Date, but ALWAYS anchored at UTC
|
|
284
|
-
// midnight regardless of process TZ — epochMilliseconds is stable across
|
|
285
|
-
// zones, only the local wall-clock read of that Date would drift the day
|
|
286
|
-
// (e.g. America/Los_Angeles reads back the 14th for a stored 15th). Routing
|
|
287
|
-
// through Instant→ZonedDateTimeISO("UTC")→PlainDate reads the calendar day
|
|
288
|
-
// back deterministically without ever touching a local Date getter — the
|
|
289
|
-
// same +value idiom instantFromDriver uses (guard-no-date-api: no
|
|
290
|
-
// `.getTime()`), just anchored to a fixed zone instead of process-local.
|
|
291
|
-
// A real `date` column always arrives as a UTC-midnight Date. Anything else means the column is
|
|
292
|
-
// still timestamptz (declared `date` before its ALTER ran); the UTC anchor then silently shifts
|
|
293
|
-
// the day, so say so once per column instead of returning the wrong day quietly.
|
|
294
|
-
const warnedNonMidnightDateColumns = new Set();
|
|
295
|
-
const MS_PER_DAY = 86_400_000;
|
|
296
|
-
const dateColumnDriftLog = createFallbackLogger("postgres-js");
|
|
297
|
-
function warnIfNotDateColumn(epochMs, column) {
|
|
298
|
-
// skip: midnight value is a correct date column, and an already-warned column is deduped
|
|
299
|
-
if (epochMs % MS_PER_DAY === 0 || warnedNonMidnightDateColumns.has(column))
|
|
300
|
-
return;
|
|
301
|
-
warnedNonMidnightDateColumns.add(column);
|
|
302
|
-
dateColumnDriftLog.warn(`column "${column}" is declared as date but returned a non-midnight timestamp; it is probably still timestamptz. Run the ALTER ... TYPE date migration, otherwise days can shift.`);
|
|
303
|
-
}
|
|
304
|
-
function plainDateFromDriver(value, column) {
|
|
305
|
-
if (value === null || value === undefined)
|
|
306
|
-
return null;
|
|
307
|
-
if (isTemporalPlainDate(value))
|
|
308
|
-
return value;
|
|
309
|
-
if (typeof value === "string") {
|
|
310
|
-
const isoDay = /^\d{4}-\d{2}-\d{2}/.exec(value)?.[0];
|
|
311
|
-
if (isoDay === undefined)
|
|
312
|
-
return null;
|
|
313
|
-
try {
|
|
314
|
-
return Temporal.PlainDate.from(isoDay);
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (value instanceof Date) {
|
|
321
|
-
warnIfNotDateColumn(+value, column);
|
|
322
|
-
return Temporal.Instant.fromEpochMilliseconds(+value).toZonedDateTimeISO("UTC").toPlainDate();
|
|
323
|
-
}
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
// Walk the driver-row, applying three boundary-conversions per known column:
|
|
327
|
-
// - rename key snake_case → camelCase JS field-name (drizzle did this
|
|
328
|
-
// invisibly via its column-mapper; native dialect rebuild lost it)
|
|
329
|
-
// - parse jsonb string → object (postgres-js returns jsonb as text by
|
|
330
|
-
// default, not parsed JSON)
|
|
331
|
-
// - coerce timestamptz Date/string → Temporal.Instant
|
|
332
|
-
//
|
|
333
|
-
// Unknown columns (computed/aliased) pass through unchanged.
|
|
334
137
|
export function coerceRow(row, info) {
|
|
335
138
|
// @cast-boundary db-row — the driver row (raw column names/types) becomes the caller's entity row shape here, and only here
|
|
336
139
|
return coerceDriverRow(row, info);
|
|
337
140
|
}
|
|
338
|
-
function coerceDriverRow(row, info) {
|
|
339
|
-
const out = {};
|
|
340
|
-
let changed = false;
|
|
341
|
-
for (const key of Object.keys(row)) {
|
|
342
|
-
// Blind-index columns (#818) are pure lookup artifacts — the
|
|
343
|
-
// deterministic HMAC must never leak into caller/API responses.
|
|
344
|
-
if ((key.endsWith("_bidx") || key.endsWith("Bidx")) && info.hasColumn(key)) {
|
|
345
|
-
changed = true;
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
const pgType = info.pgTypeOf(key);
|
|
349
|
-
const value = row[key];
|
|
350
|
-
let coerced = value;
|
|
351
|
-
if (pgType === "timestamptz" || pgType === "timestamptz(3)") {
|
|
352
|
-
const t = instantFromDriver(value);
|
|
353
|
-
if (t !== null)
|
|
354
|
-
coerced = t;
|
|
355
|
-
}
|
|
356
|
-
else if (pgType === "date") {
|
|
357
|
-
const d = plainDateFromDriver(value, key);
|
|
358
|
-
if (d !== null)
|
|
359
|
-
coerced = d;
|
|
360
|
-
}
|
|
361
|
-
else if (pgType === "jsonb" && typeof value === "string") {
|
|
362
|
-
coerced = parseJsonSafe(value, value);
|
|
363
|
-
}
|
|
364
|
-
else if ((pgType === "bigint" || pgType === "bigserial") &&
|
|
365
|
-
(typeof value === "string" || typeof value === "bigint")) {
|
|
366
|
-
const jsMode = info.bigintJsModeOf(key);
|
|
367
|
-
if (jsMode === "number") {
|
|
368
|
-
coerced = coerceBigIntFromDriver(value);
|
|
369
|
-
}
|
|
370
|
-
else if (typeof value === "string") {
|
|
371
|
-
try {
|
|
372
|
-
coerced = BigInt(value);
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
// leave as string on parse error
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
coerced = value;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
else if ((pgType === "integer" || pgType === "int4" || pgType === "smallint" || pgType === "int2") &&
|
|
383
|
-
(typeof value === "bigint" || typeof value === "string")) {
|
|
384
|
-
// Drivers may return int4 as bigint or numeric string.
|
|
385
|
-
// Drizzle coerced to number for numberField columns — match that.
|
|
386
|
-
const n = Number(value);
|
|
387
|
-
if (!Number.isNaN(n))
|
|
388
|
-
coerced = n;
|
|
389
|
-
}
|
|
390
|
-
else if (typeof pgType === "string" &&
|
|
391
|
-
pgType.startsWith("numeric") &&
|
|
392
|
-
(typeof value === "string" || typeof value === "bigint")) {
|
|
393
|
-
// pg returns numeric(p,s) as a string to preserve precision; the
|
|
394
|
-
// decimal-field contract surfaces it as JS number (safe ≤ 2^53). A
|
|
395
|
-
// non-numeric string would be DB corruption — leave it untouched so it
|
|
396
|
-
// surfaces loud downstream rather than becoming a silent NaN.
|
|
397
|
-
const n = Number(value);
|
|
398
|
-
if (!Number.isNaN(n))
|
|
399
|
-
coerced = n;
|
|
400
|
-
}
|
|
401
|
-
const fieldName = info.fieldOf(key);
|
|
402
|
-
if (fieldName !== key)
|
|
403
|
-
changed = true;
|
|
404
|
-
if (coerced !== value)
|
|
405
|
-
changed = true;
|
|
406
|
-
out[fieldName] = coerced;
|
|
407
|
-
}
|
|
408
|
-
return changed ? out : row;
|
|
409
|
-
}
|
|
410
141
|
function coerceRows(rows, info) {
|
|
411
142
|
return rows.map((r) => coerceRow(r, info));
|
|
412
143
|
}
|
|
413
|
-
// Checks the brand Symbol, not the `kind` string — a client-supplied jsonb
|
|
414
|
-
// value can fake `kind: "sql-expr"` over JSON but can never carry a Symbol,
|
|
415
|
-
// so request data can't be smuggled in as a raw SQL literal.
|
|
416
|
-
function isSqlExpression(v) {
|
|
417
|
-
return typeof v === "object" && v !== null && SQL_EXPR_BRAND in v;
|
|
418
|
-
}
|
|
419
|
-
// A `date` column takes a plain "yyyy-mm-dd" string (or PlainDate.toString())
|
|
420
|
-
// as-is — no Instant detour. Postgres parses a DATE literal without ever
|
|
421
|
-
// consulting the session timezone, so this is TZ-independent by construction
|
|
422
|
-
// (verified empirically — see plainDateFromDriver above for the read side).
|
|
423
|
-
// Compat cushion: pre-#1924 code (this field used to be an Instant) may still
|
|
424
|
-
// hand a Temporal.Instant into a where-filter or write on a `date` column —
|
|
425
|
-
// same UTC anchor as plainDateFromDriver's read side, so a filter built with
|
|
426
|
-
// `Temporal.Now.instant()` keeps working instead of binding a raw Instant the
|
|
427
|
-
// driver can't serialize.
|
|
428
|
-
function prepareDateValue(value) {
|
|
429
|
-
if (isTemporalPlainDate(value)) {
|
|
430
|
-
return { kind: "param", sql: "", bound: value.toString() };
|
|
431
|
-
}
|
|
432
|
-
if (isTemporalPlainDateTimeLike(value)) {
|
|
433
|
-
return { kind: "param", sql: "", bound: value.toPlainDate().toString() };
|
|
434
|
-
}
|
|
435
|
-
if (isTemporalInstant(value)) {
|
|
436
|
-
const day = value.toZonedDateTimeISO("UTC").toPlainDate();
|
|
437
|
-
return { kind: "param", sql: "", bound: day.toString() };
|
|
438
|
-
}
|
|
439
|
-
return undefined;
|
|
440
|
-
}
|
|
441
|
-
// structured values bind directly with an ::jsonb cast; JSON.stringify first
|
|
442
|
-
// would produce a JSON-string scalar instead of the structured value.
|
|
443
|
-
function prepareJsonbValue(value) {
|
|
444
|
-
if (typeof value === "boolean") {
|
|
445
|
-
return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
|
|
446
|
-
}
|
|
447
|
-
if (typeof value !== "object" || isTemporalInstant(value)) {
|
|
448
|
-
return undefined;
|
|
449
|
-
}
|
|
450
|
-
if (!Array.isArray(value)) {
|
|
451
|
-
return { kind: "param", sql: "::jsonb", bound: value };
|
|
452
|
-
}
|
|
453
|
-
// All-boolean arrays are inferred as boolean[] by postgres-js; route via text::jsonb.
|
|
454
|
-
if (value.length > 0 && value.every((entry) => typeof entry === "boolean")) {
|
|
455
|
-
return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
|
|
456
|
-
}
|
|
457
|
-
return { kind: "param", sql: "::jsonb", bound: value };
|
|
458
|
-
}
|
|
459
|
-
const INTEGER_PG_TYPES = new Set([
|
|
460
|
-
"integer",
|
|
461
|
-
"int4",
|
|
462
|
-
"smallint",
|
|
463
|
-
"int2",
|
|
464
|
-
"bigint",
|
|
465
|
-
"int8",
|
|
466
|
-
"serial",
|
|
467
|
-
"bigserial",
|
|
468
|
-
]);
|
|
469
|
-
// Postgres rounds sub-millisecond params into timestamptz(3); truncating to ms here keeps the
|
|
470
|
-
// stored instant identical across drivers and equal to what postgres.js sends via Date.
|
|
471
|
-
export function instantToTimestamptzParam(instant) {
|
|
472
|
-
return Temporal.Instant.fromEpochMilliseconds(instant.epochMilliseconds).toString();
|
|
473
|
-
}
|
|
474
|
-
function prepareDriverNeutralScalar(value, pgType) {
|
|
475
|
-
if ((pgType === "timestamptz" || pgType === "timestamptz(3)") && isTemporalInstant(value)) {
|
|
476
|
-
return { kind: "param", sql: "", bound: instantToTimestamptzParam(value) };
|
|
477
|
-
}
|
|
478
|
-
// postgres.js sends text, so Postgres rejects 3.5 for an integer column instead of rounding it.
|
|
479
|
-
if (typeof value === "number" && pgType !== undefined && INTEGER_PG_TYPES.has(pgType)) {
|
|
480
|
-
return { kind: "param", sql: "", bound: String(value) };
|
|
481
|
-
}
|
|
482
|
-
return undefined;
|
|
483
|
-
}
|
|
484
|
-
function prepareValue(value, pgType) {
|
|
485
|
-
if (isSqlExpression(value)) {
|
|
486
|
-
return { kind: "literal", literal: value.text };
|
|
487
|
-
}
|
|
488
|
-
// A PII-encrypted jsonb field is a JSON string scalar; a raw string bound to
|
|
489
|
-
// ::jsonb would be parsed as JSON text (or double-encoded).
|
|
490
|
-
if (pgType === "jsonb" && isPiiCiphertext(value)) {
|
|
491
|
-
return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
|
|
492
|
-
}
|
|
493
|
-
if (pgType === "jsonb" && value !== null) {
|
|
494
|
-
const prepared = prepareJsonbValue(value);
|
|
495
|
-
if (prepared)
|
|
496
|
-
return prepared;
|
|
497
|
-
}
|
|
498
|
-
const scalar = prepareDriverNeutralScalar(value, pgType);
|
|
499
|
-
if (scalar)
|
|
500
|
-
return scalar;
|
|
501
|
-
if (pgType === "date") {
|
|
502
|
-
const prepared = prepareDateValue(value);
|
|
503
|
-
if (prepared)
|
|
504
|
-
return prepared;
|
|
505
|
-
}
|
|
506
|
-
return { kind: "param", sql: "", bound: value };
|
|
507
|
-
}
|
|
508
|
-
function buildWhereClause(info, where, startIndex, qualifier) {
|
|
509
|
-
const conditions = [];
|
|
510
|
-
const values = [];
|
|
511
|
-
let idx = startIndex;
|
|
512
|
-
// Single place that renders a column reference, so a join qualifier reaches
|
|
513
|
-
// every arm (blind index, jsonb containment, IS NULL, jsonText) alike.
|
|
514
|
-
const ref = (column) => qualifier === undefined ? quoteIdent(column) : `${quoteIdent(qualifier)}.${quoteIdent(column)}`;
|
|
515
|
-
// multiSelect (and any other jsonb-array-of-scalars column) stores an
|
|
516
|
-
// array — a filter value is one option, not the whole array, so eq/ne/in
|
|
517
|
-
// must check array containment (`@>`) instead of scalar `=`/`<>`/`IN`
|
|
518
|
-
// against the jsonb column, which Postgres rejects outright (fw#2490).
|
|
519
|
-
// Mirrors event-store-executor-read.ts's applyFilter — keep in lock-step.
|
|
520
|
-
function jsonbContainsAny(col, candidates) {
|
|
521
|
-
if (candidates.length === 0)
|
|
522
|
-
return "FALSE";
|
|
523
|
-
const parts = [];
|
|
524
|
-
for (const v of candidates) {
|
|
525
|
-
const p = prepareJsonbValue([v]);
|
|
526
|
-
// prepareJsonbValue never returns kind:"literal" (only prepareValue's
|
|
527
|
-
// isSqlExpression branch does) — narrow anyway, PreparedValue is a union.
|
|
528
|
-
if (p && p.kind === "param") {
|
|
529
|
-
parts.push(`${ref(col)} @> $${idx++}${p.sql}`);
|
|
530
|
-
values.push(p.bound);
|
|
531
|
-
}
|
|
532
|
-
// A jsonb column holding the scalar itself is never `@>` an array of it.
|
|
533
|
-
if (isJsonbScalar(v)) {
|
|
534
|
-
parts.push(`${ref(col)} = $${idx++}::text::jsonb`);
|
|
535
|
-
values.push(JSON.stringify(v));
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return parts.length > 0 ? `(${parts.join(" OR ")})` : "FALSE";
|
|
539
|
-
}
|
|
540
|
-
function isJsonbScalar(value) {
|
|
541
|
-
return value !== null && typeof value !== "object";
|
|
542
|
-
}
|
|
543
|
-
for (const [field, value] of Object.entries(where)) {
|
|
544
|
-
const col = info.columnOf(field);
|
|
545
|
-
const pgType = info.pgTypeOf(col);
|
|
546
|
-
if (value === null) {
|
|
547
|
-
conditions.push(`${ref(col)} IS NULL`);
|
|
548
|
-
}
|
|
549
|
-
else if (Array.isArray(value)) {
|
|
550
|
-
if (value.length === 0) {
|
|
551
|
-
conditions.push("FALSE");
|
|
552
|
-
}
|
|
553
|
-
else if (pgType === "jsonb") {
|
|
554
|
-
conditions.push(jsonbContainsAny(col, value));
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
const parts = [];
|
|
558
|
-
for (const v of value) {
|
|
559
|
-
const p = prepareValue(v, pgType);
|
|
560
|
-
if (p.kind === "literal") {
|
|
561
|
-
parts.push(p.literal);
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
parts.push(`$${idx++}${p.sql}`);
|
|
565
|
-
values.push(p.bound);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
conditions.push(`${ref(col)} IN (${parts.join(", ")})`);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
else if (isWhereOperator(value)) {
|
|
572
|
-
if (value.jsonText !== undefined) {
|
|
573
|
-
if (Object.keys(value).length > 1) {
|
|
574
|
-
throw new Error(`where: jsonText on column "${col}" cannot be combined with other operators`);
|
|
575
|
-
}
|
|
576
|
-
if (pgType !== "jsonb") {
|
|
577
|
-
throw new Error(`where: jsonText requires a jsonb column, "${col}" is ${pgType}`);
|
|
578
|
-
}
|
|
579
|
-
const matches = isJsonTextMatchList(value.jsonText) ? value.jsonText : [value.jsonText];
|
|
580
|
-
if (matches.length === 0) {
|
|
581
|
-
conditions.push("FALSE");
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
for (const match of matches) {
|
|
585
|
-
if (match === null ||
|
|
586
|
-
typeof match !== "object" ||
|
|
587
|
-
!Array.isArray(match.keys) ||
|
|
588
|
-
match.keys.length === 0 ||
|
|
589
|
-
!match.keys.every((k) => typeof k === "string") ||
|
|
590
|
-
typeof match.eq !== "string") {
|
|
591
|
-
throw new Error(`where: jsonText on column "${col}" needs non-empty string keys and a string eq`);
|
|
592
|
-
}
|
|
593
|
-
const lookups = match.keys.map((key) => {
|
|
594
|
-
values.push(key);
|
|
595
|
-
return `${ref(col)}->>$${idx++}::text`;
|
|
596
|
-
});
|
|
597
|
-
const lhs = lookups.length === 1 ? lookups[0] : `COALESCE(${lookups.join(", ")})`;
|
|
598
|
-
values.push(match.eq);
|
|
599
|
-
conditions.push(`(${lhs} = $${idx++})`);
|
|
600
|
-
}
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
if (value.ne === null && Object.keys(value).length === 1) {
|
|
604
|
-
conditions.push(`${ref(col)} IS NOT NULL`);
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
const opMap = {
|
|
608
|
-
gt: ">",
|
|
609
|
-
gte: ">=",
|
|
610
|
-
lt: "<",
|
|
611
|
-
lte: "<=",
|
|
612
|
-
ne: "<>",
|
|
613
|
-
like: "LIKE",
|
|
614
|
-
};
|
|
615
|
-
for (const [opKey, opSym] of Object.entries(opMap)) {
|
|
616
|
-
const opVal = Reflect.get(value, opKey);
|
|
617
|
-
if (opVal === undefined)
|
|
618
|
-
continue;
|
|
619
|
-
// Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
|
|
620
|
-
// option array (`like` would crash as `jsonb ~~ jsonb`), so match nothing.
|
|
621
|
-
if (pgType === "jsonb" && opKey !== "ne") {
|
|
622
|
-
conditions.push("FALSE");
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
// Mirrors applyMultiSelectFilter: an array `ne` means "does not contain all of these".
|
|
626
|
-
if (opKey === "ne" && pgType === "jsonb" && Array.isArray(opVal)) {
|
|
627
|
-
if (opVal.length === 0) {
|
|
628
|
-
conditions.push("FALSE");
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
|
-
const p = prepareJsonbValue(opVal);
|
|
632
|
-
if (p && p.kind === "param") {
|
|
633
|
-
conditions.push(`(${ref(col)} IS NULL OR NOT (${ref(col)} @> $${idx++}${p.sql}))`);
|
|
634
|
-
values.push(p.bound);
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (opKey === "ne" && pgType === "jsonb" && isJsonbScalar(opVal)) {
|
|
639
|
-
const p = prepareJsonbValue([opVal]);
|
|
640
|
-
if (p && p.kind === "param") {
|
|
641
|
-
conditions.push(`(${ref(col)} IS NULL OR NOT (${ref(col)} @> $${idx++}${p.sql} OR ${ref(col)} = $${idx++}::text::jsonb))`);
|
|
642
|
-
values.push(p.bound, JSON.stringify(opVal));
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
const p = prepareValue(opVal, pgType);
|
|
647
|
-
if (p.kind === "literal") {
|
|
648
|
-
conditions.push(`${ref(col)} ${opSym} ${p.literal}`);
|
|
649
|
-
}
|
|
650
|
-
else {
|
|
651
|
-
conditions.push(`${ref(col)} ${opSym} $${idx++}${p.sql}`);
|
|
652
|
-
values.push(p.bound);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
const inVal = value.in;
|
|
656
|
-
if (Array.isArray(inVal)) {
|
|
657
|
-
if (pgType === "jsonb") {
|
|
658
|
-
conditions.push(jsonbContainsAny(col, inVal));
|
|
659
|
-
}
|
|
660
|
-
else if (inVal.length === 0) {
|
|
661
|
-
conditions.push("FALSE");
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
const parts = [];
|
|
665
|
-
for (const v of inVal) {
|
|
666
|
-
const p = prepareValue(v, pgType);
|
|
667
|
-
if (p.kind === "literal") {
|
|
668
|
-
parts.push(p.literal);
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
parts.push(`$${idx++}${p.sql}`);
|
|
672
|
-
values.push(p.bound);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
conditions.push(`${ref(col)} IN (${parts.join(", ")})`);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
const notInVal = value.notIn;
|
|
679
|
-
if (Array.isArray(notInVal)) {
|
|
680
|
-
if (pgType === "jsonb") {
|
|
681
|
-
throw new Error(`where: notIn is not supported on jsonb column "${col}"`);
|
|
682
|
-
}
|
|
683
|
-
const parts = [];
|
|
684
|
-
for (const v of notInVal) {
|
|
685
|
-
const p = prepareValue(v, pgType);
|
|
686
|
-
if (p.kind === "literal") {
|
|
687
|
-
parts.push(p.literal);
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
parts.push(`$${idx++}${p.sql}`);
|
|
691
|
-
values.push(p.bound);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
conditions.push(parts.length === 0 ? "TRUE" : `${ref(col)} NOT IN (${parts.join(", ")})`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
else if (pgType === "jsonb" && isJsonbScalar(value)) {
|
|
698
|
-
conditions.push(jsonbContainsAny(col, [value]));
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
const p = prepareValue(value, pgType);
|
|
702
|
-
if (p.kind === "literal") {
|
|
703
|
-
conditions.push(`${ref(col)} = ${p.literal}`);
|
|
704
|
-
}
|
|
705
|
-
else {
|
|
706
|
-
// Blind-index OR rewrite (#818): if the column has a bidx
|
|
707
|
-
// counterpart (suffix convention `<col>_bidx`, framework-reserved)
|
|
708
|
-
// and a key is configured, equality matches both arms —
|
|
709
|
-
// plaintext legacy rows via the plaintext arm, encrypted rows
|
|
710
|
-
// via the HMAC. The `kumiko-pii:` prefix on ciphertexts makes
|
|
711
|
-
// a random match on the plaintext arm practically impossible.
|
|
712
|
-
const bidxKey = configuredBlindIndexKey();
|
|
713
|
-
const bidxCol = `${col}_bidx`;
|
|
714
|
-
if (bidxKey !== undefined &&
|
|
715
|
-
typeof value === "string" &&
|
|
716
|
-
!col.endsWith("_bidx") &&
|
|
717
|
-
info.hasColumn(bidxCol)) {
|
|
718
|
-
conditions.push(`(${ref(col)} = $${idx++} OR ${ref(bidxCol)} = $${idx++})`);
|
|
719
|
-
values.push(p.bound, computeBlindIndex(bidxKey, value));
|
|
720
|
-
}
|
|
721
|
-
else {
|
|
722
|
-
conditions.push(`${ref(col)} = $${idx++}${p.sql}`);
|
|
723
|
-
values.push(p.bound);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
return { sqlText: conditions.join(" AND "), values };
|
|
729
|
-
}
|
|
730
|
-
// A pool can briefly keep handing out connections the server just closed.
|
|
731
|
-
// Each failed attempt discards one, so retries are bounded by pool size.
|
|
732
|
-
const CLOSED_CONNECTION_CODES = new Set([
|
|
733
|
-
"CONNECTION_CLOSED", // postgres-js
|
|
734
|
-
"57P01", // PG SQLSTATE: admin_shutdown / terminated backend
|
|
735
|
-
]);
|
|
736
|
-
export function isClosedConnectionError(err) {
|
|
737
|
-
const code = extractPgError(err)?.code;
|
|
738
|
-
return code !== undefined && CLOSED_CONNECTION_CODES.has(code);
|
|
739
|
-
}
|
|
740
|
-
// Tx/reserved handles are pinned to one physical connection — a retry there
|
|
741
|
-
// would run on the same dead connection, not swap in a fresh one.
|
|
742
|
-
function isPooledClient(raw) {
|
|
743
|
-
if (raw === null || (typeof raw !== "object" && typeof raw !== "function"))
|
|
744
|
-
return false;
|
|
745
|
-
return (typeof propertyOf(raw, "begin") === "function" &&
|
|
746
|
-
typeof propertyOf(raw, "savepoint") !== "function" &&
|
|
747
|
-
typeof propertyOf(raw, "release") !== "function");
|
|
748
|
-
}
|
|
749
|
-
const closedConnectionLog = createFallbackLogger("postgres-js");
|
|
750
|
-
// The driver rejects in-flight queries with this AbortError (no code) when the
|
|
751
|
-
// pool closes under them, so isClosedConnectionError can't see it.
|
|
752
|
-
const DRIVER_CLOSED_ABORT_MESSAGE = "The connection was closed.";
|
|
753
|
-
function isDriverClosedAbort(err) {
|
|
754
|
-
return (err instanceof Error && err.name === "AbortError" && err.message === DRIVER_CLOSED_ABORT_MESSAGE);
|
|
755
|
-
}
|
|
756
|
-
function isHandleLike(raw) {
|
|
757
|
-
return raw !== null && (typeof raw === "object" || typeof raw === "function");
|
|
758
|
-
}
|
|
759
|
-
// postgres-js pool and tx handles both carry `typed`.
|
|
760
|
-
function driverOf(raw) {
|
|
761
|
-
if (!isHandleLike(raw))
|
|
762
|
-
return "unknown";
|
|
763
|
-
if (typeof raw["typed"] === "function")
|
|
764
|
-
return "postgres-js";
|
|
765
|
-
return "unknown";
|
|
766
|
-
}
|
|
767
|
-
function positiveNumber(v) {
|
|
768
|
-
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
769
|
-
}
|
|
770
|
-
// postgres-js configures timeouts in seconds.
|
|
771
|
-
function timeoutOptionMs(options, driver, optionName) {
|
|
772
|
-
if (driver !== "postgres-js")
|
|
773
|
-
return undefined;
|
|
774
|
-
const seconds = positiveNumber(options[optionName]);
|
|
775
|
-
return seconds === undefined ? undefined : seconds * 1000;
|
|
776
|
-
}
|
|
777
|
-
function errorProp(err, name) {
|
|
778
|
-
return isHandleLike(err) ? err[name] : undefined;
|
|
779
|
-
}
|
|
780
|
-
// Deliberately excludes SQL text and parameters: app SQL can carry PII.
|
|
781
|
-
export function describeClosedConnectionFailure(raw, err, attempts) {
|
|
782
|
-
if (!isClosedConnectionError(err) && !isDriverClosedAbort(err))
|
|
783
|
-
return undefined;
|
|
784
|
-
const driver = driverOf(raw);
|
|
785
|
-
const rawOptions = isHandleLike(raw) ? raw["options"] : undefined;
|
|
786
|
-
const options = isHandleLike(rawOptions) ? rawOptions : {};
|
|
787
|
-
return {
|
|
788
|
-
driver,
|
|
789
|
-
pooled: isPooledClient(raw),
|
|
790
|
-
attempts,
|
|
791
|
-
poolMax: positiveNumber(options["max"]),
|
|
792
|
-
idleTimeoutMs: timeoutOptionMs(options, driver, "idle_timeout"),
|
|
793
|
-
maxLifetimeMs: timeoutOptionMs(options, driver, "max_lifetime"),
|
|
794
|
-
connectionTimeoutMs: timeoutOptionMs(options, driver, "connect_timeout"),
|
|
795
|
-
errorName: errorProp(err, "name"),
|
|
796
|
-
errorCode: extractPgError(err)?.code ?? errorProp(err, "code"),
|
|
797
|
-
errorErrno: errorProp(err, "errno"),
|
|
798
|
-
errorMessage: errorProp(err, "message"),
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
function logClosedConnection(raw, err, attempts, context) {
|
|
802
|
-
const failure = describeClosedConnectionFailure(raw, err, attempts);
|
|
803
|
-
// skip: not a closed-connection error, nothing to diagnose
|
|
804
|
-
if (!failure)
|
|
805
|
-
return;
|
|
806
|
-
// Request identity lets this line be joined to the route's `handler failed` log.
|
|
807
|
-
const request = requestContext.get();
|
|
808
|
-
closedConnectionLog.warn("closed connection surfaced", {
|
|
809
|
-
...failure,
|
|
810
|
-
...context,
|
|
811
|
-
requestId: request?.requestId,
|
|
812
|
-
correlationId: request?.correlationId,
|
|
813
|
-
handler: request?.handler,
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
144
|
// Write and other non-retried paths: log when a closed-connection error leaves
|
|
817
145
|
// the db layer, rethrow unchanged.
|
|
818
146
|
async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
|
|
@@ -825,11 +153,6 @@ async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
|
|
|
825
153
|
throw err;
|
|
826
154
|
}
|
|
827
155
|
}
|
|
828
|
-
const DEFAULT_POOL_MAX = 10;
|
|
829
|
-
function poolMaxOf(raw) {
|
|
830
|
-
const max = propertyOf(propertyOf(raw, "options"), "max");
|
|
831
|
-
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_POOL_MAX;
|
|
832
|
-
}
|
|
833
156
|
// Exported so raw-SQL query modules outside the db layer (e.g. bundled-features'
|
|
834
157
|
// db/queries/*.ts) can opt into the same closed-connection retry instead of
|
|
835
158
|
// calling asRawClient(db).unsafe(...) directly and losing it.
|
|
@@ -1191,82 +514,6 @@ export async function countWhere(db, table, where = {}) {
|
|
|
1191
514
|
const rows = await unsafeReadRetrying(db, sqlText, values);
|
|
1192
515
|
return rows[0]?.count ?? 0;
|
|
1193
516
|
}
|
|
1194
|
-
const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
|
|
1195
|
-
const AGGREGATE_ORDER_DIRECTIONS = ["asc", "desc"];
|
|
1196
|
-
function requireAggregateColumn(info, field, role) {
|
|
1197
|
-
if (!info.hasColumn(field)) {
|
|
1198
|
-
throw new Error(`aggregateWhere: ${role} "${field}" is not a column of ${info.name}`);
|
|
1199
|
-
}
|
|
1200
|
-
return quoteIdent(info.columnOf(field));
|
|
1201
|
-
}
|
|
1202
|
-
export function isTimestamptzType(pgType) {
|
|
1203
|
-
return pgType === "timestamptz" || pgType === "timestamptz(3)";
|
|
1204
|
-
}
|
|
1205
|
-
function aggregateKeyOf(raw) {
|
|
1206
|
-
if (raw === null || raw === undefined)
|
|
1207
|
-
return null;
|
|
1208
|
-
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean")
|
|
1209
|
-
return raw;
|
|
1210
|
-
if (typeof raw === "bigint")
|
|
1211
|
-
return Number(raw);
|
|
1212
|
-
if (raw instanceof Date)
|
|
1213
|
-
return raw.toISOString();
|
|
1214
|
-
return String(raw);
|
|
1215
|
-
}
|
|
1216
|
-
function aggregateMeasureSql(info, measure) {
|
|
1217
|
-
switch (measure.fn) {
|
|
1218
|
-
case "count":
|
|
1219
|
-
return "COUNT(*)";
|
|
1220
|
-
case "countDistinct":
|
|
1221
|
-
return `COUNT(DISTINCT ${requireAggregateColumn(info, measure.field, "measure field")})`;
|
|
1222
|
-
case "sum":
|
|
1223
|
-
return `COALESCE(SUM(${requireAggregateColumn(info, measure.field, "measure field")}), 0)`;
|
|
1224
|
-
case "avg":
|
|
1225
|
-
return `AVG(${requireAggregateColumn(info, measure.field, "measure field")})`;
|
|
1226
|
-
default:
|
|
1227
|
-
throw new Error(`aggregateWhere: unknown measure fn "${String(Reflect.get(measure, "fn"))}"`);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
function aggregateDimensionSql(info, dimension, index, params) {
|
|
1231
|
-
const column = requireAggregateColumn(info, dimension.field, "groupBy field");
|
|
1232
|
-
if (!("bucket" in dimension))
|
|
1233
|
-
return `${column} AS k${index}`;
|
|
1234
|
-
if (!AGGREGATE_BUCKET_UNITS.includes(dimension.bucket)) {
|
|
1235
|
-
throw new Error(`aggregateWhere: unknown bucket "${String(dimension.bucket)}"`);
|
|
1236
|
-
}
|
|
1237
|
-
if (!isTimestamptzType(info.pgTypeOf(info.columnOf(dimension.field)))) {
|
|
1238
|
-
throw new Error(`aggregateWhere: bucket on "${dimension.field}" requires a timestamptz column`);
|
|
1239
|
-
}
|
|
1240
|
-
params.push(dimension.timeZone);
|
|
1241
|
-
return `(EXTRACT(EPOCH FROM date_trunc('${dimension.bucket}', ${column}, $${params.length}::text)) * 1000)::float8 AS k${index}`;
|
|
1242
|
-
}
|
|
1243
|
-
function assertValidAggregateLimits(spec) {
|
|
1244
|
-
if (spec.limit !== undefined && (!Number.isInteger(spec.limit) || spec.limit < 0)) {
|
|
1245
|
-
throw new Error(`aggregateWhere: limit must be a non-negative integer, got ${spec.limit}`);
|
|
1246
|
-
}
|
|
1247
|
-
if (spec.orderByValue !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByValue)) {
|
|
1248
|
-
throw new Error(`aggregateWhere: unknown orderByValue "${String(spec.orderByValue)}"`);
|
|
1249
|
-
}
|
|
1250
|
-
if (spec.orderByKeys !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByKeys)) {
|
|
1251
|
-
throw new Error(`aggregateWhere: unknown orderByKeys "${String(spec.orderByKeys)}"`);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
function aggregateGroupOrderLimitSql(spec, dimensionCount) {
|
|
1255
|
-
let sqlText = "";
|
|
1256
|
-
if (dimensionCount > 0) {
|
|
1257
|
-
const positions = Array.from({ length: dimensionCount }, (_, index) => String(index + 1));
|
|
1258
|
-
sqlText += ` GROUP BY ${positions.join(", ")}`;
|
|
1259
|
-
const keyDirection = spec.orderByKeys === "desc" ? "DESC" : "ASC";
|
|
1260
|
-
const keyParts = positions.map((position) => `${position} ${keyDirection}`);
|
|
1261
|
-
const orderParts = spec.orderByValue === undefined
|
|
1262
|
-
? keyParts
|
|
1263
|
-
: [`${dimensionCount + 1} ${spec.orderByValue === "desc" ? "DESC" : "ASC"}`, ...keyParts];
|
|
1264
|
-
sqlText += ` ORDER BY ${orderParts.join(", ")}`;
|
|
1265
|
-
}
|
|
1266
|
-
if (spec.limit !== undefined)
|
|
1267
|
-
sqlText += ` LIMIT ${spec.limit}`;
|
|
1268
|
-
return sqlText;
|
|
1269
|
-
}
|
|
1270
517
|
export async function aggregateWhere(db, table, spec, where = {}) {
|
|
1271
518
|
const scoped = tenantDbDelegate(db);
|
|
1272
519
|
if (scoped) {
|