@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
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
// with only a 1→2 migration fails immediately, so a missing upcaster
|
|
9
9
|
// can never silently hand half-migrated data to consumers.
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
11
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
12
|
import { z } from "zod";
|
|
14
13
|
import { integer as pgInteger, table as pgTable, text as pgText } from "../../db/dialect";
|
|
15
14
|
import { createEventStoreExecutor } from "../../db/event-store-executor";
|
|
16
|
-
import {
|
|
15
|
+
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
16
|
+
import { buildEntityTable } from "../../db/table-builder";
|
|
17
17
|
import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
18
18
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
19
19
|
import type { StoredEvent } from "../../event-store";
|
|
@@ -36,7 +36,7 @@ const orderEntity = createEntity({
|
|
|
36
36
|
customer: createTextField({ required: true }),
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
|
-
const orderTable =
|
|
39
|
+
const orderTable = buildEntityTable("upcast-order", orderEntity);
|
|
40
40
|
|
|
41
41
|
// Projection stores the UPCAST view: the v3 shape expects `totalCents` (int)
|
|
42
42
|
// even though the earliest writes might have stored `totalEuros` (string).
|
|
@@ -78,18 +78,10 @@ const orderFeature = defineFeature("upcastshop", (r) => {
|
|
|
78
78
|
apply: {
|
|
79
79
|
[orderPriced.name]: async (event, tx) => {
|
|
80
80
|
const p = event.payload as { totalCents: number; currency: string };
|
|
81
|
-
await tx
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
tenantId: event.tenantId,
|
|
86
|
-
totalCents: p.totalCents,
|
|
87
|
-
currency: p.currency,
|
|
88
|
-
})
|
|
89
|
-
.onConflictDoUpdate({
|
|
90
|
-
target: orderSummaryTable.orderId,
|
|
91
|
-
set: { totalCents: p.totalCents, currency: p.currency },
|
|
92
|
-
});
|
|
81
|
+
await asRawClient(tx).unsafe(
|
|
82
|
+
`INSERT INTO "read_upcast_order_summary" (order_id, tenant_id, total_cents, currency) VALUES ($1, $2, $3, $4) ON CONFLICT (order_id) DO UPDATE SET total_cents = $3, currency = $4`,
|
|
83
|
+
[event.aggregateId, event.tenantId, p.totalCents, p.currency],
|
|
84
|
+
);
|
|
93
85
|
},
|
|
94
86
|
},
|
|
95
87
|
});
|
|
@@ -121,8 +113,8 @@ afterAll(async () => {
|
|
|
121
113
|
});
|
|
122
114
|
|
|
123
115
|
beforeEach(async () => {
|
|
124
|
-
await testDb.db.
|
|
125
|
-
|
|
116
|
+
await asRawClient(testDb.db).unsafe(
|
|
117
|
+
`TRUNCATE kumiko_events, read_upcast_orders, read_upcast_order_summary, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
126
118
|
);
|
|
127
119
|
});
|
|
128
120
|
|
|
@@ -280,10 +272,7 @@ describe("upcaster: projection rebuild walks the chain on replay", () => {
|
|
|
280
272
|
});
|
|
281
273
|
expect(result.eventsProcessed).toBe(3);
|
|
282
274
|
|
|
283
|
-
const rows = await testDb.db
|
|
284
|
-
.select()
|
|
285
|
-
.from(orderSummaryTable)
|
|
286
|
-
.orderBy(orderSummaryTable.orderId);
|
|
275
|
+
const rows = await selectMany(testDb.db, orderSummaryTable);
|
|
287
276
|
|
|
288
277
|
// Ordered by orderId → ord1 (10€ = 1000¢), ord2 ($25.50 = 2550¢), ord3 (9900¢)
|
|
289
278
|
expect(rows).toHaveLength(3);
|
|
@@ -305,9 +294,7 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
|
|
|
305
294
|
segment: pgText("segment").notNull(),
|
|
306
295
|
});
|
|
307
296
|
await unsafePushTables(testDb.db, { upcastAsyncCustomerSegments: customerSegments });
|
|
308
|
-
await testDb.db
|
|
309
|
-
.insert(customerSegments)
|
|
310
|
-
.values({ customerId: "c-async-1", segment: "PREMIUM" });
|
|
297
|
+
await insertOne(testDb.db, customerSegments, { customerId: "c-async-1", segment: "PREMIUM" });
|
|
311
298
|
|
|
312
299
|
const asyncSummary = pgTable("upcast_async_summary", {
|
|
313
300
|
orderId: pgText("order_id").primaryKey(),
|
|
@@ -327,11 +314,11 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
|
|
|
327
314
|
|
|
328
315
|
r.eventMigration("placed", 1, 2, async (payload, ctx) => {
|
|
329
316
|
const p = payload as { customerId: string };
|
|
330
|
-
const [row] = await ctx.db
|
|
331
|
-
|
|
332
|
-
.
|
|
333
|
-
|
|
334
|
-
|
|
317
|
+
const [row] = await selectMany(ctx.db, customerSegments, { customerId: p.customerId });
|
|
318
|
+
return {
|
|
319
|
+
customerId: p.customerId,
|
|
320
|
+
segment: (row as { segment?: string } | undefined)?.segment ?? "UNKNOWN",
|
|
321
|
+
};
|
|
335
322
|
});
|
|
336
323
|
|
|
337
324
|
r.projection({
|
|
@@ -341,7 +328,7 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
|
|
|
341
328
|
apply: {
|
|
342
329
|
[placed.name]: async (event, tx) => {
|
|
343
330
|
const p = event.payload as { customerId: string; segment: string };
|
|
344
|
-
await tx
|
|
331
|
+
await insertOne(tx, asyncSummary, {
|
|
345
332
|
orderId: event.aggregateId,
|
|
346
333
|
customerId: p.customerId,
|
|
347
334
|
segment: p.segment,
|
|
@@ -384,7 +371,7 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
|
|
|
384
371
|
});
|
|
385
372
|
expect(result.eventsProcessed).toBe(2);
|
|
386
373
|
|
|
387
|
-
const rows = await testDb.db
|
|
374
|
+
const rows = await selectMany(testDb.db, asyncSummary);
|
|
388
375
|
expect(rows).toHaveLength(2);
|
|
389
376
|
const byId = new Map(rows.map((r) => [r.orderId, r]));
|
|
390
377
|
// v1 → v2 via async DB lookup → segment from customer_segments.
|
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
// Allowlist: samples/*/migration/, scripts/migrations/, die Definition
|
|
8
8
|
// selbst, das Guard-Script selbst.
|
|
9
9
|
|
|
10
|
-
import { sql } from "drizzle-orm";
|
|
11
10
|
import type { DbRunner } from "../db";
|
|
12
11
|
import { isUniqueViolation } from "../db/pg-error";
|
|
12
|
+
import {
|
|
13
|
+
eventPredecessorExists,
|
|
14
|
+
findExistingEventVersion,
|
|
15
|
+
insertRawEventBatch,
|
|
16
|
+
insertRawFirstEvent,
|
|
17
|
+
insertRawSubsequentEvent,
|
|
18
|
+
} from "../db/queries/event-store-admin";
|
|
13
19
|
import type { TenantId } from "../engine/types";
|
|
14
20
|
import { VersionConflictError } from "./errors";
|
|
15
21
|
import type { EventMetadata } from "./event-store";
|
|
16
|
-
import { eventsTable } from "./events-schema";
|
|
17
22
|
|
|
18
23
|
export type RawEventToAppend = {
|
|
19
24
|
readonly aggregateId: string;
|
|
@@ -55,30 +60,28 @@ export async function appendRaw(runner: DbRunner, event: RawEventToAppend): Prom
|
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
function rawEventParams(event: RawEventToAppend, newVersion: number, eventVersion: number) {
|
|
64
|
+
return {
|
|
65
|
+
aggregateId: event.aggregateId,
|
|
66
|
+
aggregateType: event.aggregateType,
|
|
67
|
+
tenantId: event.tenantId,
|
|
68
|
+
newVersion,
|
|
69
|
+
type: event.type,
|
|
70
|
+
eventVersion,
|
|
71
|
+
payloadJson: JSON.stringify(event.payload),
|
|
72
|
+
metadataJson: JSON.stringify(event.metadata),
|
|
73
|
+
createdAt: event.createdAt.toString(),
|
|
74
|
+
createdBy: event.createdBy,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
async function insertRawFirst(
|
|
59
79
|
runner: DbRunner,
|
|
60
80
|
event: RawEventToAppend,
|
|
61
81
|
newVersion: number,
|
|
62
82
|
eventVersion: number,
|
|
63
83
|
): Promise<void> {
|
|
64
|
-
await runner
|
|
65
|
-
INSERT INTO ${eventsTable} (
|
|
66
|
-
aggregate_id, aggregate_type, tenant_id, version,
|
|
67
|
-
type, event_version, payload, metadata, created_at, created_by
|
|
68
|
-
)
|
|
69
|
-
VALUES (
|
|
70
|
-
${event.aggregateId}::uuid,
|
|
71
|
-
${event.aggregateType},
|
|
72
|
-
${event.tenantId}::uuid,
|
|
73
|
-
${newVersion},
|
|
74
|
-
${event.type},
|
|
75
|
-
${eventVersion},
|
|
76
|
-
${JSON.stringify(event.payload)}::jsonb,
|
|
77
|
-
${JSON.stringify(event.metadata)}::jsonb,
|
|
78
|
-
${event.createdAt.toString()}::timestamptz,
|
|
79
|
-
${event.createdBy}
|
|
80
|
-
)
|
|
81
|
-
`);
|
|
84
|
+
await insertRawFirstEvent(runner, rawEventParams(event, newVersion, eventVersion));
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
async function insertRawSubsequent(
|
|
@@ -87,30 +90,11 @@ async function insertRawSubsequent(
|
|
|
87
90
|
newVersion: number,
|
|
88
91
|
eventVersion: number,
|
|
89
92
|
): Promise<void> {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
SELECT ${event.aggregateId}::uuid,
|
|
96
|
-
${event.aggregateType},
|
|
97
|
-
${event.tenantId}::uuid,
|
|
98
|
-
${newVersion},
|
|
99
|
-
${event.type},
|
|
100
|
-
${eventVersion},
|
|
101
|
-
${JSON.stringify(event.payload)}::jsonb,
|
|
102
|
-
${JSON.stringify(event.metadata)}::jsonb,
|
|
103
|
-
${event.createdAt.toString()}::timestamptz,
|
|
104
|
-
${event.createdBy}
|
|
105
|
-
WHERE EXISTS (
|
|
106
|
-
SELECT 1 FROM ${eventsTable}
|
|
107
|
-
WHERE aggregate_id = ${event.aggregateId}::uuid
|
|
108
|
-
AND version = ${event.expectedVersion}
|
|
109
|
-
AND tenant_id = ${event.tenantId}::uuid
|
|
110
|
-
)
|
|
111
|
-
RETURNING id
|
|
112
|
-
`);
|
|
113
|
-
if (rows.length === 0) {
|
|
93
|
+
const inserted = await insertRawSubsequentEvent(runner, {
|
|
94
|
+
...rawEventParams(event, newVersion, eventVersion),
|
|
95
|
+
expectedVersion: event.expectedVersion,
|
|
96
|
+
});
|
|
97
|
+
if (!inserted) {
|
|
114
98
|
throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
115
99
|
}
|
|
116
100
|
}
|
|
@@ -133,31 +117,28 @@ export async function appendRawBatch(
|
|
|
133
117
|
await verifyPredecessors(runner, events);
|
|
134
118
|
await verifyNoDuplicates(runner, events);
|
|
135
119
|
|
|
136
|
-
const
|
|
120
|
+
const params: unknown[] = [];
|
|
121
|
+
const valuesClauses = events.map((e) => {
|
|
137
122
|
const newVersion = e.expectedVersion + 1;
|
|
138
123
|
const eventVersion = e.eventVersion ?? 1;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
124
|
+
const baseIdx = params.length;
|
|
125
|
+
params.push(
|
|
126
|
+
e.aggregateId,
|
|
127
|
+
e.aggregateType,
|
|
128
|
+
e.tenantId,
|
|
129
|
+
newVersion,
|
|
130
|
+
e.type,
|
|
131
|
+
eventVersion,
|
|
132
|
+
JSON.stringify(e.payload),
|
|
133
|
+
JSON.stringify(e.metadata),
|
|
134
|
+
e.createdAt.toString(),
|
|
135
|
+
e.createdBy,
|
|
136
|
+
);
|
|
137
|
+
return `($${baseIdx + 1}::uuid, $${baseIdx + 2}, $${baseIdx + 3}::uuid, $${baseIdx + 4}, $${baseIdx + 5}, $${baseIdx + 6}, $${baseIdx + 7}::jsonb, $${baseIdx + 8}::jsonb, $${baseIdx + 9}::timestamptz, $${baseIdx + 10})`;
|
|
151
138
|
});
|
|
152
139
|
|
|
153
140
|
try {
|
|
154
|
-
await runner.
|
|
155
|
-
INSERT INTO ${eventsTable} (
|
|
156
|
-
aggregate_id, aggregate_type, tenant_id, version,
|
|
157
|
-
type, event_version, payload, metadata, created_at, created_by
|
|
158
|
-
)
|
|
159
|
-
VALUES ${sql.join(rows, sql`, `)}
|
|
160
|
-
`);
|
|
141
|
+
await insertRawEventBatch(runner, valuesClauses.join(", "), params);
|
|
161
142
|
} catch (e) {
|
|
162
143
|
if (isUniqueViolation(e)) {
|
|
163
144
|
// Pre-flight ran but lost a race against a concurrent writer. Rare for
|
|
@@ -221,15 +202,8 @@ async function verifyPredecessors(
|
|
|
221
202
|
|
|
222
203
|
for (const g of groups.values()) {
|
|
223
204
|
if (g.minExpected === 0) continue;
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
SELECT 1 FROM ${eventsTable}
|
|
227
|
-
WHERE aggregate_id = ${g.aggregateId}::uuid
|
|
228
|
-
AND tenant_id = ${g.tenantId}::uuid
|
|
229
|
-
AND version = ${g.minExpected}
|
|
230
|
-
) AS present
|
|
231
|
-
`);
|
|
232
|
-
if (!rows[0]?.present) {
|
|
205
|
+
const present = await eventPredecessorExists(runner, g.aggregateId, g.tenantId, g.minExpected);
|
|
206
|
+
if (!present) {
|
|
233
207
|
throw new VersionConflictError(g.aggregateId, g.minExpected);
|
|
234
208
|
}
|
|
235
209
|
}
|
|
@@ -242,16 +216,14 @@ async function verifyNoDuplicates(
|
|
|
242
216
|
runner: DbRunner,
|
|
243
217
|
events: readonly RawEventToAppend[],
|
|
244
218
|
): Promise<void> {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
`);
|
|
253
|
-
const conflict = rows[0];
|
|
219
|
+
const params: unknown[] = [];
|
|
220
|
+
const tripleClauses = events.map((e) => {
|
|
221
|
+
const baseIdx = params.length;
|
|
222
|
+
params.push(e.tenantId, e.aggregateId, e.expectedVersion + 1);
|
|
223
|
+
return `($${baseIdx + 1}::uuid, $${baseIdx + 2}::uuid, $${baseIdx + 3})`;
|
|
224
|
+
});
|
|
225
|
+
const conflict = await findExistingEventVersion(runner, tripleClauses.join(", "), params);
|
|
254
226
|
if (conflict) {
|
|
255
|
-
throw new VersionConflictError(conflict.
|
|
227
|
+
throw new VersionConflictError(conflict.aggregateId, conflict.version - 1);
|
|
256
228
|
}
|
|
257
229
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
// sql now comes from native dialect
|
|
2
|
+
|
|
2
3
|
import type { DbConnection, DbRunner } from "../db/connection";
|
|
3
|
-
import { instant, table as pgTable, text, uniqueIndex, uuid } from "../db/dialect";
|
|
4
|
+
import { instant, table as pgTable, sql, text, uniqueIndex, uuid } from "../db/dialect";
|
|
5
|
+
import { upsertArchivedStream } from "../db/queries/event-store";
|
|
6
|
+
import { deleteMany, fetchOne } from "../db/query";
|
|
4
7
|
import { tableExists } from "../db/schema-inspection";
|
|
5
8
|
import type { TenantId } from "../engine/types";
|
|
6
9
|
import { unsafePushTables } from "../stack";
|
|
@@ -46,24 +49,13 @@ export type ArchiveStreamArgs = {
|
|
|
46
49
|
// aggregate) updates archivedAt/archivedBy to the latest call instead of
|
|
47
50
|
// failing. That matches Marten's "archive is a state, not an event" model.
|
|
48
51
|
export async function archiveStream(db: DbRunner, args: ArchiveStreamArgs): Promise<void> {
|
|
49
|
-
await db
|
|
50
|
-
.
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
reason: args.reason ?? null,
|
|
57
|
-
})
|
|
58
|
-
.onConflictDoUpdate({
|
|
59
|
-
target: [archivedStreamsTable.tenantId, archivedStreamsTable.aggregateId],
|
|
60
|
-
set: {
|
|
61
|
-
archivedAt: sql`now()`,
|
|
62
|
-
archivedBy: args.archivedBy,
|
|
63
|
-
aggregateType: args.aggregateType,
|
|
64
|
-
reason: args.reason ?? null,
|
|
65
|
-
},
|
|
66
|
-
});
|
|
52
|
+
await upsertArchivedStream(db, {
|
|
53
|
+
tenantId: args.tenantId,
|
|
54
|
+
aggregateId: args.aggregateId,
|
|
55
|
+
aggregateType: args.aggregateType,
|
|
56
|
+
archivedBy: args.archivedBy,
|
|
57
|
+
reason: args.reason ?? null,
|
|
58
|
+
});
|
|
67
59
|
}
|
|
68
60
|
|
|
69
61
|
// Cheap existence probe — issued in the hot read path, so keep the query to
|
|
@@ -73,17 +65,8 @@ export async function isStreamArchived(
|
|
|
73
65
|
tenantId: TenantId,
|
|
74
66
|
aggregateId: string,
|
|
75
67
|
): Promise<boolean> {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
.from(archivedStreamsTable)
|
|
79
|
-
.where(
|
|
80
|
-
and(
|
|
81
|
-
eq(archivedStreamsTable.tenantId, tenantId),
|
|
82
|
-
eq(archivedStreamsTable.aggregateId, aggregateId),
|
|
83
|
-
),
|
|
84
|
-
)
|
|
85
|
-
.limit(1);
|
|
86
|
-
return rows.length > 0;
|
|
68
|
+
const row = await fetchOne(db, archivedStreamsTable, { tenantId, aggregateId });
|
|
69
|
+
return row !== undefined;
|
|
87
70
|
}
|
|
88
71
|
|
|
89
72
|
// Undo an archive — restores the stream to writable state. Ops tool. The
|
|
@@ -95,12 +78,5 @@ export async function restoreStream(
|
|
|
95
78
|
tenantId: TenantId,
|
|
96
79
|
aggregateId: string,
|
|
97
80
|
): Promise<void> {
|
|
98
|
-
await db
|
|
99
|
-
.delete(archivedStreamsTable)
|
|
100
|
-
.where(
|
|
101
|
-
and(
|
|
102
|
-
eq(archivedStreamsTable.tenantId, tenantId),
|
|
103
|
-
eq(archivedStreamsTable.aggregateId, aggregateId),
|
|
104
|
-
),
|
|
105
|
-
);
|
|
81
|
+
await deleteMany(db, archivedStreamsTable, { tenantId, aggregateId });
|
|
106
82
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { and, asc, eq, gt, lte, max, sql } from "drizzle-orm";
|
|
2
1
|
import type { DbRunner } from "../db";
|
|
3
2
|
import { isUniqueViolation } from "../db/pg-error";
|
|
3
|
+
import {
|
|
4
|
+
insertSubsequentEventRow,
|
|
5
|
+
notifyPgChannel,
|
|
6
|
+
selectAggregateMaxVersion,
|
|
7
|
+
selectEventsHighWaterMark,
|
|
8
|
+
selectStreamMaxVersion,
|
|
9
|
+
} from "../db/queries/event-store";
|
|
10
|
+
import { insertOne, selectMany } from "../db/query";
|
|
4
11
|
import type { TenantId } from "../engine/types";
|
|
5
12
|
import { isStreamArchived } from "./archive";
|
|
6
13
|
import { VersionConflictError } from "./errors";
|
|
@@ -60,7 +67,19 @@ export type StoredEvent<TPayload = Record<string, unknown>> = {
|
|
|
60
67
|
readonly createdBy: string;
|
|
61
68
|
};
|
|
62
69
|
|
|
63
|
-
type SelectedEvent =
|
|
70
|
+
type SelectedEvent = {
|
|
71
|
+
readonly id: bigint;
|
|
72
|
+
readonly aggregateId: string;
|
|
73
|
+
readonly aggregateType: string;
|
|
74
|
+
readonly tenantId: TenantId;
|
|
75
|
+
readonly version: number;
|
|
76
|
+
readonly type: string;
|
|
77
|
+
readonly eventVersion: number;
|
|
78
|
+
readonly payload: Record<string, unknown>;
|
|
79
|
+
readonly metadata: EventMetadata;
|
|
80
|
+
readonly createdAt: Temporal.Instant;
|
|
81
|
+
readonly createdBy: string;
|
|
82
|
+
};
|
|
64
83
|
|
|
65
84
|
// Append one event atomically. Two guarantees combined:
|
|
66
85
|
//
|
|
@@ -99,7 +118,7 @@ export async function append(db: DbRunner, event: EventToAppend): Promise<Stored
|
|
|
99
118
|
// NOTIFY fires on commit (PG buffers NOTIFY per TX), so subscribers never
|
|
100
119
|
// see a wake-up for an event that later rolled back. Harmless no-op when
|
|
101
120
|
// no LISTENer is attached.
|
|
102
|
-
await db
|
|
121
|
+
await notifyPgChannel(db, EVENTS_PUBSUB_CHANNEL);
|
|
103
122
|
|
|
104
123
|
return buildStoredEvent(event, newVersion, eventVersion, row);
|
|
105
124
|
} catch (e) {
|
|
@@ -122,22 +141,19 @@ async function insertFirstEvent(
|
|
|
122
141
|
newVersion: number,
|
|
123
142
|
eventVersion: number,
|
|
124
143
|
): Promise<InsertReturn> {
|
|
125
|
-
const
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
createdBy: event.metadata.userId,
|
|
137
|
-
})
|
|
138
|
-
.returning({ id: eventsTable.id, createdAt: eventsTable.createdAt });
|
|
144
|
+
const row = await insertOne<{ id: bigint; createdAt: Temporal.Instant }>(db, eventsTable, {
|
|
145
|
+
aggregateId: event.aggregateId,
|
|
146
|
+
aggregateType: event.aggregateType,
|
|
147
|
+
tenantId: event.tenantId,
|
|
148
|
+
version: newVersion,
|
|
149
|
+
type: event.type,
|
|
150
|
+
eventVersion,
|
|
151
|
+
payload: event.payload,
|
|
152
|
+
metadata: event.metadata,
|
|
153
|
+
createdBy: event.metadata.userId,
|
|
154
|
+
});
|
|
139
155
|
if (!row) throw new Error("insertFirstEvent: INSERT RETURNING produced no row");
|
|
140
|
-
return row;
|
|
156
|
+
return { id: row.id, createdAt: row.createdAt };
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
// Subsequent event — predecessor must exist AND belong to the same tenant.
|
|
@@ -150,31 +166,21 @@ async function insertSubsequentEvent(
|
|
|
150
166
|
newVersion: number,
|
|
151
167
|
eventVersion: number,
|
|
152
168
|
): Promise<InsertReturn> {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
WHERE aggregate_id = ${event.aggregateId}::uuid
|
|
166
|
-
AND version = ${event.expectedVersion}
|
|
167
|
-
AND tenant_id = ${event.tenantId}::uuid
|
|
168
|
-
)
|
|
169
|
-
RETURNING id, created_at;
|
|
170
|
-
`);
|
|
171
|
-
const row = rows[0];
|
|
169
|
+
const row = await insertSubsequentEventRow(db, {
|
|
170
|
+
aggregateId: event.aggregateId,
|
|
171
|
+
aggregateType: event.aggregateType,
|
|
172
|
+
tenantId: event.tenantId,
|
|
173
|
+
newVersion,
|
|
174
|
+
type: event.type,
|
|
175
|
+
eventVersion,
|
|
176
|
+
payloadJson: JSON.stringify(event.payload),
|
|
177
|
+
metadataJson: JSON.stringify(event.metadata),
|
|
178
|
+
createdBy: event.metadata.userId,
|
|
179
|
+
expectedVersion: event.expectedVersion,
|
|
180
|
+
});
|
|
172
181
|
if (!row) throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
173
182
|
return {
|
|
174
|
-
id: BigInt(row.id),
|
|
175
|
-
// Raw SQL bypasses Drizzle's customType — postgres-js returns Date or
|
|
176
|
-
// string depending on driver-config. Normalize through Temporal.Instant
|
|
177
|
-
// so the InsertReturn shape matches the typed-builder path.
|
|
183
|
+
id: typeof row.id === "bigint" ? row.id : BigInt(row.id),
|
|
178
184
|
createdAt:
|
|
179
185
|
row.created_at instanceof Date
|
|
180
186
|
? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
|
|
@@ -221,11 +227,12 @@ export async function loadAggregate(
|
|
|
221
227
|
const archived = await isStreamArchived(db, tenantId, aggregateId);
|
|
222
228
|
if (archived) return [];
|
|
223
229
|
}
|
|
224
|
-
const rows = await
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
const rows = await selectMany<SelectedEvent>(
|
|
231
|
+
db,
|
|
232
|
+
eventsTable,
|
|
233
|
+
{ aggregateId, tenantId },
|
|
234
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
235
|
+
);
|
|
229
236
|
return rows.map(toStoredEvent);
|
|
230
237
|
}
|
|
231
238
|
|
|
@@ -243,17 +250,12 @@ export async function loadAggregateAsOf(
|
|
|
243
250
|
const archived = await isStreamArchived(db, tenantId, aggregateId);
|
|
244
251
|
if (archived) return [];
|
|
245
252
|
}
|
|
246
|
-
const rows = await
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
eq(eventsTable.tenantId, tenantId),
|
|
253
|
-
lte(eventsTable.createdAt, asOf),
|
|
254
|
-
),
|
|
255
|
-
)
|
|
256
|
-
.orderBy(asc(eventsTable.version));
|
|
253
|
+
const rows = await selectMany<SelectedEvent>(
|
|
254
|
+
db,
|
|
255
|
+
eventsTable,
|
|
256
|
+
{ aggregateId, tenantId, createdAt: { lte: asOf } },
|
|
257
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
258
|
+
);
|
|
257
259
|
return rows.map(toStoredEvent);
|
|
258
260
|
}
|
|
259
261
|
|
|
@@ -268,11 +270,15 @@ export async function getStreamVersion(
|
|
|
268
270
|
aggregateId: string,
|
|
269
271
|
tenantId: TenantId,
|
|
270
272
|
): Promise<number> {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
return selectStreamMaxVersion(db, aggregateId, tenantId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** MAX(version) for one aggregate — no tenant filter. Used by seed idempotency. */
|
|
277
|
+
export async function getAggregateStreamMaxVersion(
|
|
278
|
+
db: DbRunner,
|
|
279
|
+
aggregateId: string,
|
|
280
|
+
): Promise<number> {
|
|
281
|
+
return selectAggregateMaxVersion(db, aggregateId);
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
// Global high-water-mark = MAX(events.id). Marten/Wolverine standard for
|
|
@@ -280,8 +286,7 @@ export async function getStreamVersion(
|
|
|
280
286
|
// the bigserial PK index — sub-millisecond cost. Returns 0n on an empty log
|
|
281
287
|
// (boot, fresh tenant, post-archive).
|
|
282
288
|
export async function getEventsHighWaterMark(db: DbRunner): Promise<bigint> {
|
|
283
|
-
|
|
284
|
-
return row?.max ?? 0n;
|
|
289
|
+
return selectEventsHighWaterMark(db);
|
|
285
290
|
}
|
|
286
291
|
|
|
287
292
|
// Load events strictly newer than a given version. Used by snapshot-aware
|
|
@@ -293,17 +298,12 @@ export async function loadEventsAfterVersion(
|
|
|
293
298
|
tenantId: TenantId,
|
|
294
299
|
afterVersion: number,
|
|
295
300
|
): Promise<readonly StoredEvent[]> {
|
|
296
|
-
const rows = await
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
eq(eventsTable.tenantId, tenantId),
|
|
303
|
-
gt(eventsTable.version, afterVersion),
|
|
304
|
-
),
|
|
305
|
-
)
|
|
306
|
-
.orderBy(asc(eventsTable.version));
|
|
301
|
+
const rows = await selectMany<SelectedEvent>(
|
|
302
|
+
db,
|
|
303
|
+
eventsTable,
|
|
304
|
+
{ aggregateId, tenantId, version: { gt: afterVersion } },
|
|
305
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
306
|
+
);
|
|
307
307
|
return rows.map(toStoredEvent);
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -319,11 +319,17 @@ export async function loadAllEventsByType(
|
|
|
319
319
|
db: DbRunner,
|
|
320
320
|
aggregateType: string,
|
|
321
321
|
): Promise<readonly StoredEvent[]> {
|
|
322
|
-
const rows = await
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
322
|
+
const rows = await selectMany<SelectedEvent>(
|
|
323
|
+
db,
|
|
324
|
+
eventsTable,
|
|
325
|
+
{ aggregateType },
|
|
326
|
+
{
|
|
327
|
+
orderBy: [
|
|
328
|
+
{ col: "createdAt", direction: "asc" },
|
|
329
|
+
{ col: "id", direction: "asc" },
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
);
|
|
327
333
|
return rows.map(toStoredEvent);
|
|
328
334
|
}
|
|
329
335
|
|
|
@@ -359,12 +365,12 @@ export async function* streamAllEventsByType(
|
|
|
359
365
|
let cursorId = 0n;
|
|
360
366
|
while (true) {
|
|
361
367
|
signal?.throwIfAborted();
|
|
362
|
-
const rows = await
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
const rows = await selectMany<SelectedEvent>(
|
|
369
|
+
db,
|
|
370
|
+
eventsTable,
|
|
371
|
+
{ aggregateType, id: { gt: cursorId } },
|
|
372
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: batchSize },
|
|
373
|
+
);
|
|
368
374
|
|
|
369
375
|
if (rows.length === 0) {
|
|
370
376
|
// skip: end of stream — generator exit is the natural termination.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// sql now comes from native dialect
|
|
2
2
|
import { type DbConnection, tableExists } from "../db";
|
|
3
3
|
import {
|
|
4
4
|
bigserial,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
integer,
|
|
8
8
|
jsonb,
|
|
9
9
|
table as pgTable,
|
|
10
|
+
sql,
|
|
10
11
|
text,
|
|
11
12
|
uniqueIndex,
|
|
12
13
|
uuid,
|