@cosmicdrift/kumiko-framework 0.14.0 → 0.16.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/package.json +6 -6
- package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
- package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
- package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
- package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
- package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
- package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
- package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
- package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
- package/src/api/__tests__/api.test.ts +1 -1
- package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
- package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
- package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/csrf-middleware.test.ts +1 -1
- package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
- package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
- package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
- package/src/api/__tests__/readiness.test.ts +1 -1
- package/src/api/__tests__/request-id-middleware.test.ts +1 -1
- package/src/api/__tests__/sse-broker.test.ts +12 -12
- package/src/api/__tests__/sse-route.test.ts +1 -1
- package/src/api/auth-routes.ts +2 -5
- package/src/api/readiness.ts +2 -2
- package/src/auth/__tests__/roles.test.ts +2 -2
- package/src/bun-db/__tests__/PATTERN.md +73 -0
- package/src/bun-db/__tests__/_helpers.ts +103 -0
- package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
- package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
- package/src/bun-db/__tests__/bun-test-db.ts +19 -0
- package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
- package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
- package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
- package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
- package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
- package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
- package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
- package/src/bun-db/connection.ts +84 -0
- package/src/bun-db/index.ts +31 -0
- package/src/bun-db/query.ts +842 -0
- package/src/compliance/__tests__/duration-spec.test.ts +1 -1
- package/src/compliance/__tests__/profiles.test.ts +1 -1
- package/src/compliance/__tests__/sub-processors.test.ts +1 -1
- package/src/compliance/profiles.ts +1 -4
- package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
- package/src/db/__tests__/big-int-field.test.ts +15 -14
- package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
- package/src/db/__tests__/connection-options.test.ts +1 -1
- package/src/db/__tests__/cursor.test.ts +8 -32
- package/src/db/__tests__/dialect-instant.test.ts +1 -1
- package/src/db/__tests__/encryption.test.ts +1 -1
- package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
- package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
- package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
- package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/migrate-generator.test.ts +71 -0
- package/src/db/__tests__/migrate-runner.test.ts +19 -0
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
- package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
- package/src/db/__tests__/pg-error.test.ts +43 -0
- package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
- package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
- package/src/db/__tests__/sql-inventory.test.ts +56 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
- package/src/db/__tests__/table-builder-required.test.ts +20 -22
- package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
- package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
- package/src/db/api.ts +46 -0
- package/src/db/apply-entity-event.ts +45 -36
- package/src/db/assert-exists-in.ts +5 -16
- package/src/db/bun-provider.ts +37 -0
- package/src/db/config-seed.ts +4 -4
- package/src/db/connection.ts +14 -57
- package/src/db/cursor.ts +5 -56
- package/src/db/dialect.ts +472 -99
- package/src/db/eagerload.ts +5 -12
- package/src/db/entity-table-meta.ts +390 -0
- package/src/db/event-store-executor.ts +158 -100
- package/src/db/index.ts +33 -5
- package/src/db/migrate-generator.ts +350 -0
- package/src/db/migrate-runner.ts +206 -0
- package/src/db/postgres-provider.ts +25 -0
- package/src/db/queries/entity-read.ts +15 -0
- package/src/db/queries/es-ops.ts +17 -0
- package/src/db/queries/event-consumer.ts +170 -0
- package/src/db/queries/event-store-admin.ts +127 -0
- package/src/db/queries/event-store.ts +155 -0
- package/src/db/queries/projection-rebuild.ts +59 -0
- package/src/db/queries/raw-sql.ts +15 -0
- package/src/db/queries/schema-drift.ts +35 -0
- package/src/db/queries/seed-context.ts +58 -0
- package/src/db/queries/table-ops.ts +11 -0
- package/src/db/queries/test-stack.ts +56 -0
- package/src/db/query-api.ts +22 -0
- package/src/db/query.ts +30 -0
- package/src/db/reference-data.ts +19 -22
- package/src/db/render-ddl.ts +57 -0
- package/src/db/row-helpers.ts +3 -52
- package/src/db/schema-inspection.ts +17 -4
- package/src/db/sql-inventory.ts +208 -0
- package/src/db/table-builder.ts +54 -46
- package/src/db/tenant-db.ts +105 -326
- package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
- package/src/engine/__tests__/boot-validator.test.ts +4 -3
- package/src/engine/__tests__/build-app-schema.test.ts +1 -1
- package/src/engine/__tests__/build-target.test.ts +1 -1
- package/src/engine/__tests__/claim-keys.test.ts +1 -1
- package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
- package/src/engine/__tests__/config-helpers.test.ts +1 -1
- package/src/engine/__tests__/duration-utils.test.ts +16 -0
- package/src/engine/__tests__/effective-features.test.ts +1 -1
- package/src/engine/__tests__/engine.test.ts +1 -1
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +3 -3
- package/src/engine/__tests__/extends-registrar.test.ts +4 -4
- package/src/engine/__tests__/factories-long-text.test.ts +1 -1
- package/src/engine/__tests__/factories-time.test.ts +1 -1
- package/src/engine/__tests__/field-access.test.ts +38 -0
- package/src/engine/__tests__/field-predicates.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +1 -1
- package/src/engine/__tests__/identifiers.test.ts +1 -1
- package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/no-return-guard.test.ts +17 -0
- package/src/engine/__tests__/ownership.test.ts +10 -11
- package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
- package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
- package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
- package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +1 -1
- package/src/engine/__tests__/projection-helpers.test.ts +25 -17
- package/src/engine/__tests__/projection.test.ts +4 -4
- package/src/engine/__tests__/qualified-name.test.ts +1 -1
- package/src/engine/__tests__/raw-table.test.ts +9 -8
- package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
- package/src/engine/__tests__/run-in.test.ts +1 -1
- package/src/engine/__tests__/schema-builder.test.ts +1 -1
- package/src/engine/__tests__/screen.test.ts +1 -1
- package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
- package/src/engine/__tests__/state-machine.test.ts +1 -1
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
- package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
- package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
- package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
- package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
- package/src/engine/__tests__/steps-read.test.ts +34 -40
- package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
- package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
- package/src/engine/__tests__/steps-workflow.test.ts +7 -7
- package/src/engine/__tests__/system-user.test.ts +1 -1
- package/src/engine/__tests__/unmanaged-table.test.ts +98 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
- package/src/engine/__tests__/validation-hooks.test.ts +1 -1
- package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
- package/src/engine/boot-validator/entity-handler.ts +3 -3
- package/src/engine/boot-validator/ownership.ts +1 -1
- package/src/engine/define-feature.ts +37 -2
- package/src/engine/entity-handlers.ts +5 -5
- package/src/engine/factories.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
- package/src/engine/feature-ast/extractors/shared.ts +2 -3
- package/src/engine/ownership.ts +113 -41
- package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
- package/src/engine/projection-helpers.ts +2 -11
- package/src/engine/registry.ts +21 -2
- package/src/engine/steps/read-find-many.ts +13 -13
- package/src/engine/steps/read-find-one.ts +7 -9
- package/src/engine/steps/unsafe-projection-delete.ts +4 -5
- package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
- package/src/engine/types/feature.ts +47 -2
- package/src/engine/types/fields.ts +4 -5
- package/src/engine/types/index.ts +2 -0
- package/src/engine/types/step.ts +10 -10
- package/src/engine/validate-projection-allowlist.ts +23 -3
- package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
- package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
- package/src/env/__tests__/compose-env-schema.test.ts +1 -1
- package/src/env/__tests__/dry-run.test.ts +1 -1
- package/src/errors/__tests__/classes.test.ts +1 -1
- package/src/errors/__tests__/error-helpers.test.ts +44 -0
- package/src/errors/__tests__/field-issue-compat.test.ts +16 -0
- package/src/errors/__tests__/write-failures.test.ts +1 -1
- package/src/errors/classes.ts +5 -19
- package/src/errors/field-issue.ts +11 -0
- package/src/errors/index.ts +1 -0
- package/src/errors/zod-bridge.ts +3 -2
- package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
- package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
- package/src/es-ops/__tests__/runner.test.ts +29 -19
- package/src/es-ops/context.ts +11 -56
- package/src/es-ops/operations-schema.ts +2 -2
- package/src/es-ops/runner.ts +12 -26
- package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
- package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
- package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
- package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
- package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
- package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
- package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
- package/src/event-store/admin-api.ts +55 -83
- package/src/event-store/archive.ts +15 -39
- package/src/event-store/event-store.ts +92 -86
- package/src/event-store/events-schema.ts +2 -1
- package/src/event-store/index.ts +1 -0
- package/src/event-store/snapshot.ts +26 -24
- package/src/event-store/upcaster-dead-letter.ts +19 -18
- package/src/files/__tests__/content-disposition.test.ts +1 -1
- package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
- package/src/files/__tests__/file-handle.test.ts +1 -1
- package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
- package/src/files/__tests__/read-stream.test.ts +1 -1
- package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
- package/src/files/__tests__/write-stream.test.ts +1 -1
- package/src/files/__tests__/zip-stream.test.ts +1 -1
- package/src/files/file-ref-table.ts +2 -2
- package/src/files/file-routes.ts +7 -9
- package/src/files/storage-tracking.ts +9 -17
- package/src/i18n/__tests__/i18n.test.ts +1 -1
- package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
- package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
- package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
- package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
- package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
- package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
- package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
- package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
- package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
- package/src/migrations/projection-detection.ts +12 -1
- package/src/migrations/schema-drift.ts +7 -23
- package/src/observability/__tests__/console-provider.test.ts +1 -1
- package/src/observability/__tests__/metric-validator.test.ts +1 -1
- package/src/observability/__tests__/noop-provider.test.ts +1 -1
- package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
- package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-tracer.test.ts +1 -1
- package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
- package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
- package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
- package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
- package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
- package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
- package/src/pipeline/__tests__/dispatcher-utils.test.ts +107 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
- package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
- package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
- package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
- package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
- package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
- package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
- package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
- package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
- package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
- package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
- package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
- package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
- package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
- package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
- package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
- package/src/pipeline/__tests__/redis-keys.test.ts +12 -0
- package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- package/src/pipeline/dispatcher-utils.ts +8 -7
- package/src/pipeline/dispatcher.ts +43 -48
- package/src/pipeline/event-consumer-state.ts +11 -2
- package/src/pipeline/event-dispatcher.ts +86 -146
- package/src/pipeline/event-retention.ts +14 -24
- package/src/pipeline/msp-rebuild.ts +54 -78
- package/src/pipeline/projection-rebuild.ts +65 -67
- package/src/pipeline/projection-state.ts +2 -2
- package/src/random/__tests__/generate.test.ts +13 -13
- package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
- package/src/redis/__tests__/redis-options.test.ts +1 -1
- package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
- package/src/search/__tests__/search-adapter.test.ts +1 -1
- package/src/secrets/__tests__/dek-cache.test.ts +1 -3
- package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/__tests__/leak-guard.test.ts +1 -1
- package/src/secrets/__tests__/rotation.test.ts +1 -1
- package/src/stack/db.ts +25 -48
- package/src/stack/push-entity-projection-tables.ts +2 -4
- package/src/stack/table-helpers.ts +98 -61
- package/src/stack/test-stack.ts +10 -9
- package/src/testing/__tests__/db-cleanup.test.ts +40 -0
- package/src/testing/__tests__/e2e-generator.test.ts +1 -1
- package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
- package/src/testing/db-cleanup.ts +44 -0
- package/src/testing/expect-error.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/multipart-helper.ts +94 -0
- package/src/testing/shared-entities.ts +5 -5
- package/src/time/__tests__/polyfill.test.ts +1 -1
- package/src/time/__tests__/tz-context.test.ts +1 -1
- package/src/utils/__tests__/assert.test.ts +1 -1
- package/src/utils/__tests__/case.test.ts +16 -0
- package/src/utils/__tests__/env-parse.test.ts +1 -1
- package/src/utils/__tests__/is-plain-object.test.ts +16 -0
- package/src/utils/__tests__/parse-string-array-json.test.ts +16 -0
- package/src/utils/__tests__/safe-json.test.ts +22 -0
- package/src/utils/case.ts +6 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/is-plain-object.ts +4 -0
- package/src/utils/parse-string-array-json.ts +14 -0
- package/CHANGELOG.md +0 -474
- package/src/db/__tests__/db-helpers.test.ts +0 -369
- package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
- package/src/db/__tests__/row-helpers.test.ts +0 -59
- package/src/engine/steps/_drizzle-boundary.ts +0 -19
- package/src/files/__tests__/file-field-column.integration.ts +0 -103
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
/** Serialise against consumer-bootstrap INSERTs during event retention prune. */
|
|
5
|
+
export async function lockEventConsumersShareMode(db: AnyDb): Promise<void> {
|
|
6
|
+
await asRawClient(db).unsafe(`LOCK TABLE "kumiko_event_consumers" IN SHARE MODE`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function insertConsumerIfAbsent(
|
|
10
|
+
db: AnyDb,
|
|
11
|
+
name: string,
|
|
12
|
+
instanceId: string,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
await asRawClient(db).unsafe(
|
|
15
|
+
`INSERT INTO "kumiko_event_consumers" ("name", "instance_id", "status") VALUES ($1, $2, 'idle')
|
|
16
|
+
ON CONFLICT ("name", "instance_id") DO NOTHING`,
|
|
17
|
+
[name, instanceId],
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function selectConsumerForUpdateSkipLocked(
|
|
22
|
+
db: AnyDb,
|
|
23
|
+
name: string,
|
|
24
|
+
instanceId: string,
|
|
25
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
26
|
+
const rows = (await asRawClient(db).unsafe(
|
|
27
|
+
`SELECT * FROM "kumiko_event_consumers" WHERE "name" = $1 AND "instance_id" = $2 FOR UPDATE SKIP LOCKED`,
|
|
28
|
+
[name, instanceId],
|
|
29
|
+
)) as ReadonlyArray<Record<string, unknown>>;
|
|
30
|
+
return rows[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function markConsumerProcessing(
|
|
34
|
+
db: AnyDb,
|
|
35
|
+
name: string,
|
|
36
|
+
instanceId: string,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
await asRawClient(db).unsafe(
|
|
39
|
+
`UPDATE "kumiko_event_consumers" SET "status" = 'processing', "updated_at" = now()
|
|
40
|
+
WHERE "name" = $1 AND "instance_id" = $2`,
|
|
41
|
+
[name, instanceId],
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ConsumerDeliveryOutcome = {
|
|
46
|
+
readonly cursor: bigint;
|
|
47
|
+
readonly attempts: number;
|
|
48
|
+
readonly lastError: string | null;
|
|
49
|
+
readonly deadLettered: boolean;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export async function updateConsumerDeliveryOutcome(
|
|
53
|
+
db: AnyDb,
|
|
54
|
+
name: string,
|
|
55
|
+
instanceId: string,
|
|
56
|
+
outcome: ConsumerDeliveryOutcome,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
await asRawClient(db).unsafe(
|
|
59
|
+
`UPDATE "kumiko_event_consumers" SET
|
|
60
|
+
"last_processed_event_id" = $1,
|
|
61
|
+
"attempts" = $2,
|
|
62
|
+
"status" = $3,
|
|
63
|
+
"last_error" = $4,
|
|
64
|
+
"updated_at" = now()
|
|
65
|
+
WHERE "name" = $5 AND "instance_id" = $6`,
|
|
66
|
+
[
|
|
67
|
+
outcome.cursor,
|
|
68
|
+
outcome.attempts,
|
|
69
|
+
outcome.deadLettered ? "dead" : "idle",
|
|
70
|
+
outcome.lastError,
|
|
71
|
+
name,
|
|
72
|
+
instanceId,
|
|
73
|
+
],
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function updateConsumerStatusReturning(
|
|
78
|
+
db: AnyDb,
|
|
79
|
+
name: string,
|
|
80
|
+
instanceId: string,
|
|
81
|
+
status: "idle" | "disabled",
|
|
82
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
83
|
+
const rows = (await asRawClient(db).unsafe(
|
|
84
|
+
`UPDATE "kumiko_event_consumers" SET "status" = $1, "attempts" = 0, "last_error" = NULL, "updated_at" = now()
|
|
85
|
+
WHERE "name" = $2 AND "instance_id" = $3
|
|
86
|
+
RETURNING *`,
|
|
87
|
+
[status, name, instanceId],
|
|
88
|
+
)) as ReadonlyArray<Record<string, unknown>>;
|
|
89
|
+
return rows[0];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function advanceConsumerPastEventReturning(
|
|
93
|
+
db: AnyDb,
|
|
94
|
+
name: string,
|
|
95
|
+
instanceId: string,
|
|
96
|
+
eventId: bigint,
|
|
97
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
98
|
+
const rows = (await asRawClient(db).unsafe(
|
|
99
|
+
`UPDATE "kumiko_event_consumers" SET
|
|
100
|
+
"last_processed_event_id" = $1,
|
|
101
|
+
"status" = 'idle',
|
|
102
|
+
"attempts" = 0,
|
|
103
|
+
"last_error" = NULL,
|
|
104
|
+
"updated_at" = now()
|
|
105
|
+
WHERE "name" = $2 AND "instance_id" = $3
|
|
106
|
+
RETURNING *`,
|
|
107
|
+
[eventId, name, instanceId],
|
|
108
|
+
)) as ReadonlyArray<Record<string, unknown>>;
|
|
109
|
+
return rows[0];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function resetConsumerForMspRebuild(
|
|
113
|
+
db: AnyDb,
|
|
114
|
+
name: string,
|
|
115
|
+
instanceId: string,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
await asRawClient(db).unsafe(
|
|
118
|
+
`INSERT INTO "kumiko_event_consumers" ("name", "instance_id", "last_processed_event_id", "status")
|
|
119
|
+
VALUES ($1, $2, 0, 'idle')
|
|
120
|
+
ON CONFLICT ("name", "instance_id") DO UPDATE SET
|
|
121
|
+
"last_processed_event_id" = 0,
|
|
122
|
+
"status" = 'idle',
|
|
123
|
+
"attempts" = 0,
|
|
124
|
+
"last_error" = NULL,
|
|
125
|
+
"updated_at" = now()`,
|
|
126
|
+
[name, instanceId],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function selectConsumerForUpdate(
|
|
131
|
+
db: AnyDb,
|
|
132
|
+
name: string,
|
|
133
|
+
instanceId: string,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
await asRawClient(db).unsafe(
|
|
136
|
+
`SELECT * FROM "kumiko_event_consumers" WHERE "name" = $1 AND "instance_id" = $2 FOR UPDATE`,
|
|
137
|
+
[name, instanceId],
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function updateConsumerRebuildCursor(
|
|
142
|
+
db: AnyDb,
|
|
143
|
+
name: string,
|
|
144
|
+
instanceId: string,
|
|
145
|
+
lastProcessedEventId: bigint,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
await asRawClient(db).unsafe(
|
|
148
|
+
`UPDATE "kumiko_event_consumers" SET
|
|
149
|
+
"last_processed_event_id" = $1,
|
|
150
|
+
"status" = 'idle',
|
|
151
|
+
"attempts" = 0,
|
|
152
|
+
"last_error" = NULL,
|
|
153
|
+
"updated_at" = now()
|
|
154
|
+
WHERE "name" = $2 AND "instance_id" = $3`,
|
|
155
|
+
[lastProcessedEventId, name, instanceId],
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function markConsumerRebuildFailed(
|
|
160
|
+
db: AnyDb,
|
|
161
|
+
name: string,
|
|
162
|
+
instanceId: string,
|
|
163
|
+
errorMessage: string,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
await asRawClient(db).unsafe(
|
|
166
|
+
`UPDATE "kumiko_event_consumers" SET "status" = 'dead', "last_error" = $1, "updated_at" = now()
|
|
167
|
+
WHERE "name" = $2 AND "instance_id" = $3`,
|
|
168
|
+
[errorMessage, name, instanceId],
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
export type RawFirstEventInsertParams = {
|
|
5
|
+
readonly aggregateId: string;
|
|
6
|
+
readonly aggregateType: string;
|
|
7
|
+
readonly tenantId: string;
|
|
8
|
+
readonly newVersion: number;
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly eventVersion: number;
|
|
11
|
+
readonly payloadJson: string;
|
|
12
|
+
readonly metadataJson: string;
|
|
13
|
+
readonly createdAt: string;
|
|
14
|
+
readonly createdBy: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function insertRawFirstEvent(
|
|
18
|
+
db: AnyDb,
|
|
19
|
+
params: RawFirstEventInsertParams,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
await asRawClient(db).unsafe(
|
|
22
|
+
`INSERT INTO "kumiko_events" (
|
|
23
|
+
aggregate_id, aggregate_type, tenant_id, version,
|
|
24
|
+
type, event_version, payload, metadata, created_at, created_by
|
|
25
|
+
)
|
|
26
|
+
VALUES ($1::uuid, $2, $3::uuid, $4, $5, $6, $7::jsonb, $8::jsonb, $9::timestamptz, $10)`,
|
|
27
|
+
[
|
|
28
|
+
params.aggregateId,
|
|
29
|
+
params.aggregateType,
|
|
30
|
+
params.tenantId,
|
|
31
|
+
params.newVersion,
|
|
32
|
+
params.type,
|
|
33
|
+
params.eventVersion,
|
|
34
|
+
params.payloadJson,
|
|
35
|
+
params.metadataJson,
|
|
36
|
+
params.createdAt,
|
|
37
|
+
params.createdBy,
|
|
38
|
+
],
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type RawSubsequentEventInsertParams = RawFirstEventInsertParams & {
|
|
43
|
+
readonly expectedVersion: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export async function insertRawSubsequentEvent(
|
|
47
|
+
db: AnyDb,
|
|
48
|
+
params: RawSubsequentEventInsertParams,
|
|
49
|
+
): Promise<boolean> {
|
|
50
|
+
const rows = (await asRawClient(db).unsafe(
|
|
51
|
+
`INSERT INTO "kumiko_events" (
|
|
52
|
+
aggregate_id, aggregate_type, tenant_id, version,
|
|
53
|
+
type, event_version, payload, metadata, created_at, created_by
|
|
54
|
+
)
|
|
55
|
+
SELECT $1::uuid, $2, $3::uuid, $4, $5, $6, $7::jsonb, $8::jsonb, $9::timestamptz, $10
|
|
56
|
+
WHERE EXISTS (
|
|
57
|
+
SELECT 1 FROM "kumiko_events"
|
|
58
|
+
WHERE aggregate_id = $1::uuid
|
|
59
|
+
AND version = $11
|
|
60
|
+
AND tenant_id = $3::uuid
|
|
61
|
+
)
|
|
62
|
+
RETURNING id`,
|
|
63
|
+
[
|
|
64
|
+
params.aggregateId,
|
|
65
|
+
params.aggregateType,
|
|
66
|
+
params.tenantId,
|
|
67
|
+
params.newVersion,
|
|
68
|
+
params.type,
|
|
69
|
+
params.eventVersion,
|
|
70
|
+
params.payloadJson,
|
|
71
|
+
params.metadataJson,
|
|
72
|
+
params.createdAt,
|
|
73
|
+
params.createdBy,
|
|
74
|
+
params.expectedVersion,
|
|
75
|
+
],
|
|
76
|
+
)) as ReadonlyArray<{ id: string }>;
|
|
77
|
+
return rows.length > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function insertRawEventBatch(
|
|
81
|
+
db: AnyDb,
|
|
82
|
+
sqlValues: string,
|
|
83
|
+
params: unknown[],
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
await asRawClient(db).unsafe(
|
|
86
|
+
`INSERT INTO "kumiko_events" (
|
|
87
|
+
aggregate_id, aggregate_type, tenant_id, version,
|
|
88
|
+
type, event_version, payload, metadata, created_at, created_by
|
|
89
|
+
)
|
|
90
|
+
VALUES ${sqlValues}`,
|
|
91
|
+
params,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function eventPredecessorExists(
|
|
96
|
+
db: AnyDb,
|
|
97
|
+
aggregateId: string,
|
|
98
|
+
tenantId: string,
|
|
99
|
+
version: number,
|
|
100
|
+
): Promise<boolean> {
|
|
101
|
+
const rows = (await asRawClient(db).unsafe(
|
|
102
|
+
`SELECT EXISTS(
|
|
103
|
+
SELECT 1 FROM "kumiko_events"
|
|
104
|
+
WHERE aggregate_id = $1::uuid
|
|
105
|
+
AND tenant_id = $2::uuid
|
|
106
|
+
AND version = $3
|
|
107
|
+
) AS present`,
|
|
108
|
+
[aggregateId, tenantId, version],
|
|
109
|
+
)) as ReadonlyArray<{ present: boolean }>;
|
|
110
|
+
return rows[0]?.present === true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function findExistingEventVersion(
|
|
114
|
+
db: AnyDb,
|
|
115
|
+
sqlInClause: string,
|
|
116
|
+
params: unknown[],
|
|
117
|
+
): Promise<{ aggregateId: string; version: number } | undefined> {
|
|
118
|
+
const rows = (await asRawClient(db).unsafe(
|
|
119
|
+
`SELECT aggregate_id, version FROM "kumiko_events"
|
|
120
|
+
WHERE (tenant_id, aggregate_id, version) IN (${sqlInClause})
|
|
121
|
+
LIMIT 1`,
|
|
122
|
+
params,
|
|
123
|
+
)) as ReadonlyArray<{ aggregate_id: string; version: number }>;
|
|
124
|
+
const row = rows[0];
|
|
125
|
+
if (!row) return undefined;
|
|
126
|
+
return { aggregateId: row.aggregate_id, version: row.version };
|
|
127
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
/** NOTIFY on commit — wakes LISTEN subscribers (event-dispatcher). */
|
|
5
|
+
export async function notifyPgChannel(db: AnyDb, channel: string): Promise<void> {
|
|
6
|
+
await asRawClient(db).unsafe(`SELECT pg_notify($1, '')`, [channel]);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type SubsequentEventInsertParams = {
|
|
10
|
+
readonly aggregateId: string;
|
|
11
|
+
readonly aggregateType: string;
|
|
12
|
+
readonly tenantId: string;
|
|
13
|
+
readonly newVersion: number;
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly eventVersion: number;
|
|
16
|
+
readonly payloadJson: string;
|
|
17
|
+
readonly metadataJson: string;
|
|
18
|
+
readonly createdBy: string;
|
|
19
|
+
readonly expectedVersion: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SubsequentEventInsertRow = {
|
|
23
|
+
readonly id: string | bigint;
|
|
24
|
+
readonly created_at: Date | string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** INSERT … SELECT … WHERE EXISTS predecessor — stays raw (typed builder can't express this). */
|
|
28
|
+
export async function insertSubsequentEventRow(
|
|
29
|
+
db: AnyDb,
|
|
30
|
+
params: SubsequentEventInsertParams,
|
|
31
|
+
): Promise<SubsequentEventInsertRow | undefined> {
|
|
32
|
+
const rows = (await asRawClient(db).unsafe(
|
|
33
|
+
`INSERT INTO "kumiko_events" (
|
|
34
|
+
aggregate_id, aggregate_type, tenant_id, version,
|
|
35
|
+
type, event_version, payload, metadata, created_by
|
|
36
|
+
)
|
|
37
|
+
SELECT $1::uuid, $2, $3::uuid, $4,
|
|
38
|
+
$5, $6, $7::jsonb,
|
|
39
|
+
$8::jsonb, $9
|
|
40
|
+
WHERE EXISTS (
|
|
41
|
+
SELECT 1 FROM "kumiko_events"
|
|
42
|
+
WHERE aggregate_id = $1::uuid
|
|
43
|
+
AND version = $10
|
|
44
|
+
AND tenant_id = $3::uuid
|
|
45
|
+
)
|
|
46
|
+
RETURNING id, created_at`,
|
|
47
|
+
[
|
|
48
|
+
params.aggregateId,
|
|
49
|
+
params.aggregateType,
|
|
50
|
+
params.tenantId,
|
|
51
|
+
params.newVersion,
|
|
52
|
+
params.type,
|
|
53
|
+
params.eventVersion,
|
|
54
|
+
params.payloadJson,
|
|
55
|
+
params.metadataJson,
|
|
56
|
+
params.createdBy,
|
|
57
|
+
params.expectedVersion,
|
|
58
|
+
],
|
|
59
|
+
)) as ReadonlyArray<SubsequentEventInsertRow>;
|
|
60
|
+
return rows[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function selectStreamMaxVersion(
|
|
64
|
+
db: AnyDb,
|
|
65
|
+
aggregateId: string,
|
|
66
|
+
tenantId: string,
|
|
67
|
+
): Promise<number> {
|
|
68
|
+
const rows = (await asRawClient(db).unsafe(
|
|
69
|
+
`SELECT MAX("version") AS v FROM "kumiko_events" WHERE "aggregate_id" = $1 AND "tenant_id" = $2`,
|
|
70
|
+
[aggregateId, tenantId],
|
|
71
|
+
)) as ReadonlyArray<{ v: number | null }>;
|
|
72
|
+
return rows[0]?.v ?? 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** MAX(version) for one aggregate stream — no tenant filter (seed idempotency). */
|
|
76
|
+
export async function selectAggregateMaxVersion(db: AnyDb, aggregateId: string): Promise<number> {
|
|
77
|
+
const rows = (await asRawClient(db).unsafe(
|
|
78
|
+
`SELECT MAX("version") AS v FROM "kumiko_events" WHERE "aggregate_id" = $1`,
|
|
79
|
+
[aggregateId],
|
|
80
|
+
)) as ReadonlyArray<{ v: number | null }>;
|
|
81
|
+
return rows[0]?.v ?? 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function selectEventsHighWaterMark(db: AnyDb): Promise<bigint> {
|
|
85
|
+
const rows = (await asRawClient(db).unsafe(
|
|
86
|
+
`SELECT COALESCE(MAX("id"), 0)::bigint AS max FROM "kumiko_events"`,
|
|
87
|
+
)) as ReadonlyArray<{ max: bigint | string | number | null }>;
|
|
88
|
+
const raw = rows[0]?.max;
|
|
89
|
+
if (typeof raw === "bigint") return raw;
|
|
90
|
+
if (raw === null || raw === undefined) return 0n;
|
|
91
|
+
return BigInt(raw);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Head event id for lag metrics — same aggregate as selectEventsHighWaterMark. */
|
|
95
|
+
export async function selectEventsHeadId(db: AnyDb): Promise<bigint> {
|
|
96
|
+
const rows = (await asRawClient(db).unsafe(
|
|
97
|
+
`SELECT COALESCE(MAX(id), 0)::bigint AS head FROM kumiko_events`,
|
|
98
|
+
)) as ReadonlyArray<{ head?: bigint | string | null }>;
|
|
99
|
+
const raw = rows[0]?.head;
|
|
100
|
+
if (typeof raw === "bigint") return raw;
|
|
101
|
+
return BigInt(raw ?? 0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function selectNextEventIdAfter(db: AnyDb, afterId: bigint): Promise<bigint | null> {
|
|
105
|
+
const rows = (await asRawClient(db).unsafe(
|
|
106
|
+
`SELECT "id" FROM "kumiko_events" WHERE "id" > $1 ORDER BY "id" ASC LIMIT 1`,
|
|
107
|
+
[afterId],
|
|
108
|
+
)) as ReadonlyArray<{ id: string | bigint }>;
|
|
109
|
+
const row = rows[0];
|
|
110
|
+
if (!row) return null;
|
|
111
|
+
return typeof row.id === "bigint" ? row.id : BigInt(row.id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type SaveSnapshotParams = {
|
|
115
|
+
readonly aggregateId: string;
|
|
116
|
+
readonly tenantId: string;
|
|
117
|
+
readonly aggregateType: string;
|
|
118
|
+
readonly version: number;
|
|
119
|
+
readonly stateJson: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export async function upsertSnapshot(db: AnyDb, params: SaveSnapshotParams): Promise<void> {
|
|
123
|
+
await asRawClient(db).unsafe(
|
|
124
|
+
`INSERT INTO "kumiko_snapshots"
|
|
125
|
+
("aggregate_id", "tenant_id", "aggregate_type", "version", "state")
|
|
126
|
+
VALUES ($1, $2, $3, $4, $5::jsonb)
|
|
127
|
+
ON CONFLICT ("aggregate_id", "version") DO UPDATE SET
|
|
128
|
+
"state" = $5::jsonb,
|
|
129
|
+
"aggregate_type" = $3,
|
|
130
|
+
"created_at" = now()`,
|
|
131
|
+
[params.aggregateId, params.tenantId, params.aggregateType, params.version, params.stateJson],
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ArchiveStreamParams = {
|
|
136
|
+
readonly tenantId: string;
|
|
137
|
+
readonly aggregateId: string;
|
|
138
|
+
readonly aggregateType: string;
|
|
139
|
+
readonly archivedBy: string;
|
|
140
|
+
readonly reason: string | null;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export async function upsertArchivedStream(db: AnyDb, params: ArchiveStreamParams): Promise<void> {
|
|
144
|
+
await asRawClient(db).unsafe(
|
|
145
|
+
`INSERT INTO "kumiko_archived_streams"
|
|
146
|
+
("tenant_id", "aggregate_id", "aggregate_type", "archived_by", "reason")
|
|
147
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
148
|
+
ON CONFLICT ("tenant_id", "aggregate_id") DO UPDATE SET
|
|
149
|
+
"archived_at" = now(),
|
|
150
|
+
"archived_by" = $4,
|
|
151
|
+
"aggregate_type" = $3,
|
|
152
|
+
"reason" = $5`,
|
|
153
|
+
[params.tenantId, params.aggregateId, params.aggregateType, params.archivedBy, params.reason],
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
export async function markProjectionRebuilding(db: AnyDb, projectionName: string): Promise<void> {
|
|
5
|
+
await asRawClient(db).unsafe(
|
|
6
|
+
`INSERT INTO "kumiko_projections" ("name", "status") VALUES ($1, 'rebuilding')
|
|
7
|
+
ON CONFLICT ("name") DO UPDATE SET
|
|
8
|
+
"status" = 'rebuilding',
|
|
9
|
+
"last_error" = NULL,
|
|
10
|
+
"updated_at" = now()`,
|
|
11
|
+
[projectionName],
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function selectEventsForProjectionRebuild(
|
|
16
|
+
db: AnyDb,
|
|
17
|
+
aggregateTypes: readonly string[],
|
|
18
|
+
eventTypes: readonly string[],
|
|
19
|
+
): Promise<ReadonlyArray<Record<string, unknown>>> {
|
|
20
|
+
return (await asRawClient(db).unsafe(
|
|
21
|
+
`SELECT * FROM "kumiko_events"
|
|
22
|
+
WHERE "aggregate_type" = ANY($1::text[])
|
|
23
|
+
AND "type" = ANY($2::text[])
|
|
24
|
+
ORDER BY "id" ASC`,
|
|
25
|
+
[aggregateTypes, eventTypes],
|
|
26
|
+
)) as ReadonlyArray<Record<string, unknown>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function finalizeProjectionRebuild(
|
|
30
|
+
db: AnyDb,
|
|
31
|
+
projectionName: string,
|
|
32
|
+
lastProcessedEventId: bigint,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
await asRawClient(db).unsafe(
|
|
35
|
+
`UPDATE "kumiko_projections" SET
|
|
36
|
+
"last_processed_event_id" = $1,
|
|
37
|
+
"status" = 'idle',
|
|
38
|
+
"last_rebuild_at" = now(),
|
|
39
|
+
"last_error" = NULL,
|
|
40
|
+
"updated_at" = now()
|
|
41
|
+
WHERE "name" = $2`,
|
|
42
|
+
[lastProcessedEventId, projectionName],
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function markProjectionRebuildFailed(
|
|
47
|
+
db: AnyDb,
|
|
48
|
+
projectionName: string,
|
|
49
|
+
errorMessage: string,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
await asRawClient(db).unsafe(
|
|
52
|
+
`INSERT INTO "kumiko_projections" ("name", "status", "last_error") VALUES ($1, 'failed', $2)
|
|
53
|
+
ON CONFLICT ("name") DO UPDATE SET
|
|
54
|
+
"status" = 'failed',
|
|
55
|
+
"last_error" = $2,
|
|
56
|
+
"updated_at" = now()`,
|
|
57
|
+
[projectionName, errorMessage],
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
/** Escape hatch for caller-built SQL (ownership clauses, entity list queries). */
|
|
5
|
+
export async function executeRawQuery<T = Record<string, unknown>>(
|
|
6
|
+
db: AnyDb,
|
|
7
|
+
sqlText: string,
|
|
8
|
+
params: readonly unknown[] = [],
|
|
9
|
+
): Promise<readonly T[]> {
|
|
10
|
+
return (await asRawClient(db).unsafe(sqlText, params)) as readonly T[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function pingDatabase(db: AnyDb): Promise<void> {
|
|
14
|
+
await asRawClient(db).unsafe("SELECT 1");
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
export type AppliedMigrationRow = {
|
|
5
|
+
readonly hash: string;
|
|
6
|
+
readonly created_at: bigint | number | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type DbColumnInfoRow = {
|
|
10
|
+
readonly column_name: string;
|
|
11
|
+
readonly data_type: string;
|
|
12
|
+
readonly is_nullable: "YES" | "NO";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** tableRef must be `drizzle.__drizzle_migrations` or `public.__drizzle_migrations`. */
|
|
16
|
+
export async function selectAppliedMigrations(
|
|
17
|
+
db: AnyDb,
|
|
18
|
+
tableRef: "drizzle.__drizzle_migrations" | "public.__drizzle_migrations",
|
|
19
|
+
): Promise<readonly AppliedMigrationRow[]> {
|
|
20
|
+
return (await asRawClient(db).unsafe(
|
|
21
|
+
`SELECT hash, created_at FROM ${tableRef} ORDER BY id`,
|
|
22
|
+
)) as readonly AppliedMigrationRow[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function selectPublicTableColumns(
|
|
26
|
+
db: AnyDb,
|
|
27
|
+
tableName: string,
|
|
28
|
+
): Promise<readonly DbColumnInfoRow[]> {
|
|
29
|
+
return (await asRawClient(db).unsafe(
|
|
30
|
+
`SELECT column_name, data_type, is_nullable
|
|
31
|
+
FROM information_schema.columns
|
|
32
|
+
WHERE table_schema = 'public' AND table_name = $1`,
|
|
33
|
+
[tableName],
|
|
34
|
+
)) as readonly DbColumnInfoRow[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
export type SeedUserRow = {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly email: string;
|
|
7
|
+
readonly tenantId: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type SeedMembershipDbRow = {
|
|
11
|
+
readonly user_id: string;
|
|
12
|
+
readonly tenant_id: string;
|
|
13
|
+
readonly stream_tenant_id: string;
|
|
14
|
+
readonly roles: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SeedTenantDbRow = {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly tenant_key: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function selectUserByEmail(db: AnyDb, email: string): Promise<SeedUserRow | null> {
|
|
24
|
+
const rows = (await asRawClient(db).unsafe(
|
|
25
|
+
`SELECT id::text AS id, email, tenant_id::text AS tenant_id
|
|
26
|
+
FROM read_users
|
|
27
|
+
WHERE email = $1
|
|
28
|
+
LIMIT 1`,
|
|
29
|
+
[email],
|
|
30
|
+
)) as readonly { id: string; email: string; tenant_id: string }[];
|
|
31
|
+
const row = rows[0];
|
|
32
|
+
if (!row) return null;
|
|
33
|
+
return { id: row.id, email: row.email, tenantId: row.tenant_id };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function selectMembershipsOfUser(
|
|
37
|
+
db: AnyDb,
|
|
38
|
+
userId: string,
|
|
39
|
+
): Promise<readonly SeedMembershipDbRow[]> {
|
|
40
|
+
return (await asRawClient(db).unsafe(
|
|
41
|
+
`SELECT m.user_id::text AS user_id,
|
|
42
|
+
m.tenant_id::text AS tenant_id,
|
|
43
|
+
e.tenant_id::text AS stream_tenant_id,
|
|
44
|
+
m.roles
|
|
45
|
+
FROM read_tenant_memberships m
|
|
46
|
+
JOIN kumiko_events e ON e.aggregate_id = m.id AND e.version = 1
|
|
47
|
+
WHERE m.user_id = $1`,
|
|
48
|
+
[userId],
|
|
49
|
+
)) as readonly SeedMembershipDbRow[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function selectAllTenants(db: AnyDb): Promise<readonly SeedTenantDbRow[]> {
|
|
53
|
+
return (await asRawClient(db).unsafe(
|
|
54
|
+
`SELECT id::text AS id, name, tenant_key
|
|
55
|
+
FROM read_tenants
|
|
56
|
+
ORDER BY inserted_at`,
|
|
57
|
+
)) as readonly SeedTenantDbRow[];
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
/** Double-quote + escape — Postgres identifier rules. */
|
|
5
|
+
export function quoteTableIdent(name: string): string {
|
|
6
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function truncateTable(db: AnyDb, tableName: string): Promise<void> {
|
|
10
|
+
await asRawClient(db).unsafe(`TRUNCATE TABLE ${quoteTableIdent(tableName)}`);
|
|
11
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
import { quoteTableIdent } from "./table-ops";
|
|
4
|
+
|
|
5
|
+
export async function executeDdlStatement(db: AnyDb, sqlText: string): Promise<void> {
|
|
6
|
+
await asRawClient(db).unsafe(sqlText);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function alterTableAddColumn(
|
|
10
|
+
db: AnyDb,
|
|
11
|
+
tableName: string,
|
|
12
|
+
columnName: string,
|
|
13
|
+
columnType: string,
|
|
14
|
+
defaultClause: string,
|
|
15
|
+
notNull: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
await asRawClient(db).unsafe(
|
|
18
|
+
`ALTER TABLE ${quoteTableIdent(tableName)} ADD COLUMN ${quoteTableIdent(columnName)} ${columnType}${defaultClause}${notNull}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function createIndexIfNotExists(
|
|
23
|
+
db: AnyDb,
|
|
24
|
+
indexKind: "UNIQUE INDEX" | "INDEX",
|
|
25
|
+
indexName: string,
|
|
26
|
+
tableName: string,
|
|
27
|
+
columnList: string,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
await asRawClient(db).unsafe(
|
|
30
|
+
`CREATE ${indexKind} IF NOT EXISTS ${quoteTableIdent(indexName)} ON ${quoteTableIdent(tableName)} (${columnList})`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function truncateTablesRestartIdentity(
|
|
35
|
+
db: AnyDb,
|
|
36
|
+
tableNames: readonly string[],
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const quoted = tableNames.map((name) => quoteTableIdent(name)).join(", ");
|
|
39
|
+
await asRawClient(db).unsafe(`TRUNCATE ${quoted} RESTART IDENTITY CASCADE`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function databaseExists(db: AnyDb, dbName: string): Promise<boolean> {
|
|
43
|
+
const rows = (await asRawClient(db).unsafe(
|
|
44
|
+
`SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1) AS exists`,
|
|
45
|
+
[dbName],
|
|
46
|
+
)) as readonly { exists?: boolean }[];
|
|
47
|
+
return rows[0]?.exists === true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function createDatabase(db: AnyDb, dbName: string): Promise<void> {
|
|
51
|
+
await asRawClient(db).unsafe(`CREATE DATABASE ${quoteTableIdent(dbName)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function dropDatabaseIfExists(db: AnyDb, dbName: string): Promise<void> {
|
|
55
|
+
await asRawClient(db).unsafe(`DROP DATABASE IF EXISTS ${quoteTableIdent(dbName)}`);
|
|
56
|
+
}
|