@cosmicdrift/kumiko-framework 0.13.0 → 0.15.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 +7 -7
- 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/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 +845 -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/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__/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__/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__/{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 +48 -40
- 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__/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-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__/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__/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 +1 -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/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 +2 -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 +7 -2
- package/src/engine/types/fields.ts +4 -5
- 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__/write-failures.test.ts +1 -1
- 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 +9 -43
- 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.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-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- 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 +8 -7
- 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__/env-parse.test.ts +1 -1
- package/CHANGELOG.md +0 -472
- package/src/db/__tests__/cursor.test.ts +0 -41
- 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
package/src/es-ops/runner.ts
CHANGED
|
@@ -22,20 +22,13 @@
|
|
|
22
22
|
|
|
23
23
|
import { readdir, readFile } from "node:fs/promises";
|
|
24
24
|
import path from "node:path";
|
|
25
|
-
import { eq, sql } from "drizzle-orm";
|
|
26
25
|
import type { DbConnection, DbRunner } from "../db";
|
|
26
|
+
import { acquireEsOpsAdvisoryLock, esOperationExists } from "../db/queries/es-ops";
|
|
27
|
+
import { insertOne, selectMany } from "../db/query";
|
|
27
28
|
import type { Registry } from "../engine";
|
|
28
29
|
import { esOperationsTable } from "./operations-schema";
|
|
29
30
|
import type { EsOperationAppliedBy, SeedMigration, SeedMigrationContext } from "./types";
|
|
30
31
|
|
|
31
|
-
// Stabiler 32-bit-Integer-Lock-Key für pg_advisory_xact_lock. Multi-Replica-
|
|
32
|
-
// Boots gegen den selben Stack greifen denselben Lock — sequentialisiert
|
|
33
|
-
// die Migration ohne dass jedes Pod alle pending Files parallel anwendet.
|
|
34
|
-
// Ohne Lock: Pod A + Pod B sehen beide dieselbe pending-Liste → beide
|
|
35
|
-
// laufen migration.run() → events DOUBLED, marker-unique-constraint
|
|
36
|
-
// catched zu spät (nur den Marker, nicht die schon-committed Events).
|
|
37
|
-
const ES_OPS_LOCK_KEY = 0x65_73_6f_70; // 'esop' als hex
|
|
38
|
-
|
|
39
32
|
export type RunPendingSeedMigrationsArgs = {
|
|
40
33
|
readonly db: DbConnection;
|
|
41
34
|
/** Absoluter Pfad zum seeds-Directory (typically <appRoot>/seeds). */
|
|
@@ -130,30 +123,24 @@ export async function runPendingSeedMigrations(
|
|
|
130
123
|
|
|
131
124
|
const start = Date.now();
|
|
132
125
|
try {
|
|
133
|
-
await args.db.
|
|
126
|
+
await args.db.begin(async (tx: DbRunner) => {
|
|
134
127
|
// Advisory-Lock: sequentialisiert Multi-Replica-Boots. Zweiter
|
|
135
128
|
// Pod blockt bis erster fertig ist, dann re-checked sein
|
|
136
129
|
// applied-set (außerhalb dieser Funktion in nächster Iteration)
|
|
137
130
|
// und findet den Marker → skip. Lock wird beim Tx-Commit
|
|
138
131
|
// automatisch released (xact-scope).
|
|
139
|
-
await tx
|
|
132
|
+
await acquireEsOpsAdvisoryLock(tx);
|
|
140
133
|
|
|
141
|
-
|
|
142
|
-
// wo Pod-A schon committed hat während Pod-B vor dem Lock
|
|
143
|
-
// war. Sonst würde Pod-B die Migration nochmal ausführen.
|
|
144
|
-
const reCheck = (await tx.execute(
|
|
145
|
-
sql`SELECT 1 FROM kumiko_es_operations WHERE id = ${entry.id} LIMIT 1`,
|
|
146
|
-
)) as unknown as readonly unknown[];
|
|
147
|
-
if (reCheck.length > 0) {
|
|
134
|
+
if (await esOperationExists(tx, entry.id)) {
|
|
148
135
|
log(`${LOG_PREFIX} race-skip "${entry.id}" — applied by parallel boot`);
|
|
149
|
-
// skip:
|
|
150
|
-
//
|
|
136
|
+
// skip: parallel boot won the advisory-lock and already applied
|
|
137
|
+
// this migration. Nothing more to do in this tx.
|
|
151
138
|
return;
|
|
152
139
|
}
|
|
153
140
|
|
|
154
141
|
const ctx = args.createContext(tx);
|
|
155
142
|
await migration.run(ctx);
|
|
156
|
-
await tx
|
|
143
|
+
await insertOne(tx, esOperationsTable, {
|
|
157
144
|
id: entry.id,
|
|
158
145
|
operationType: "seed-migration",
|
|
159
146
|
durationMs: Date.now() - start,
|
|
@@ -204,11 +191,10 @@ async function listSeedFiles(seedsDir: string): Promise<readonly SeedFileEntry[]
|
|
|
204
191
|
}
|
|
205
192
|
|
|
206
193
|
async function loadAppliedIds(db: DbConnection): Promise<Set<string>> {
|
|
207
|
-
const rows = await db
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return new Set(rows.map((r: { id: string }) => r.id));
|
|
194
|
+
const rows = await selectMany<{ id: string }>(db, esOperationsTable, {
|
|
195
|
+
operationType: "seed-migration",
|
|
196
|
+
});
|
|
197
|
+
return new Set(rows.map((r) => r.id));
|
|
212
198
|
}
|
|
213
199
|
|
|
214
200
|
async function loadSeedModule(filePath: string): Promise<SeedMigration> {
|
package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts}
RENAMED
|
@@ -9,15 +9,35 @@
|
|
|
9
9
|
// - Batch: single INSERT with multi-VALUES; atomic rollback on any
|
|
10
10
|
// failure; predecessor pre-flight per aggregate in the batch.
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import type { DbConnection, PgClient } from "../../db/connection";
|
|
14
|
+
import { asRawClient, selectMany } from "../../db/query";
|
|
15
15
|
import { createTestDb, type TestDb } from "../../stack";
|
|
16
16
|
import { generateId as uuid } from "../../utils";
|
|
17
17
|
import { appendRaw, appendRawBatch, type RawEventToAppend } from "../admin-api";
|
|
18
18
|
import { VersionConflictError } from "../errors";
|
|
19
19
|
import { append, loadAggregate } from "../event-store";
|
|
20
|
-
import { createEventsTable } from "../events-schema";
|
|
20
|
+
import { createEventsTable, eventsTable } from "../events-schema";
|
|
21
|
+
|
|
22
|
+
// Test-only spy: wrap a DbConnection's `.unsafe()` to capture the SQL
|
|
23
|
+
// string of every query the framework runs. Used to assert batching
|
|
24
|
+
// behaviour (single multi-VALUES INSERT vs N statements).
|
|
25
|
+
function spyQueries(db: DbConnection): { db: DbConnection; queries: string[] } {
|
|
26
|
+
const queries: string[] = [];
|
|
27
|
+
const wrapped = new Proxy(db, {
|
|
28
|
+
get(target, prop, receiver) {
|
|
29
|
+
if (prop === "unsafe") {
|
|
30
|
+
return (sql: string, params?: readonly unknown[]) => {
|
|
31
|
+
queries.push(sql);
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: postgres-js .unsafe signature variance
|
|
33
|
+
return (target as any).unsafe(sql, params);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return Reflect.get(target, prop, receiver);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return { db: wrapped, queries };
|
|
40
|
+
}
|
|
21
41
|
|
|
22
42
|
let testDb: TestDb;
|
|
23
43
|
|
|
@@ -35,7 +55,7 @@ afterAll(async () => {
|
|
|
35
55
|
});
|
|
36
56
|
|
|
37
57
|
beforeEach(async () => {
|
|
38
|
-
await testDb.db.
|
|
58
|
+
await asRawClient(testDb.db).unsafe(`TRUNCATE kumiko_events RESTART IDENTITY`);
|
|
39
59
|
});
|
|
40
60
|
|
|
41
61
|
function makeEvent(partial: Partial<RawEventToAppend> = {}): RawEventToAppend {
|
|
@@ -86,9 +106,12 @@ describe("appendRaw — single event", () => {
|
|
|
86
106
|
}),
|
|
87
107
|
);
|
|
88
108
|
|
|
89
|
-
const rows = await testDb.db.
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
const rows = await asRawClient(testDb.db).unsafe<{ created_by: string }>(
|
|
110
|
+
`
|
|
111
|
+
SELECT created_by FROM kumiko_events WHERE aggregate_id = $1::uuid
|
|
112
|
+
`,
|
|
113
|
+
[aggregateId],
|
|
114
|
+
);
|
|
92
115
|
expect(rows[0]?.created_by).toBe(legacyUser);
|
|
93
116
|
expect(rows[0]?.created_by).not.toBe(userMigration);
|
|
94
117
|
});
|
|
@@ -113,12 +136,7 @@ describe("appendRaw — single event", () => {
|
|
|
113
136
|
}),
|
|
114
137
|
);
|
|
115
138
|
|
|
116
|
-
const rows = await testDb.db
|
|
117
|
-
payload: Record<string, unknown>;
|
|
118
|
-
metadata: Record<string, unknown>;
|
|
119
|
-
}>(sql`
|
|
120
|
-
SELECT payload, metadata FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
|
|
121
|
-
`);
|
|
139
|
+
const rows = await selectMany(testDb.db, eventsTable, { aggregateId });
|
|
122
140
|
expect(rows[0]?.payload).toEqual(payload);
|
|
123
141
|
expect(rows[0]?.metadata).toEqual(metadata);
|
|
124
142
|
});
|
|
@@ -128,9 +146,12 @@ describe("appendRaw — single event", () => {
|
|
|
128
146
|
// client; the callback is invoked per NOTIFY payload. The resolved value
|
|
129
147
|
// is a meta-object with an `unlisten` method, not a plain function.
|
|
130
148
|
const notifications: string[] = [];
|
|
131
|
-
const subscription = await testDb.client.listen(
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
const subscription = await (testDb.client as PgClient).listen(
|
|
150
|
+
"kumiko_events_new",
|
|
151
|
+
(payload: string) => {
|
|
152
|
+
notifications.push(payload);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
134
155
|
|
|
135
156
|
try {
|
|
136
157
|
// appendRaw path — MUST NOT fire.
|
|
@@ -176,9 +197,12 @@ describe("appendRaw — single event", () => {
|
|
|
176
197
|
).rejects.toBeInstanceOf(VersionConflictError);
|
|
177
198
|
|
|
178
199
|
// Sanity: no row landed.
|
|
179
|
-
const rows = await testDb.db.
|
|
180
|
-
|
|
181
|
-
|
|
200
|
+
const rows = await asRawClient(testDb.db).unsafe<{ c: number }>(
|
|
201
|
+
`
|
|
202
|
+
SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = $1::uuid
|
|
203
|
+
`,
|
|
204
|
+
[aggregateId],
|
|
205
|
+
);
|
|
182
206
|
expect(rows[0]?.c).toBe(0);
|
|
183
207
|
});
|
|
184
208
|
|
|
@@ -188,21 +212,19 @@ describe("appendRaw — single event", () => {
|
|
|
188
212
|
await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 1 }));
|
|
189
213
|
await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 2 }));
|
|
190
214
|
|
|
191
|
-
const rows = await testDb.db.
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
const rows = await asRawClient(testDb.db).unsafe<{ version: number }>(
|
|
216
|
+
`
|
|
217
|
+
SELECT version FROM kumiko_events WHERE aggregate_id = $1::uuid ORDER BY version
|
|
218
|
+
`,
|
|
219
|
+
[aggregateId],
|
|
220
|
+
);
|
|
194
221
|
expect(rows.map((r) => r.version)).toEqual([1, 2, 3]);
|
|
195
222
|
});
|
|
196
223
|
});
|
|
197
224
|
|
|
198
225
|
describe("appendRawBatch — multi-event", () => {
|
|
199
226
|
test("writes all events in a single INSERT statement (query-log spy)", async () => {
|
|
200
|
-
const
|
|
201
|
-
const loggedDb = drizzle(testDb.client, {
|
|
202
|
-
logger: {
|
|
203
|
-
logQuery: (q) => queries.push(q),
|
|
204
|
-
},
|
|
205
|
-
});
|
|
227
|
+
const { db: loggedDb, queries } = spyQueries(testDb.db);
|
|
206
228
|
|
|
207
229
|
const aggregateId = uuid();
|
|
208
230
|
const events: readonly RawEventToAppend[] = [
|
|
@@ -217,9 +239,12 @@ describe("appendRawBatch — multi-event", () => {
|
|
|
217
239
|
expect(inserts).toHaveLength(1);
|
|
218
240
|
|
|
219
241
|
// All three events persisted with ascending versions.
|
|
220
|
-
const rows = await testDb.db.
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
const rows = await asRawClient(testDb.db).unsafe<{ version: number; type: string }>(
|
|
243
|
+
`
|
|
244
|
+
SELECT version, type FROM kumiko_events WHERE aggregate_id = $1::uuid ORDER BY version
|
|
245
|
+
`,
|
|
246
|
+
[aggregateId],
|
|
247
|
+
);
|
|
223
248
|
expect(rows.map((r) => ({ v: r.version, t: r.type }))).toEqual([
|
|
224
249
|
{ v: 1, t: "legacy.order.created" },
|
|
225
250
|
{ v: 2, t: "legacy.order.accepted" },
|
|
@@ -263,7 +288,7 @@ describe("appendRawBatch — multi-event", () => {
|
|
|
263
288
|
await expect(appendRawBatch(testDb.db, batch)).rejects.toBeInstanceOf(VersionConflictError);
|
|
264
289
|
|
|
265
290
|
// Only the seed event survived — multi-VALUES INSERT is atomic.
|
|
266
|
-
const rows = await testDb.db.
|
|
291
|
+
const rows = await asRawClient(testDb.db).unsafe<{ c: number }>(`
|
|
267
292
|
SELECT count(*)::int as c FROM kumiko_events
|
|
268
293
|
`);
|
|
269
294
|
expect(rows[0]?.c).toBe(1);
|
|
@@ -280,9 +305,12 @@ describe("appendRawBatch — multi-event", () => {
|
|
|
280
305
|
]),
|
|
281
306
|
).rejects.toBeInstanceOf(VersionConflictError);
|
|
282
307
|
|
|
283
|
-
const rows = await testDb.db.
|
|
284
|
-
|
|
285
|
-
|
|
308
|
+
const rows = await asRawClient(testDb.db).unsafe<{ c: number }>(
|
|
309
|
+
`
|
|
310
|
+
SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = $1::uuid
|
|
311
|
+
`,
|
|
312
|
+
[aggregateId],
|
|
313
|
+
);
|
|
286
314
|
expect(rows[0]?.c).toBe(0);
|
|
287
315
|
});
|
|
288
316
|
|
|
@@ -301,9 +329,12 @@ describe("appendRawBatch — multi-event", () => {
|
|
|
301
329
|
).rejects.toBeInstanceOf(VersionConflictError);
|
|
302
330
|
|
|
303
331
|
// Zero events persisted — the whole batch is rejected before the INSERT.
|
|
304
|
-
const rows = await testDb.db.
|
|
305
|
-
|
|
306
|
-
|
|
332
|
+
const rows = await asRawClient(testDb.db).unsafe<{ c: number }>(
|
|
333
|
+
`
|
|
334
|
+
SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = $1::uuid
|
|
335
|
+
`,
|
|
336
|
+
[aggregateId],
|
|
337
|
+
);
|
|
307
338
|
expect(rows[0]?.c).toBe(0);
|
|
308
339
|
});
|
|
309
340
|
|
|
@@ -326,12 +357,7 @@ describe("appendRawBatch — multi-event", () => {
|
|
|
326
357
|
});
|
|
327
358
|
|
|
328
359
|
test("empty array is a no-op — no query, no throw", async () => {
|
|
329
|
-
const
|
|
330
|
-
const loggedDb = drizzle(testDb.client, {
|
|
331
|
-
logger: {
|
|
332
|
-
logQuery: (q) => queries.push(q),
|
|
333
|
-
},
|
|
334
|
-
});
|
|
360
|
+
const { db: loggedDb, queries } = spyQueries(testDb.db);
|
|
335
361
|
|
|
336
362
|
await appendRawBatch(loggedDb, []);
|
|
337
363
|
expect(queries).toHaveLength(0);
|
package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts}
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
3
|
+
import { asRawClient } from "../../db/query";
|
|
4
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
4
5
|
import { generateId as uuid } from "../../utils";
|
|
5
6
|
import {
|
|
6
7
|
append,
|
|
@@ -14,13 +15,14 @@ import {
|
|
|
14
15
|
VersionConflictError,
|
|
15
16
|
} from "../index";
|
|
16
17
|
|
|
17
|
-
let testDb:
|
|
18
|
+
let testDb: BunTestDb;
|
|
18
19
|
|
|
19
20
|
const tenantA = uuid();
|
|
20
21
|
const tenantB = uuid();
|
|
21
22
|
const userA = uuid();
|
|
22
23
|
|
|
23
24
|
beforeAll(async () => {
|
|
25
|
+
await ensureTemporalPolyfill();
|
|
24
26
|
testDb = await createTestDb();
|
|
25
27
|
await createEventsTable(testDb.db);
|
|
26
28
|
});
|
|
@@ -30,7 +32,7 @@ afterAll(async () => {
|
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
beforeEach(async () => {
|
|
33
|
-
await testDb.db.
|
|
35
|
+
await asRawClient(testDb.db).unsafe(`TRUNCATE kumiko_events RESTART IDENTITY`);
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
describe("event-store: append + load", () => {
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
// Not a strict SLA test — the threshold is generous enough to survive CI
|
|
10
10
|
// noise but tight enough that an index-miss regression would fail loudly.
|
|
11
11
|
|
|
12
|
-
import { afterAll, beforeAll, describe, expect, test } from "
|
|
12
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
13
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
13
14
|
import type { TenantId } from "../../engine/types";
|
|
14
|
-
import {
|
|
15
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
15
16
|
import { generateId as uuid } from "../../utils";
|
|
16
17
|
import { append, createEventsTable, getStreamVersion } from "../index";
|
|
17
18
|
|
|
18
|
-
let testDb:
|
|
19
|
+
let testDb: BunTestDb;
|
|
19
20
|
const tenantId: TenantId = uuid();
|
|
20
21
|
const userId = uuid();
|
|
21
22
|
|
|
22
23
|
beforeAll(async () => {
|
|
24
|
+
await ensureTemporalPolyfill();
|
|
23
25
|
testDb = await createTestDb();
|
|
24
26
|
await createEventsTable(testDb.db);
|
|
25
27
|
});
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
// latency, single-node PG. Production deploys are slower; these numbers
|
|
13
13
|
// are the ceiling. Red test = framework regression, no slack tolerated.
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
17
|
+
import { asRawClient } from "../../db/query";
|
|
17
18
|
import type { TenantId } from "../../engine/types";
|
|
18
|
-
import {
|
|
19
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
19
20
|
import { generateId as uuid } from "../../utils";
|
|
20
21
|
import {
|
|
21
22
|
append,
|
|
@@ -25,11 +26,12 @@ import {
|
|
|
25
26
|
saveSnapshot,
|
|
26
27
|
} from "../index";
|
|
27
28
|
|
|
28
|
-
let testDb:
|
|
29
|
+
let testDb: BunTestDb;
|
|
29
30
|
const tenantId = uuid() as TenantId;
|
|
30
31
|
const userId = uuid();
|
|
31
32
|
|
|
32
33
|
beforeAll(async () => {
|
|
34
|
+
await ensureTemporalPolyfill();
|
|
33
35
|
testDb = await createTestDb();
|
|
34
36
|
await createEventsTable(testDb.db);
|
|
35
37
|
});
|
|
@@ -39,8 +41,8 @@ afterAll(async () => {
|
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
beforeEach(async () => {
|
|
42
|
-
await testDb.db.
|
|
43
|
-
|
|
44
|
+
await asRawClient(testDb.db).unsafe(
|
|
45
|
+
`TRUNCATE kumiko_events, kumiko_snapshots, kumiko_archived_streams RESTART IDENTITY CASCADE`,
|
|
44
46
|
);
|
|
45
47
|
});
|
|
46
48
|
|
|
@@ -196,21 +198,27 @@ describe("event-store performance — Gate A", () => {
|
|
|
196
198
|
// loadAggregateWithSnapshot performance on a finished stream, not
|
|
197
199
|
// the seed phase.
|
|
198
200
|
const aggregateId = uuid();
|
|
199
|
-
await testDb.db.
|
|
201
|
+
await asRawClient(testDb.db).unsafe(
|
|
202
|
+
`
|
|
200
203
|
INSERT INTO kumiko_events (aggregate_id, aggregate_type, tenant_id, version, type, payload, metadata, created_by)
|
|
201
|
-
SELECT $
|
|
204
|
+
SELECT $1::uuid, 'task', $2::uuid, 1, 'task.created',
|
|
202
205
|
jsonb_build_object('title', 'v1'),
|
|
203
|
-
jsonb_build_object('userId', $
|
|
204
|
-
$
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
jsonb_build_object('userId', $3::text),
|
|
207
|
+
$4::text;
|
|
208
|
+
`,
|
|
209
|
+
[aggregateId, tenantId, userId, userId],
|
|
210
|
+
);
|
|
211
|
+
await asRawClient(testDb.db).unsafe(
|
|
212
|
+
`
|
|
207
213
|
INSERT INTO kumiko_events (aggregate_id, aggregate_type, tenant_id, version, type, payload, metadata, created_by)
|
|
208
|
-
SELECT $
|
|
214
|
+
SELECT $1::uuid, 'task', $2::uuid, gs.v, 'task.updated',
|
|
209
215
|
jsonb_build_object('title', 'v' || gs.v),
|
|
210
|
-
jsonb_build_object('userId', $
|
|
211
|
-
$
|
|
216
|
+
jsonb_build_object('userId', $3::text),
|
|
217
|
+
$4::text
|
|
212
218
|
FROM generate_series(2, 1000) gs(v);
|
|
213
|
-
|
|
219
|
+
`,
|
|
220
|
+
[aggregateId, tenantId, userId, userId],
|
|
221
|
+
);
|
|
214
222
|
|
|
215
223
|
// Snapshot @ version 900 — typische Policy: snapshot every N events
|
|
216
224
|
await saveSnapshot(testDb.db, {
|
|
@@ -9,10 +9,15 @@
|
|
|
9
9
|
// in < 50ms (typical asOf/reducer rehydrate budget). Same gate the
|
|
10
10
|
// spike proved on raw SQL, now enforced on the framework path.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Bun.SQL-only setup. KEIN postgres-js, KEIN createTestDb.
|
|
13
|
+
// Event-store functions expect DbRunner (= postgres-js) but Bun.SQL
|
|
14
|
+
// ist strukturell kompatibel (beide haben .unsafe()/.begin()) — kein Cast nötig.
|
|
15
|
+
|
|
16
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
17
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
|
+
import { asRawClient } from "../../db/query";
|
|
14
19
|
import type { TenantId } from "../../engine/types";
|
|
15
|
-
import {
|
|
20
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
16
21
|
import { generateId as uuid } from "../../utils";
|
|
17
22
|
import {
|
|
18
23
|
append,
|
|
@@ -25,7 +30,7 @@ import {
|
|
|
25
30
|
saveSnapshot,
|
|
26
31
|
} from "../index";
|
|
27
32
|
|
|
28
|
-
let
|
|
33
|
+
let bun: BunTestDb;
|
|
29
34
|
const tenant = uuid() as TenantId;
|
|
30
35
|
const userId = uuid();
|
|
31
36
|
|
|
@@ -49,17 +54,18 @@ const reducer: SnapshotReducer<CounterState> = (state, event) => {
|
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
beforeAll(async () => {
|
|
52
|
-
|
|
53
|
-
await
|
|
57
|
+
await ensureTemporalPolyfill();
|
|
58
|
+
bun = await createTestDb();
|
|
59
|
+
await createEventsTable(bun.db);
|
|
54
60
|
});
|
|
55
61
|
|
|
56
62
|
afterAll(async () => {
|
|
57
|
-
await
|
|
63
|
+
await bun.cleanup();
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
beforeEach(async () => {
|
|
61
|
-
await
|
|
62
|
-
|
|
67
|
+
await asRawClient(bun.db).unsafe(
|
|
68
|
+
`TRUNCATE kumiko_events, kumiko_snapshots, kumiko_archived_streams RESTART IDENTITY`,
|
|
63
69
|
);
|
|
64
70
|
});
|
|
65
71
|
|
|
@@ -67,7 +73,7 @@ beforeEach(async () => {
|
|
|
67
73
|
async function seedAggregate(eventCount: number): Promise<string> {
|
|
68
74
|
const aggId = uuid();
|
|
69
75
|
for (let i = 0; i < eventCount; i++) {
|
|
70
|
-
await append(
|
|
76
|
+
await append(bun.db, {
|
|
71
77
|
aggregateId: aggId,
|
|
72
78
|
aggregateType: "counter",
|
|
73
79
|
tenantId: tenant,
|
|
@@ -85,7 +91,7 @@ async function seedAggregate(eventCount: number): Promise<string> {
|
|
|
85
91
|
// raw SQL) keeps the invariant honest: if loadAggregate semantics drift,
|
|
86
92
|
// this helper shifts with them.
|
|
87
93
|
async function loadFullState(aggregateId: string): Promise<CounterState> {
|
|
88
|
-
const events = await loadAggregate(
|
|
94
|
+
const events = await loadAggregate(bun.db, aggregateId, tenant);
|
|
89
95
|
let state: CounterState = initial;
|
|
90
96
|
for (const event of events) {
|
|
91
97
|
state = reducer(state, event);
|
|
@@ -96,7 +102,7 @@ async function loadFullState(aggregateId: string): Promise<CounterState> {
|
|
|
96
102
|
describe("snapshot-store — round-trip", () => {
|
|
97
103
|
test("saveSnapshot + loadLatestSnapshot roundtrip the state", async () => {
|
|
98
104
|
const aggId = uuid();
|
|
99
|
-
await saveSnapshot(
|
|
105
|
+
await saveSnapshot(bun.db, {
|
|
100
106
|
aggregateId: aggId,
|
|
101
107
|
tenantId: tenant,
|
|
102
108
|
aggregateType: "counter",
|
|
@@ -104,7 +110,7 @@ describe("snapshot-store — round-trip", () => {
|
|
|
104
110
|
state: { count: 100, label: "checkpoint" },
|
|
105
111
|
});
|
|
106
112
|
|
|
107
|
-
const loaded = await loadLatestSnapshot<CounterState>(
|
|
113
|
+
const loaded = await loadLatestSnapshot<CounterState>(bun.db, aggId, tenant);
|
|
108
114
|
expect(loaded).not.toBeNull();
|
|
109
115
|
expect(loaded?.version).toBe(42);
|
|
110
116
|
expect(loaded?.state).toEqual({ count: 100, label: "checkpoint" });
|
|
@@ -113,28 +119,28 @@ describe("snapshot-store — round-trip", () => {
|
|
|
113
119
|
|
|
114
120
|
test("saveSnapshot is idempotent — re-snapshotting the same version upserts", async () => {
|
|
115
121
|
const aggId = uuid();
|
|
116
|
-
await saveSnapshot(
|
|
122
|
+
await saveSnapshot(bun.db, {
|
|
117
123
|
aggregateId: aggId,
|
|
118
124
|
tenantId: tenant,
|
|
119
125
|
aggregateType: "counter",
|
|
120
126
|
version: 10,
|
|
121
127
|
state: { count: 10, label: "v1" },
|
|
122
128
|
});
|
|
123
|
-
await saveSnapshot(
|
|
129
|
+
await saveSnapshot(bun.db, {
|
|
124
130
|
aggregateId: aggId,
|
|
125
131
|
tenantId: tenant,
|
|
126
132
|
aggregateType: "counter",
|
|
127
133
|
version: 10,
|
|
128
134
|
state: { count: 10, label: "v2-updated" },
|
|
129
135
|
});
|
|
130
|
-
const loaded = await loadLatestSnapshot<CounterState>(
|
|
136
|
+
const loaded = await loadLatestSnapshot<CounterState>(bun.db, aggId, tenant);
|
|
131
137
|
expect(loaded?.state.label).toBe("v2-updated");
|
|
132
138
|
});
|
|
133
139
|
|
|
134
140
|
test("loadLatestSnapshot picks the highest version when multiple exist", async () => {
|
|
135
141
|
const aggId = uuid();
|
|
136
142
|
for (const v of [5, 50, 20, 100, 75]) {
|
|
137
|
-
await saveSnapshot(
|
|
143
|
+
await saveSnapshot(bun.db, {
|
|
138
144
|
aggregateId: aggId,
|
|
139
145
|
tenantId: tenant,
|
|
140
146
|
aggregateType: "counter",
|
|
@@ -142,7 +148,7 @@ describe("snapshot-store — round-trip", () => {
|
|
|
142
148
|
state: { count: v * 2, label: `at-${v}` },
|
|
143
149
|
});
|
|
144
150
|
}
|
|
145
|
-
const loaded = await loadLatestSnapshot<CounterState>(
|
|
151
|
+
const loaded = await loadLatestSnapshot<CounterState>(bun.db, aggId, tenant);
|
|
146
152
|
expect(loaded?.version).toBe(100);
|
|
147
153
|
expect(loaded?.state.label).toBe("at-100");
|
|
148
154
|
});
|
|
@@ -155,7 +161,7 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
155
161
|
// Snapshot MUST reflect that truth; otherwise the "snapshot + deltas =
|
|
156
162
|
// full replay" invariant is meaningless.
|
|
157
163
|
const partial = { count: 40, label: "snap-at-40" };
|
|
158
|
-
await saveSnapshot(
|
|
164
|
+
await saveSnapshot(bun.db, {
|
|
159
165
|
aggregateId: aggId,
|
|
160
166
|
tenantId: tenant,
|
|
161
167
|
aggregateType: "counter",
|
|
@@ -165,7 +171,7 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
165
171
|
|
|
166
172
|
const full = await loadFullState(aggId);
|
|
167
173
|
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
168
|
-
|
|
174
|
+
bun.db,
|
|
169
175
|
aggId,
|
|
170
176
|
tenant,
|
|
171
177
|
reducer,
|
|
@@ -181,7 +187,7 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
181
187
|
const aggId = await seedAggregate(20);
|
|
182
188
|
const full = await loadFullState(aggId);
|
|
183
189
|
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
184
|
-
|
|
190
|
+
bun.db,
|
|
185
191
|
aggId,
|
|
186
192
|
tenant,
|
|
187
193
|
reducer,
|
|
@@ -198,14 +204,14 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
198
204
|
// presence. Otherwise a snapshot would silently survive a GDPR-style
|
|
199
205
|
// archival and leak state that the event log hid.
|
|
200
206
|
const aggId = await seedAggregate(10);
|
|
201
|
-
await saveSnapshot(
|
|
207
|
+
await saveSnapshot(bun.db, {
|
|
202
208
|
aggregateId: aggId,
|
|
203
209
|
tenantId: tenant,
|
|
204
210
|
aggregateType: "counter",
|
|
205
211
|
version: 10,
|
|
206
212
|
state: { count: 10, label: "pre-archive" },
|
|
207
213
|
});
|
|
208
|
-
await archiveStream(
|
|
214
|
+
await archiveStream(bun.db, {
|
|
209
215
|
tenantId: tenant,
|
|
210
216
|
aggregateId: aggId,
|
|
211
217
|
aggregateType: "counter",
|
|
@@ -213,7 +219,7 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
213
219
|
});
|
|
214
220
|
|
|
215
221
|
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
216
|
-
|
|
222
|
+
bun.db,
|
|
217
223
|
aggId,
|
|
218
224
|
tenant,
|
|
219
225
|
reducer,
|
|
@@ -225,7 +231,7 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
225
231
|
|
|
226
232
|
// includeArchived opt-in surfaces the snapshot + deltas for ops tooling.
|
|
227
233
|
const archived = await loadAggregateWithSnapshot<CounterState>(
|
|
228
|
-
|
|
234
|
+
bun.db,
|
|
229
235
|
aggId,
|
|
230
236
|
tenant,
|
|
231
237
|
reducer,
|
|
@@ -238,7 +244,7 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
238
244
|
|
|
239
245
|
test("1000-event aggregate with snapshot at v900 loads in under 50ms", async () => {
|
|
240
246
|
const aggId = await seedAggregate(1000);
|
|
241
|
-
await saveSnapshot(
|
|
247
|
+
await saveSnapshot(bun.db, {
|
|
242
248
|
aggregateId: aggId,
|
|
243
249
|
tenantId: tenant,
|
|
244
250
|
aggregateType: "counter",
|
|
@@ -247,11 +253,11 @@ describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
|
247
253
|
});
|
|
248
254
|
|
|
249
255
|
// Warm cache
|
|
250
|
-
await loadAggregateWithSnapshot<CounterState>(
|
|
256
|
+
await loadAggregateWithSnapshot<CounterState>(bun.db, aggId, tenant, reducer, initial);
|
|
251
257
|
|
|
252
258
|
const start = performance.now();
|
|
253
259
|
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
254
|
-
|
|
260
|
+
bun.db,
|
|
255
261
|
aggId,
|
|
256
262
|
tenant,
|
|
257
263
|
reducer,
|