@cosmicdrift/kumiko-framework 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -1
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +206 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +299 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +101 -0
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +61 -2
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
package/dist/db/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Typed query helpers over the
|
|
1
|
+
// Typed query helpers over the Bun.SQL driver: EntityTableMeta in, rows out.
|
|
2
2
|
//
|
|
3
3
|
// API:
|
|
4
4
|
// selectMany<T>(db, table, where?, opts?) → readonly T[]
|
|
@@ -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`/…
|
|
@@ -53,9 +39,9 @@ function isRawClient(value) {
|
|
|
53
39
|
return typeof propertyOf(value, "unsafe") === "function";
|
|
54
40
|
}
|
|
55
41
|
export function asRawClient(db) {
|
|
56
|
-
// Direct:
|
|
57
|
-
// expose `.unsafe`; only
|
|
58
|
-
// `.savepoint` for
|
|
42
|
+
// Direct: Bun.SQL pool / transaction handle. Both
|
|
43
|
+
// expose `.unsafe`; only the pool starts a transaction — a transaction handle uses
|
|
44
|
+
// `.savepoint` for nesting. We only require `.unsafe` here; callers that
|
|
59
45
|
// need `.begin` (e.g. `transaction()`) verify it themselves.
|
|
60
46
|
if (isRawClient(db))
|
|
61
47
|
return db;
|
|
@@ -66,15 +52,15 @@ export function asRawClient(db) {
|
|
|
66
52
|
"tenant filter — use a ctx.db.<method>, or ctx.db.unsafeRaw() with escapeHatch: " +
|
|
67
53
|
"{ reason } on the handler/hook (r.systemScope() features: ctx.systemDb.unsafeRaw(reason)).");
|
|
68
54
|
}
|
|
69
|
-
// Wrapper handle: $client =
|
|
55
|
+
// Wrapper handle: $client = Bun.SQL pool.
|
|
70
56
|
const $client = propertyOf(db, "$client");
|
|
71
57
|
if (isRawClient($client))
|
|
72
58
|
return $client;
|
|
73
|
-
// Wrapper transaction handle: session.client =
|
|
59
|
+
// Wrapper transaction handle: session.client = Bun.SQL pool.
|
|
74
60
|
const sessionClient = propertyOf(propertyOf(db, "session"), "client");
|
|
75
61
|
if (isRawClient(sessionClient))
|
|
76
62
|
return sessionClient;
|
|
77
|
-
throw new Error("asRawClient: db argument has no .unsafe() — pass a
|
|
63
|
+
throw new Error("asRawClient: db argument has no .unsafe() — pass a Bun.SQL pool or a transaction handle.");
|
|
78
64
|
}
|
|
79
65
|
export function runnerCanBeginTransaction(db) {
|
|
80
66
|
return hasBegin(asRawClient(db));
|
|
@@ -86,10 +72,31 @@ function savepointRunnerOf(sp) {
|
|
|
86
72
|
export async function runInSavepoint(tx, fn) {
|
|
87
73
|
const raw = asRawClient(tx);
|
|
88
74
|
if (typeof raw.savepoint !== "function") {
|
|
89
|
-
throw new Error("runInSavepoint: transaction handle has no savepoint() — pass the driver tx (
|
|
75
|
+
throw new Error("runInSavepoint: transaction handle has no savepoint() — pass the driver tx (Bun.SQL TransactionSQL).");
|
|
90
76
|
}
|
|
91
77
|
return raw.savepoint((sp) => fn(savepointRunnerOf(sp)));
|
|
92
78
|
}
|
|
79
|
+
// Same error-confinement as runInSavepoint, but for call sites that don't
|
|
80
|
+
// know whether `db` is a bare pool connection or an active transaction
|
|
81
|
+
// (e.g. the CRUD executor, invoked both from a dispatcher tx and directly
|
|
82
|
+
// against the pool by seeds/tests). A pool connection has no savepoint() —
|
|
83
|
+
// and doesn't need one, since each statement there is its own auto-committed
|
|
84
|
+
// unit and a failed statement can't poison anything downstream.
|
|
85
|
+
//
|
|
86
|
+
// `.savepoint` being present isn't proof the transaction is still open: a
|
|
87
|
+
// committed transaction handle still exposes `.savepoint`, and calling it
|
|
88
|
+
// fails (Bun.SQL: ERR_POSTGRES_INVALID_TRANSACTION_STATE, Postgres: 25P01)
|
|
89
|
+
// since there's no BEGIN left to nest into. That is now a programming error, not something
|
|
90
|
+
// to paper over by re-running `fn` without the savepoint's isolation.
|
|
91
|
+
const CONNECTION_CLOSED_CODE = "ERR_POSTGRES_CONNECTION_CLOSED";
|
|
92
|
+
const SAVEPOINT_ON_COMMITTED_CODES = new Set([
|
|
93
|
+
"25P01",
|
|
94
|
+
"ERR_POSTGRES_INVALID_TRANSACTION_STATE",
|
|
95
|
+
]);
|
|
96
|
+
function isSavepointOnDeadTransactionCode(code) {
|
|
97
|
+
return (code !== undefined &&
|
|
98
|
+
(code === CONNECTION_CLOSED_CODE || SAVEPOINT_ON_COMMITTED_CODES.has(code)));
|
|
99
|
+
}
|
|
93
100
|
export async function runInSavepointIfSupported(db, fn) {
|
|
94
101
|
const raw = asRawClient(db);
|
|
95
102
|
if (typeof raw.savepoint !== "function")
|
|
@@ -104,17 +111,17 @@ export async function runInSavepointIfSupported(db, fn) {
|
|
|
104
111
|
}
|
|
105
112
|
catch (e) {
|
|
106
113
|
const code = extractPgError(e)?.code;
|
|
107
|
-
if (!fnStarted && (code
|
|
114
|
+
if (!fnStarted && isSavepointOnDeadTransactionCode(code)) {
|
|
108
115
|
throw new InternalError({
|
|
109
|
-
message: code ===
|
|
110
|
-
? "runInSavepointIfSupported:
|
|
111
|
-
"this is almost always database work in an afterCommit hook. afterCommit is the default " +
|
|
112
|
-
"phase of r.hook/postSave/postDelete. Use HookPhases.inTransaction for writes that must " +
|
|
113
|
-
"roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback."
|
|
114
|
-
: "runInSavepointIfSupported: the transaction handle's connection is gone — the transaction " +
|
|
116
|
+
message: code === CONNECTION_CLOSED_CODE
|
|
117
|
+
? "runInSavepointIfSupported: the transaction handle's connection is gone — the transaction " +
|
|
115
118
|
"already committed (almost always database work in an afterCommit hook) or the connection dropped. " +
|
|
116
119
|
"afterCommit is the default phase of r.hook/postSave/postDelete. Use HookPhases.inTransaction " +
|
|
117
|
-
"for writes that must roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback."
|
|
120
|
+
"for writes that must roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback."
|
|
121
|
+
: "runInSavepointIfSupported: savepoint on a transaction that has already committed — " +
|
|
122
|
+
"this is almost always database work in an afterCommit hook. afterCommit is the default " +
|
|
123
|
+
"phase of r.hook/postSave/postDelete. Use HookPhases.inTransaction for writes that must " +
|
|
124
|
+
"roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback.",
|
|
118
125
|
cause: e instanceof Error ? e : undefined,
|
|
119
126
|
});
|
|
120
127
|
}
|
|
@@ -145,677 +152,20 @@ function assertNotTenantScoped(db, fnName) {
|
|
|
145
152
|
if (tenantDbDelegate(db) !== undefined) {
|
|
146
153
|
throw new Error(`${fnName}: received a tenant-scoped db but this helper does not apply the tenant filter. ` +
|
|
147
154
|
"Pass ctx.db.unsafeRaw() (with escapeHatch: { reason } on the handler/hook) for an " +
|
|
148
|
-
"explicit cross-tenant op, or use a scoped TenantDb method
|
|
149
|
-
|
|
150
|
-
}
|
|
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().");
|
|
155
|
+
"explicit cross-tenant op, or use a scoped TenantDb method " +
|
|
156
|
+
"(for an upsert: ctx.db.upsert(table, values, { conflictTarget })).");
|
|
182
157
|
}
|
|
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
158
|
}
|
|
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: 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
159
|
export function coerceRow(row, info) {
|
|
335
160
|
// @cast-boundary db-row — the driver row (raw column names/types) becomes the caller's entity row shape here, and only here
|
|
336
161
|
return coerceDriverRow(row, info);
|
|
337
162
|
}
|
|
338
|
-
function coerceDriverRow(row, info) {
|
|
339
|
-
const out = {};
|
|
340
|
-
let changed = false;
|
|
341
|
-
for (const key of Object.keys(row)) {
|
|
342
|
-
// Blind-index columns 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
163
|
function coerceRows(rows, info) {
|
|
411
164
|
return rows.map((r) => coerceRow(r, info));
|
|
412
165
|
}
|
|
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: 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.
|
|
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: 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
166
|
// Write and other non-retried paths: log when a closed-connection error leaves
|
|
817
167
|
// the db layer, rethrow unchanged.
|
|
818
|
-
async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
|
|
168
|
+
export async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
|
|
819
169
|
const raw = asRawClient(db);
|
|
820
170
|
try {
|
|
821
171
|
return await raw.unsafe(sqlText, params);
|
|
@@ -825,11 +175,6 @@ async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
|
|
|
825
175
|
throw err;
|
|
826
176
|
}
|
|
827
177
|
}
|
|
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
178
|
// Exported so raw-SQL query modules outside the db layer (e.g. bundled-features'
|
|
834
179
|
// db/queries/*.ts) can opt into the same closed-connection retry instead of
|
|
835
180
|
// calling asRawClient(db).unsafe(...) directly and losing it.
|
|
@@ -1130,14 +475,14 @@ export async function deleteMany(db, table, where) {
|
|
|
1130
475
|
operation: "deleteMany",
|
|
1131
476
|
table: info.name,
|
|
1132
477
|
});
|
|
1133
|
-
//
|
|
478
|
+
// Bun.SQL reports the affected-row count of a DELETE on the result, no RETURNING needed.
|
|
1134
479
|
const count = propertyOf(result, "count");
|
|
1135
480
|
if (typeof count !== "number") {
|
|
1136
481
|
throw new Error(`deleteMany(${info.name}): the driver result carries no affected-row count`);
|
|
1137
482
|
}
|
|
1138
483
|
return count;
|
|
1139
484
|
}
|
|
1140
|
-
function insertEntries(info, values) {
|
|
485
|
+
export function insertEntries(info, values) {
|
|
1141
486
|
return Object.entries(values)
|
|
1142
487
|
.filter(([k]) => info.hasColumn(k))
|
|
1143
488
|
.map(([k, v]) => {
|
|
@@ -1163,7 +508,31 @@ export function resolveConflictColumns(table, info, conflictKeys) {
|
|
|
1163
508
|
return [info.columnOf("id")];
|
|
1164
509
|
throw new Error("upsert: cannot infer conflict keys — pass conflictKeys explicitly");
|
|
1165
510
|
}
|
|
1166
|
-
|
|
511
|
+
// Appends bound values to `params`; `skipCols` only applies to the default EXCLUDED set.
|
|
512
|
+
export function conflictUpdateParts(info, entries, update, skipCols, params) {
|
|
513
|
+
const parts = [];
|
|
514
|
+
if (update === undefined) {
|
|
515
|
+
for (const e of entries) {
|
|
516
|
+
if (skipCols.has(e.col))
|
|
517
|
+
continue;
|
|
518
|
+
parts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
|
|
519
|
+
}
|
|
520
|
+
return parts;
|
|
521
|
+
}
|
|
522
|
+
for (const [field, value] of Object.entries(update)) {
|
|
523
|
+
const col = info.columnOf(field);
|
|
524
|
+
const p = prepareValue(value, info.pgTypeOf(col));
|
|
525
|
+
if (p.kind === "literal") {
|
|
526
|
+
parts.push(`${quoteIdent(col)} = ${p.literal}`);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
params.push(p.bound);
|
|
530
|
+
parts.push(`${quoteIdent(col)} = $${params.length}${p.sql}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return parts;
|
|
534
|
+
}
|
|
535
|
+
export function buildInsertSql(info, entries) {
|
|
1167
536
|
if (entries.length === 0)
|
|
1168
537
|
throw new Error("insert: empty values object");
|
|
1169
538
|
const cols = entries.map((e) => quoteIdent(e.col)).join(", ");
|
|
@@ -1191,82 +560,6 @@ export async function countWhere(db, table, where = {}) {
|
|
|
1191
560
|
const rows = await unsafeReadRetrying(db, sqlText, values);
|
|
1192
561
|
return rows[0]?.count ?? 0;
|
|
1193
562
|
}
|
|
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
563
|
export async function aggregateWhere(db, table, spec, where = {}) {
|
|
1271
564
|
const scoped = tenantDbDelegate(db);
|
|
1272
565
|
if (scoped) {
|
|
@@ -1309,28 +602,7 @@ async function upsertOnConflictChecked(db, table, values, options) {
|
|
|
1309
602
|
const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
|
|
1310
603
|
const conflictSet = new Set(conflictCols);
|
|
1311
604
|
const { sqlPrefix, params } = buildInsertSql(info, entries);
|
|
1312
|
-
const updateParts =
|
|
1313
|
-
let idx = params.length + 1;
|
|
1314
|
-
if (options.update !== undefined) {
|
|
1315
|
-
for (const [field, value] of Object.entries(options.update)) {
|
|
1316
|
-
const col = info.columnOf(field);
|
|
1317
|
-
const p = prepareValue(value, info.pgTypeOf(col));
|
|
1318
|
-
if (p.kind === "literal") {
|
|
1319
|
-
updateParts.push(`${quoteIdent(col)} = ${p.literal}`);
|
|
1320
|
-
}
|
|
1321
|
-
else {
|
|
1322
|
-
updateParts.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
|
|
1323
|
-
params.push(p.bound);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
else {
|
|
1328
|
-
for (const e of entries) {
|
|
1329
|
-
if (conflictSet.has(e.col))
|
|
1330
|
-
continue;
|
|
1331
|
-
updateParts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
605
|
+
const updateParts = conflictUpdateParts(info, entries, options.update, conflictSet, params);
|
|
1334
606
|
if (updateParts.length === 0) {
|
|
1335
607
|
throw new Error("upsertOnConflict: nothing to update on conflict");
|
|
1336
608
|
}
|