@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,257 @@
|
|
|
1
|
+
// Event-Store Admin-API — Marten-Bypass für Legacy-Daten-Importe.
|
|
2
|
+
// Spec + Verhalten: docs/plans/features/event-store-admin-api.md
|
|
3
|
+
//
|
|
4
|
+
// Guard-Rail: dieses Modul ist NICHT aus event-store/index.ts re-exportiert.
|
|
5
|
+
// Import nur via deep-path `@cosmicdrift/kumiko-framework/event-store/admin-api`. Das
|
|
6
|
+
// Guard-Script `scripts/guard-admin-api.ts` blockt Aufrufe aus App-Code —
|
|
7
|
+
// Allowlist: samples/*/migration/, scripts/migrations/, die Definition
|
|
8
|
+
// selbst, das Guard-Script selbst.
|
|
9
|
+
|
|
10
|
+
import { sql } from "drizzle-orm";
|
|
11
|
+
import type { DbRunner } from "../db";
|
|
12
|
+
import { isUniqueViolation } from "../db/pg-error";
|
|
13
|
+
import type { TenantId } from "../engine/types";
|
|
14
|
+
import { VersionConflictError } from "./errors";
|
|
15
|
+
import type { EventMetadata } from "./event-store";
|
|
16
|
+
import { eventsTable } from "./events-schema";
|
|
17
|
+
|
|
18
|
+
export type RawEventToAppend = {
|
|
19
|
+
readonly aggregateId: string;
|
|
20
|
+
readonly aggregateType: string;
|
|
21
|
+
readonly tenantId: TenantId;
|
|
22
|
+
// Predecessor version. 0 writes a new stream at version 1; > 0 requires
|
|
23
|
+
// the predecessor to exist (same UUID, same tenant). Mirrors EventToAppend.
|
|
24
|
+
readonly expectedVersion: number;
|
|
25
|
+
readonly type: string;
|
|
26
|
+
readonly eventVersion?: number;
|
|
27
|
+
readonly payload: Record<string, unknown>;
|
|
28
|
+
readonly metadata: EventMetadata;
|
|
29
|
+
// Historisch preserved — MUSS gesetzt sein, kein Default auf now().
|
|
30
|
+
readonly createdAt: Temporal.Instant;
|
|
31
|
+
// Historisch preserved. Legacy-UserId oder 'system' für pre-auth Daten.
|
|
32
|
+
// Bewusst getrennt von metadata.userId: der Migration-Runner läuft unter
|
|
33
|
+
// einer eigenen Service-Identität, der Legacy-Actor ist der Ursprung.
|
|
34
|
+
readonly createdBy: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Mirrors append()'s two-path structure: typed builder equivalent for v=0,
|
|
38
|
+
// INSERT … SELECT … WHERE EXISTS gate for v>0. Caller-supplied createdAt +
|
|
39
|
+
// createdBy skip the usual userResolver/now() paths.
|
|
40
|
+
export async function appendRaw(runner: DbRunner, event: RawEventToAppend): Promise<void> {
|
|
41
|
+
const newVersion = event.expectedVersion + 1;
|
|
42
|
+
const eventVersion = event.eventVersion ?? 1;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (event.expectedVersion === 0) {
|
|
46
|
+
await insertRawFirst(runner, event, newVersion, eventVersion);
|
|
47
|
+
} else {
|
|
48
|
+
await insertRawSubsequent(runner, event, newVersion, eventVersion);
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (isUniqueViolation(e)) {
|
|
52
|
+
throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
53
|
+
}
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function insertRawFirst(
|
|
59
|
+
runner: DbRunner,
|
|
60
|
+
event: RawEventToAppend,
|
|
61
|
+
newVersion: number,
|
|
62
|
+
eventVersion: number,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
await runner.execute(sql`
|
|
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
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function insertRawSubsequent(
|
|
85
|
+
runner: DbRunner,
|
|
86
|
+
event: RawEventToAppend,
|
|
87
|
+
newVersion: number,
|
|
88
|
+
eventVersion: number,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const rows = await runner.execute<{ id: string }>(sql`
|
|
91
|
+
INSERT INTO ${eventsTable} (
|
|
92
|
+
aggregate_id, aggregate_type, tenant_id, version,
|
|
93
|
+
type, event_version, payload, metadata, created_at, created_by
|
|
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) {
|
|
114
|
+
throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Batch append. One multi-VALUES INSERT — atomic by PG statement semantics.
|
|
119
|
+
// Three pre-flight checks identify the specific conflicting aggregate, so the
|
|
120
|
+
// thrown VersionConflictError points at a real event (not at a batch-first
|
|
121
|
+
// placeholder). The INSERT's UNIQUE constraint is still the authoritative
|
|
122
|
+
// gate — pre-flight is for diagnostic precision, not correctness.
|
|
123
|
+
export async function appendRawBatch(
|
|
124
|
+
runner: DbRunner,
|
|
125
|
+
events: readonly RawEventToAppend[],
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const firstEvent = events[0];
|
|
128
|
+
// skip: empty batch is a no-op by contract — callers that chunk a stream
|
|
129
|
+
// into size-N batches shouldn't need to guard the tail-case themselves.
|
|
130
|
+
if (!firstEvent) return;
|
|
131
|
+
|
|
132
|
+
verifyContiguousWithinBatch(events);
|
|
133
|
+
await verifyPredecessors(runner, events);
|
|
134
|
+
await verifyNoDuplicates(runner, events);
|
|
135
|
+
|
|
136
|
+
const rows = events.map((e) => {
|
|
137
|
+
const newVersion = e.expectedVersion + 1;
|
|
138
|
+
const eventVersion = e.eventVersion ?? 1;
|
|
139
|
+
return sql`(
|
|
140
|
+
${e.aggregateId}::uuid,
|
|
141
|
+
${e.aggregateType},
|
|
142
|
+
${e.tenantId}::uuid,
|
|
143
|
+
${newVersion},
|
|
144
|
+
${e.type},
|
|
145
|
+
${eventVersion},
|
|
146
|
+
${JSON.stringify(e.payload)}::jsonb,
|
|
147
|
+
${JSON.stringify(e.metadata)}::jsonb,
|
|
148
|
+
${e.createdAt.toString()}::timestamptz,
|
|
149
|
+
${e.createdBy}
|
|
150
|
+
)`;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await runner.execute(sql`
|
|
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
|
+
`);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (isUniqueViolation(e)) {
|
|
163
|
+
// Pre-flight ran but lost a race against a concurrent writer. Rare for
|
|
164
|
+
// migration (single-runner) but possible; we can't name the exact row.
|
|
165
|
+
throw new VersionConflictError(firstEvent.aggregateId, firstEvent.expectedVersion);
|
|
166
|
+
}
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Defense-in-depth against a buggy event-mapper: within one batch, for each
|
|
172
|
+
// aggregate the expectedVersion sequence must be contiguous (no gaps). Without
|
|
173
|
+
// this, [expectedVersion=0, expectedVersion=2] for the same aggregate would
|
|
174
|
+
// write v=1 and v=3 with v=2 missing — UNIQUE won't catch it (no collision),
|
|
175
|
+
// predecessor-EXISTS won't catch it (min is 0, check skipped), and the
|
|
176
|
+
// orphan only surfaces at projection-rebuild time. Fail-loud here instead.
|
|
177
|
+
function verifyContiguousWithinBatch(events: readonly RawEventToAppend[]): void {
|
|
178
|
+
const byAggregate = new Map<string, RawEventToAppend[]>();
|
|
179
|
+
for (const e of events) {
|
|
180
|
+
const key = `${e.tenantId}:${e.aggregateId}`;
|
|
181
|
+
const list = byAggregate.get(key) ?? [];
|
|
182
|
+
list.push(e);
|
|
183
|
+
byAggregate.set(key, list);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const list of byAggregate.values()) {
|
|
187
|
+
if (list.length < 2) continue;
|
|
188
|
+
const sorted = [...list].sort((a, b) => a.expectedVersion - b.expectedVersion);
|
|
189
|
+
const [first, ...rest] = sorted;
|
|
190
|
+
if (!first) continue;
|
|
191
|
+
let prev = first;
|
|
192
|
+
for (const curr of rest) {
|
|
193
|
+
if (curr.expectedVersion !== prev.expectedVersion + 1) {
|
|
194
|
+
throw new VersionConflictError(curr.aggregateId, curr.expectedVersion);
|
|
195
|
+
}
|
|
196
|
+
prev = curr;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Per aggregate-group, check the predecessor (min(expectedVersion) > 0)
|
|
202
|
+
// exists in the DB. For migration batches that are usually single-aggregate
|
|
203
|
+
// or fresh-stream, this loops zero or one times.
|
|
204
|
+
async function verifyPredecessors(
|
|
205
|
+
runner: DbRunner,
|
|
206
|
+
events: readonly RawEventToAppend[],
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
type GroupKey = { tenantId: TenantId; aggregateId: string; minExpected: number };
|
|
209
|
+
const groups = new Map<string, GroupKey>();
|
|
210
|
+
for (const e of events) {
|
|
211
|
+
const key = `${e.tenantId}:${e.aggregateId}`;
|
|
212
|
+
const existing = groups.get(key);
|
|
213
|
+
if (!existing || e.expectedVersion < existing.minExpected) {
|
|
214
|
+
groups.set(key, {
|
|
215
|
+
tenantId: e.tenantId,
|
|
216
|
+
aggregateId: e.aggregateId,
|
|
217
|
+
minExpected: e.expectedVersion,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const g of groups.values()) {
|
|
223
|
+
if (g.minExpected === 0) continue;
|
|
224
|
+
const rows = await runner.execute<{ present: boolean }>(sql`
|
|
225
|
+
SELECT EXISTS(
|
|
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) {
|
|
233
|
+
throw new VersionConflictError(g.aggregateId, g.minExpected);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Single IN-query checks whether any (tenant, aggregate, newVersion) tuple
|
|
239
|
+
// already exists. Returns the first collision, so the thrown error names
|
|
240
|
+
// the real conflicting aggregate instead of the batch's first event.
|
|
241
|
+
async function verifyNoDuplicates(
|
|
242
|
+
runner: DbRunner,
|
|
243
|
+
events: readonly RawEventToAppend[],
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
const triples = events.map(
|
|
246
|
+
(e) => sql`(${e.tenantId}::uuid, ${e.aggregateId}::uuid, ${e.expectedVersion + 1})`,
|
|
247
|
+
);
|
|
248
|
+
const rows = await runner.execute<{ aggregate_id: string; version: number }>(sql`
|
|
249
|
+
SELECT aggregate_id, version FROM ${eventsTable}
|
|
250
|
+
WHERE (tenant_id, aggregate_id, version) IN (${sql.join(triples, sql`, `)})
|
|
251
|
+
LIMIT 1
|
|
252
|
+
`);
|
|
253
|
+
const conflict = rows[0];
|
|
254
|
+
if (conflict) {
|
|
255
|
+
throw new VersionConflictError(conflict.aggregate_id, conflict.version - 1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
2
|
+
import type { DbConnection, DbRunner } from "../db/connection";
|
|
3
|
+
import { instant, table as pgTable, text, uniqueIndex, uuid } from "../db/dialect";
|
|
4
|
+
import { tableExists } from "../db/schema-inspection";
|
|
5
|
+
import type { TenantId } from "../engine/types";
|
|
6
|
+
import { pushTables } from "../stack";
|
|
7
|
+
|
|
8
|
+
// Marten-aligned stream archival. Archived streams become read-only: fresh
|
|
9
|
+
// appendEvent on an archived aggregate throws, and loadAggregate returns
|
|
10
|
+
// an empty slice unless the caller passes { includeArchived: true }.
|
|
11
|
+
//
|
|
12
|
+
// Storage: sparse table — only ARCHIVED streams have a row. Active streams
|
|
13
|
+
// stay out of this table to keep the hot path free of extra writes. A
|
|
14
|
+
// tenant-scoped PK guards against cross-tenant reuse of aggregate IDs.
|
|
15
|
+
|
|
16
|
+
export const archivedStreamsTable = pgTable(
|
|
17
|
+
"kumiko_archived_streams",
|
|
18
|
+
{
|
|
19
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
20
|
+
aggregateId: uuid("aggregate_id").notNull(),
|
|
21
|
+
aggregateType: text("aggregate_type").notNull(),
|
|
22
|
+
archivedAt: instant("archived_at", { precision: 3 }).notNull().default(sql`now()`),
|
|
23
|
+
archivedBy: text("archived_by").notNull(),
|
|
24
|
+
reason: text("reason"),
|
|
25
|
+
},
|
|
26
|
+
(t) => ({
|
|
27
|
+
pk: uniqueIndex("kumiko_archived_streams_pk").on(t.tenantId, t.aggregateId),
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export async function createArchivedStreamsTable(db: DbConnection): Promise<void> {
|
|
32
|
+
// skip: table already exists — idempotent boot + test-setup call
|
|
33
|
+
if (await tableExists(db, "public.kumiko_archived_streams")) return;
|
|
34
|
+
await pushTables(db, { kumikoArchivedStreams: archivedStreamsTable });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ArchiveStreamArgs = {
|
|
38
|
+
readonly tenantId: TenantId;
|
|
39
|
+
readonly aggregateId: string;
|
|
40
|
+
readonly aggregateType: string;
|
|
41
|
+
readonly archivedBy: string;
|
|
42
|
+
readonly reason?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Mark a stream as archived. Idempotent — re-archiving (same tenant +
|
|
46
|
+
// aggregate) updates archivedAt/archivedBy to the latest call instead of
|
|
47
|
+
// failing. That matches Marten's "archive is a state, not an event" model.
|
|
48
|
+
export async function archiveStream(db: DbRunner, args: ArchiveStreamArgs): Promise<void> {
|
|
49
|
+
await db
|
|
50
|
+
.insert(archivedStreamsTable)
|
|
51
|
+
.values({
|
|
52
|
+
tenantId: args.tenantId,
|
|
53
|
+
aggregateId: args.aggregateId,
|
|
54
|
+
aggregateType: args.aggregateType,
|
|
55
|
+
archivedBy: args.archivedBy,
|
|
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
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cheap existence probe — issued in the hot read path, so keep the query to
|
|
70
|
+
// a single indexed lookup on the composite PK.
|
|
71
|
+
export async function isStreamArchived(
|
|
72
|
+
db: DbRunner,
|
|
73
|
+
tenantId: TenantId,
|
|
74
|
+
aggregateId: string,
|
|
75
|
+
): Promise<boolean> {
|
|
76
|
+
const rows = await db
|
|
77
|
+
.select({ one: sql`1` })
|
|
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;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Undo an archive — restores the stream to writable state. Ops tool. The
|
|
90
|
+
// historical archivedAt is lost; if auditing needs the archive-history,
|
|
91
|
+
// use domain events on the aggregate (e.g. "stream.archived" / "stream.
|
|
92
|
+
// restored") instead of relying on this row.
|
|
93
|
+
export async function restoreStream(
|
|
94
|
+
db: DbRunner,
|
|
95
|
+
tenantId: TenantId,
|
|
96
|
+
aggregateId: string,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
await db
|
|
99
|
+
.delete(archivedStreamsTable)
|
|
100
|
+
.where(
|
|
101
|
+
and(
|
|
102
|
+
eq(archivedStreamsTable.tenantId, tenantId),
|
|
103
|
+
eq(archivedStreamsTable.aggregateId, aggregateId),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Failure modes of the event-store's append() path. Surfaced as typed
|
|
2
|
+
// errors so the executor layer can map them to the framework's
|
|
3
|
+
// WriteResult error contract (version_conflict).
|
|
4
|
+
|
|
5
|
+
export class VersionConflictError extends Error {
|
|
6
|
+
public readonly aggregateId: string;
|
|
7
|
+
public readonly expectedVersion: number;
|
|
8
|
+
constructor(aggregateId: string, expectedVersion: number) {
|
|
9
|
+
super(
|
|
10
|
+
`Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}`,
|
|
11
|
+
);
|
|
12
|
+
this.name = "VersionConflictError";
|
|
13
|
+
this.aggregateId = aggregateId;
|
|
14
|
+
this.expectedVersion = expectedVersion;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Thrown when ctx.appendEvent targets an archived stream. Archived aggregates
|
|
19
|
+
// are read-only — restoreStream() makes them writable again. The archive
|
|
20
|
+
// state is not carried on the events themselves; it lives on the sparse
|
|
21
|
+
// kumiko_archived_streams table. Handlers that need to branch on archive
|
|
22
|
+
// state should call ctx.isStreamArchived(id) first.
|
|
23
|
+
export class ArchivedStreamError extends Error {
|
|
24
|
+
public readonly tenantId: string;
|
|
25
|
+
public readonly aggregateId: string;
|
|
26
|
+
constructor(tenantId: string, aggregateId: string) {
|
|
27
|
+
super(
|
|
28
|
+
`Aggregate ${aggregateId} on tenant ${tenantId} is archived — appendEvent is blocked. ` +
|
|
29
|
+
`Call restoreStream() to re-open the stream before writing.`,
|
|
30
|
+
);
|
|
31
|
+
this.name = "ArchivedStreamError";
|
|
32
|
+
this.tenantId = tenantId;
|
|
33
|
+
this.aggregateId = aggregateId;
|
|
34
|
+
}
|
|
35
|
+
}
|