@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
|
@@ -10,25 +10,27 @@
|
|
|
10
10
|
// Feature in den Tests zu schwer wäre — wir testen nur den Read-Helper-
|
|
11
11
|
// Layer, nicht die volle Event-Store-Pipeline.
|
|
12
12
|
|
|
13
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
13
14
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { tmpdir } from "node:os";
|
|
15
16
|
import { join } from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
|
+
import { asRawClient, selectMany } from "../../db/query";
|
|
19
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
19
20
|
import { createSeedMigrationContext } from "../context";
|
|
20
21
|
import { createEsOperationsTable, esOperationsTable } from "../operations-schema";
|
|
21
22
|
import { runPendingSeedMigrations } from "../runner";
|
|
22
23
|
|
|
23
|
-
let testDb:
|
|
24
|
+
let testDb: BunTestDb;
|
|
24
25
|
|
|
25
26
|
beforeAll(async () => {
|
|
27
|
+
await ensureTemporalPolyfill();
|
|
26
28
|
testDb = await createTestDb();
|
|
27
29
|
await createEsOperationsTable(testDb.db);
|
|
28
30
|
|
|
29
31
|
// Minimal-Schema-Stubs für die 3 Read-Tabellen die context.ts liest.
|
|
30
32
|
// Spalten matchen production (siehe Sysadmin-Stream-Tenant-Bug Memory).
|
|
31
|
-
await testDb.db.
|
|
33
|
+
await asRawClient(testDb.db).unsafe(`
|
|
32
34
|
CREATE TABLE IF NOT EXISTS read_users (
|
|
33
35
|
id uuid PRIMARY KEY,
|
|
34
36
|
email text NOT NULL,
|
|
@@ -54,7 +56,7 @@ afterAll(async () => {
|
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
beforeEach(async () => {
|
|
57
|
-
await testDb.db.
|
|
59
|
+
await asRawClient(testDb.db).unsafe(`
|
|
58
60
|
TRUNCATE kumiko_es_operations, kumiko_events, read_users, read_tenant_memberships, read_tenants
|
|
59
61
|
RESTART IDENTITY CASCADE
|
|
60
62
|
`);
|
|
@@ -70,26 +72,32 @@ async function insertMembershipWithEvent(args: {
|
|
|
70
72
|
readonly streamTenantId: string;
|
|
71
73
|
readonly roles: string;
|
|
72
74
|
}): Promise<void> {
|
|
73
|
-
await testDb.db.
|
|
75
|
+
await asRawClient(testDb.db).unsafe(
|
|
76
|
+
`
|
|
74
77
|
INSERT INTO read_tenant_memberships (id, user_id, tenant_id, roles)
|
|
75
|
-
VALUES ($
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
VALUES ($1::uuid, $2, $3::uuid, $4)
|
|
79
|
+
`,
|
|
80
|
+
[args.id, args.userId, args.payloadTenantId, args.roles],
|
|
81
|
+
);
|
|
82
|
+
await asRawClient(testDb.db).unsafe(
|
|
83
|
+
`
|
|
78
84
|
INSERT INTO kumiko_events
|
|
79
85
|
(aggregate_id, aggregate_type, tenant_id, version, type, payload, metadata, created_by)
|
|
80
86
|
VALUES
|
|
81
|
-
($
|
|
87
|
+
($1::uuid, 'tenant-membership', $2::uuid, 1,
|
|
82
88
|
'tenant-membership.created', '{}'::jsonb, '{"userId":"system"}'::jsonb, 'system')
|
|
83
|
-
|
|
89
|
+
`,
|
|
90
|
+
[args.id, args.streamTenantId],
|
|
91
|
+
);
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
function makeMockDispatcher() {
|
|
87
95
|
return {
|
|
88
|
-
write:
|
|
89
|
-
query:
|
|
90
|
-
command:
|
|
91
|
-
batch:
|
|
92
|
-
resolveAuthClaims:
|
|
96
|
+
write: mock(async () => ({ isSuccess: true as const, data: {} })),
|
|
97
|
+
query: mock(),
|
|
98
|
+
command: mock(),
|
|
99
|
+
batch: mock(),
|
|
100
|
+
resolveAuthClaims: mock(),
|
|
93
101
|
};
|
|
94
102
|
}
|
|
95
103
|
|
|
@@ -105,10 +113,13 @@ describe("SeedMigrationContext.findUserByEmail (integration)", () => {
|
|
|
105
113
|
test("liest existing user-row korrekt + maps tenant_id → tenantId", async () => {
|
|
106
114
|
const userId = "01900000-0000-7000-8000-000000000001";
|
|
107
115
|
const tenantId = "00000000-0000-4000-8000-000000000099";
|
|
108
|
-
await testDb.db.
|
|
116
|
+
await asRawClient(testDb.db).unsafe(
|
|
117
|
+
`
|
|
109
118
|
INSERT INTO read_users (id, email, tenant_id)
|
|
110
|
-
VALUES ($
|
|
111
|
-
|
|
119
|
+
VALUES ($1::uuid, 'admin@example.com', $2::uuid)
|
|
120
|
+
`,
|
|
121
|
+
[userId, tenantId],
|
|
122
|
+
);
|
|
112
123
|
|
|
113
124
|
const ctx = createSeedMigrationContext({
|
|
114
125
|
dispatcher: makeMockDispatcher() as never,
|
|
@@ -231,11 +242,14 @@ describe("SeedMigrationContext.findMembershipsOfUser (integration)", () => {
|
|
|
231
242
|
// statt einer mit fehlendem stream-tenant zu arbeiten und schwer
|
|
232
243
|
// diagnostizierbare version_conflict-Errors zu produzieren.
|
|
233
244
|
const userId = "01900000-0000-7000-8000-000000000003";
|
|
234
|
-
await testDb.db.
|
|
245
|
+
await asRawClient(testDb.db).unsafe(
|
|
246
|
+
`
|
|
235
247
|
INSERT INTO read_tenant_memberships (id, user_id, tenant_id, roles) VALUES
|
|
236
|
-
('00000000-0000-4000-8000-0000000000d1'::uuid, $
|
|
248
|
+
('00000000-0000-4000-8000-0000000000d1'::uuid, $1,
|
|
237
249
|
'00000000-0000-4000-8000-000000000005'::uuid, '["Admin"]')
|
|
238
|
-
|
|
250
|
+
`,
|
|
251
|
+
[userId],
|
|
252
|
+
);
|
|
239
253
|
const ctx = createSeedMigrationContext({
|
|
240
254
|
dispatcher: makeMockDispatcher() as never,
|
|
241
255
|
dbRunner: testDb.db,
|
|
@@ -247,7 +261,7 @@ describe("SeedMigrationContext.findMembershipsOfUser (integration)", () => {
|
|
|
247
261
|
|
|
248
262
|
describe("SeedMigrationContext.findTenants (integration)", () => {
|
|
249
263
|
test("returnt alle Tenants sortiert nach inserted_at", async () => {
|
|
250
|
-
await testDb.db.
|
|
264
|
+
await asRawClient(testDb.db).unsafe(`
|
|
251
265
|
INSERT INTO read_tenants (id, name, tenant_key, inserted_at) VALUES
|
|
252
266
|
('00000000-0000-4000-8000-000000000002'::uuid, 'Beta', 'beta', '2026-01-02'),
|
|
253
267
|
('00000000-0000-4000-8000-000000000001'::uuid, 'Alpha', 'alpha', '2026-01-01')
|
|
@@ -296,7 +310,7 @@ describe("runPendingSeedMigrations: skippable + env-flag (integration)", () => {
|
|
|
296
310
|
|
|
297
311
|
// Kritisch: KEIN Marker — beim nächsten Boot ohne env-flag würde
|
|
298
312
|
// der Seed dann tatsächlich laufen.
|
|
299
|
-
const markers = await testDb.db
|
|
313
|
+
const markers = await selectMany(testDb.db, esOperationsTable);
|
|
300
314
|
expect(markers).toHaveLength(0);
|
|
301
315
|
} finally {
|
|
302
316
|
delete process.env[envKey];
|
|
@@ -329,7 +343,7 @@ describe("runPendingSeedMigrations: skippable + env-flag (integration)", () => {
|
|
|
329
343
|
expect(r.appliedIds).toEqual(["2026-05-20-skippable-but-no-flag"]);
|
|
330
344
|
expect(r.skippedIds).toEqual([]);
|
|
331
345
|
|
|
332
|
-
const markers = await testDb.db
|
|
346
|
+
const markers = await selectMany(testDb.db, esOperationsTable);
|
|
333
347
|
expect(markers).toHaveLength(1);
|
|
334
348
|
} finally {
|
|
335
349
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -341,7 +355,7 @@ describe("runPendingSeedMigrations: skippable + env-flag (integration)", () => {
|
|
|
341
355
|
|
|
342
356
|
describe("SeedMigrationContext.db (escape-hatch, integration)", () => {
|
|
343
357
|
test("ctx.db kann für eigene Lookups genutzt werden (read-only)", async () => {
|
|
344
|
-
await testDb.db.
|
|
358
|
+
await asRawClient(testDb.db).unsafe(`
|
|
345
359
|
INSERT INTO read_tenants (id, name, tenant_key) VALUES
|
|
346
360
|
('00000000-0000-4000-8000-000000000007'::uuid, 'Lucky', 'lucky')
|
|
347
361
|
`);
|
|
@@ -349,8 +363,8 @@ describe("SeedMigrationContext.db (escape-hatch, integration)", () => {
|
|
|
349
363
|
dispatcher: makeMockDispatcher() as never,
|
|
350
364
|
dbRunner: testDb.db,
|
|
351
365
|
});
|
|
352
|
-
const rows = (await ctx.db.
|
|
353
|
-
|
|
366
|
+
const rows = (await asRawClient(ctx.db).unsafe(
|
|
367
|
+
`SELECT name FROM read_tenants WHERE tenant_key = 'lucky'`,
|
|
354
368
|
)) as unknown as readonly { name: string }[];
|
|
355
369
|
expect(rows[0]?.name).toBe("Lucky");
|
|
356
370
|
});
|
|
@@ -8,19 +8,21 @@
|
|
|
8
8
|
// Heavy lifting (mock-dispatcher, in-memory-applied-set) liegt in
|
|
9
9
|
// runner.test.ts. Hier nur DB-Round-Trip-Wahrheit.
|
|
10
10
|
|
|
11
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
12
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
14
|
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
16
|
+
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
17
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
17
18
|
import { createSeedMigrationContext } from "../context";
|
|
18
19
|
import { createEsOperationsTable, esOperationsTable } from "../operations-schema";
|
|
19
20
|
import { runPendingSeedMigrations } from "../runner";
|
|
20
21
|
|
|
21
|
-
let testDb:
|
|
22
|
+
let testDb: BunTestDb;
|
|
22
23
|
|
|
23
24
|
beforeAll(async () => {
|
|
25
|
+
await ensureTemporalPolyfill();
|
|
24
26
|
testDb = await createTestDb();
|
|
25
27
|
await createEsOperationsTable(testDb.db);
|
|
26
28
|
});
|
|
@@ -30,7 +32,7 @@ afterAll(async () => {
|
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
beforeEach(async () => {
|
|
33
|
-
await testDb.db.
|
|
35
|
+
await asRawClient(testDb.db).unsafe(`TRUNCATE kumiko_es_operations RESTART IDENTITY`);
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
function makeTempSeedsDir(files: readonly { name: string; content: string }[]): string {
|
|
@@ -42,14 +44,14 @@ function makeTempSeedsDir(files: readonly { name: string; content: string }[]):
|
|
|
42
44
|
function makeMockDispatcher() {
|
|
43
45
|
const calls: Array<{ qn: string; payload: unknown }> = [];
|
|
44
46
|
return {
|
|
45
|
-
write:
|
|
47
|
+
write: mock(async (qn: string, payload: unknown) => {
|
|
46
48
|
calls.push({ qn, payload });
|
|
47
49
|
return { isSuccess: true as const, data: {} };
|
|
48
50
|
}),
|
|
49
|
-
query:
|
|
50
|
-
command:
|
|
51
|
-
batch:
|
|
52
|
-
resolveAuthClaims:
|
|
51
|
+
query: mock(),
|
|
52
|
+
command: mock(),
|
|
53
|
+
batch: mock(),
|
|
54
|
+
resolveAuthClaims: mock(),
|
|
53
55
|
calls,
|
|
54
56
|
};
|
|
55
57
|
}
|
|
@@ -82,7 +84,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
82
84
|
expect(r1.appliedIds).toEqual(["2026-05-20-noop"]);
|
|
83
85
|
|
|
84
86
|
// Marker landed
|
|
85
|
-
const markers1 = await testDb.db
|
|
87
|
+
const markers1 = await selectMany(testDb.db, esOperationsTable);
|
|
86
88
|
expect(markers1).toHaveLength(1);
|
|
87
89
|
expect(markers1[0]?.id).toBe("2026-05-20-noop");
|
|
88
90
|
expect(markers1[0]?.operationType).toBe("seed-migration");
|
|
@@ -98,7 +100,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
98
100
|
logger: () => {},
|
|
99
101
|
});
|
|
100
102
|
expect(r2.appliedIds).toEqual([]);
|
|
101
|
-
const markers2 = await testDb.db
|
|
103
|
+
const markers2 = await selectMany(testDb.db, esOperationsTable);
|
|
102
104
|
expect(markers2).toHaveLength(1);
|
|
103
105
|
} finally {
|
|
104
106
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -130,7 +132,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
130
132
|
}),
|
|
131
133
|
).rejects.toThrow(/boom/);
|
|
132
134
|
|
|
133
|
-
const markers = await testDb.db
|
|
135
|
+
const markers = await selectMany(testDb.db, esOperationsTable);
|
|
134
136
|
expect(markers).toHaveLength(0);
|
|
135
137
|
} finally {
|
|
136
138
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -192,14 +194,14 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
192
194
|
]);
|
|
193
195
|
try {
|
|
194
196
|
const dispatcher = {
|
|
195
|
-
write:
|
|
197
|
+
write: mock(async () => ({
|
|
196
198
|
isSuccess: false as const,
|
|
197
199
|
error: { code: "version_conflict", message: "stream changed" },
|
|
198
200
|
})),
|
|
199
|
-
query:
|
|
200
|
-
command:
|
|
201
|
-
batch:
|
|
202
|
-
resolveAuthClaims:
|
|
201
|
+
query: mock(),
|
|
202
|
+
command: mock(),
|
|
203
|
+
batch: mock(),
|
|
204
|
+
resolveAuthClaims: mock(),
|
|
203
205
|
};
|
|
204
206
|
|
|
205
207
|
await expect(
|
|
@@ -214,7 +216,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
214
216
|
).rejects.toThrow(/version_conflict/);
|
|
215
217
|
|
|
216
218
|
// Kein Marker — bei nächstem Boot würde der Seed retried
|
|
217
|
-
const markers = await testDb.db
|
|
219
|
+
const markers = await selectMany(testDb.db, esOperationsTable);
|
|
218
220
|
expect(markers).toHaveLength(0);
|
|
219
221
|
} finally {
|
|
220
222
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -264,7 +266,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
264
266
|
expect(dispatcher.write).toHaveBeenCalledTimes(1);
|
|
265
267
|
// Marker NICHT gesetzt — retry beim nächsten Boot wird die Migration
|
|
266
268
|
// nochmal ausführen. Wenn der Write nicht idempotent ist → Duplikat.
|
|
267
|
-
const markers = await testDb.db
|
|
269
|
+
const markers = await selectMany(testDb.db, esOperationsTable);
|
|
268
270
|
expect(markers).toHaveLength(0);
|
|
269
271
|
} finally {
|
|
270
272
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -293,7 +295,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
293
295
|
]);
|
|
294
296
|
try {
|
|
295
297
|
// Pre-seed marker als wäre ein parallel-Pod schon durch
|
|
296
|
-
await testDb.db
|
|
298
|
+
await insertOne(testDb.db, esOperationsTable, {
|
|
297
299
|
id: "2026-05-20-race",
|
|
298
300
|
operationType: "seed-migration",
|
|
299
301
|
durationMs: 42,
|
|
@@ -318,7 +320,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
318
320
|
});
|
|
319
321
|
|
|
320
322
|
expect(dispatcher.write).not.toHaveBeenCalled();
|
|
321
|
-
const markers = await testDb.db
|
|
323
|
+
const markers = await selectMany(testDb.db, esOperationsTable);
|
|
322
324
|
expect(markers).toHaveLength(1); // nur der pre-seeded
|
|
323
325
|
} finally {
|
|
324
326
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -354,7 +356,7 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
354
356
|
).rejects.toThrow(/stop here/);
|
|
355
357
|
|
|
356
358
|
// Nur first hat marker — fails warf, never wurde nie attempted
|
|
357
|
-
const markers = await testDb.db
|
|
359
|
+
const markers = await selectMany(testDb.db, esOperationsTable);
|
|
358
360
|
expect(markers.map((m) => m.id)).toEqual(["2026-05-19-first"]);
|
|
359
361
|
} finally {
|
|
360
362
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// dispatcher-call) testen wir gegen Postgres in der integration-test.
|
|
3
3
|
// Hier nur die pure-logic-Pfade die kein echtes DB brauchen.
|
|
4
4
|
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
5
6
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { tmpdir } from "node:os";
|
|
7
8
|
import { join } from "node:path";
|
|
8
|
-
import { describe, expect, test } from "vitest";
|
|
9
9
|
import { runPendingSeedMigrations } from "../runner";
|
|
10
10
|
|
|
11
11
|
function makeTempSeedsDir(files: readonly { name: string; content: string }[]): string {
|
|
@@ -14,31 +14,41 @@ function makeTempSeedsDir(files: readonly { name: string; content: string }[]):
|
|
|
14
14
|
return dir;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// Minimal DB-Stub — Runner
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// Minimal DB-Stub — Runner uses bun-db helpers (selectMany, insertOne) +
|
|
18
|
+
// asRawClient(tx).unsafe(...) for the advisory lock + re-check + insert.
|
|
19
|
+
// Returns parsed rows based on the SQL text shape:
|
|
20
|
+
// SELECT * FROM "kumiko_es_operations" ... → applied-set rows
|
|
21
|
+
// SELECT pg_advisory_xact_lock(...) → ignored
|
|
22
|
+
// SELECT 1 FROM "kumiko_es_operations" ... → empty (= not applied)
|
|
23
|
+
// INSERT INTO "kumiko_es_operations" ... → record + add to applied
|
|
20
24
|
function makeStubDb(initialApplied: readonly string[] = []) {
|
|
21
25
|
const inserts: Array<Record<string, unknown>> = [];
|
|
22
26
|
const applied = new Set(initialApplied);
|
|
27
|
+
const unsafe = async (
|
|
28
|
+
sqlText: string,
|
|
29
|
+
params?: readonly unknown[],
|
|
30
|
+
): Promise<readonly unknown[]> => {
|
|
31
|
+
if (/SELECT \* FROM "kumiko_es_operations"/.test(sqlText)) {
|
|
32
|
+
return Array.from(applied).map((id) => ({
|
|
33
|
+
id,
|
|
34
|
+
operationType: "seed-migration",
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
if (/INSERT INTO "kumiko_es_operations"/.test(sqlText)) {
|
|
38
|
+
const id = String(params?.[0]);
|
|
39
|
+
inserts.push({ id });
|
|
40
|
+
applied.add(id);
|
|
41
|
+
return [{ id }];
|
|
42
|
+
}
|
|
43
|
+
// pg_advisory_xact_lock + re-check both yield empty → "not applied, run".
|
|
44
|
+
return [];
|
|
45
|
+
};
|
|
23
46
|
const db = {
|
|
47
|
+
unsafe,
|
|
48
|
+
begin: async (cb: (tx: unknown) => Promise<unknown>) => cb(db),
|
|
24
49
|
transaction: async (cb: (tx: unknown) => Promise<void>) => {
|
|
25
50
|
await cb(db);
|
|
26
51
|
},
|
|
27
|
-
select: () => ({
|
|
28
|
-
from: () => ({
|
|
29
|
-
where: async () => Array.from(applied).map((id) => ({ id })),
|
|
30
|
-
}),
|
|
31
|
-
}),
|
|
32
|
-
insert: () => ({
|
|
33
|
-
values: async (row: Record<string, unknown>) => {
|
|
34
|
-
inserts.push(row);
|
|
35
|
-
if (typeof row["id"] === "string") applied.add(row["id"]);
|
|
36
|
-
},
|
|
37
|
-
}),
|
|
38
|
-
// execute: für pg_advisory_xact_lock + re-check. Leere Liste = "nicht
|
|
39
|
-
// applied im Inner-Lock-Scope, weiter mit Run". applied-set check via
|
|
40
|
-
// select() oben wird sowieso schon angewendet.
|
|
41
|
-
execute: async (_q: unknown) => [],
|
|
42
52
|
};
|
|
43
53
|
return { db, inserts, applied };
|
|
44
54
|
}
|
package/src/es-ops/context.ts
CHANGED
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
// config-seed.ts:40). Events haben createdBy = SYSTEM_TENANT_ID-User
|
|
8
8
|
// → audit-fähig.
|
|
9
9
|
|
|
10
|
-
import { sql } from "drizzle-orm";
|
|
11
10
|
import type { DbRunner } from "../db";
|
|
11
|
+
import {
|
|
12
|
+
selectAllTenants,
|
|
13
|
+
selectMembershipsOfUser,
|
|
14
|
+
selectUserByEmail,
|
|
15
|
+
} from "../db/queries/seed-context";
|
|
12
16
|
import { createSystemUser, SYSTEM_TENANT_ID } from "../engine";
|
|
13
17
|
import type { Dispatcher } from "../pipeline/dispatcher";
|
|
18
|
+
import { parseStringArrayJson } from "../utils/parse-string-array-json";
|
|
14
19
|
import type { SeedMembershipRow, SeedMigrationContext, SeedTenantRow } from "./types";
|
|
15
20
|
|
|
16
21
|
export type CreateSeedMigrationContextArgs = {
|
|
@@ -59,63 +64,25 @@ export function createSeedMigrationContext(
|
|
|
59
64
|
},
|
|
60
65
|
|
|
61
66
|
findUserByEmail: async (email) => {
|
|
62
|
-
|
|
63
|
-
// UserQueries.findForAuth aber ohne Dispatcher-Roundtrip; Seeds
|
|
64
|
-
// greifen oft 1-N Lookups → direkt schneller).
|
|
65
|
-
// @cast-boundary db-row — drizzle execute(sql) returns row-array
|
|
66
|
-
// direkt (kein { rows }-Wrapper); column-types vom SQL-Cast oben
|
|
67
|
-
const rows = (await args.dbRunner.execute(
|
|
68
|
-
sql`SELECT id::text AS id, email, tenant_id::text AS tenant_id
|
|
69
|
-
FROM read_users
|
|
70
|
-
WHERE email = ${email}
|
|
71
|
-
LIMIT 1`,
|
|
72
|
-
)) as unknown as readonly { id: string; email: string; tenant_id: string }[];
|
|
73
|
-
const row = rows[0];
|
|
67
|
+
const row = await selectUserByEmail(args.dbRunner, email);
|
|
74
68
|
if (!row) return null;
|
|
75
|
-
return { id: row.id, email: row.email, tenantId: row.
|
|
69
|
+
return { id: row.id, email: row.email, tenantId: row.tenantId };
|
|
76
70
|
},
|
|
77
71
|
|
|
78
72
|
findMembershipsOfUser: async (userId) => {
|
|
79
|
-
|
|
80
|
-
// der v1-Row) neben dem payload-tenant (memberships.tenant_id) zu
|
|
81
|
-
// liefern. Die beiden divergieren wenn das Aggregate von einem
|
|
82
|
-
// Executor mit fremder tenantId angelegt wurde (seedTenantMembership
|
|
83
|
-
// by=systemAdmin) — typischer publicstatus-Driver-Use-Case.
|
|
84
|
-
// INNER (nicht LEFT): kein v1-Event bei vorhandener Read-Row wäre
|
|
85
|
-
// Data-Drift, kein legitimer Zustand für Seed-Migrations.
|
|
86
|
-
// @cast-boundary db-row — roles ist JSON-string in der text-Spalte
|
|
87
|
-
// (Memory: tenant-membership.created payload "[\"User\"]"), wird unten geparst
|
|
88
|
-
const rows = (await args.dbRunner.execute(
|
|
89
|
-
sql`SELECT m.user_id::text AS user_id,
|
|
90
|
-
m.tenant_id::text AS tenant_id,
|
|
91
|
-
e.tenant_id::text AS stream_tenant_id,
|
|
92
|
-
m.roles
|
|
93
|
-
FROM read_tenant_memberships m
|
|
94
|
-
JOIN kumiko_events e ON e.aggregate_id = m.id AND e.version = 1
|
|
95
|
-
WHERE m.user_id = ${userId}`,
|
|
96
|
-
)) as unknown as readonly {
|
|
97
|
-
user_id: string;
|
|
98
|
-
tenant_id: string;
|
|
99
|
-
stream_tenant_id: string;
|
|
100
|
-
roles: string;
|
|
101
|
-
}[];
|
|
73
|
+
const rows = await selectMembershipsOfUser(args.dbRunner, userId);
|
|
102
74
|
return rows.map(
|
|
103
75
|
(r): SeedMembershipRow => ({
|
|
104
76
|
userId: r.user_id,
|
|
105
77
|
tenantId: r.tenant_id,
|
|
106
78
|
streamTenantId: r.stream_tenant_id,
|
|
107
|
-
roles:
|
|
79
|
+
roles: parseStringArrayJson(r.roles),
|
|
108
80
|
}),
|
|
109
81
|
);
|
|
110
82
|
},
|
|
111
83
|
|
|
112
84
|
findTenants: async () => {
|
|
113
|
-
|
|
114
|
-
const rows = (await args.dbRunner.execute(
|
|
115
|
-
sql`SELECT id::text AS id, name, tenant_key
|
|
116
|
-
FROM read_tenants
|
|
117
|
-
ORDER BY inserted_at`,
|
|
118
|
-
)) as unknown as readonly { id: string; name: string; tenant_key: string }[];
|
|
85
|
+
const rows = await selectAllTenants(args.dbRunner);
|
|
119
86
|
return rows.map((r): SeedTenantRow => ({ id: r.id, name: r.name, tenantKey: r.tenant_key }));
|
|
120
87
|
},
|
|
121
88
|
|
|
@@ -123,17 +90,5 @@ export function createSeedMigrationContext(
|
|
|
123
90
|
};
|
|
124
91
|
}
|
|
125
92
|
|
|
126
|
-
function safeParseRolesJson(raw: string): readonly string[] {
|
|
127
|
-
try {
|
|
128
|
-
const parsed: unknown = JSON.parse(raw);
|
|
129
|
-
if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
|
|
130
|
-
return parsed;
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
// Fallthrough — return empty rather than throwing in a seed context.
|
|
134
|
-
}
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
93
|
// Re-export für Caller-Convenience.
|
|
139
94
|
export type { SeedMigrationContext } from "./types";
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// bunx kumiko ops seed:status → operation_type = "seed-migration"
|
|
11
11
|
// bunx kumiko ops projection:status → operation_type = "projection-rebuild"
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// sql now comes from native dialect
|
|
14
14
|
import { type DbConnection, tableExists } from "../db";
|
|
15
|
-
import { index, integer, table as pgTable, text, timestamp } from "../db/dialect";
|
|
15
|
+
import { index, integer, table as pgTable, sql, text, timestamp } from "../db/dialect";
|
|
16
16
|
import { unsafePushTables } from "../stack";
|
|
17
17
|
|
|
18
18
|
export type EsOperationType = "seed-migration";
|
package/src/es-ops/runner.ts
CHANGED
|
@@ -22,20 +22,13 @@
|
|
|
22
22
|
|
|
23
23
|
import { readdir, readFile } from "node:fs/promises";
|
|
24
24
|
import path from "node:path";
|
|
25
|
-
import { eq, sql } from "drizzle-orm";
|
|
26
25
|
import type { DbConnection, DbRunner } from "../db";
|
|
26
|
+
import { acquireEsOpsAdvisoryLock, esOperationExists } from "../db/queries/es-ops";
|
|
27
|
+
import { insertOne, selectMany } from "../db/query";
|
|
27
28
|
import type { Registry } from "../engine";
|
|
28
29
|
import { esOperationsTable } from "./operations-schema";
|
|
29
30
|
import type { EsOperationAppliedBy, SeedMigration, SeedMigrationContext } from "./types";
|
|
30
31
|
|
|
31
|
-
// Stabiler 32-bit-Integer-Lock-Key für pg_advisory_xact_lock. Multi-Replica-
|
|
32
|
-
// Boots gegen den selben Stack greifen denselben Lock — sequentialisiert
|
|
33
|
-
// die Migration ohne dass jedes Pod alle pending Files parallel anwendet.
|
|
34
|
-
// Ohne Lock: Pod A + Pod B sehen beide dieselbe pending-Liste → beide
|
|
35
|
-
// laufen migration.run() → events DOUBLED, marker-unique-constraint
|
|
36
|
-
// catched zu spät (nur den Marker, nicht die schon-committed Events).
|
|
37
|
-
const ES_OPS_LOCK_KEY = 0x65_73_6f_70; // 'esop' als hex
|
|
38
|
-
|
|
39
32
|
export type RunPendingSeedMigrationsArgs = {
|
|
40
33
|
readonly db: DbConnection;
|
|
41
34
|
/** Absoluter Pfad zum seeds-Directory (typically <appRoot>/seeds). */
|
|
@@ -130,30 +123,24 @@ export async function runPendingSeedMigrations(
|
|
|
130
123
|
|
|
131
124
|
const start = Date.now();
|
|
132
125
|
try {
|
|
133
|
-
await args.db.
|
|
126
|
+
await args.db.begin(async (tx: DbRunner) => {
|
|
134
127
|
// Advisory-Lock: sequentialisiert Multi-Replica-Boots. Zweiter
|
|
135
128
|
// Pod blockt bis erster fertig ist, dann re-checked sein
|
|
136
129
|
// applied-set (außerhalb dieser Funktion in nächster Iteration)
|
|
137
130
|
// und findet den Marker → skip. Lock wird beim Tx-Commit
|
|
138
131
|
// automatisch released (xact-scope).
|
|
139
|
-
await tx
|
|
132
|
+
await acquireEsOpsAdvisoryLock(tx);
|
|
140
133
|
|
|
141
|
-
|
|
142
|
-
// wo Pod-A schon committed hat während Pod-B vor dem Lock
|
|
143
|
-
// war. Sonst würde Pod-B die Migration nochmal ausführen.
|
|
144
|
-
const reCheck = (await tx.execute(
|
|
145
|
-
sql`SELECT 1 FROM kumiko_es_operations WHERE id = ${entry.id} LIMIT 1`,
|
|
146
|
-
)) as unknown as readonly unknown[];
|
|
147
|
-
if (reCheck.length > 0) {
|
|
134
|
+
if (await esOperationExists(tx, entry.id)) {
|
|
148
135
|
log(`${LOG_PREFIX} race-skip "${entry.id}" — applied by parallel boot`);
|
|
149
|
-
// skip:
|
|
150
|
-
//
|
|
136
|
+
// skip: parallel boot won the advisory-lock and already applied
|
|
137
|
+
// this migration. Nothing more to do in this tx.
|
|
151
138
|
return;
|
|
152
139
|
}
|
|
153
140
|
|
|
154
141
|
const ctx = args.createContext(tx);
|
|
155
142
|
await migration.run(ctx);
|
|
156
|
-
await tx
|
|
143
|
+
await insertOne(tx, esOperationsTable, {
|
|
157
144
|
id: entry.id,
|
|
158
145
|
operationType: "seed-migration",
|
|
159
146
|
durationMs: Date.now() - start,
|
|
@@ -204,11 +191,10 @@ async function listSeedFiles(seedsDir: string): Promise<readonly SeedFileEntry[]
|
|
|
204
191
|
}
|
|
205
192
|
|
|
206
193
|
async function loadAppliedIds(db: DbConnection): Promise<Set<string>> {
|
|
207
|
-
const rows = await db
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return new Set(rows.map((r: { id: string }) => r.id));
|
|
194
|
+
const rows = await selectMany<{ id: string }>(db, esOperationsTable, {
|
|
195
|
+
operationType: "seed-migration",
|
|
196
|
+
});
|
|
197
|
+
return new Set(rows.map((r) => r.id));
|
|
212
198
|
}
|
|
213
199
|
|
|
214
200
|
async function loadSeedModule(filePath: string): Promise<SeedMigration> {
|