@cosmicdrift/kumiko-framework 0.1.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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Admin-API integration tests — appendRaw + appendRawBatch.
|
|
2
|
+
//
|
|
3
|
+
// Contract (Prod-Readiness Welle 3, Step 3.1):
|
|
4
|
+
// - Pipeline-Bypass: no pg_notify, no projection, no SSE/Search/Audit.
|
|
5
|
+
// - Historical timestamps preserved: createdAt + createdBy flow through
|
|
6
|
+
// from caller parameter to DB row unchanged.
|
|
7
|
+
// - Version-check kept: UNIQUE (tenant_id, aggregate_id, version) catches
|
|
8
|
+
// duplicates; predecessor-EXISTS for expectedVersion > 0 catches gaps.
|
|
9
|
+
// - Batch: single INSERT with multi-VALUES; atomic rollback on any
|
|
10
|
+
// failure; predecessor pre-flight per aggregate in the batch.
|
|
11
|
+
|
|
12
|
+
import { sql } from "drizzle-orm";
|
|
13
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
14
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
15
|
+
import { createTestDb, type TestDb } from "../../stack";
|
|
16
|
+
import { generateId as uuid } from "../../utils";
|
|
17
|
+
import { appendRaw, appendRawBatch, type RawEventToAppend } from "../admin-api";
|
|
18
|
+
import { VersionConflictError } from "../errors";
|
|
19
|
+
import { append, loadAggregate } from "../event-store";
|
|
20
|
+
import { createEventsTable } from "../events-schema";
|
|
21
|
+
|
|
22
|
+
let testDb: TestDb;
|
|
23
|
+
|
|
24
|
+
const tenantA = uuid();
|
|
25
|
+
const userMigration = "migration-importer";
|
|
26
|
+
const legacyUser = "legacy-user-42";
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
testDb = await createTestDb();
|
|
30
|
+
await createEventsTable(testDb.db);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
await testDb.cleanup();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
await testDb.db.execute(sql`TRUNCATE kumiko_events RESTART IDENTITY`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function makeEvent(partial: Partial<RawEventToAppend> = {}): RawEventToAppend {
|
|
42
|
+
return {
|
|
43
|
+
aggregateId: partial.aggregateId ?? uuid(),
|
|
44
|
+
aggregateType: partial.aggregateType ?? "legacy-order",
|
|
45
|
+
tenantId: partial.tenantId ?? tenantA,
|
|
46
|
+
expectedVersion: partial.expectedVersion ?? 0,
|
|
47
|
+
type: partial.type ?? "legacy.order.created",
|
|
48
|
+
payload: partial.payload ?? { legacyId: 100 },
|
|
49
|
+
metadata: partial.metadata ?? { userId: userMigration, requestId: "import-batch-1" },
|
|
50
|
+
createdAt: partial.createdAt ?? Temporal.Instant.from("2023-01-15T10:00:00Z"),
|
|
51
|
+
createdBy: partial.createdBy ?? legacyUser,
|
|
52
|
+
eventVersion: partial.eventVersion,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("appendRaw — single event", () => {
|
|
57
|
+
test("preserves historical createdAt (NOT now())", async () => {
|
|
58
|
+
// Sub-second precision matters for migration: Legacy events with
|
|
59
|
+
// distinct millisecond timestamps must keep their exact ordering.
|
|
60
|
+
// Comparing via Temporal.Instant (not Date → isoString) preserves
|
|
61
|
+
// the full precision the `instant()` column round-trips.
|
|
62
|
+
const historicTs = Temporal.Instant.from("2021-06-03T14:22:10.123Z");
|
|
63
|
+
const aggregateId = uuid();
|
|
64
|
+
|
|
65
|
+
await appendRaw(testDb.db, makeEvent({ aggregateId, createdAt: historicTs }));
|
|
66
|
+
|
|
67
|
+
// loadAggregate uses the typed-builder path → createdAt already comes
|
|
68
|
+
// back as Temporal.Instant. Compare via epochMilliseconds for an exact
|
|
69
|
+
// moment-level match (no Date-roundtrip precision loss).
|
|
70
|
+
const [stored] = await loadAggregate(testDb.db, aggregateId, tenantA);
|
|
71
|
+
expect(stored).toBeDefined();
|
|
72
|
+
expect(stored!.createdAt.epochMilliseconds).toBe(historicTs.epochMilliseconds);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("preserves historical createdBy (NOT metadata.userId)", async () => {
|
|
76
|
+
const aggregateId = uuid();
|
|
77
|
+
// Migration-runner's own id lives in metadata.userId; the legacy actor
|
|
78
|
+
// lives in createdBy. These are DIFFERENT for an import — the raw-path
|
|
79
|
+
// must NOT conflate them.
|
|
80
|
+
await appendRaw(
|
|
81
|
+
testDb.db,
|
|
82
|
+
makeEvent({
|
|
83
|
+
aggregateId,
|
|
84
|
+
createdBy: legacyUser,
|
|
85
|
+
metadata: { userId: userMigration, requestId: "import-batch-1" },
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const rows = await testDb.db.execute<{ created_by: string }>(sql`
|
|
90
|
+
SELECT created_by FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
|
|
91
|
+
`);
|
|
92
|
+
expect(rows[0]?.created_by).toBe(legacyUser);
|
|
93
|
+
expect(rows[0]?.created_by).not.toBe(userMigration);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("events written via appendRaw are structurally identical to append — full payload/metadata round-trip", async () => {
|
|
97
|
+
const aggregateId = uuid();
|
|
98
|
+
const payload = { legacyId: 7, state: "Accepted", nested: { amount: "12.50" } };
|
|
99
|
+
const metadata = {
|
|
100
|
+
userId: userMigration,
|
|
101
|
+
requestId: "import-batch-2",
|
|
102
|
+
correlationId: "legacy-order-7",
|
|
103
|
+
headers: { source: "beammycar-prod-dump" },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await appendRaw(
|
|
107
|
+
testDb.db,
|
|
108
|
+
makeEvent({
|
|
109
|
+
aggregateId,
|
|
110
|
+
type: "legacy.order.accepted",
|
|
111
|
+
payload,
|
|
112
|
+
metadata,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const rows = await testDb.db.execute<{
|
|
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
|
+
`);
|
|
122
|
+
expect(rows[0]?.payload).toEqual(payload);
|
|
123
|
+
expect(rows[0]?.metadata).toEqual(metadata);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("does NOT fire pg_notify on EVENTS_PUBSUB_CHANNEL — contrast with append", async () => {
|
|
127
|
+
// Open a dedicated LISTEN connection. postgres-js exposes listen() on the
|
|
128
|
+
// client; the callback is invoked per NOTIFY payload. The resolved value
|
|
129
|
+
// is a meta-object with an `unlisten` method, not a plain function.
|
|
130
|
+
const notifications: string[] = [];
|
|
131
|
+
const subscription = await testDb.client.listen("kumiko_events_new", (payload) => {
|
|
132
|
+
notifications.push(payload);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// appendRaw path — MUST NOT fire.
|
|
137
|
+
await appendRaw(testDb.db, makeEvent());
|
|
138
|
+
// Give the event-loop a moment; LISTEN delivery is async but within
|
|
139
|
+
// the same tick usually.
|
|
140
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
141
|
+
expect(notifications).toHaveLength(0);
|
|
142
|
+
|
|
143
|
+
// Control: regular append() DOES fire. Same test-DB, same LISTEN.
|
|
144
|
+
await append(testDb.db, {
|
|
145
|
+
aggregateId: uuid(),
|
|
146
|
+
aggregateType: "control",
|
|
147
|
+
tenantId: tenantA,
|
|
148
|
+
expectedVersion: 0,
|
|
149
|
+
type: "control.created",
|
|
150
|
+
payload: {},
|
|
151
|
+
metadata: { userId: userMigration },
|
|
152
|
+
});
|
|
153
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
154
|
+
expect(notifications.length).toBeGreaterThan(0);
|
|
155
|
+
} finally {
|
|
156
|
+
await subscription.unlisten();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("version_conflict on duplicate (aggregateId, expectedVersion)", async () => {
|
|
161
|
+
const aggregateId = uuid();
|
|
162
|
+
await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 }));
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 })),
|
|
166
|
+
).rejects.toBeInstanceOf(VersionConflictError);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("version_conflict on missing predecessor (appendRaw v=5 without v=1..4)", async () => {
|
|
170
|
+
const aggregateId = uuid();
|
|
171
|
+
// Try to write version=5 (expectedVersion=4) against an empty stream.
|
|
172
|
+
// Predecessor check must catch this — otherwise orphaned events would
|
|
173
|
+
// land in the DB.
|
|
174
|
+
await expect(
|
|
175
|
+
appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 4 })),
|
|
176
|
+
).rejects.toBeInstanceOf(VersionConflictError);
|
|
177
|
+
|
|
178
|
+
// Sanity: no row landed.
|
|
179
|
+
const rows = await testDb.db.execute<{ c: number }>(sql`
|
|
180
|
+
SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
|
|
181
|
+
`);
|
|
182
|
+
expect(rows[0]?.c).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("appendRaw writes version = expectedVersion + 1 — matches append semantics", async () => {
|
|
186
|
+
const aggregateId = uuid();
|
|
187
|
+
await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 }));
|
|
188
|
+
await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 1 }));
|
|
189
|
+
await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 2 }));
|
|
190
|
+
|
|
191
|
+
const rows = await testDb.db.execute<{ version: number }>(sql`
|
|
192
|
+
SELECT version FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid ORDER BY version
|
|
193
|
+
`);
|
|
194
|
+
expect(rows.map((r) => r.version)).toEqual([1, 2, 3]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("appendRawBatch — multi-event", () => {
|
|
199
|
+
test("writes all events in a single INSERT statement (query-log spy)", async () => {
|
|
200
|
+
const queries: string[] = [];
|
|
201
|
+
const loggedDb = drizzle(testDb.client, {
|
|
202
|
+
logger: {
|
|
203
|
+
logQuery: (q) => queries.push(q),
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const aggregateId = uuid();
|
|
208
|
+
const events: readonly RawEventToAppend[] = [
|
|
209
|
+
makeEvent({ aggregateId, expectedVersion: 0, type: "legacy.order.created" }),
|
|
210
|
+
makeEvent({ aggregateId, expectedVersion: 1, type: "legacy.order.accepted" }),
|
|
211
|
+
makeEvent({ aggregateId, expectedVersion: 2, type: "legacy.order.canceled" }),
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
await appendRawBatch(loggedDb, events);
|
|
215
|
+
|
|
216
|
+
const inserts = queries.filter((q) => /insert\s+into\s+"?kumiko_events"?/i.test(q));
|
|
217
|
+
expect(inserts).toHaveLength(1);
|
|
218
|
+
|
|
219
|
+
// All three events persisted with ascending versions.
|
|
220
|
+
const rows = await testDb.db.execute<{ version: number; type: string }>(sql`
|
|
221
|
+
SELECT version, type FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid ORDER BY version
|
|
222
|
+
`);
|
|
223
|
+
expect(rows.map((r) => ({ v: r.version, t: r.type }))).toEqual([
|
|
224
|
+
{ v: 1, t: "legacy.order.created" },
|
|
225
|
+
{ v: 2, t: "legacy.order.accepted" },
|
|
226
|
+
{ v: 3, t: "legacy.order.canceled" },
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("preserves per-event historical createdAt across the batch", async () => {
|
|
231
|
+
// Three distinct sub-second timestamps: verifies the batch INSERT path
|
|
232
|
+
// doesn't collapse them to now() or to a single batch-timestamp.
|
|
233
|
+
const aggregateId = uuid();
|
|
234
|
+
const t1 = Temporal.Instant.from("2020-03-01T08:00:00.111Z");
|
|
235
|
+
const t2 = Temporal.Instant.from("2020-03-02T09:30:00.222Z");
|
|
236
|
+
const t3 = Temporal.Instant.from("2020-03-05T12:15:45.333Z");
|
|
237
|
+
|
|
238
|
+
await appendRawBatch(testDb.db, [
|
|
239
|
+
makeEvent({ aggregateId, expectedVersion: 0, createdAt: t1 }),
|
|
240
|
+
makeEvent({ aggregateId, expectedVersion: 1, createdAt: t2 }),
|
|
241
|
+
makeEvent({ aggregateId, expectedVersion: 2, createdAt: t3 }),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
const stored = await loadAggregate(testDb.db, aggregateId, tenantA);
|
|
245
|
+
expect(stored.map((s) => s.createdAt.epochMilliseconds)).toEqual([
|
|
246
|
+
t1.epochMilliseconds,
|
|
247
|
+
t2.epochMilliseconds,
|
|
248
|
+
t3.epochMilliseconds,
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("atomic rollback: if ANY event in the batch conflicts, NO events land in the DB", async () => {
|
|
253
|
+
const aggregateId = uuid();
|
|
254
|
+
// Seed version 1 so the batch's first event collides.
|
|
255
|
+
await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 }));
|
|
256
|
+
|
|
257
|
+
const batch: readonly RawEventToAppend[] = [
|
|
258
|
+
makeEvent({ aggregateId, expectedVersion: 0 }), // DUPLICATE — will fail
|
|
259
|
+
makeEvent({ aggregateId: uuid(), expectedVersion: 0 }), // valid
|
|
260
|
+
makeEvent({ aggregateId: uuid(), expectedVersion: 0 }), // valid
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
await expect(appendRawBatch(testDb.db, batch)).rejects.toBeInstanceOf(VersionConflictError);
|
|
264
|
+
|
|
265
|
+
// Only the seed event survived — multi-VALUES INSERT is atomic.
|
|
266
|
+
const rows = await testDb.db.execute<{ c: number }>(sql`
|
|
267
|
+
SELECT count(*)::int as c FROM kumiko_events
|
|
268
|
+
`);
|
|
269
|
+
expect(rows[0]?.c).toBe(1);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("version_conflict when first-in-aggregate event has missing predecessor", async () => {
|
|
273
|
+
// Batch tries to write v=5..6 for an empty stream. Pre-flight predecessor
|
|
274
|
+
// check per aggregate-group catches the gap before the INSERT runs.
|
|
275
|
+
const aggregateId = uuid();
|
|
276
|
+
await expect(
|
|
277
|
+
appendRawBatch(testDb.db, [
|
|
278
|
+
makeEvent({ aggregateId, expectedVersion: 4 }),
|
|
279
|
+
makeEvent({ aggregateId, expectedVersion: 5 }),
|
|
280
|
+
]),
|
|
281
|
+
).rejects.toBeInstanceOf(VersionConflictError);
|
|
282
|
+
|
|
283
|
+
const rows = await testDb.db.execute<{ c: number }>(sql`
|
|
284
|
+
SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
|
|
285
|
+
`);
|
|
286
|
+
expect(rows[0]?.c).toBe(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("version_conflict on gap within a single-aggregate batch (defense-in-depth against buggy mapper)", async () => {
|
|
290
|
+
// Mapper bug scenario: produces events [v=1, v=3] for the same aggregate
|
|
291
|
+
// (expectedVersions [0, 2] — v=2 missing). Without the contiguity check,
|
|
292
|
+
// UNIQUE wouldn't catch the gap (no collision), predecessor-EXISTS
|
|
293
|
+
// wouldn't catch it (min expectedVersion is 0, check skipped), and v=2
|
|
294
|
+
// would silently be orphaned. Must fail loud at batch-entry.
|
|
295
|
+
const aggregateId = uuid();
|
|
296
|
+
await expect(
|
|
297
|
+
appendRawBatch(testDb.db, [
|
|
298
|
+
makeEvent({ aggregateId, expectedVersion: 0 }),
|
|
299
|
+
makeEvent({ aggregateId, expectedVersion: 2 }),
|
|
300
|
+
]),
|
|
301
|
+
).rejects.toBeInstanceOf(VersionConflictError);
|
|
302
|
+
|
|
303
|
+
// Zero events persisted — the whole batch is rejected before the INSERT.
|
|
304
|
+
const rows = await testDb.db.execute<{ c: number }>(sql`
|
|
305
|
+
SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
|
|
306
|
+
`);
|
|
307
|
+
expect(rows[0]?.c).toBe(0);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("contiguity check is per-aggregate — independent aggregates with non-overlapping versions pass", async () => {
|
|
311
|
+
// Two different aggregates. agg1 at [v=1,2], agg2 at [v=1]. The contiguity
|
|
312
|
+
// check groups by aggregate_id, so agg1's [0→1, 1→2] and agg2's [0→1] are
|
|
313
|
+
// checked independently — no spurious cross-aggregate gap false-positive.
|
|
314
|
+
const agg1 = uuid();
|
|
315
|
+
const agg2 = uuid();
|
|
316
|
+
await appendRawBatch(testDb.db, [
|
|
317
|
+
makeEvent({ aggregateId: agg1, expectedVersion: 0 }),
|
|
318
|
+
makeEvent({ aggregateId: agg2, expectedVersion: 0 }),
|
|
319
|
+
makeEvent({ aggregateId: agg1, expectedVersion: 1 }),
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const s1 = await loadAggregate(testDb.db, agg1, tenantA);
|
|
323
|
+
const s2 = await loadAggregate(testDb.db, agg2, tenantA);
|
|
324
|
+
expect(s1.map((e) => e.version)).toEqual([1, 2]);
|
|
325
|
+
expect(s2.map((e) => e.version)).toEqual([1]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("empty array is a no-op — no query, no throw", async () => {
|
|
329
|
+
const queries: string[] = [];
|
|
330
|
+
const loggedDb = drizzle(testDb.client, {
|
|
331
|
+
logger: {
|
|
332
|
+
logQuery: (q) => queries.push(q),
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await appendRawBatch(loggedDb, []);
|
|
337
|
+
expect(queries).toHaveLength(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("multi-aggregate batch: each aggregate lands on its own stream with the right type", async () => {
|
|
341
|
+
// Mixed batch: two DIFFERENT aggregates, v=0 each. Single INSERT, both
|
|
342
|
+
// land, each on its own stream with the event type that was paired with
|
|
343
|
+
// it at call-time (no cross-talk between rows in the multi-VALUES list).
|
|
344
|
+
const agg1 = uuid();
|
|
345
|
+
const agg2 = uuid();
|
|
346
|
+
await appendRawBatch(testDb.db, [
|
|
347
|
+
makeEvent({ aggregateId: agg1, expectedVersion: 0, type: "legacy.order.created" }),
|
|
348
|
+
makeEvent({ aggregateId: agg2, expectedVersion: 0, type: "legacy.driver.created" }),
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
const stream1 = await loadAggregate(testDb.db, agg1, tenantA);
|
|
352
|
+
const stream2 = await loadAggregate(testDb.db, agg2, tenantA);
|
|
353
|
+
|
|
354
|
+
expect(stream1.map((s) => ({ v: s.version, t: s.type }))).toEqual([
|
|
355
|
+
{ v: 1, t: "legacy.order.created" },
|
|
356
|
+
]);
|
|
357
|
+
expect(stream2.map((s) => ({ v: s.version, t: s.type }))).toEqual([
|
|
358
|
+
{ v: 1, t: "legacy.driver.created" },
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
});
|