@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,405 @@
|
|
|
1
|
+
import { and, asc, eq, gt, lte, max, sql } from "drizzle-orm";
|
|
2
|
+
import type { DbRunner } from "../db";
|
|
3
|
+
import { isUniqueViolation } from "../db/pg-error";
|
|
4
|
+
import type { TenantId } from "../engine/types";
|
|
5
|
+
import { isStreamArchived } from "./archive";
|
|
6
|
+
import { VersionConflictError } from "./errors";
|
|
7
|
+
import { eventsTable } from "./events-schema";
|
|
8
|
+
|
|
9
|
+
export type EventMetadata = {
|
|
10
|
+
readonly userId: string;
|
|
11
|
+
readonly requestId?: string;
|
|
12
|
+
// End-to-end business-operation id. Root HTTP requests get it from the
|
|
13
|
+
// x-correlation-id header (default: requestId). MSP-applies inherit it
|
|
14
|
+
// from the triggering event. Lets you trace "which user click caused
|
|
15
|
+
// this email 3 streams later?".
|
|
16
|
+
readonly correlationId?: string;
|
|
17
|
+
// Stored event id that triggered this write. Null for root commands;
|
|
18
|
+
// set to event.id when an MSP-apply runs ctx.appendEvent. Together with
|
|
19
|
+
// correlationId forms a causation DAG across aggregate streams.
|
|
20
|
+
readonly causationId?: string;
|
|
21
|
+
// Marten-conform free key/value space for app-specific metadata that
|
|
22
|
+
// doesn't deserve its own EventMetadata field. Examples: A/B-test bucket,
|
|
23
|
+
// feature-flag snapshot, geo-region, client SDK version. Persisted into
|
|
24
|
+
// events.metadata jsonb (no schema change — it's already a free-form
|
|
25
|
+
// jsonb column), survives upcasters untouched, available on every
|
|
26
|
+
// StoredEvent.metadata.headers. Framework does not interpret values; the
|
|
27
|
+
// app reads them when filtering/auditing. Keep values JSON-primitive
|
|
28
|
+
// (string|number|boolean) so JSON serialization stays bulletproof.
|
|
29
|
+
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type EventToAppend = {
|
|
33
|
+
readonly aggregateId: string;
|
|
34
|
+
readonly aggregateType: string;
|
|
35
|
+
readonly tenantId: TenantId;
|
|
36
|
+
// Predecessor version. 0 for a brand-new aggregate — framework writes version 1.
|
|
37
|
+
readonly expectedVersion: number;
|
|
38
|
+
readonly type: string;
|
|
39
|
+
readonly eventVersion?: number;
|
|
40
|
+
readonly payload: Record<string, unknown>;
|
|
41
|
+
readonly metadata: EventMetadata;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Generic über payload-shape. Default = Record<string, unknown> macht
|
|
45
|
+
// alle existierenden Konsumenten backwards-compatible. Konkrete Apply-
|
|
46
|
+
// Handler / Tests können `StoredEvent<MyEventPayload>` annotieren um
|
|
47
|
+
// payload typed zu lesen. Type-Propagation kommt durch r.defineEvent +
|
|
48
|
+
// SingleStreamApplyFn<T> in apply-Maps.
|
|
49
|
+
export type StoredEvent<TPayload = Record<string, unknown>> = {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly aggregateId: string;
|
|
52
|
+
readonly aggregateType: string;
|
|
53
|
+
readonly tenantId: TenantId;
|
|
54
|
+
readonly version: number;
|
|
55
|
+
readonly type: string;
|
|
56
|
+
readonly eventVersion: number;
|
|
57
|
+
readonly payload: TPayload;
|
|
58
|
+
readonly metadata: EventMetadata;
|
|
59
|
+
readonly createdAt: Temporal.Instant;
|
|
60
|
+
readonly createdBy: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type SelectedEvent = typeof eventsTable.$inferSelect;
|
|
64
|
+
|
|
65
|
+
// Append one event atomically. Two guarantees combined:
|
|
66
|
+
//
|
|
67
|
+
// 1. UNIQUE (tenant_id, aggregate_id, version) serializes concurrent writers
|
|
68
|
+
// within a tenant — a second writer racing the same expectedVersion
|
|
69
|
+
// receives a PG unique violation (SQLSTATE 23505) → VersionConflictError.
|
|
70
|
+
// Cross-tenant aggregate_id collisions are not conflicts by definition:
|
|
71
|
+
// two tenants owning a row with the same UUID is just isolation, not a
|
|
72
|
+
// race.
|
|
73
|
+
//
|
|
74
|
+
// 2. For updates (expectedVersion > 0), INSERT … SELECT … WHERE EXISTS
|
|
75
|
+
// requires the predecessor event to exist within the same tenant — i.e.
|
|
76
|
+
// "you can't append v6 to a stream whose v5 was never written." The
|
|
77
|
+
// tenant filter inside the EXISTS is belt-and-suspenders now that the
|
|
78
|
+
// unique index carries tenant_id; we keep it so the predecessor check
|
|
79
|
+
// stays semantically obvious when read in isolation.
|
|
80
|
+
//
|
|
81
|
+
// Creates (expectedVersion === 0) skip the predecessor check — no predecessor
|
|
82
|
+
// exists yet. Colliding creates fall out via UNIQUE (tenant_id, aggregate_id, version=1).
|
|
83
|
+
// Channel name used by append() → NOTIFY and the event-dispatcher → LISTEN
|
|
84
|
+
// (Sprint E.4). The event-dispatcher subscribes to this channel on start and
|
|
85
|
+
// fires a runOnce immediately on each commit, so delivery latency is bounded
|
|
86
|
+
// by TCP round-trip instead of pollIntervalMs.
|
|
87
|
+
export const EVENTS_PUBSUB_CHANNEL = "kumiko_events_new";
|
|
88
|
+
|
|
89
|
+
export async function append(db: DbRunner, event: EventToAppend): Promise<StoredEvent> {
|
|
90
|
+
const newVersion = event.expectedVersion + 1;
|
|
91
|
+
const eventVersion = event.eventVersion ?? 1;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const row =
|
|
95
|
+
event.expectedVersion === 0
|
|
96
|
+
? await insertFirstEvent(db, event, newVersion, eventVersion)
|
|
97
|
+
: await insertSubsequentEvent(db, event, newVersion, eventVersion);
|
|
98
|
+
|
|
99
|
+
// NOTIFY fires on commit (PG buffers NOTIFY per TX), so subscribers never
|
|
100
|
+
// see a wake-up for an event that later rolled back. Harmless no-op when
|
|
101
|
+
// no LISTENer is attached.
|
|
102
|
+
await db.execute(sql`SELECT pg_notify(${EVENTS_PUBSUB_CHANNEL}, '')`);
|
|
103
|
+
|
|
104
|
+
return buildStoredEvent(event, newVersion, eventVersion, row);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (isUniqueViolation(e)) {
|
|
107
|
+
// Only constraint left on the events table: events_aggregate_version_uq
|
|
108
|
+
// on (tenant_id, aggregate_id, version). A unique violation here always
|
|
109
|
+
// means a concurrent writer in the same tenant won the race to the
|
|
110
|
+
// next version — retry-able conflict.
|
|
111
|
+
throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
112
|
+
}
|
|
113
|
+
throw e;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type InsertReturn = { id: bigint; createdAt: Temporal.Instant };
|
|
118
|
+
|
|
119
|
+
async function insertFirstEvent(
|
|
120
|
+
db: DbRunner,
|
|
121
|
+
event: EventToAppend,
|
|
122
|
+
newVersion: number,
|
|
123
|
+
eventVersion: number,
|
|
124
|
+
): Promise<InsertReturn> {
|
|
125
|
+
const [row] = await db
|
|
126
|
+
.insert(eventsTable)
|
|
127
|
+
.values({
|
|
128
|
+
aggregateId: event.aggregateId,
|
|
129
|
+
aggregateType: event.aggregateType,
|
|
130
|
+
tenantId: event.tenantId,
|
|
131
|
+
version: newVersion,
|
|
132
|
+
type: event.type,
|
|
133
|
+
eventVersion,
|
|
134
|
+
payload: event.payload,
|
|
135
|
+
metadata: event.metadata,
|
|
136
|
+
createdBy: event.metadata.userId,
|
|
137
|
+
})
|
|
138
|
+
.returning({ id: eventsTable.id, createdAt: eventsTable.createdAt });
|
|
139
|
+
if (!row) throw new Error("insertFirstEvent: INSERT RETURNING produced no row");
|
|
140
|
+
return row;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Subsequent event — predecessor must exist AND belong to the same tenant.
|
|
144
|
+
// INSERT … SELECT … WHERE EXISTS is awkward in the typed builder, so this
|
|
145
|
+
// one stays raw. Uses ${eventsTable} for the table reference so renames
|
|
146
|
+
// don't silently break the statement.
|
|
147
|
+
async function insertSubsequentEvent(
|
|
148
|
+
db: DbRunner,
|
|
149
|
+
event: EventToAppend,
|
|
150
|
+
newVersion: number,
|
|
151
|
+
eventVersion: number,
|
|
152
|
+
): Promise<InsertReturn> {
|
|
153
|
+
const payloadJson = JSON.stringify(event.payload);
|
|
154
|
+
const metadataJson = JSON.stringify(event.metadata);
|
|
155
|
+
const rows = await db.execute<{ id: string; created_at: Date | string }>(sql`
|
|
156
|
+
INSERT INTO ${eventsTable} (
|
|
157
|
+
aggregate_id, aggregate_type, tenant_id, version,
|
|
158
|
+
type, event_version, payload, metadata, created_by
|
|
159
|
+
)
|
|
160
|
+
SELECT ${event.aggregateId}::uuid, ${event.aggregateType}, ${event.tenantId}::uuid, ${newVersion},
|
|
161
|
+
${event.type}, ${eventVersion}, ${payloadJson}::jsonb,
|
|
162
|
+
${metadataJson}::jsonb, ${event.metadata.userId}
|
|
163
|
+
WHERE EXISTS (
|
|
164
|
+
SELECT 1 FROM ${eventsTable}
|
|
165
|
+
WHERE aggregate_id = ${event.aggregateId}::uuid
|
|
166
|
+
AND version = ${event.expectedVersion}
|
|
167
|
+
AND tenant_id = ${event.tenantId}::uuid
|
|
168
|
+
)
|
|
169
|
+
RETURNING id, created_at;
|
|
170
|
+
`);
|
|
171
|
+
const row = rows[0];
|
|
172
|
+
if (!row) throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
173
|
+
return {
|
|
174
|
+
id: BigInt(row.id),
|
|
175
|
+
// Raw SQL bypasses Drizzle's customType — postgres-js returns Date or
|
|
176
|
+
// string depending on driver-config. Normalize through Temporal.Instant
|
|
177
|
+
// so the InsertReturn shape matches the typed-builder path.
|
|
178
|
+
createdAt:
|
|
179
|
+
row.created_at instanceof Date
|
|
180
|
+
? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
|
|
181
|
+
: Temporal.Instant.from(row.created_at),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildStoredEvent(
|
|
186
|
+
event: EventToAppend,
|
|
187
|
+
newVersion: number,
|
|
188
|
+
eventVersion: number,
|
|
189
|
+
row: InsertReturn,
|
|
190
|
+
): StoredEvent {
|
|
191
|
+
return {
|
|
192
|
+
id: String(row.id),
|
|
193
|
+
aggregateId: event.aggregateId,
|
|
194
|
+
aggregateType: event.aggregateType,
|
|
195
|
+
tenantId: event.tenantId,
|
|
196
|
+
version: newVersion,
|
|
197
|
+
type: event.type,
|
|
198
|
+
eventVersion,
|
|
199
|
+
payload: event.payload,
|
|
200
|
+
metadata: event.metadata,
|
|
201
|
+
createdAt: row.createdAt,
|
|
202
|
+
createdBy: event.metadata.userId,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Load all events for an aggregate, ordered by version. Tenant check is
|
|
207
|
+
// belt + suspenders: even if a caller passes a correct aggregate_id by
|
|
208
|
+
// mistake, the tenant filter prevents cross-tenant reads.
|
|
209
|
+
//
|
|
210
|
+
// Archived streams return an empty slice by default. Pass
|
|
211
|
+
// { includeArchived: true } for ops tools / audit that must see the tail
|
|
212
|
+
// of an archived aggregate. The archive check is a single indexed lookup —
|
|
213
|
+
// negligible on the hot path.
|
|
214
|
+
export async function loadAggregate(
|
|
215
|
+
db: DbRunner,
|
|
216
|
+
aggregateId: string,
|
|
217
|
+
tenantId: TenantId,
|
|
218
|
+
options?: { readonly includeArchived?: boolean },
|
|
219
|
+
): Promise<readonly StoredEvent[]> {
|
|
220
|
+
if (!options?.includeArchived) {
|
|
221
|
+
const archived = await isStreamArchived(db, tenantId, aggregateId);
|
|
222
|
+
if (archived) return [];
|
|
223
|
+
}
|
|
224
|
+
const rows = await db
|
|
225
|
+
.select()
|
|
226
|
+
.from(eventsTable)
|
|
227
|
+
.where(and(eq(eventsTable.aggregateId, aggregateId), eq(eventsTable.tenantId, tenantId)))
|
|
228
|
+
.orderBy(asc(eventsTable.version));
|
|
229
|
+
return rows.map(toStoredEvent);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load events up to a point in time. Used for asOf queries that reconstruct
|
|
233
|
+
// historical state. Includes events whose created_at is <= asOf. Same
|
|
234
|
+
// archive semantics as loadAggregate.
|
|
235
|
+
export async function loadAggregateAsOf(
|
|
236
|
+
db: DbRunner,
|
|
237
|
+
aggregateId: string,
|
|
238
|
+
tenantId: TenantId,
|
|
239
|
+
asOf: Temporal.Instant,
|
|
240
|
+
options?: { readonly includeArchived?: boolean },
|
|
241
|
+
): Promise<readonly StoredEvent[]> {
|
|
242
|
+
if (!options?.includeArchived) {
|
|
243
|
+
const archived = await isStreamArchived(db, tenantId, aggregateId);
|
|
244
|
+
if (archived) return [];
|
|
245
|
+
}
|
|
246
|
+
const rows = await db
|
|
247
|
+
.select()
|
|
248
|
+
.from(eventsTable)
|
|
249
|
+
.where(
|
|
250
|
+
and(
|
|
251
|
+
eq(eventsTable.aggregateId, aggregateId),
|
|
252
|
+
eq(eventsTable.tenantId, tenantId),
|
|
253
|
+
lte(eventsTable.createdAt, asOf),
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
.orderBy(asc(eventsTable.version));
|
|
257
|
+
return rows.map(toStoredEvent);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Cheapest possible read of "what's the latest version on this stream?". The
|
|
261
|
+
// CRUD executor uses this as expectedVersion for its append, so a domain
|
|
262
|
+
// event appended via ctx.appendEvent between two CRUD writes doesn't cause
|
|
263
|
+
// the next CRUD write to fail with version_conflict. Indexed lookup on the
|
|
264
|
+
// existing (aggregate_id, version) unique index. Returns 0 for empty/unknown
|
|
265
|
+
// streams (matches create()'s expectedVersion=0 convention).
|
|
266
|
+
export async function getStreamVersion(
|
|
267
|
+
db: DbRunner,
|
|
268
|
+
aggregateId: string,
|
|
269
|
+
tenantId: TenantId,
|
|
270
|
+
): Promise<number> {
|
|
271
|
+
const [row] = await db
|
|
272
|
+
.select({ v: max(eventsTable.version) })
|
|
273
|
+
.from(eventsTable)
|
|
274
|
+
.where(and(eq(eventsTable.aggregateId, aggregateId), eq(eventsTable.tenantId, tenantId)));
|
|
275
|
+
return row?.v ?? 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Global high-water-mark = MAX(events.id). Marten/Wolverine standard for
|
|
279
|
+
// projection/consumer lag math: lag = HWM - cursor. Single-row aggregate over
|
|
280
|
+
// the bigserial PK index — sub-millisecond cost. Returns 0n on an empty log
|
|
281
|
+
// (boot, fresh tenant, post-archive).
|
|
282
|
+
export async function getEventsHighWaterMark(db: DbRunner): Promise<bigint> {
|
|
283
|
+
const [row] = await db.select({ max: max(eventsTable.id) }).from(eventsTable);
|
|
284
|
+
return row?.max ?? 0n;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Load events strictly newer than a given version. Used by snapshot-aware
|
|
288
|
+
// reads: snapshot provides state up to version N, apply events v > N to
|
|
289
|
+
// catch up to current.
|
|
290
|
+
export async function loadEventsAfterVersion(
|
|
291
|
+
db: DbRunner,
|
|
292
|
+
aggregateId: string,
|
|
293
|
+
tenantId: TenantId,
|
|
294
|
+
afterVersion: number,
|
|
295
|
+
): Promise<readonly StoredEvent[]> {
|
|
296
|
+
const rows = await db
|
|
297
|
+
.select()
|
|
298
|
+
.from(eventsTable)
|
|
299
|
+
.where(
|
|
300
|
+
and(
|
|
301
|
+
eq(eventsTable.aggregateId, aggregateId),
|
|
302
|
+
eq(eventsTable.tenantId, tenantId),
|
|
303
|
+
gt(eventsTable.version, afterVersion),
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
.orderBy(asc(eventsTable.version));
|
|
307
|
+
return rows.map(toStoredEvent);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Load every event for an aggregate_type across all tenants. Ordered by
|
|
311
|
+
// (created_at, id) — chronological replay order for projection rebuilds.
|
|
312
|
+
//
|
|
313
|
+
// CAUTION — buffers ALL matching events in memory. Safe for smaller
|
|
314
|
+
// aggregate-types (≤ 100k events), a memory cliff for large stores.
|
|
315
|
+
// For >100k events use `streamAllEventsByType` (yields batchwise).
|
|
316
|
+
// Mostly called from tests today — production rebuild goes through
|
|
317
|
+
// projection-rebuild's own streaming path.
|
|
318
|
+
export async function loadAllEventsByType(
|
|
319
|
+
db: DbRunner,
|
|
320
|
+
aggregateType: string,
|
|
321
|
+
): Promise<readonly StoredEvent[]> {
|
|
322
|
+
const rows = await db
|
|
323
|
+
.select()
|
|
324
|
+
.from(eventsTable)
|
|
325
|
+
.where(eq(eventsTable.aggregateType, aggregateType))
|
|
326
|
+
.orderBy(asc(eventsTable.createdAt), asc(eventsTable.id));
|
|
327
|
+
return rows.map(toStoredEvent);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Stream every event for an aggregate_type across all tenants, batchwise
|
|
331
|
+
// instead of buffered. Memory-bounded: never more than `batchSize` rows
|
|
332
|
+
// resident. Cursor walks `events.id` (bigserial monotonic — concurrent
|
|
333
|
+
// inserts get distinct ids in commit order, so no duplicates and no skips
|
|
334
|
+
// past the cursor).
|
|
335
|
+
//
|
|
336
|
+
// Use case: projection-rebuild on a large event log (>100k events per
|
|
337
|
+
// aggregate-type). loadAllEventsByType would OOM; this iterator yields
|
|
338
|
+
// in batches and the caller accumulates only what it needs.
|
|
339
|
+
//
|
|
340
|
+
// Default batchSize 1000 — trade-off between DB round-trips (smaller =
|
|
341
|
+
// more queries) and memory (larger = more resident).
|
|
342
|
+
//
|
|
343
|
+
// The caller may pause / do async work between yields; the next batch is
|
|
344
|
+
// only fetched when consumed.
|
|
345
|
+
//
|
|
346
|
+
// Cancellation: pass `signal` (typically `ctx.signal` from a handler) to
|
|
347
|
+
// abort iteration. Checked both at batch boundaries (before the next
|
|
348
|
+
// fetch) AND between yields (so abort lands within one event regardless
|
|
349
|
+
// of batch size). Throws AbortError on the first check after abort;
|
|
350
|
+
// in-flight queries are not actively cancelled (postgres-js connection-
|
|
351
|
+
// cancel is a separate, riskier concern handled per-query at the TenantDb
|
|
352
|
+
// boundary).
|
|
353
|
+
export async function* streamAllEventsByType(
|
|
354
|
+
db: DbRunner,
|
|
355
|
+
aggregateType: string,
|
|
356
|
+
batchSize = 1000,
|
|
357
|
+
signal?: AbortSignal,
|
|
358
|
+
): AsyncIterable<StoredEvent> {
|
|
359
|
+
let cursorId = 0n;
|
|
360
|
+
while (true) {
|
|
361
|
+
signal?.throwIfAborted();
|
|
362
|
+
const rows = await db
|
|
363
|
+
.select()
|
|
364
|
+
.from(eventsTable)
|
|
365
|
+
.where(and(eq(eventsTable.aggregateType, aggregateType), gt(eventsTable.id, cursorId)))
|
|
366
|
+
.orderBy(asc(eventsTable.id))
|
|
367
|
+
.limit(batchSize);
|
|
368
|
+
|
|
369
|
+
if (rows.length === 0) {
|
|
370
|
+
// skip: end of stream — generator exit is the natural termination.
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Track the highest id seen in this batch as we yield. Avoids both the
|
|
375
|
+
// non-null assertion and a redundant array index — the cursor falls out
|
|
376
|
+
// of the same loop that produces the events.
|
|
377
|
+
let nextCursor = cursorId;
|
|
378
|
+
for (const row of rows) {
|
|
379
|
+
// Per-yield abort check. Cheap (one boolean read), keeps cancel
|
|
380
|
+
// semantics independent of the batchSize knob — at batchSize=1000
|
|
381
|
+
// a batch-boundary-only check would still yield 1000 events after
|
|
382
|
+
// an abort which isn't what callers expect.
|
|
383
|
+
signal?.throwIfAborted();
|
|
384
|
+
yield toStoredEvent(row);
|
|
385
|
+
nextCursor = row.id;
|
|
386
|
+
}
|
|
387
|
+
cursorId = nextCursor;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function toStoredEvent(row: SelectedEvent): StoredEvent {
|
|
392
|
+
return {
|
|
393
|
+
id: String(row.id),
|
|
394
|
+
aggregateId: row.aggregateId,
|
|
395
|
+
aggregateType: row.aggregateType,
|
|
396
|
+
tenantId: row.tenantId,
|
|
397
|
+
version: row.version,
|
|
398
|
+
type: row.type,
|
|
399
|
+
eventVersion: row.eventVersion,
|
|
400
|
+
payload: row.payload,
|
|
401
|
+
metadata: row.metadata,
|
|
402
|
+
createdAt: row.createdAt,
|
|
403
|
+
createdBy: row.createdBy,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { type DbConnection, tableExists } from "../db";
|
|
3
|
+
import {
|
|
4
|
+
bigserial,
|
|
5
|
+
index,
|
|
6
|
+
instant,
|
|
7
|
+
integer,
|
|
8
|
+
jsonb,
|
|
9
|
+
table as pgTable,
|
|
10
|
+
text,
|
|
11
|
+
uniqueIndex,
|
|
12
|
+
uuid,
|
|
13
|
+
} from "../db/dialect";
|
|
14
|
+
import { pushTables } from "../stack";
|
|
15
|
+
import { createArchivedStreamsTable } from "./archive";
|
|
16
|
+
import { createSnapshotsTable } from "./snapshot";
|
|
17
|
+
|
|
18
|
+
// Event-store schema as a Drizzle table. The typed select/insert path handles
|
|
19
|
+
// most operations; append() for subsequent versions uses raw SQL because
|
|
20
|
+
// INSERT ... SELECT ... WHERE EXISTS isn't ergonomic in the typed builder.
|
|
21
|
+
//
|
|
22
|
+
// HTTP-level retry idempotency is handled by pipeline/idempotency.ts
|
|
23
|
+
// (Redis-backed check + cached-response replay). The event-store itself
|
|
24
|
+
// imposes no idempotency index — a single HTTP request may write N events
|
|
25
|
+
// freely, metadata.requestId is purely a trace marker.
|
|
26
|
+
export type EventMetadata = {
|
|
27
|
+
readonly userId: string;
|
|
28
|
+
readonly requestId?: string;
|
|
29
|
+
readonly correlationId?: string;
|
|
30
|
+
readonly causationId?: string;
|
|
31
|
+
// App-specific free key/value (Marten "headers"). Mirror of the canonical
|
|
32
|
+
// type in event-store.ts — kept duplicate because events-schema must stay
|
|
33
|
+
// import-cycle-free vs the event-store module.
|
|
34
|
+
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const eventsTable = pgTable(
|
|
38
|
+
"kumiko_events",
|
|
39
|
+
{
|
|
40
|
+
// bigserial PK: global chronological ordering cheap to index, safe past
|
|
41
|
+
// 2^53 as long as we stay < ~9e15 events. Returned to JS as BigInt.
|
|
42
|
+
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
|
43
|
+
aggregateId: uuid("aggregate_id").notNull(),
|
|
44
|
+
aggregateType: text("aggregate_type").notNull(),
|
|
45
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
46
|
+
version: integer("version").notNull(),
|
|
47
|
+
type: text("type").notNull(),
|
|
48
|
+
eventVersion: integer("event_version").notNull().default(1),
|
|
49
|
+
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
|
|
50
|
+
metadata: jsonb("metadata").$type<EventMetadata>().notNull(),
|
|
51
|
+
// Millisecond precision: matches what asOf-queries can compare reliably.
|
|
52
|
+
// Sprint F: instant() = Temporal.Instant round-trip via dialect.ts customType.
|
|
53
|
+
createdAt: instant("created_at", { precision: 3 }).notNull().default(sql`now()`),
|
|
54
|
+
// Text rather than uuid: the framework's SessionUser.id is a number
|
|
55
|
+
// (serial) by default. Stringified here so both integer- and UUID-shaped
|
|
56
|
+
// user ids round-trip cleanly. Aggregate-IDs stay uuid because events are
|
|
57
|
+
// aggregated by UUID end-to-end.
|
|
58
|
+
createdBy: text("created_by").notNull(),
|
|
59
|
+
},
|
|
60
|
+
(t) => ({
|
|
61
|
+
// Tenant-scoped unique: two tenants that happen to pick the same
|
|
62
|
+
// aggregate_id (deterministic IDs, replay, restores) don't collide, and
|
|
63
|
+
// insertFirstEvent needs no extra tenant check for cross-tenant safety —
|
|
64
|
+
// the constraint itself guarantees it. For expectedVersion > 0 the raw
|
|
65
|
+
// INSERT … SELECT … WHERE EXISTS still pairs the predecessor with the
|
|
66
|
+
// same tenant, which is now just predecessor-existence and no longer
|
|
67
|
+
// doing double duty as an anti-hijack check.
|
|
68
|
+
aggregateVersionUq: uniqueIndex("events_aggregate_version_uq").on(
|
|
69
|
+
t.tenantId,
|
|
70
|
+
t.aggregateId,
|
|
71
|
+
t.version,
|
|
72
|
+
),
|
|
73
|
+
loadIdx: index("events_load_idx").on(t.aggregateId, t.version),
|
|
74
|
+
tenantTypeIdx: index("events_tenant_type_idx").on(t.tenantId, t.aggregateType, t.createdAt),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Convenience used by framework integration tests. Creates the table via
|
|
79
|
+
// drizzle-kit diffing. Also materializes kumiko_archived_streams and
|
|
80
|
+
// kumiko_snapshots — loadAggregate / appendEvent / loadAggregateWithSnapshot
|
|
81
|
+
// consult them on the hot path, so the three tables must come up together.
|
|
82
|
+
export async function createEventsTable(db: DbConnection): Promise<void> {
|
|
83
|
+
// skip: events table already exists — createEventsTable is called from both
|
|
84
|
+
// setupTestStack and explicit test-setups, the guard keeps it idempotent.
|
|
85
|
+
if (!(await tableExists(db, "public.kumiko_events"))) {
|
|
86
|
+
await pushTables(db, { kumikoEvents: eventsTable });
|
|
87
|
+
}
|
|
88
|
+
await createArchivedStreamsTable(db);
|
|
89
|
+
await createSnapshotsTable(db);
|
|
90
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type ArchiveStreamArgs,
|
|
3
|
+
archivedStreamsTable,
|
|
4
|
+
archiveStream,
|
|
5
|
+
createArchivedStreamsTable,
|
|
6
|
+
isStreamArchived,
|
|
7
|
+
restoreStream,
|
|
8
|
+
} from "./archive";
|
|
9
|
+
export { ArchivedStreamError, VersionConflictError } from "./errors";
|
|
10
|
+
export {
|
|
11
|
+
append,
|
|
12
|
+
EVENTS_PUBSUB_CHANNEL,
|
|
13
|
+
type EventMetadata,
|
|
14
|
+
type EventToAppend,
|
|
15
|
+
getEventsHighWaterMark,
|
|
16
|
+
getStreamVersion,
|
|
17
|
+
loadAggregate,
|
|
18
|
+
loadAggregateAsOf,
|
|
19
|
+
loadAllEventsByType,
|
|
20
|
+
loadEventsAfterVersion,
|
|
21
|
+
type StoredEvent,
|
|
22
|
+
streamAllEventsByType,
|
|
23
|
+
} from "./event-store";
|
|
24
|
+
export { createEventsTable, eventsTable } from "./events-schema";
|
|
25
|
+
export {
|
|
26
|
+
createSnapshotsTable,
|
|
27
|
+
type LoadAggregateWithSnapshotResult,
|
|
28
|
+
loadAggregateWithSnapshot,
|
|
29
|
+
loadLatestSnapshot,
|
|
30
|
+
type SaveSnapshotArgs,
|
|
31
|
+
type Snapshot,
|
|
32
|
+
type SnapshotReducer,
|
|
33
|
+
saveSnapshot,
|
|
34
|
+
snapshotsTable,
|
|
35
|
+
} from "./snapshot";
|
|
36
|
+
export {
|
|
37
|
+
type EventUpcasters,
|
|
38
|
+
makeUpcastCtx,
|
|
39
|
+
type UpcasterErrorPolicy,
|
|
40
|
+
type UpcastOptions,
|
|
41
|
+
upcastStoredEvent,
|
|
42
|
+
upcastStoredEvents,
|
|
43
|
+
} from "./upcaster";
|
|
44
|
+
export {
|
|
45
|
+
createUpcasterDeadLetterTable,
|
|
46
|
+
type DeadLetterRow,
|
|
47
|
+
listDeadLetters,
|
|
48
|
+
recordUpcasterDeadLetter,
|
|
49
|
+
upcasterDeadLetterTable,
|
|
50
|
+
} from "./upcaster-dead-letter";
|