@cosmicdrift/kumiko-framework 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
- package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
- package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
- package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
- package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
- package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
- package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
- package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
- package/src/api/__tests__/api.test.ts +1 -1
- package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
- package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
- package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/csrf-middleware.test.ts +1 -1
- package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
- package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
- package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
- package/src/api/__tests__/readiness.test.ts +1 -1
- package/src/api/__tests__/request-id-middleware.test.ts +1 -1
- package/src/api/__tests__/sse-broker.test.ts +12 -12
- package/src/api/__tests__/sse-route.test.ts +1 -1
- package/src/api/auth-routes.ts +2 -5
- package/src/api/readiness.ts +2 -2
- package/src/auth/__tests__/roles.test.ts +2 -2
- package/src/bun-db/__tests__/PATTERN.md +73 -0
- package/src/bun-db/__tests__/_helpers.ts +103 -0
- package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
- package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
- package/src/bun-db/__tests__/bun-test-db.ts +19 -0
- package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
- package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
- package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
- package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
- package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
- package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
- package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
- package/src/bun-db/connection.ts +84 -0
- package/src/bun-db/index.ts +31 -0
- package/src/bun-db/query.ts +842 -0
- package/src/compliance/__tests__/duration-spec.test.ts +1 -1
- package/src/compliance/__tests__/profiles.test.ts +1 -1
- package/src/compliance/__tests__/sub-processors.test.ts +1 -1
- package/src/compliance/profiles.ts +1 -4
- package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
- package/src/db/__tests__/big-int-field.test.ts +15 -14
- package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
- package/src/db/__tests__/connection-options.test.ts +1 -1
- package/src/db/__tests__/cursor.test.ts +8 -32
- package/src/db/__tests__/dialect-instant.test.ts +1 -1
- package/src/db/__tests__/encryption.test.ts +1 -1
- package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
- package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
- package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
- package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/migrate-generator.test.ts +71 -0
- package/src/db/__tests__/migrate-runner.test.ts +19 -0
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
- package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
- package/src/db/__tests__/pg-error.test.ts +43 -0
- package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
- package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
- package/src/db/__tests__/sql-inventory.test.ts +56 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
- package/src/db/__tests__/table-builder-required.test.ts +20 -22
- package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
- package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
- package/src/db/api.ts +46 -0
- package/src/db/apply-entity-event.ts +45 -36
- package/src/db/assert-exists-in.ts +5 -16
- package/src/db/bun-provider.ts +37 -0
- package/src/db/config-seed.ts +4 -4
- package/src/db/connection.ts +14 -57
- package/src/db/cursor.ts +5 -56
- package/src/db/dialect.ts +472 -99
- package/src/db/eagerload.ts +5 -12
- package/src/db/entity-table-meta.ts +390 -0
- package/src/db/event-store-executor.ts +158 -100
- package/src/db/index.ts +33 -5
- package/src/db/migrate-generator.ts +350 -0
- package/src/db/migrate-runner.ts +206 -0
- package/src/db/postgres-provider.ts +25 -0
- package/src/db/queries/entity-read.ts +15 -0
- package/src/db/queries/es-ops.ts +17 -0
- package/src/db/queries/event-consumer.ts +170 -0
- package/src/db/queries/event-store-admin.ts +127 -0
- package/src/db/queries/event-store.ts +155 -0
- package/src/db/queries/projection-rebuild.ts +59 -0
- package/src/db/queries/raw-sql.ts +15 -0
- package/src/db/queries/schema-drift.ts +35 -0
- package/src/db/queries/seed-context.ts +58 -0
- package/src/db/queries/table-ops.ts +11 -0
- package/src/db/queries/test-stack.ts +56 -0
- package/src/db/query-api.ts +22 -0
- package/src/db/query.ts +30 -0
- package/src/db/reference-data.ts +19 -22
- package/src/db/render-ddl.ts +57 -0
- package/src/db/row-helpers.ts +3 -52
- package/src/db/schema-inspection.ts +17 -4
- package/src/db/sql-inventory.ts +208 -0
- package/src/db/table-builder.ts +54 -46
- package/src/db/tenant-db.ts +105 -326
- package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
- package/src/engine/__tests__/boot-validator.test.ts +4 -3
- package/src/engine/__tests__/build-app-schema.test.ts +1 -1
- package/src/engine/__tests__/build-target.test.ts +1 -1
- package/src/engine/__tests__/claim-keys.test.ts +1 -1
- package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
- package/src/engine/__tests__/config-helpers.test.ts +1 -1
- package/src/engine/__tests__/duration-utils.test.ts +16 -0
- package/src/engine/__tests__/effective-features.test.ts +1 -1
- package/src/engine/__tests__/engine.test.ts +1 -1
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +3 -3
- package/src/engine/__tests__/extends-registrar.test.ts +4 -4
- package/src/engine/__tests__/factories-long-text.test.ts +1 -1
- package/src/engine/__tests__/factories-time.test.ts +1 -1
- package/src/engine/__tests__/field-access.test.ts +38 -0
- package/src/engine/__tests__/field-predicates.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +1 -1
- package/src/engine/__tests__/identifiers.test.ts +1 -1
- package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/no-return-guard.test.ts +17 -0
- package/src/engine/__tests__/ownership.test.ts +10 -11
- package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
- package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
- package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
- package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +1 -1
- package/src/engine/__tests__/projection-helpers.test.ts +25 -17
- package/src/engine/__tests__/projection.test.ts +4 -4
- package/src/engine/__tests__/qualified-name.test.ts +1 -1
- package/src/engine/__tests__/raw-table.test.ts +9 -8
- package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
- package/src/engine/__tests__/run-in.test.ts +1 -1
- package/src/engine/__tests__/schema-builder.test.ts +1 -1
- package/src/engine/__tests__/screen.test.ts +1 -1
- package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
- package/src/engine/__tests__/state-machine.test.ts +1 -1
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
- package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
- package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
- package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
- package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
- package/src/engine/__tests__/steps-read.test.ts +34 -40
- package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
- package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
- package/src/engine/__tests__/steps-workflow.test.ts +7 -7
- package/src/engine/__tests__/system-user.test.ts +1 -1
- package/src/engine/__tests__/unmanaged-table.test.ts +98 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
- package/src/engine/__tests__/validation-hooks.test.ts +1 -1
- package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
- package/src/engine/boot-validator/entity-handler.ts +3 -3
- package/src/engine/boot-validator/ownership.ts +1 -1
- package/src/engine/define-feature.ts +37 -2
- package/src/engine/entity-handlers.ts +5 -5
- package/src/engine/factories.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
- package/src/engine/feature-ast/extractors/shared.ts +2 -3
- package/src/engine/ownership.ts +113 -41
- package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
- package/src/engine/projection-helpers.ts +2 -11
- package/src/engine/registry.ts +21 -2
- package/src/engine/steps/read-find-many.ts +13 -13
- package/src/engine/steps/read-find-one.ts +7 -9
- package/src/engine/steps/unsafe-projection-delete.ts +4 -5
- package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
- package/src/engine/types/feature.ts +47 -2
- package/src/engine/types/fields.ts +4 -5
- package/src/engine/types/index.ts +2 -0
- package/src/engine/types/step.ts +10 -10
- package/src/engine/validate-projection-allowlist.ts +23 -3
- package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
- package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
- package/src/env/__tests__/compose-env-schema.test.ts +1 -1
- package/src/env/__tests__/dry-run.test.ts +1 -1
- package/src/errors/__tests__/classes.test.ts +1 -1
- package/src/errors/__tests__/error-helpers.test.ts +44 -0
- package/src/errors/__tests__/field-issue-compat.test.ts +16 -0
- package/src/errors/__tests__/write-failures.test.ts +1 -1
- package/src/errors/classes.ts +5 -19
- package/src/errors/field-issue.ts +11 -0
- package/src/errors/index.ts +1 -0
- package/src/errors/zod-bridge.ts +3 -2
- package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
- package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
- package/src/es-ops/__tests__/runner.test.ts +29 -19
- package/src/es-ops/context.ts +11 -56
- package/src/es-ops/operations-schema.ts +2 -2
- package/src/es-ops/runner.ts +12 -26
- package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
- package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
- package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
- package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
- package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
- package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
- package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
- package/src/event-store/admin-api.ts +55 -83
- package/src/event-store/archive.ts +15 -39
- package/src/event-store/event-store.ts +92 -86
- package/src/event-store/events-schema.ts +2 -1
- package/src/event-store/index.ts +1 -0
- package/src/event-store/snapshot.ts +26 -24
- package/src/event-store/upcaster-dead-letter.ts +19 -18
- package/src/files/__tests__/content-disposition.test.ts +1 -1
- package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
- package/src/files/__tests__/file-handle.test.ts +1 -1
- package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
- package/src/files/__tests__/read-stream.test.ts +1 -1
- package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
- package/src/files/__tests__/write-stream.test.ts +1 -1
- package/src/files/__tests__/zip-stream.test.ts +1 -1
- package/src/files/file-ref-table.ts +2 -2
- package/src/files/file-routes.ts +7 -9
- package/src/files/storage-tracking.ts +9 -17
- package/src/i18n/__tests__/i18n.test.ts +1 -1
- package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
- package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
- package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
- package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
- package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
- package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
- package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
- package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
- package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
- package/src/migrations/projection-detection.ts +12 -1
- package/src/migrations/schema-drift.ts +7 -23
- package/src/observability/__tests__/console-provider.test.ts +1 -1
- package/src/observability/__tests__/metric-validator.test.ts +1 -1
- package/src/observability/__tests__/noop-provider.test.ts +1 -1
- package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
- package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-tracer.test.ts +1 -1
- package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
- package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
- package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
- package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
- package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
- package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
- package/src/pipeline/__tests__/dispatcher-utils.test.ts +107 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
- package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
- package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
- package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
- package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
- package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
- package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
- package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
- package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
- package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
- package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
- package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
- package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
- package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
- package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
- package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
- package/src/pipeline/__tests__/redis-keys.test.ts +12 -0
- package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- package/src/pipeline/dispatcher-utils.ts +8 -7
- package/src/pipeline/dispatcher.ts +43 -48
- package/src/pipeline/event-consumer-state.ts +11 -2
- package/src/pipeline/event-dispatcher.ts +86 -146
- package/src/pipeline/event-retention.ts +14 -24
- package/src/pipeline/msp-rebuild.ts +54 -78
- package/src/pipeline/projection-rebuild.ts +65 -67
- package/src/pipeline/projection-state.ts +2 -2
- package/src/random/__tests__/generate.test.ts +13 -13
- package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
- package/src/redis/__tests__/redis-options.test.ts +1 -1
- package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
- package/src/search/__tests__/search-adapter.test.ts +1 -1
- package/src/secrets/__tests__/dek-cache.test.ts +1 -3
- package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/__tests__/leak-guard.test.ts +1 -1
- package/src/secrets/__tests__/rotation.test.ts +1 -1
- package/src/stack/db.ts +25 -48
- package/src/stack/push-entity-projection-tables.ts +2 -4
- package/src/stack/table-helpers.ts +98 -61
- package/src/stack/test-stack.ts +10 -9
- package/src/testing/__tests__/db-cleanup.test.ts +40 -0
- package/src/testing/__tests__/e2e-generator.test.ts +1 -1
- package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
- package/src/testing/db-cleanup.ts +44 -0
- package/src/testing/expect-error.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/multipart-helper.ts +94 -0
- package/src/testing/shared-entities.ts +5 -5
- package/src/time/__tests__/polyfill.test.ts +1 -1
- package/src/time/__tests__/tz-context.test.ts +1 -1
- package/src/utils/__tests__/assert.test.ts +1 -1
- package/src/utils/__tests__/case.test.ts +16 -0
- package/src/utils/__tests__/env-parse.test.ts +1 -1
- package/src/utils/__tests__/is-plain-object.test.ts +16 -0
- package/src/utils/__tests__/parse-string-array-json.test.ts +16 -0
- package/src/utils/__tests__/safe-json.test.ts +22 -0
- package/src/utils/case.ts +6 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/is-plain-object.ts +4 -0
- package/src/utils/parse-string-array-json.ts +14 -0
- package/CHANGELOG.md +0 -474
- package/src/db/__tests__/db-helpers.test.ts +0 -369
- package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
- package/src/db/__tests__/row-helpers.test.ts +0 -59
- package/src/engine/steps/_drizzle-boundary.ts +0 -19
- package/src/files/__tests__/file-field-column.integration.ts +0 -103
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type AnyColumn, eq } from "drizzle-orm";
|
|
2
1
|
import { requestContext } from "../api/request-context";
|
|
3
2
|
import type { DbConnection, DbRow, DbTx } from "../db/connection";
|
|
4
|
-
import {
|
|
3
|
+
import { selectRowForUpdateById } from "../db/queries/entity-read";
|
|
4
|
+
import { selectMany, transaction } from "../db/query";
|
|
5
|
+
import { buildEntityTable, toSnakeCase } from "../db/table-builder";
|
|
5
6
|
import { createTenantDb } from "../db/tenant-db";
|
|
6
7
|
import { hasAccess } from "../engine/access";
|
|
7
8
|
import { checkWriteFieldRoles, filterReadFields } from "../engine/field-access";
|
|
@@ -173,15 +174,22 @@ export function createDispatcher(
|
|
|
173
174
|
): Dispatcher {
|
|
174
175
|
const { idempotency, lifecycle, jobRunner, effectiveFeatures } = options;
|
|
175
176
|
|
|
177
|
+
// Narrowing-helper: AppContext.db ist DbConnection|TenantDb|undefined. Die
|
|
178
|
+
// dispatch-Pfade brauchen DbConnection (oder DbTx aus Caller-Scope) für
|
|
179
|
+
// appendEvent/projection-writes; TenantDb-Branch wird hier ausgeschlossen.
|
|
180
|
+
function resolveDbSource(tx: DbTx | undefined): DbConnection | DbTx | undefined {
|
|
181
|
+
return tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
182
|
+
}
|
|
183
|
+
|
|
176
184
|
// Pre-build tables and transition maps for auto-guard (avoid per-request allocation)
|
|
177
|
-
const tableCache = new Map<string, ReturnType<typeof
|
|
185
|
+
const tableCache = new Map<string, ReturnType<typeof buildEntityTable>>();
|
|
178
186
|
const transitionCache = new Map<string, ReturnType<typeof defineTransitions>>();
|
|
179
187
|
|
|
180
|
-
function getTable(entityName: string): ReturnType<typeof
|
|
188
|
+
function getTable(entityName: string): ReturnType<typeof buildEntityTable> | undefined {
|
|
181
189
|
if (tableCache.has(entityName)) return tableCache.get(entityName);
|
|
182
190
|
const entity = registry.getEntity(entityName);
|
|
183
191
|
if (!entity) return undefined;
|
|
184
|
-
const table =
|
|
192
|
+
const table = buildEntityTable(entityName, entity, {
|
|
185
193
|
relations: registry.getRelations(entityName),
|
|
186
194
|
});
|
|
187
195
|
tableCache.set(entityName, table);
|
|
@@ -215,8 +223,7 @@ export function createDispatcher(
|
|
|
215
223
|
tx: DbTx | undefined,
|
|
216
224
|
callerFeature: string | undefined,
|
|
217
225
|
): Promise<void> {
|
|
218
|
-
const dbSource
|
|
219
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
226
|
+
const dbSource = resolveDbSource(tx);
|
|
220
227
|
if (!dbSource) {
|
|
221
228
|
throw new InternalError({
|
|
222
229
|
message: `ctx.appendEvent("${args.type}") requires a database connection — none is configured.`,
|
|
@@ -245,8 +252,7 @@ export function createDispatcher(
|
|
|
245
252
|
// The outer dispatcher receives a DbConnection from the server/stack;
|
|
246
253
|
// AppContext's `db` union also allows TenantDb (for downstream hook calls),
|
|
247
254
|
// but at this point we're the root of the pipeline — cast is safe.
|
|
248
|
-
const dbSource
|
|
249
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
255
|
+
const dbSource = resolveDbSource(tx);
|
|
250
256
|
const reqCtx = requestContext.get();
|
|
251
257
|
const db = dbSource
|
|
252
258
|
? createTenantDb(
|
|
@@ -313,8 +319,7 @@ export function createDispatcher(
|
|
|
313
319
|
await appendDomainEvent(args, user, tx, registry.getHandlerFeature(type));
|
|
314
320
|
},
|
|
315
321
|
fetchForWriting: async (args: FetchForWritingArgs): Promise<AggregateStreamHandle> => {
|
|
316
|
-
const dbSource
|
|
317
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
322
|
+
const dbSource = resolveDbSource(tx);
|
|
318
323
|
if (!dbSource) {
|
|
319
324
|
throw new InternalError({
|
|
320
325
|
message: `ctx.fetchForWriting("${args.aggregateId}") requires a database connection — none is configured.`,
|
|
@@ -377,8 +382,7 @@ export function createDispatcher(
|
|
|
377
382
|
aggregateId: string,
|
|
378
383
|
loadOptions?: { readonly asOf?: Temporal.Instant },
|
|
379
384
|
): Promise<readonly StoredEvent[]> => {
|
|
380
|
-
const dbSource
|
|
381
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
385
|
+
const dbSource = resolveDbSource(tx);
|
|
382
386
|
if (!dbSource) {
|
|
383
387
|
throw new InternalError({
|
|
384
388
|
message: `ctx.loadAggregate("${aggregateId}") requires a database connection — none is configured.`,
|
|
@@ -396,8 +400,7 @@ export function createDispatcher(
|
|
|
396
400
|
aggregateId: string,
|
|
397
401
|
archiveArgs: { readonly aggregateType: string; readonly reason?: string },
|
|
398
402
|
): Promise<void> => {
|
|
399
|
-
const dbSource
|
|
400
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
403
|
+
const dbSource = resolveDbSource(tx);
|
|
401
404
|
if (!dbSource) {
|
|
402
405
|
throw new InternalError({
|
|
403
406
|
message: `ctx.archiveStream("${aggregateId}") requires a database connection — none is configured.`,
|
|
@@ -412,8 +415,7 @@ export function createDispatcher(
|
|
|
412
415
|
});
|
|
413
416
|
},
|
|
414
417
|
restoreStream: async (aggregateId: string): Promise<void> => {
|
|
415
|
-
const dbSource
|
|
416
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
418
|
+
const dbSource = resolveDbSource(tx);
|
|
417
419
|
if (!dbSource) {
|
|
418
420
|
throw new InternalError({
|
|
419
421
|
message: `ctx.restoreStream("${aggregateId}") requires a database connection — none is configured.`,
|
|
@@ -422,8 +424,7 @@ export function createDispatcher(
|
|
|
422
424
|
await restoreStreamHelper(dbSource, user.tenantId, aggregateId);
|
|
423
425
|
},
|
|
424
426
|
isStreamArchived: async (aggregateId: string): Promise<boolean> => {
|
|
425
|
-
const dbSource
|
|
426
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
427
|
+
const dbSource = resolveDbSource(tx);
|
|
427
428
|
if (!dbSource) {
|
|
428
429
|
throw new InternalError({
|
|
429
430
|
message: `ctx.isStreamArchived("${aggregateId}") requires a database connection — none is configured.`,
|
|
@@ -437,8 +438,7 @@ export function createDispatcher(
|
|
|
437
438
|
readonly version: number;
|
|
438
439
|
readonly state: Record<string, unknown>;
|
|
439
440
|
}): Promise<void> => {
|
|
440
|
-
const dbSource
|
|
441
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
441
|
+
const dbSource = resolveDbSource(tx);
|
|
442
442
|
if (!dbSource) {
|
|
443
443
|
throw new InternalError({
|
|
444
444
|
message: `ctx.snapshotAggregate("${snapshotArgs.aggregateId}") requires a database connection — none is configured.`,
|
|
@@ -457,8 +457,7 @@ export function createDispatcher(
|
|
|
457
457
|
reducer: SnapshotReducer<TState>,
|
|
458
458
|
initial: TState,
|
|
459
459
|
): Promise<LoadAggregateWithSnapshotResult<TState>> => {
|
|
460
|
-
const dbSource
|
|
461
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
460
|
+
const dbSource = resolveDbSource(tx);
|
|
462
461
|
if (!dbSource) {
|
|
463
462
|
throw new InternalError({
|
|
464
463
|
message: `ctx.loadAggregateWithSnapshot("${aggregateId}") requires a database connection — none is configured.`,
|
|
@@ -502,8 +501,7 @@ export function createDispatcher(
|
|
|
502
501
|
`table-less MSP (side-effect-only). Known queryable projections: ${all.join(", ") || "(none)"}`,
|
|
503
502
|
});
|
|
504
503
|
}
|
|
505
|
-
const dbSource
|
|
506
|
-
tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
|
|
504
|
+
const dbSource = resolveDbSource(tx);
|
|
507
505
|
if (!dbSource) {
|
|
508
506
|
throw new InternalError({
|
|
509
507
|
message: `ctx.queryProjection("${qualifiedName}") requires a database connection — none is configured.`,
|
|
@@ -513,17 +511,10 @@ export function createDispatcher(
|
|
|
513
511
|
// filter keeps cross-tenant leaks out unless the handler explicitly
|
|
514
512
|
// opts in. Works with any drizzle-table whose tenant column is named
|
|
515
513
|
// tenantId on the JS side.
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
rows = (await dbSource
|
|
521
|
-
.select()
|
|
522
|
-
.from(projTable)
|
|
523
|
-
.where(eq(tenantCol, user.tenantId))) as readonly Record<string, unknown>[]; // @cast-boundary db-row
|
|
524
|
-
} else {
|
|
525
|
-
rows = (await dbSource.select().from(projTable)) as readonly Record<string, unknown>[]; // @cast-boundary db-row
|
|
526
|
-
}
|
|
514
|
+
const tenantCol = (projTable as Record<string, unknown>)["tenantId"];
|
|
515
|
+
const where =
|
|
516
|
+
tenantCol && !queryOptions?.unsafeAllTenants ? { tenantId: user.tenantId } : undefined;
|
|
517
|
+
const rows = await selectMany<Record<string, unknown>>(dbSource, projTable, where);
|
|
527
518
|
return rows as readonly T[]; // @cast-boundary engine-payload
|
|
528
519
|
},
|
|
529
520
|
// Thin pass-through: one resolve impl lives on the dispatcher, the
|
|
@@ -1123,9 +1114,12 @@ export function createDispatcher(
|
|
|
1123
1114
|
// can false-pass; optimistic locking would catch it later, but with
|
|
1124
1115
|
// a less specific error. Falls back to a plain SELECT if no tx is
|
|
1125
1116
|
// active (tests without a DB connection).
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1117
|
+
const tableName = String(
|
|
1118
|
+
(table as { [key: symbol]: unknown })[Symbol.for("kumiko:schema:Name")],
|
|
1119
|
+
);
|
|
1120
|
+
const rows = tx
|
|
1121
|
+
? await selectRowForUpdateById(handlerContext.db, tableName, id)
|
|
1122
|
+
: await selectMany(handlerContext.db, table, { id });
|
|
1129
1123
|
const row = rows[0];
|
|
1130
1124
|
|
|
1131
1125
|
if (!row) continue;
|
|
@@ -1133,10 +1127,13 @@ export function createDispatcher(
|
|
|
1133
1127
|
// at all; a handler that wants to move a deleted row should use
|
|
1134
1128
|
// unsafeSkipTransitionGuard or restore first.
|
|
1135
1129
|
const rowAsRow = row as DbRow; // @cast-boundary engine-payload
|
|
1136
|
-
|
|
1130
|
+
const isDeleted = rowAsRow["isDeleted"] ?? rowAsRow["is_deleted"];
|
|
1131
|
+
if (entity.softDelete && isDeleted === true) {
|
|
1137
1132
|
continue;
|
|
1138
1133
|
}
|
|
1139
|
-
const currentValue =
|
|
1134
|
+
const currentValue =
|
|
1135
|
+
((row as DbRow)[fieldName] as string | undefined) ??
|
|
1136
|
+
((row as DbRow)[toSnakeCase(fieldName)] as string); // @cast-boundary engine-bridge
|
|
1140
1137
|
guardTransition(
|
|
1141
1138
|
getTransitions({ entityName, fieldName, map: transitionMap }),
|
|
1142
1139
|
currentValue,
|
|
@@ -1278,7 +1275,9 @@ export function createDispatcher(
|
|
|
1278
1275
|
}
|
|
1279
1276
|
};
|
|
1280
1277
|
|
|
1281
|
-
|
|
1278
|
+
// batch() opens its own outer transaction — needs the top-level
|
|
1279
|
+
// connection's `.begin()` (TransactionSql exposes only `.savepoint()`).
|
|
1280
|
+
const db = resolveDbSource(undefined) as DbConnection | undefined;
|
|
1282
1281
|
if (!db) {
|
|
1283
1282
|
// Without a DB connection there is no transaction to open. Fall back to
|
|
1284
1283
|
// sequential execution — useful for unit tests that don't touch the DB.
|
|
@@ -1306,7 +1305,7 @@ export function createDispatcher(
|
|
|
1306
1305
|
}
|
|
1307
1306
|
|
|
1308
1307
|
try {
|
|
1309
|
-
await
|
|
1308
|
+
await transaction(db, async (tx) => {
|
|
1310
1309
|
for (let i = 0; i < commands.length; i++) {
|
|
1311
1310
|
const cmd = commands[i];
|
|
1312
1311
|
if (!cmd) continue;
|
|
@@ -1326,10 +1325,6 @@ export function createDispatcher(
|
|
|
1326
1325
|
results,
|
|
1327
1326
|
});
|
|
1328
1327
|
}
|
|
1329
|
-
// Unexpected throw — typically a DB driver error from commit/rollback.
|
|
1330
|
-
// executeWrite already traps handler + lifecycle throws into WriteResult,
|
|
1331
|
-
// so anything reaching here is infrastructure-level. Wrap as InternalError
|
|
1332
|
-
// so the contract ("non-Kumiko → InternalError") holds uniformly.
|
|
1333
1328
|
return finalize({
|
|
1334
1329
|
isSuccess: false,
|
|
1335
1330
|
error: toWriteErrorInfo(wrapToKumiko(e)),
|
|
@@ -1368,7 +1363,7 @@ export function createDispatcher(
|
|
|
1368
1363
|
// scoped as "tenant" and no tx is threaded through. Hooks that need
|
|
1369
1364
|
// cross-tenant lookups opt in explicitly via queryAs(systemUser, ...).
|
|
1370
1365
|
function buildAuthClaimsContext(user: SessionUser): AuthClaimsContext {
|
|
1371
|
-
const dbSource
|
|
1366
|
+
const dbSource = resolveDbSource(undefined);
|
|
1372
1367
|
if (!dbSource) {
|
|
1373
1368
|
throw new InternalError({
|
|
1374
1369
|
message:
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
// sql now comes from native dialect
|
|
2
2
|
import type { DbConnection } from "../db/connection";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
bigint,
|
|
5
|
+
index,
|
|
6
|
+
instant,
|
|
7
|
+
integer,
|
|
8
|
+
table as pgTable,
|
|
9
|
+
primaryKey,
|
|
10
|
+
sql,
|
|
11
|
+
text,
|
|
12
|
+
} from "../db/dialect";
|
|
4
13
|
import { tableExists } from "../db/schema-inspection";
|
|
5
14
|
import { unsafePushTables } from "../stack";
|
|
6
15
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { and, asc, eq, gt, sql } from "drizzle-orm";
|
|
2
1
|
import { requestContext } from "../api/request-context";
|
|
3
2
|
import type { DbConnection, DbTx, PgClient } from "../db/connection";
|
|
3
|
+
import {
|
|
4
|
+
advanceConsumerPastEventReturning,
|
|
5
|
+
insertConsumerIfAbsent,
|
|
6
|
+
markConsumerProcessing,
|
|
7
|
+
selectConsumerForUpdateSkipLocked,
|
|
8
|
+
updateConsumerDeliveryOutcome,
|
|
9
|
+
updateConsumerStatusReturning,
|
|
10
|
+
} from "../db/queries/event-consumer";
|
|
11
|
+
import { selectEventsHeadId, selectNextEventIdAfter } from "../db/queries/event-store";
|
|
12
|
+
import { coerceRow, extractTableInfo, selectMany } from "../db/query";
|
|
4
13
|
import type { AppContext } from "../engine/types";
|
|
5
14
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers";
|
|
6
15
|
import {
|
|
@@ -154,7 +163,30 @@ const DEFAULT_MAX_ATTEMPTS = 10;
|
|
|
154
163
|
// dispatcher's main pass logic stays under ~50 LOC. Every helper takes an
|
|
155
164
|
// explicit `tx` — none of them use the outer dispatcher's closure state.
|
|
156
165
|
|
|
157
|
-
type
|
|
166
|
+
type ConsumerStateRowShape = {
|
|
167
|
+
readonly name: string;
|
|
168
|
+
readonly instanceId: string;
|
|
169
|
+
readonly lastProcessedEventId: bigint;
|
|
170
|
+
readonly status: string;
|
|
171
|
+
readonly attempts: number;
|
|
172
|
+
readonly lastError: string | null;
|
|
173
|
+
readonly updatedAt: Temporal.Instant;
|
|
174
|
+
};
|
|
175
|
+
type ConsumerStateRow = ConsumerStateRowShape;
|
|
176
|
+
|
|
177
|
+
type StoredEventRow = {
|
|
178
|
+
readonly id: bigint;
|
|
179
|
+
readonly aggregateId: string;
|
|
180
|
+
readonly aggregateType: string;
|
|
181
|
+
readonly tenantId: string;
|
|
182
|
+
readonly version: number;
|
|
183
|
+
readonly type: string;
|
|
184
|
+
readonly eventVersion: number;
|
|
185
|
+
readonly payload: Record<string, unknown>;
|
|
186
|
+
readonly metadata: import("../event-store/event-store").EventMetadata;
|
|
187
|
+
readonly createdAt: Temporal.Instant;
|
|
188
|
+
readonly createdBy: string;
|
|
189
|
+
};
|
|
158
190
|
|
|
159
191
|
type AcquireOutcome =
|
|
160
192
|
| { readonly state: ConsumerStateRow; readonly skip: null }
|
|
@@ -180,16 +212,13 @@ async function acquireConsumerState(
|
|
|
180
212
|
name: string,
|
|
181
213
|
instanceId: string,
|
|
182
214
|
): Promise<AcquireOutcome> {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
),
|
|
191
|
-
)
|
|
192
|
-
.for("update", { skipLocked: true })) as [ConsumerStateRow | undefined]; // @cast-boundary db-row
|
|
215
|
+
const rawState = await selectConsumerForUpdateSkipLocked(tx, name, instanceId);
|
|
216
|
+
|
|
217
|
+
if (!rawState) {
|
|
218
|
+
return { state: null, skip: "not_registered" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow;
|
|
193
222
|
|
|
194
223
|
if (!state) {
|
|
195
224
|
// Either the row never existed (no pre-reg, no ensureRegistered) or
|
|
@@ -218,12 +247,7 @@ async function preRegisterConsumers(
|
|
|
218
247
|
): Promise<void> {
|
|
219
248
|
for (const consumer of consumers) {
|
|
220
249
|
const instanceId = consumerInstanceId(consumer, dispatcherInstanceId);
|
|
221
|
-
await db
|
|
222
|
-
.insert(eventConsumerStateTable)
|
|
223
|
-
.values({ name: consumer.name, instanceId, status: "idle" })
|
|
224
|
-
.onConflictDoNothing({
|
|
225
|
-
target: [eventConsumerStateTable.name, eventConsumerStateTable.instanceId],
|
|
226
|
-
});
|
|
250
|
+
await insertConsumerIfAbsent(db, consumer.name, instanceId);
|
|
227
251
|
}
|
|
228
252
|
}
|
|
229
253
|
|
|
@@ -249,28 +273,20 @@ function consumerInstanceId(
|
|
|
249
273
|
// lock already guarantees single-writer semantics; this is purely
|
|
250
274
|
// informational (and resets on commit to idle/dead via persistConsumerOutcome).
|
|
251
275
|
async function markProcessing(tx: DbTx, name: string, instanceId: string): Promise<void> {
|
|
252
|
-
await tx
|
|
253
|
-
.update(eventConsumerStateTable)
|
|
254
|
-
.set({ status: "processing", updatedAt: sql`now()` })
|
|
255
|
-
.where(
|
|
256
|
-
and(
|
|
257
|
-
eq(eventConsumerStateTable.name, name),
|
|
258
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
259
|
-
),
|
|
260
|
-
);
|
|
276
|
+
await markConsumerProcessing(tx, name, instanceId);
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
async function fetchPendingEvents(
|
|
264
280
|
tx: DbTx,
|
|
265
281
|
cursor: bigint,
|
|
266
282
|
batchSize: number,
|
|
267
|
-
): Promise<ReadonlyArray<
|
|
268
|
-
return (await
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
): Promise<ReadonlyArray<StoredEventRow>> {
|
|
284
|
+
return (await selectMany(
|
|
285
|
+
tx,
|
|
286
|
+
eventsTable,
|
|
287
|
+
{ id: { gt: cursor } },
|
|
288
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: batchSize },
|
|
289
|
+
)) as ReadonlyArray<StoredEventRow>; // @cast-boundary db-row
|
|
274
290
|
}
|
|
275
291
|
|
|
276
292
|
type DeliveryOutcome = {
|
|
@@ -282,7 +298,7 @@ type DeliveryOutcome = {
|
|
|
282
298
|
readonly failed: number;
|
|
283
299
|
};
|
|
284
300
|
|
|
285
|
-
function rowToStoredEvent(row:
|
|
301
|
+
function rowToStoredEvent(row: StoredEventRow): StoredEvent {
|
|
286
302
|
return {
|
|
287
303
|
id: String(row.id),
|
|
288
304
|
aggregateId: row.aggregateId,
|
|
@@ -305,7 +321,7 @@ function rowToStoredEvent(row: typeof eventsTable.$inferSelect): StoredEvent {
|
|
|
305
321
|
// restartConsumer / skipPoisonEvent).
|
|
306
322
|
async function deliverEvents(
|
|
307
323
|
consumer: EventConsumer,
|
|
308
|
-
events: ReadonlyArray<
|
|
324
|
+
events: ReadonlyArray<StoredEventRow>,
|
|
309
325
|
context: AppContext,
|
|
310
326
|
maxAttempts: number,
|
|
311
327
|
state: ConsumerStateRow,
|
|
@@ -376,21 +392,7 @@ async function persistConsumerOutcome(
|
|
|
376
392
|
instanceId: string,
|
|
377
393
|
outcome: DeliveryOutcome,
|
|
378
394
|
): Promise<void> {
|
|
379
|
-
await tx
|
|
380
|
-
.update(eventConsumerStateTable)
|
|
381
|
-
.set({
|
|
382
|
-
lastProcessedEventId: outcome.cursor,
|
|
383
|
-
attempts: outcome.attempts,
|
|
384
|
-
status: outcome.deadLettered ? "dead" : "idle",
|
|
385
|
-
lastError: outcome.lastError,
|
|
386
|
-
updatedAt: sql`now()`,
|
|
387
|
-
})
|
|
388
|
-
.where(
|
|
389
|
-
and(
|
|
390
|
-
eq(eventConsumerStateTable.name, name),
|
|
391
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
392
|
-
),
|
|
393
|
-
);
|
|
395
|
+
await updateConsumerDeliveryOutcome(tx, name, instanceId, outcome);
|
|
394
396
|
}
|
|
395
397
|
|
|
396
398
|
// Emit the lag gauge inside the consumer pass's tx so ops sees a snapshot
|
|
@@ -403,13 +405,7 @@ async function emitLagFromTx(
|
|
|
403
405
|
cursor: bigint,
|
|
404
406
|
meter: Meter,
|
|
405
407
|
): Promise<void> {
|
|
406
|
-
const
|
|
407
|
-
sql`SELECT COALESCE(MAX(id), 0)::bigint AS head FROM kumiko_events`,
|
|
408
|
-
);
|
|
409
|
-
// @cast-boundary db-row — raw drizzle.execute() COALESCE-aggregate row
|
|
410
|
-
const rows = Array.isArray(result) ? (result as Array<{ head?: bigint | string | null }>) : []; // @cast-boundary db-row
|
|
411
|
-
const raw = rows[0]?.head;
|
|
412
|
-
const head = typeof raw === "bigint" ? raw : BigInt(raw ?? 0);
|
|
408
|
+
const head = await selectEventsHeadId(tx);
|
|
413
409
|
const lag = head > cursor ? Number(head - cursor) : 0;
|
|
414
410
|
emitEventConsumerLag(meter, { consumer: consumerName, instanceId }, lag);
|
|
415
411
|
}
|
|
@@ -535,7 +531,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
|
|
|
535
531
|
});
|
|
536
532
|
|
|
537
533
|
try {
|
|
538
|
-
await db.
|
|
534
|
+
await db.begin(async (tx: DbTx) => {
|
|
539
535
|
const acquired = await acquireConsumerState(tx, consumer.name, instanceId);
|
|
540
536
|
// skip: another instance holds the lock, or the consumer is
|
|
541
537
|
// disabled/dead. Nothing to deliver this pass.
|
|
@@ -701,9 +697,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
|
|
|
701
697
|
// lastError=null, status="idle". For events that will
|
|
702
698
|
// never succeed (broken payload, removed feature code).
|
|
703
699
|
|
|
704
|
-
function normalizeConsumerState(
|
|
705
|
-
row: typeof eventConsumerStateTable.$inferSelect,
|
|
706
|
-
): ConsumerRecoveryState {
|
|
700
|
+
function normalizeConsumerState(row: ConsumerStateRowShape): ConsumerRecoveryState {
|
|
707
701
|
return {
|
|
708
702
|
name: row.name,
|
|
709
703
|
instanceId: row.instanceId,
|
|
@@ -735,16 +729,11 @@ async function requireConsumerRow(
|
|
|
735
729
|
db: DbConnection,
|
|
736
730
|
name: string,
|
|
737
731
|
instanceId: string,
|
|
738
|
-
): Promise<
|
|
739
|
-
const [row] = await db
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
and(
|
|
744
|
-
eq(eventConsumerStateTable.name, name),
|
|
745
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
746
|
-
),
|
|
747
|
-
);
|
|
732
|
+
): Promise<ConsumerStateRowShape> {
|
|
733
|
+
const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
|
|
734
|
+
name,
|
|
735
|
+
instanceId,
|
|
736
|
+
});
|
|
748
737
|
if (!row) {
|
|
749
738
|
throw new Error(
|
|
750
739
|
`Consumer "${name}" (instance_id="${instanceId}") has no state row — it hasn't run yet, the name is misspelled, or the instance is misspelled. ` +
|
|
@@ -765,16 +754,9 @@ export async function restartConsumer(
|
|
|
765
754
|
`Consumer "${name}" (instance_id="${instanceId}") is not dead (status="${before.status}"). Restart only applies to dead consumers; use "enable" for a disabled one.`,
|
|
766
755
|
);
|
|
767
756
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
.where(
|
|
772
|
-
and(
|
|
773
|
-
eq(eventConsumerStateTable.name, name),
|
|
774
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
775
|
-
),
|
|
776
|
-
)
|
|
777
|
-
.returning();
|
|
757
|
+
const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
|
|
758
|
+
const updated =
|
|
759
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
778
760
|
if (!updated) {
|
|
779
761
|
throw new Error(
|
|
780
762
|
`Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
|
|
@@ -789,16 +771,9 @@ export async function disableConsumer(
|
|
|
789
771
|
instanceId: string = SHARED_INSTANCE_SENTINEL,
|
|
790
772
|
): Promise<ConsumerRecoveryState> {
|
|
791
773
|
await requireConsumerRow(db, name, instanceId);
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
.where(
|
|
796
|
-
and(
|
|
797
|
-
eq(eventConsumerStateTable.name, name),
|
|
798
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
799
|
-
),
|
|
800
|
-
)
|
|
801
|
-
.returning();
|
|
774
|
+
const raw = await updateConsumerStatusReturning(db, name, instanceId, "disabled");
|
|
775
|
+
const updated =
|
|
776
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
802
777
|
if (!updated) {
|
|
803
778
|
throw new Error(
|
|
804
779
|
`Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
|
|
@@ -818,16 +793,9 @@ export async function enableConsumer(
|
|
|
818
793
|
`Consumer "${name}" (instance_id="${instanceId}") is not disabled (status="${before.status}"). Enable only flips disabled → idle; use "restart" for a dead consumer.`,
|
|
819
794
|
);
|
|
820
795
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
.where(
|
|
825
|
-
and(
|
|
826
|
-
eq(eventConsumerStateTable.name, name),
|
|
827
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
828
|
-
),
|
|
829
|
-
)
|
|
830
|
-
.returning();
|
|
796
|
+
const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
|
|
797
|
+
const updated =
|
|
798
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
831
799
|
if (!updated) {
|
|
832
800
|
throw new Error(
|
|
833
801
|
`Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
|
|
@@ -846,48 +814,25 @@ export async function skipPoisonEvent(
|
|
|
846
814
|
instanceId: string = SHARED_INSTANCE_SENTINEL,
|
|
847
815
|
): Promise<ConsumerRecoveryState & { readonly skippedEventId: bigint | null }> {
|
|
848
816
|
const before = await requireConsumerRow(db, name, instanceId);
|
|
849
|
-
return db.
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (!poison) {
|
|
857
|
-
const [unchanged] = await tx
|
|
858
|
-
.select()
|
|
859
|
-
.from(eventConsumerStateTable)
|
|
860
|
-
.where(
|
|
861
|
-
and(
|
|
862
|
-
eq(eventConsumerStateTable.name, name),
|
|
863
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
864
|
-
),
|
|
865
|
-
);
|
|
817
|
+
return db.begin(async (tx: DbTx) => {
|
|
818
|
+
const poisonId = await selectNextEventIdAfter(tx, before.lastProcessedEventId);
|
|
819
|
+
if (poisonId === null) {
|
|
820
|
+
const [unchanged] = await selectMany<ConsumerStateRow>(tx, eventConsumerStateTable, {
|
|
821
|
+
name,
|
|
822
|
+
instanceId,
|
|
823
|
+
});
|
|
866
824
|
if (!unchanged)
|
|
867
825
|
throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished — retry.`);
|
|
868
826
|
return { ...normalizeConsumerState(unchanged), skippedEventId: null };
|
|
869
827
|
}
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
lastProcessedEventId: poison.id,
|
|
874
|
-
status: "idle",
|
|
875
|
-
attempts: 0,
|
|
876
|
-
lastError: null,
|
|
877
|
-
updatedAt: sql`now()`,
|
|
878
|
-
})
|
|
879
|
-
.where(
|
|
880
|
-
and(
|
|
881
|
-
eq(eventConsumerStateTable.name, name),
|
|
882
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
883
|
-
),
|
|
884
|
-
)
|
|
885
|
-
.returning();
|
|
828
|
+
const raw = await advanceConsumerPastEventReturning(tx, name, instanceId, poisonId);
|
|
829
|
+
const updated =
|
|
830
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
886
831
|
if (!updated)
|
|
887
832
|
throw new Error(
|
|
888
833
|
`Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`,
|
|
889
834
|
);
|
|
890
|
-
return { ...normalizeConsumerState(updated), skippedEventId:
|
|
835
|
+
return { ...normalizeConsumerState(updated), skippedEventId: poisonId };
|
|
891
836
|
});
|
|
892
837
|
}
|
|
893
838
|
|
|
@@ -905,15 +850,10 @@ export async function getConsumerState(
|
|
|
905
850
|
readonly lastError: string | null;
|
|
906
851
|
readonly updatedAt: Temporal.Instant;
|
|
907
852
|
} | null> {
|
|
908
|
-
const [row] = await db
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
and(
|
|
913
|
-
eq(eventConsumerStateTable.name, name),
|
|
914
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
915
|
-
),
|
|
916
|
-
);
|
|
853
|
+
const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
|
|
854
|
+
name,
|
|
855
|
+
instanceId,
|
|
856
|
+
});
|
|
917
857
|
if (!row) return null;
|
|
918
858
|
return {
|
|
919
859
|
name: row.name,
|
|
@@ -947,7 +887,7 @@ export async function listConsumersWithState(
|
|
|
947
887
|
readonly lastError: string | null;
|
|
948
888
|
}>
|
|
949
889
|
> {
|
|
950
|
-
const stateRows = await db
|
|
890
|
+
const stateRows = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable);
|
|
951
891
|
const registered = new Set(registeredNames);
|
|
952
892
|
|
|
953
893
|
// Materialize one output row per (name, instance_id). Registered names
|