@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
|
@@ -53,10 +53,10 @@ export function expireLegacySessionCookieVariants(c, cookies) {
|
|
|
53
53
|
// skip: the unprefixed names are the only legacy variant when the config writes __Host- names.
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
56
|
+
// Sent even with secureCookies:false: TLS may still terminate at a proxy, and over plain HTTP the
|
|
57
|
+
// browser drops the Secure delete without effect.
|
|
58
|
+
deleteCookie(c, sessionCookieName(AUTH_COOKIE_BASE, { secure: true }), hostPrefixedDelete);
|
|
59
|
+
deleteCookie(c, sessionCookieName(CSRF_COOKIE_BASE, { secure: true }), hostPrefixedDelete);
|
|
60
60
|
}
|
|
61
61
|
const hostPrefixedDelete = { path: "/", secure: true };
|
|
62
62
|
// Expire every variant: the Domain cookie (current) and the host-only one
|
package/dist/api/sse-route.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import type { AccessRule } from "../engine/types/index.ts";
|
|
2
|
+
import type { AccessRule, OwnershipMap } from "../engine/types/index.ts";
|
|
3
3
|
import { type SseBroker } from "./sse-broker.ts";
|
|
4
4
|
/**
|
|
5
5
|
* Heartbeat cadence for SSE streams.
|
|
@@ -27,6 +27,7 @@ export declare const SSE_MAX_PENDING_WRITES_PER_CLIENT = 10000;
|
|
|
27
27
|
export type SseRouteOptions = {
|
|
28
28
|
readonly anonymousLiveEntities: ReadonlySet<string>;
|
|
29
29
|
readonly entityReadAccess: ReadonlyMap<string, readonly AccessRule[]>;
|
|
30
|
+
readonly entityRowReadMaps?: ReadonlyMap<string, OwnershipMap>;
|
|
30
31
|
readonly now?: () => number;
|
|
31
32
|
};
|
|
32
33
|
export declare function createSseRoute(broker: SseBroker, options: SseRouteOptions): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
package/dist/api/sse-route.js
CHANGED
|
@@ -84,7 +84,17 @@ function entitySignalDetail(user, aggregateType, options) {
|
|
|
84
84
|
const granting = (options.entityReadAccess.get(aggregateType) ?? []).filter((rule) => hasAccess(user, rule));
|
|
85
85
|
if (granting.length === 0)
|
|
86
86
|
return "none";
|
|
87
|
-
|
|
87
|
+
if (!granting.some((rule) => "roles" in rule))
|
|
88
|
+
return "anonymous";
|
|
89
|
+
return readsEveryRow(user, options.entityRowReadMaps?.get(aggregateType))
|
|
90
|
+
? "identified"
|
|
91
|
+
: "anonymous";
|
|
92
|
+
}
|
|
93
|
+
// Mirrors buildOwnershipClause: without a map every row is visible, otherwise only a role mapped to "all" sees them all.
|
|
94
|
+
function readsEveryRow(user, rowReadMap) {
|
|
95
|
+
if (rowReadMap === undefined)
|
|
96
|
+
return true;
|
|
97
|
+
return user.roles.some((role) => rowReadMap[role] === "all");
|
|
88
98
|
}
|
|
89
99
|
function decideWireFrame(event, user, options) {
|
|
90
100
|
const data = event.data;
|
|
@@ -136,6 +146,20 @@ export function createSseRoute(broker, options) {
|
|
|
136
146
|
let pendingWrites = 0;
|
|
137
147
|
let lastProgressAt = now();
|
|
138
148
|
const isStalled = () => pendingWrites > 0 && now() - lastProgressAt > SSE_STALLED_CLIENT_TIMEOUT_MS;
|
|
149
|
+
// Pings go through the same accounting: an awaited ping would block on a client that
|
|
150
|
+
// stopped reading, and the stall check below would never run again.
|
|
151
|
+
const writeTracked = (event, data) => {
|
|
152
|
+
if (pendingWrites === 0)
|
|
153
|
+
lastProgressAt = now();
|
|
154
|
+
pendingWrites += 1;
|
|
155
|
+
stream
|
|
156
|
+
.writeSSE({ event, data })
|
|
157
|
+
.catch(closeStream)
|
|
158
|
+
.finally(() => {
|
|
159
|
+
pendingWrites -= 1;
|
|
160
|
+
lastProgressAt = now();
|
|
161
|
+
});
|
|
162
|
+
};
|
|
139
163
|
const clientId = broker.addClient(channel, (event) => {
|
|
140
164
|
// skip: the stream is already being torn down
|
|
141
165
|
if (stream.aborted || stream.closed)
|
|
@@ -152,17 +176,7 @@ export function createSseRoute(broker, options) {
|
|
|
152
176
|
// skip: this client is gone, the frame has no audience
|
|
153
177
|
return;
|
|
154
178
|
}
|
|
155
|
-
|
|
156
|
-
if (pendingWrites === 0)
|
|
157
|
-
lastProgressAt = now();
|
|
158
|
-
pendingWrites += 1;
|
|
159
|
-
stream
|
|
160
|
-
.writeSSE({ event: frame.name, data })
|
|
161
|
-
.catch(closeStream)
|
|
162
|
-
.finally(() => {
|
|
163
|
-
pendingWrites -= 1;
|
|
164
|
-
lastProgressAt = now();
|
|
165
|
-
});
|
|
179
|
+
writeTracked(frame.name, JSON.stringify(frame.data));
|
|
166
180
|
}
|
|
167
181
|
catch {
|
|
168
182
|
// skip: an unserializable frame ends this client's stream, not the broadcast
|
|
@@ -192,7 +206,7 @@ export function createSseRoute(broker, options) {
|
|
|
192
206
|
stream.abort();
|
|
193
207
|
break;
|
|
194
208
|
}
|
|
195
|
-
|
|
209
|
+
writeTracked("ping", "");
|
|
196
210
|
await Promise.race([stream.sleep(SSE_HEARTBEAT_INTERVAL_MS), ended]);
|
|
197
211
|
}
|
|
198
212
|
}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ValidateBootOptions } from "../engine/boot-validator.ts";
|
|
2
|
-
import type { FeatureDefinition } from "../engine/types/index.ts";
|
|
2
|
+
import type { BundledListsOptions, FeatureDefinition } from "../engine/types/index.ts";
|
|
3
3
|
export type AppInfraPorts = {
|
|
4
4
|
readonly postgres?: number;
|
|
5
5
|
readonly redis?: number;
|
|
@@ -35,6 +35,8 @@ export type AppConfig = {
|
|
|
35
35
|
readonly ports: AppPorts;
|
|
36
36
|
/** Passed to validateBoot by `kumiko check` and, via the app's bin files, by the runners. */
|
|
37
37
|
readonly validateBootOptions?: AppValidateBootOptions;
|
|
38
|
+
/** One table block for every list of the bundled features. kumiko/schema.ts passes it to composeFeatures; the app's bin files pass it to the runners, like validateBootOptions. */
|
|
39
|
+
readonly bundledLists?: BundledListsOptions;
|
|
38
40
|
};
|
|
39
41
|
export declare const APP_CONFIG_FILE = "kumiko.config.ts";
|
|
40
42
|
declare const INFRA_PORT_NAMES: readonly ["postgres", "redis", "meilisearch", "s3"];
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { EntityDefinition } from "../engine/types/fields.ts";
|
|
2
|
-
export declare function isPlainUserOwnedField(field: EntityDefinition["fields"][string]):
|
|
2
|
+
export declare function isPlainUserOwnedField(field: EntityDefinition["fields"][string]): field is EntityDefinition["fields"][string] & {
|
|
3
|
+
readonly userOwned: {
|
|
4
|
+
readonly ownerField: string;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
3
7
|
export declare function personalFieldsRekeyedOnOwnerTransition(entity: EntityDefinition, previous: Record<string, unknown>, mergedNew: Record<string, unknown>, submittedChanges: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -17,7 +17,7 @@ export function isPlainUserOwnedField(field) {
|
|
|
17
17
|
export function personalFieldsRekeyedOnOwnerTransition(entity, previous, mergedNew, submittedChanges) {
|
|
18
18
|
const rekeyed = {};
|
|
19
19
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
20
|
-
if (!isPlainUserOwnedField(field)
|
|
20
|
+
if (!isPlainUserOwnedField(field))
|
|
21
21
|
continue;
|
|
22
22
|
if (name in submittedChanges)
|
|
23
23
|
continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { createDbConnection } from "../db/connection.js";
|
|
3
|
+
import { isUniqueViolation } from "../db/pg-error.js";
|
|
4
4
|
import { KeyAlreadyExistsError, KeyErasedError, KeyNotFoundError, subjectIdToKey, } from "./kms-adapter.js";
|
|
5
5
|
const IV_LENGTH = 12;
|
|
6
6
|
const TAG_LENGTH = 16;
|
|
@@ -28,21 +28,15 @@ function decodePlatformKek(base64) {
|
|
|
28
28
|
}
|
|
29
29
|
return kek;
|
|
30
30
|
}
|
|
31
|
-
const PG_UNIQUE_VIOLATION = "23505";
|
|
32
31
|
// pg_advisory_xact_lock key for createSchema — serializes concurrent cold-start
|
|
33
32
|
// DDL across processes sharing one subject-keys DB.
|
|
34
33
|
const SCHEMA_ADVISORY_LOCK_KEY = 0x6b_6d_73_31; // 'kms1'
|
|
35
|
-
function isPgUniqueViolation(error) {
|
|
36
|
-
return (typeof error === "object" &&
|
|
37
|
-
error !== null &&
|
|
38
|
-
"code" in error &&
|
|
39
|
-
error.code === PG_UNIQUE_VIOLATION);
|
|
40
|
-
}
|
|
41
34
|
// Default production adapter: DEKs live in a separate Postgres cluster,
|
|
42
35
|
// KEK-wrapped at rest. Schema is created lazily on first use — the
|
|
43
36
|
// subject-keys cluster has no drizzle migration pipeline of its own.
|
|
44
37
|
export class PgKmsAdapter {
|
|
45
38
|
capabilities = { mode: "local-key" };
|
|
39
|
+
connection;
|
|
46
40
|
sql;
|
|
47
41
|
kek;
|
|
48
42
|
kekVersion;
|
|
@@ -60,12 +54,11 @@ export class PgKmsAdapter {
|
|
|
60
54
|
previous.set(v, decodePlatformKek(kek));
|
|
61
55
|
}
|
|
62
56
|
this.previousKeks = previous;
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// idempotent IF NOT EXISTS — notices are pure boot-log noise.
|
|
67
|
-
onnotice: () => { },
|
|
57
|
+
this.connection = createDbConnection({
|
|
58
|
+
url: options.databaseUrl,
|
|
59
|
+
maxConnections: options.maxConnections ?? 4,
|
|
68
60
|
});
|
|
61
|
+
this.sql = this.connection.client;
|
|
69
62
|
}
|
|
70
63
|
async createKey(subject, ctx) {
|
|
71
64
|
await this.ensureSchema();
|
|
@@ -76,7 +69,7 @@ export class PgKmsAdapter {
|
|
|
76
69
|
VALUES (${subjectIdToKey(subject)}, ${wrapped}, ${this.kekVersion}, ${ctx.userId ?? null})`;
|
|
77
70
|
}
|
|
78
71
|
catch (error) {
|
|
79
|
-
if (
|
|
72
|
+
if (isUniqueViolation(error))
|
|
80
73
|
throw new KeyAlreadyExistsError(subject);
|
|
81
74
|
throw error;
|
|
82
75
|
}
|
|
@@ -128,7 +121,7 @@ export class PgKmsAdapter {
|
|
|
128
121
|
return { ok: true, latencyMs: Date.now() - start };
|
|
129
122
|
}
|
|
130
123
|
async close() {
|
|
131
|
-
await this.
|
|
124
|
+
await this.connection.close();
|
|
132
125
|
}
|
|
133
126
|
ensureSchema() {
|
|
134
127
|
// A failed attempt resets the memo so a transient DB outage does not
|
|
@@ -178,7 +171,8 @@ export async function rewrapSubjectKeys(options) {
|
|
|
178
171
|
fromKeks.set(Number(version), decodePlatformKek(kek));
|
|
179
172
|
}
|
|
180
173
|
const batchSize = options.batchSize ?? 200;
|
|
181
|
-
const
|
|
174
|
+
const connection = createDbConnection({ url: options.databaseUrl, maxConnections: 2 });
|
|
175
|
+
const sql = connection.client;
|
|
182
176
|
const failures = [];
|
|
183
177
|
const result = { scanned: 0, rewrapped: 0, skippedErased: 0, failures };
|
|
184
178
|
try {
|
|
@@ -226,7 +220,7 @@ export async function rewrapSubjectKeys(options) {
|
|
|
226
220
|
}
|
|
227
221
|
}
|
|
228
222
|
finally {
|
|
229
|
-
await
|
|
223
|
+
await connection.close();
|
|
230
224
|
}
|
|
231
225
|
return result;
|
|
232
226
|
}
|
|
@@ -7,13 +7,17 @@ export declare function piiCiphertextSubject(value: string): SubjectId;
|
|
|
7
7
|
export declare function encryptPiiValueForSubject(kms: LocalKeyKmsAdapter, subject: SubjectId, value: string, kmsCtx: KmsContext, field: string): Promise<string>;
|
|
8
8
|
export declare function encryptPiiJsonValueForSubject(kms: LocalKeyKmsAdapter, subject: SubjectId, value: unknown, kmsCtx: KmsContext, field: string): Promise<string>;
|
|
9
9
|
export declare function decryptPiiValueForSubject(kms: LocalKeyKmsAdapter, value: string, kmsCtx: KmsContext, field: string): Promise<string>;
|
|
10
|
+
export declare const ErasedSubjectHandlings: {
|
|
11
|
+
readonly sentinel: "sentinel";
|
|
12
|
+
};
|
|
13
|
+
export type ErasedSubjectHandling = (typeof ErasedSubjectHandlings)[keyof typeof ErasedSubjectHandlings];
|
|
10
14
|
export interface EncryptPiiOptions {
|
|
11
15
|
readonly onlyKeys?: Iterable<string>;
|
|
12
16
|
readonly tenantId?: TenantId;
|
|
13
17
|
readonly subjectSource?: Record<string, unknown>;
|
|
14
18
|
readonly entityName: string;
|
|
15
19
|
readonly ownerFallback?: Record<string, unknown>;
|
|
16
|
-
readonly onErasedSubject?:
|
|
20
|
+
readonly onErasedSubject?: ErasedSubjectHandling;
|
|
17
21
|
}
|
|
18
22
|
export type PiiEncryptedRow<TRow extends Record<string, unknown>> = {
|
|
19
23
|
[K in keyof TRow]: TRow[K] extends string ? string : TRow[K] | string;
|
|
@@ -130,6 +130,7 @@ export async function decryptPiiValueForSubject(kms, value, kmsCtx, field) {
|
|
|
130
130
|
return PII_ERASED_SENTINEL;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
export const ErasedSubjectHandlings = { sentinel: "sentinel" };
|
|
133
134
|
export async function encryptPiiFieldValues(row, entity, piiFields, kms, kmsCtx, opts) {
|
|
134
135
|
if (piiFields.length === 0)
|
|
135
136
|
return row;
|
|
@@ -166,7 +167,8 @@ export async function encryptPiiFieldValues(row, entity, piiFields, kms, kmsCtx,
|
|
|
166
167
|
out[name] = encryptValue(subject, dek, plaintext, name, isJsonbField);
|
|
167
168
|
}
|
|
168
169
|
catch (e) {
|
|
169
|
-
if (!(e instanceof KeyErasedError) ||
|
|
170
|
+
if (!(e instanceof KeyErasedError) ||
|
|
171
|
+
opts.onErasedSubject !== ErasedSubjectHandlings.sentinel)
|
|
170
172
|
throw e;
|
|
171
173
|
out[name] = PII_ERASED_SENTINEL;
|
|
172
174
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AggregateDimension, AggregateKey, AggregateSpec } from "@cosmicdrift/kumiko-types/aggregate-types";
|
|
2
|
+
import type { FieldDefinition } from "@cosmicdrift/kumiko-types/fields";
|
|
3
|
+
import type { ListAggregateMeasureKind, ListAggregateValue } from "@cosmicdrift/kumiko-types/list-aggregate-types";
|
|
4
|
+
import type { TableInfo } from "./table-info.ts";
|
|
5
|
+
export declare function isTimestamptzType(pgType: string | undefined): boolean;
|
|
6
|
+
export declare function aggregateKeyOf(raw: unknown): AggregateKey;
|
|
7
|
+
export type AggregateFunction = "count" | "countDistinct" | "sum" | "avg" | "min" | "max";
|
|
8
|
+
export declare function aggregateFunctionSql(fn: AggregateFunction, columnSql: string | undefined, filterSql?: string): string;
|
|
9
|
+
export declare function aggregateMeasureSql(info: TableInfo, measure: AggregateSpec["measure"]): string;
|
|
10
|
+
type FieldType = FieldDefinition["type"];
|
|
11
|
+
export declare function isListAggregateMeasureSupported(kind: ListAggregateMeasureKind, fieldType: FieldType): boolean;
|
|
12
|
+
export declare function isListAggregateGroupable(fieldType: FieldType, multiple: boolean): boolean;
|
|
13
|
+
export declare function listAggregateKeySql(fieldType: FieldType, columnSql: string, alias: string): string;
|
|
14
|
+
export declare function listAggregateMeasureSql(kind: ListAggregateMeasureKind, fieldType: FieldType, columnSql: string, filterSql?: string): string;
|
|
15
|
+
export declare function isPerCurrencyMeasure(kind: ListAggregateMeasureKind, fieldType: FieldType): boolean;
|
|
16
|
+
export declare function listCurrencyMeasureSql(kind: ListAggregateMeasureKind, columnSql: string, filterSql: string): string;
|
|
17
|
+
export declare function groupedKeyOrderLimitSql(limit: number): string;
|
|
18
|
+
export declare function decodeListAggregateValue(raw: unknown): ListAggregateValue;
|
|
19
|
+
export declare function emptyListAggregateValue(kind: ListAggregateMeasureKind): ListAggregateValue;
|
|
20
|
+
export declare function aggregateDimensionSql(info: TableInfo, dimension: AggregateDimension, index: number, params: unknown[]): string;
|
|
21
|
+
export declare function assertValidAggregateLimits(spec: AggregateSpec): void;
|
|
22
|
+
export declare function aggregateGroupOrderLimitSql(spec: AggregateSpec, dimensionCount: number): string;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { quoteIdent } from "../crypto/ciphertext-pattern.js";
|
|
2
|
+
const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
|
|
3
|
+
const AGGREGATE_ORDER_DIRECTIONS = ["asc", "desc"];
|
|
4
|
+
function requireAggregateColumn(info, field, role) {
|
|
5
|
+
if (!info.hasColumn(field)) {
|
|
6
|
+
throw new Error(`aggregateWhere: ${role} "${field}" is not a column of ${info.name}`);
|
|
7
|
+
}
|
|
8
|
+
return quoteIdent(info.columnOf(field));
|
|
9
|
+
}
|
|
10
|
+
export function isTimestamptzType(pgType) {
|
|
11
|
+
return pgType === "timestamptz" || pgType === "timestamptz(3)";
|
|
12
|
+
}
|
|
13
|
+
export function aggregateKeyOf(raw) {
|
|
14
|
+
if (raw === null || raw === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean")
|
|
17
|
+
return raw;
|
|
18
|
+
if (typeof raw === "bigint")
|
|
19
|
+
return Number(raw);
|
|
20
|
+
if (raw instanceof Date)
|
|
21
|
+
return raw.toISOString();
|
|
22
|
+
return String(raw);
|
|
23
|
+
}
|
|
24
|
+
// FILTER sits inside the aggregate so a row-dependent read clause restricts only
|
|
25
|
+
// this measure; COALESCE stays outside to keep an all-filtered SUM at 0.
|
|
26
|
+
export function aggregateFunctionSql(fn, columnSql, filterSql) {
|
|
27
|
+
const filter = filterSql === undefined ? "" : ` FILTER (WHERE ${filterSql})`;
|
|
28
|
+
const column = columnSql ?? "*";
|
|
29
|
+
switch (fn) {
|
|
30
|
+
case "count":
|
|
31
|
+
return `COUNT(${column})${filter}`;
|
|
32
|
+
case "countDistinct":
|
|
33
|
+
return `COUNT(DISTINCT ${column})${filter}`;
|
|
34
|
+
case "sum":
|
|
35
|
+
return `COALESCE(SUM(${column})${filter}, 0)`;
|
|
36
|
+
case "avg":
|
|
37
|
+
return `AVG(${column})${filter}`;
|
|
38
|
+
case "min":
|
|
39
|
+
return `MIN(${column})${filter}`;
|
|
40
|
+
case "max":
|
|
41
|
+
return `MAX(${column})${filter}`;
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`aggregateWhere: unknown aggregate fn "${String(fn)}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function aggregateMeasureSql(info, measure) {
|
|
47
|
+
if (measure.fn === "count")
|
|
48
|
+
return aggregateFunctionSql("count", undefined);
|
|
49
|
+
return aggregateFunctionSql(measure.fn, requireAggregateColumn(info, measure.field, "measure field"));
|
|
50
|
+
}
|
|
51
|
+
const NUMERIC_MEASURE_TYPES = ["number", "bigInt", "decimal", "money"];
|
|
52
|
+
const ORDERED_MEASURE_TYPES = [...NUMERIC_MEASURE_TYPES, "date", "timestamp"];
|
|
53
|
+
// Group keys must round-trip as a plain `field eq key` filter, which rules out
|
|
54
|
+
// composite, multi-valued, currency-paired and zone-paired types.
|
|
55
|
+
const GROUPABLE_TYPES = [
|
|
56
|
+
"text",
|
|
57
|
+
"longText",
|
|
58
|
+
"select",
|
|
59
|
+
"boolean",
|
|
60
|
+
"number",
|
|
61
|
+
"bigInt",
|
|
62
|
+
"decimal",
|
|
63
|
+
"date",
|
|
64
|
+
"timestamp",
|
|
65
|
+
];
|
|
66
|
+
export function isListAggregateMeasureSupported(kind, fieldType) {
|
|
67
|
+
if (kind === "count")
|
|
68
|
+
return true;
|
|
69
|
+
const allowed = kind === "sum" || kind === "avg" ? NUMERIC_MEASURE_TYPES : ORDERED_MEASURE_TYPES;
|
|
70
|
+
return allowed.includes(fieldType);
|
|
71
|
+
}
|
|
72
|
+
export function isListAggregateGroupable(fieldType, multiple) {
|
|
73
|
+
return fieldType === "reference" ? !multiple : GROUPABLE_TYPES.includes(fieldType);
|
|
74
|
+
}
|
|
75
|
+
export function listAggregateKeySql(fieldType, columnSql, alias) {
|
|
76
|
+
// A date column must not pass through a JS Date, so it travels as text.
|
|
77
|
+
return fieldType === "date" ? `${columnSql}::text AS ${alias}` : `${columnSql} AS ${alias}`;
|
|
78
|
+
}
|
|
79
|
+
const ISO_UTC_TIMESTAMP_FORMAT = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;
|
|
80
|
+
// Numbers leave PG as float8 and dates/timestamps as text, so the driver never
|
|
81
|
+
// hands back a bigint or numeric string that JSON could not carry.
|
|
82
|
+
export function listAggregateMeasureSql(kind, fieldType, columnSql, filterSql) {
|
|
83
|
+
const fn = aggregateFunctionSql(kind, columnSql, filterSql);
|
|
84
|
+
if (kind === "count")
|
|
85
|
+
return `(${fn})::float8`;
|
|
86
|
+
if (kind === "min" || kind === "max") {
|
|
87
|
+
if (fieldType === "date")
|
|
88
|
+
return `(${fn})::text`;
|
|
89
|
+
if (fieldType === "timestamp") {
|
|
90
|
+
return `to_char((${fn}) AT TIME ZONE 'UTC', ${ISO_UTC_TIMESTAMP_FORMAT})`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Money is integer minor units, so an average rounds to one.
|
|
94
|
+
if (kind === "avg" && fieldType === "money")
|
|
95
|
+
return `ROUND(${fn})::float8`;
|
|
96
|
+
return `(${fn})::float8`;
|
|
97
|
+
}
|
|
98
|
+
export function isPerCurrencyMeasure(kind, fieldType) {
|
|
99
|
+
return fieldType === "money" && kind !== "count";
|
|
100
|
+
}
|
|
101
|
+
// No COALESCE on SUM: a currency absent from a group must read NULL so it can be left out.
|
|
102
|
+
export function listCurrencyMeasureSql(kind, columnSql, filterSql) {
|
|
103
|
+
const fn = kind === "sum"
|
|
104
|
+
? `SUM(${columnSql}) FILTER (WHERE ${filterSql})`
|
|
105
|
+
: aggregateFunctionSql(kind, columnSql, filterSql);
|
|
106
|
+
return kind === "avg" ? `ROUND(${fn})::float8` : `(${fn})::float8`;
|
|
107
|
+
}
|
|
108
|
+
export function groupedKeyOrderLimitSql(limit) {
|
|
109
|
+
return ` GROUP BY 1 ORDER BY 1 ASC NULLS LAST LIMIT ${limit}`;
|
|
110
|
+
}
|
|
111
|
+
export function decodeListAggregateValue(raw) {
|
|
112
|
+
if (raw === null || raw === undefined)
|
|
113
|
+
return null;
|
|
114
|
+
if (typeof raw === "number" || typeof raw === "string")
|
|
115
|
+
return raw;
|
|
116
|
+
if (typeof raw === "bigint")
|
|
117
|
+
return Number(raw);
|
|
118
|
+
if (raw instanceof Date)
|
|
119
|
+
return raw.toISOString();
|
|
120
|
+
return String(raw);
|
|
121
|
+
}
|
|
122
|
+
// What the measure reports over zero rows, mirroring the SQL (SUM coalesces, COUNT is 0).
|
|
123
|
+
export function emptyListAggregateValue(kind) {
|
|
124
|
+
return kind === "sum" || kind === "count" ? 0 : null;
|
|
125
|
+
}
|
|
126
|
+
export function aggregateDimensionSql(info, dimension, index, params) {
|
|
127
|
+
const column = requireAggregateColumn(info, dimension.field, "groupBy field");
|
|
128
|
+
if (!("bucket" in dimension))
|
|
129
|
+
return `${column} AS k${index}`;
|
|
130
|
+
if (!AGGREGATE_BUCKET_UNITS.includes(dimension.bucket)) {
|
|
131
|
+
throw new Error(`aggregateWhere: unknown bucket "${String(dimension.bucket)}"`);
|
|
132
|
+
}
|
|
133
|
+
if (!isTimestamptzType(info.pgTypeOf(info.columnOf(dimension.field)))) {
|
|
134
|
+
throw new Error(`aggregateWhere: bucket on "${dimension.field}" requires a timestamptz column`);
|
|
135
|
+
}
|
|
136
|
+
params.push(dimension.timeZone);
|
|
137
|
+
return `(EXTRACT(EPOCH FROM date_trunc('${dimension.bucket}', ${column}, $${params.length}::text)) * 1000)::float8 AS k${index}`;
|
|
138
|
+
}
|
|
139
|
+
export function assertValidAggregateLimits(spec) {
|
|
140
|
+
if (spec.limit !== undefined && (!Number.isInteger(spec.limit) || spec.limit < 0)) {
|
|
141
|
+
throw new Error(`aggregateWhere: limit must be a non-negative integer, got ${spec.limit}`);
|
|
142
|
+
}
|
|
143
|
+
if (spec.orderByValue !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByValue)) {
|
|
144
|
+
throw new Error(`aggregateWhere: unknown orderByValue "${String(spec.orderByValue)}"`);
|
|
145
|
+
}
|
|
146
|
+
if (spec.orderByKeys !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByKeys)) {
|
|
147
|
+
throw new Error(`aggregateWhere: unknown orderByKeys "${String(spec.orderByKeys)}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function aggregateGroupOrderLimitSql(spec, dimensionCount) {
|
|
151
|
+
let sqlText = "";
|
|
152
|
+
if (dimensionCount > 0) {
|
|
153
|
+
const positions = Array.from({ length: dimensionCount }, (_, index) => String(index + 1));
|
|
154
|
+
sqlText += ` GROUP BY ${positions.join(", ")}`;
|
|
155
|
+
const keyDirection = spec.orderByKeys === "desc" ? "DESC" : "ASC";
|
|
156
|
+
const keyParts = positions.map((position) => `${position} ${keyDirection}`);
|
|
157
|
+
const orderParts = spec.orderByValue === undefined
|
|
158
|
+
? keyParts
|
|
159
|
+
: [`${dimensionCount + 1} ${spec.orderByValue === "desc" ? "DESC" : "ASC"}`, ...keyParts];
|
|
160
|
+
sqlText += ` ORDER BY ${orderParts.join(", ")}`;
|
|
161
|
+
}
|
|
162
|
+
if (spec.limit !== undefined)
|
|
163
|
+
sqlText += ` LIMIT ${spec.limit}`;
|
|
164
|
+
return sqlText;
|
|
165
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function isClosedConnectionError(err: unknown): boolean;
|
|
2
|
+
export declare function isPooledClient(raw: unknown): boolean;
|
|
3
|
+
export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
|
|
4
|
+
export type ClosedConnectionContext = {
|
|
5
|
+
readonly operation: string;
|
|
6
|
+
readonly table?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function logClosedConnection(raw: unknown, err: unknown, attempts: number, context?: ClosedConnectionContext): void;
|
|
9
|
+
export declare function poolMaxOf(raw: unknown): number;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { requestContext } from "../api/request-context.js";
|
|
3
|
+
import { createFallbackLogger } from "../logging/index.js";
|
|
4
|
+
import { extractPgError } from "./pg-error.js";
|
|
5
|
+
// A pool can briefly keep handing out connections the server just closed.
|
|
6
|
+
// Each failed attempt discards one, so retries are bounded by pool size.
|
|
7
|
+
const CLOSED_CONNECTION_CODES = new Set([
|
|
8
|
+
"ERR_POSTGRES_CONNECTION_CLOSED", // Bun.SQL
|
|
9
|
+
"57P01", // PG SQLSTATE: admin_shutdown / terminated backend
|
|
10
|
+
]);
|
|
11
|
+
export function isClosedConnectionError(err) {
|
|
12
|
+
const code = extractPgError(err)?.code;
|
|
13
|
+
return code !== undefined && CLOSED_CONNECTION_CODES.has(code);
|
|
14
|
+
}
|
|
15
|
+
// Tx/reserved handles are pinned to one physical connection — a retry there
|
|
16
|
+
// would run on the same dead connection, not swap in a fresh one.
|
|
17
|
+
export function isPooledClient(raw) {
|
|
18
|
+
if (raw === null || (typeof raw !== "object" && typeof raw !== "function"))
|
|
19
|
+
return false;
|
|
20
|
+
return (typeof propertyOf(raw, "begin") === "function" &&
|
|
21
|
+
typeof propertyOf(raw, "savepoint") !== "function" &&
|
|
22
|
+
typeof propertyOf(raw, "release") !== "function");
|
|
23
|
+
}
|
|
24
|
+
const closedConnectionLog = createFallbackLogger("bun-sql");
|
|
25
|
+
function isHandleLike(raw) {
|
|
26
|
+
return raw !== null && (typeof raw === "object" || typeof raw === "function");
|
|
27
|
+
}
|
|
28
|
+
// Bun.SQL pool and tx handles both carry `flush`.
|
|
29
|
+
function driverOf(raw) {
|
|
30
|
+
if (!isHandleLike(raw))
|
|
31
|
+
return "unknown";
|
|
32
|
+
if (typeof raw["flush"] === "function")
|
|
33
|
+
return "bun-sql";
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
function positiveNumber(v) {
|
|
37
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
38
|
+
}
|
|
39
|
+
// Bun.SQL reports its timeout options in milliseconds.
|
|
40
|
+
function timeoutOptionMs(options, driver, optionName) {
|
|
41
|
+
if (driver !== "bun-sql")
|
|
42
|
+
return undefined;
|
|
43
|
+
return positiveNumber(options[optionName]);
|
|
44
|
+
}
|
|
45
|
+
function errorProp(err, name) {
|
|
46
|
+
return isHandleLike(err) ? err[name] : undefined;
|
|
47
|
+
}
|
|
48
|
+
// Deliberately excludes SQL text and parameters: app SQL can carry PII.
|
|
49
|
+
export function describeClosedConnectionFailure(raw, err, attempts) {
|
|
50
|
+
if (!isClosedConnectionError(err))
|
|
51
|
+
return undefined;
|
|
52
|
+
const driver = driverOf(raw);
|
|
53
|
+
const rawOptions = isHandleLike(raw) ? raw["options"] : undefined;
|
|
54
|
+
const options = isHandleLike(rawOptions) ? rawOptions : {};
|
|
55
|
+
return {
|
|
56
|
+
driver,
|
|
57
|
+
pooled: isPooledClient(raw),
|
|
58
|
+
attempts,
|
|
59
|
+
poolMax: positiveNumber(options["max"]),
|
|
60
|
+
idleTimeoutMs: timeoutOptionMs(options, driver, "idleTimeout"),
|
|
61
|
+
maxLifetimeMs: timeoutOptionMs(options, driver, "maxLifetime"),
|
|
62
|
+
connectionTimeoutMs: timeoutOptionMs(options, driver, "connectionTimeout"),
|
|
63
|
+
errorName: errorProp(err, "name"),
|
|
64
|
+
errorCode: extractPgError(err)?.code ?? errorProp(err, "code"),
|
|
65
|
+
errorErrno: errorProp(err, "errno"),
|
|
66
|
+
errorMessage: errorProp(err, "message"),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function logClosedConnection(raw, err, attempts, context) {
|
|
70
|
+
const failure = describeClosedConnectionFailure(raw, err, attempts);
|
|
71
|
+
// skip: not a closed-connection error, nothing to diagnose
|
|
72
|
+
if (!failure)
|
|
73
|
+
return;
|
|
74
|
+
// Request identity lets this line be joined to the route's `handler failed` log.
|
|
75
|
+
const request = requestContext.get();
|
|
76
|
+
closedConnectionLog.warn("closed connection surfaced", {
|
|
77
|
+
...failure,
|
|
78
|
+
...context,
|
|
79
|
+
requestId: request?.requestId,
|
|
80
|
+
correlationId: request?.correlationId,
|
|
81
|
+
handler: request?.handler,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const DEFAULT_POOL_MAX = 10;
|
|
85
|
+
export function poolMaxOf(raw) {
|
|
86
|
+
const max = propertyOf(propertyOf(raw, "options"), "max");
|
|
87
|
+
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_POOL_MAX;
|
|
88
|
+
}
|
package/dist/db/connection.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import type { DbConnection, PgClient } from "@cosmicdrift/kumiko-types/db-connection";
|
|
2
|
-
import postgres from "postgres";
|
|
1
|
+
import type { DbConnection, PgClient, PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
|
|
3
2
|
export type * from "@cosmicdrift/kumiko-types/db-connection";
|
|
4
|
-
type
|
|
3
|
+
type BunSqlOptions = Bun.SQL.PostgresOrMySQLOptions;
|
|
5
4
|
export type DbConnectionOptions = {
|
|
6
5
|
readonly maxConnections?: number;
|
|
7
6
|
readonly idleTimeoutSeconds?: number;
|
|
8
7
|
readonly connectTimeoutSeconds?: number;
|
|
9
8
|
readonly closeTimeoutSeconds?: number;
|
|
10
|
-
readonly ssl?:
|
|
11
|
-
readonly onNotice?: (notice: postgres.Notice) => void;
|
|
9
|
+
readonly ssl?: BunSqlOptions["tls"];
|
|
12
10
|
readonly statementTimeoutMs?: number;
|
|
13
11
|
readonly idleInTransactionSessionTimeoutMs?: number;
|
|
14
12
|
readonly lockTimeoutMs?: number;
|
|
@@ -19,6 +17,7 @@ export type CreateDbConnectionOptions = DbConnectionOptions & {
|
|
|
19
17
|
export type DbConnectionHandle = {
|
|
20
18
|
db: DbConnection;
|
|
21
19
|
client: PgClient;
|
|
20
|
+
listenClient: PgListenClient;
|
|
22
21
|
close: () => Promise<void>;
|
|
23
22
|
};
|
|
24
23
|
export declare const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
|