@cosmicdrift/kumiko-framework 0.14.0 → 0.15.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/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 +845 -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/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__/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__/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__/{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 +48 -40
- 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__/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-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__/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__/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 +1 -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/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 +2 -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 +7 -2
- package/src/engine/types/fields.ts +4 -5
- 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__/write-failures.test.ts +1 -1
- 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 +9 -43
- 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.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-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- 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 +8 -7
- 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__/env-parse.test.ts +1 -1
- package/CHANGELOG.md +0 -474
- package/src/db/__tests__/cursor.test.ts +0 -41
- 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,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { asRawClient } from "../../db/query";
|
|
3
3
|
import { createBooleanField, createEntity, createTextField } from "../../engine";
|
|
4
4
|
import { createEventsTable } from "../../event-store";
|
|
5
5
|
import {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
unsafeCreateEntityTable,
|
|
11
11
|
} from "../../stack";
|
|
12
12
|
import { createEventStoreExecutor } from "../event-store-executor";
|
|
13
|
-
import {
|
|
13
|
+
import { buildEntityTable } from "../table-builder";
|
|
14
14
|
import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
15
15
|
|
|
16
16
|
const entity = createEntity({
|
|
@@ -22,7 +22,7 @@ const entity = createEntity({
|
|
|
22
22
|
},
|
|
23
23
|
softDelete: true,
|
|
24
24
|
});
|
|
25
|
-
const table =
|
|
25
|
+
const table = buildEntityTable("esExecUser", entity);
|
|
26
26
|
|
|
27
27
|
let testDb: TestDb;
|
|
28
28
|
let tdb: TenantDb;
|
|
@@ -40,7 +40,9 @@ afterAll(async () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
beforeEach(async () => {
|
|
43
|
-
await testDb.db.
|
|
43
|
+
await asRawClient(testDb.db).unsafe(
|
|
44
|
+
`TRUNCATE kumiko_events, read_es_exec_users RESTART IDENTITY CASCADE`,
|
|
45
|
+
);
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
describe("event-store-executor", () => {
|
|
@@ -117,7 +119,7 @@ const sensitiveEntity = createEntity({
|
|
|
117
119
|
},
|
|
118
120
|
softDelete: true,
|
|
119
121
|
});
|
|
120
|
-
const sensitiveTable =
|
|
122
|
+
const sensitiveTable = buildEntityTable("esExecSensitive", sensitiveEntity);
|
|
121
123
|
|
|
122
124
|
describe("event-store-executor — sensitive fields", () => {
|
|
123
125
|
const crud = createEventStoreExecutor(sensitiveTable, sensitiveEntity, {
|
|
@@ -129,8 +131,8 @@ describe("event-store-executor — sensitive fields", () => {
|
|
|
129
131
|
});
|
|
130
132
|
|
|
131
133
|
beforeEach(async () => {
|
|
132
|
-
await testDb.db.
|
|
133
|
-
|
|
134
|
+
await asRawClient(testDb.db).unsafe(
|
|
135
|
+
`TRUNCATE kumiko_events, read_es_exec_sensitive RESTART IDENTITY CASCADE`,
|
|
134
136
|
);
|
|
135
137
|
});
|
|
136
138
|
|
|
@@ -138,12 +140,17 @@ describe("event-store-executor — sensitive fields", () => {
|
|
|
138
140
|
type: string;
|
|
139
141
|
payload: TPayload;
|
|
140
142
|
}> {
|
|
141
|
-
const rows = await testDb.db.
|
|
142
|
-
|
|
143
|
-
);
|
|
143
|
+
const rows = (await asRawClient(testDb.db).unsafe(
|
|
144
|
+
`SELECT type, payload FROM kumiko_events ORDER BY id DESC LIMIT 1`,
|
|
145
|
+
)) as Array<{ type: string; payload: unknown }>;
|
|
144
146
|
const row = rows[0];
|
|
145
147
|
if (!row) throw new Error("no events in store");
|
|
146
|
-
return
|
|
148
|
+
return {
|
|
149
|
+
type: row["type"],
|
|
150
|
+
payload: (typeof row["payload"] === "string"
|
|
151
|
+
? JSON.parse(row["payload"])
|
|
152
|
+
: row["payload"]) as TPayload,
|
|
153
|
+
};
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
test("create event payload excludes sensitive fields but entity row keeps them", async () => {
|
|
@@ -15,16 +15,17 @@
|
|
|
15
15
|
// 5. Snapshot erneut nehmen
|
|
16
16
|
// 6. deep-equal: identische Rows in identischer Reihenfolge
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
19
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
20
20
|
import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
|
|
21
21
|
import { createRegistry } from "../../engine/registry";
|
|
22
22
|
import { createEventsTable } from "../../event-store";
|
|
23
23
|
import { rebuildProjection } from "../../pipeline";
|
|
24
24
|
import { createProjectionStateTable } from "../../pipeline/projection-state";
|
|
25
|
-
import {
|
|
25
|
+
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
26
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
26
27
|
import { createEventStoreExecutor } from "../event-store-executor";
|
|
27
|
-
import {
|
|
28
|
+
import { buildEntityTable } from "../table-builder";
|
|
28
29
|
import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
29
30
|
|
|
30
31
|
const userEntity = createEntity({
|
|
@@ -41,13 +42,14 @@ const userFeature = defineFeature("implicittest", (r) => {
|
|
|
41
42
|
r.entity("user", userEntity);
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
const userTable =
|
|
45
|
+
const userTable = buildEntityTable("user", userEntity);
|
|
45
46
|
|
|
46
|
-
let testDb:
|
|
47
|
+
let testDb: BunTestDb;
|
|
47
48
|
let tdb: TenantDb;
|
|
48
49
|
const adminUser = TestUsers.admin;
|
|
49
50
|
|
|
50
51
|
beforeAll(async () => {
|
|
52
|
+
await ensureTemporalPolyfill();
|
|
51
53
|
testDb = await createTestDb();
|
|
52
54
|
await unsafeCreateEntityTable(testDb.db, userEntity, "user");
|
|
53
55
|
await createEventsTable(testDb.db);
|
|
@@ -60,13 +62,18 @@ afterAll(async () => {
|
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
beforeEach(async () => {
|
|
63
|
-
await testDb.db.
|
|
64
|
-
|
|
65
|
+
await asRawClient(testDb.db).unsafe(
|
|
66
|
+
`TRUNCATE kumiko_events, read_implicit_users, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
65
67
|
);
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
async function snapshotTable(): Promise<readonly Record<string, unknown>[]> {
|
|
69
|
-
const rows = await
|
|
71
|
+
const rows = await selectMany(
|
|
72
|
+
testDb.db,
|
|
73
|
+
userTable,
|
|
74
|
+
{},
|
|
75
|
+
{ orderBy: { col: "id", direction: "asc" } },
|
|
76
|
+
);
|
|
70
77
|
return rows as readonly Record<string, unknown>[];
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -224,7 +231,7 @@ describe("implicit-projection / Live==Rebuild equivalence", () => {
|
|
|
224
231
|
// sensitive-Spalte oder verschlüsseltem Event-Payload), bricht der Test
|
|
225
232
|
// und zwingt zu Aufmerksamkeit.
|
|
226
233
|
|
|
227
|
-
import {
|
|
234
|
+
import { asRawClient, selectMany } from "../../db/query";
|
|
228
235
|
|
|
229
236
|
const sensitiveTable = "read_implicit_sensitive_users";
|
|
230
237
|
|
|
@@ -240,7 +247,7 @@ const sensitiveFeature = defineFeature("implicitsensitive", (r) => {
|
|
|
240
247
|
r.entity("sensitive-user", sensitiveEntity);
|
|
241
248
|
});
|
|
242
249
|
|
|
243
|
-
const
|
|
250
|
+
const sensitiveEntityTable = buildEntityTable("sensitive-user", sensitiveEntity);
|
|
244
251
|
|
|
245
252
|
describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
246
253
|
beforeAll(async () => {
|
|
@@ -248,15 +255,13 @@ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
|
248
255
|
});
|
|
249
256
|
|
|
250
257
|
beforeEach(async () => {
|
|
251
|
-
await testDb.db.
|
|
252
|
-
|
|
253
|
-
`TRUNCATE ${sensitiveTable}, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
254
|
-
),
|
|
258
|
+
await asRawClient(testDb.db).unsafe(
|
|
259
|
+
`TRUNCATE ${sensitiveTable}, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
255
260
|
);
|
|
256
261
|
});
|
|
257
262
|
|
|
258
263
|
test("Live schreibt sensitive-Felder, Rebuild lässt sie NULL (Welle-3-Roadmap)", async () => {
|
|
259
|
-
const crud = createEventStoreExecutor(
|
|
264
|
+
const crud = createEventStoreExecutor(sensitiveEntityTable, sensitiveEntity, {
|
|
260
265
|
entityName: "sensitive-user",
|
|
261
266
|
});
|
|
262
267
|
|
|
@@ -269,20 +274,22 @@ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
|
269
274
|
);
|
|
270
275
|
if (!created.isSuccess) throw new Error("setup failed");
|
|
271
276
|
|
|
272
|
-
const [liveRow] = await testDb.db
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
.where(eq(sensitiveDrizzleTable["id"], created.data.id as string));
|
|
277
|
+
const [liveRow] = await selectMany(testDb.db, sensitiveEntityTable, {
|
|
278
|
+
id: created.data.id as string,
|
|
279
|
+
});
|
|
276
280
|
expect(liveRow?.["apiKey"]).toBe("secret-token-abc");
|
|
277
281
|
expect(liveRow?.["email"]).toBe("x@test.de");
|
|
278
282
|
|
|
279
283
|
// 2. Verifiziere dass das Event-Log das Feld NICHT enthält (stripped).
|
|
280
|
-
const
|
|
281
|
-
|
|
284
|
+
const { eventsTable } = await import("../../event-store");
|
|
285
|
+
const [event] = await selectMany(
|
|
286
|
+
testDb.db,
|
|
287
|
+
eventsTable,
|
|
288
|
+
{ aggregateId: created.data.id },
|
|
289
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
282
290
|
);
|
|
283
|
-
expect(
|
|
284
|
-
expect(
|
|
285
|
-
expect(events[0]?.payload?.["email"]).toBe("x@test.de");
|
|
291
|
+
expect(event?.payload?.["apiKey"]).toBeUndefined();
|
|
292
|
+
expect(event?.payload?.["email"]).toBe("x@test.de");
|
|
286
293
|
|
|
287
294
|
// 3. Rebuild über die ImplicitProjection. Read-Tabelle wird aus
|
|
288
295
|
// event.payload neu materialisiert — apiKey ist nicht im Log,
|
|
@@ -293,10 +300,9 @@ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
|
293
300
|
registry,
|
|
294
301
|
});
|
|
295
302
|
|
|
296
|
-
const [rebuiltRow] = await testDb.db
|
|
297
|
-
.
|
|
298
|
-
|
|
299
|
-
.where(eq(sensitiveDrizzleTable["id"], created.data.id as string));
|
|
303
|
+
const [rebuiltRow] = await selectMany(testDb.db, sensitiveEntityTable, {
|
|
304
|
+
id: created.data.id as string,
|
|
305
|
+
});
|
|
300
306
|
expect(rebuiltRow?.["email"]).toBe("x@test.de");
|
|
301
307
|
// DAS ist die Drift: sensitive Feld ist nach Rebuild weg.
|
|
302
308
|
expect(rebuiltRow?.["apiKey"]).toBeNull();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure Unit-Tests für die flatten/rehydrate Helpers — Auto-Convert für
|
|
2
2
|
// locatedTimestamp-Felder. Keine DB, kein Stack, nur Daten-Transform.
|
|
3
3
|
|
|
4
|
-
import { describe, expect, test } from "
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
5
|
import { createEntity, createLocatedTimestampField, createTextField } from "../../engine";
|
|
6
6
|
import type { EntityDefinition } from "../../engine/types";
|
|
7
7
|
import { flattenLocatedTimestamp, rehydrateLocatedTimestamp } from "../located-timestamp";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Pure Unit-Tests für money flatten/rehydrate Helpers.
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from "
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
4
|
import { createEntity, createMoneyField, createTextField } from "../../engine";
|
|
5
5
|
import type { EntityDefinition } from "../../engine/types";
|
|
6
6
|
import { flattenMoney, rehydrateMoney } from "../money";
|
package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts}
RENAMED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// have a real footgun, or whether the showcase bug had a different root
|
|
8
8
|
// cause.
|
|
9
9
|
|
|
10
|
-
import { afterAll, beforeAll, describe, expect, test } from "
|
|
11
|
-
import {
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { insertOne, selectMany } from "../../db/query";
|
|
12
|
+
import { buildEntityTable } from "../../db/table-builder";
|
|
12
13
|
import { createEntity, createTextField } from "../../engine";
|
|
13
14
|
import { setupTestStack, type TestStack, unsafeCreateEntityTable } from "../../stack";
|
|
14
15
|
|
|
@@ -19,7 +20,7 @@ const linkEntity = createEntity({
|
|
|
19
20
|
rightId: createTextField({ required: true }),
|
|
20
21
|
},
|
|
21
22
|
});
|
|
22
|
-
const linkTable =
|
|
23
|
+
const linkTable = buildEntityTable("link", linkEntity);
|
|
23
24
|
|
|
24
25
|
let stack: TestStack;
|
|
25
26
|
|
|
@@ -41,7 +42,7 @@ describe("instant() customType is forgiving with ISO strings", () => {
|
|
|
41
42
|
table: "mri_ts",
|
|
42
43
|
fields: { name: createTextField({ required: true }) },
|
|
43
44
|
});
|
|
44
|
-
const tsTable =
|
|
45
|
+
const tsTable = buildEntityTable("ts-row", tsEntity);
|
|
45
46
|
|
|
46
47
|
test("INSERT accepts an ISO string for an instant column (forgiving path)", async () => {
|
|
47
48
|
await unsafeCreateEntityTable(stack.db, tsEntity, "ts-row");
|
|
@@ -50,12 +51,12 @@ describe("instant() customType is forgiving with ISO strings", () => {
|
|
|
50
51
|
// would call .toString() on a string and produce a malformed driver
|
|
51
52
|
// value that PG rejects.
|
|
52
53
|
const isoString = "2026-01-15T12:00:00Z";
|
|
53
|
-
await stack.db
|
|
54
|
+
await insertOne(stack.db, tsTable, {
|
|
54
55
|
name: "x",
|
|
55
56
|
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
56
57
|
insertedAt: isoString as unknown as Temporal.Instant,
|
|
57
58
|
});
|
|
58
|
-
const rows = await stack.db
|
|
59
|
+
const rows = await selectMany(stack.db, tsTable);
|
|
59
60
|
expect(rows).toHaveLength(1);
|
|
60
61
|
expect(rows[0]?.["insertedAt"]).toBeInstanceOf(Temporal.Instant);
|
|
61
62
|
});
|
|
@@ -63,11 +64,17 @@ describe("instant() customType is forgiving with ISO strings", () => {
|
|
|
63
64
|
|
|
64
65
|
describe("multi-row INSERT", () => {
|
|
65
66
|
test("two rows with no id supplied → both rows persist (PG gen_random_uuid per row)", async () => {
|
|
66
|
-
await stack.db
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
await insertOne(stack.db, linkTable, {
|
|
68
|
+
leftId: "L1",
|
|
69
|
+
rightId: "R1",
|
|
70
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
71
|
+
});
|
|
72
|
+
await insertOne(stack.db, linkTable, {
|
|
73
|
+
leftId: "L2",
|
|
74
|
+
rightId: "R2",
|
|
75
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
76
|
+
});
|
|
77
|
+
const rows = await selectMany(stack.db, linkTable);
|
|
71
78
|
expect(rows).toHaveLength(2);
|
|
72
79
|
// Each row got its own id from the PG default.
|
|
73
80
|
const ids = new Set(rows.map((r) => r["id"] as string));
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Wenn parseAutoVerb für ein Domain-Event versehentlich einen Verb
|
|
8
8
|
// returnt, würde die ImplicitProjection den falschen Handler firen.
|
|
9
9
|
|
|
10
|
-
import { describe, expect, test } from "
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
11
|
import type { StoredEvent } from "../../event-store";
|
|
12
12
|
import { parseAutoVerb } from "../apply-entity-event";
|
|
13
13
|
|
|
@@ -16,13 +16,15 @@
|
|
|
16
16
|
// solchen Migration eine Sanity-Query auf NULL-Counts in den betroffenen
|
|
17
17
|
// Spalten laufen, oder DB drop'pen wenn der State Demo-State ist.
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
19
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
20
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
21
|
+
import { asRawClient } from "../../db/query";
|
|
22
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
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
|
});
|
|
28
30
|
|
|
@@ -31,53 +33,53 @@ afterAll(async () => {
|
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
beforeEach(async () => {
|
|
34
|
-
await testDb.db.
|
|
36
|
+
await asRawClient(testDb.db).unsafe(`DROP TABLE IF EXISTS migration_safety_test`);
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
describe("ALTER TABLE SET NOT NULL — Daten-Sicherheits-Verhalten", () => {
|
|
38
40
|
test("SET NOT NULL kracht wenn die Spalte NULL-Zeilen enthält", async () => {
|
|
39
|
-
await testDb.db.
|
|
41
|
+
await asRawClient(testDb.db).unsafe(`
|
|
40
42
|
CREATE TABLE migration_safety_test (
|
|
41
43
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
42
44
|
key text
|
|
43
45
|
)
|
|
44
46
|
`);
|
|
45
47
|
// NULL-Zeile einschleusen — simuliert prod state vor dem Drift-Fix.
|
|
46
|
-
await testDb.db.
|
|
48
|
+
await asRawClient(testDb.db).unsafe(`INSERT INTO migration_safety_test (key) VALUES (NULL)`);
|
|
47
49
|
|
|
48
50
|
let caught: unknown;
|
|
49
51
|
try {
|
|
50
|
-
await testDb.db.
|
|
52
|
+
await asRawClient(testDb.db).unsafe(
|
|
53
|
+
`ALTER TABLE migration_safety_test ALTER COLUMN key SET NOT NULL`,
|
|
54
|
+
);
|
|
51
55
|
} catch (err) {
|
|
52
56
|
caught = err;
|
|
53
57
|
}
|
|
54
58
|
expect(caught).toBeDefined();
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// `.message`. Wir prüfen pragmatisch beide Pfade.
|
|
59
|
-
const cause = (caught as { cause?: unknown }).cause;
|
|
60
|
-
const causeCode = (cause as { code?: string } | undefined)?.code;
|
|
61
|
-
expect(causeCode).toBe("23502");
|
|
59
|
+
// Postgres-js throws PostgresError directly (no drizzle wrapper anymore).
|
|
60
|
+
const code = (caught as { code?: string } | undefined)?.code;
|
|
61
|
+
expect(code).toBe("23502");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
test("SET NOT NULL läuft sauber durch wenn alle Zeilen Werte haben", async () => {
|
|
65
|
-
await testDb.db.
|
|
65
|
+
await asRawClient(testDb.db).unsafe(`
|
|
66
66
|
CREATE TABLE migration_safety_test (
|
|
67
67
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
68
68
|
key text
|
|
69
69
|
)
|
|
70
70
|
`);
|
|
71
|
-
await testDb.db.
|
|
72
|
-
await testDb.db.
|
|
71
|
+
await asRawClient(testDb.db).unsafe(`INSERT INTO migration_safety_test (key) VALUES ('foo')`);
|
|
72
|
+
await asRawClient(testDb.db).unsafe(`INSERT INTO migration_safety_test (key) VALUES ('bar')`);
|
|
73
73
|
|
|
74
74
|
// Sollte ohne Throw durchlaufen.
|
|
75
|
-
await testDb.db.
|
|
75
|
+
await asRawClient(testDb.db).unsafe(
|
|
76
|
+
`ALTER TABLE migration_safety_test ALTER COLUMN key SET NOT NULL`,
|
|
77
|
+
);
|
|
76
78
|
|
|
77
79
|
// Verifizieren: zukünftige NULL-Inserts werden jetzt blockiert.
|
|
78
80
|
let caught: unknown;
|
|
79
81
|
try {
|
|
80
|
-
await testDb.db.
|
|
82
|
+
await asRawClient(testDb.db).unsafe(`INSERT INTO migration_safety_test (key) VALUES (NULL)`);
|
|
81
83
|
} catch (err) {
|
|
82
84
|
caught = err;
|
|
83
85
|
}
|
|
@@ -88,17 +90,19 @@ describe("ALTER TABLE SET NOT NULL — Daten-Sicherheits-Verhalten", () => {
|
|
|
88
90
|
// Frisch erstellt, keine Zeilen — der Fall in dem `migrate apply` nach
|
|
89
91
|
// einem DB-drop läuft. Dieser Pfad muss IMMER grün sein, sonst wäre
|
|
90
92
|
// jeder Greenfield-Deploy kaputt.
|
|
91
|
-
await testDb.db.
|
|
93
|
+
await asRawClient(testDb.db).unsafe(`
|
|
92
94
|
CREATE TABLE migration_safety_test (
|
|
93
95
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
94
96
|
key text
|
|
95
97
|
)
|
|
96
98
|
`);
|
|
97
|
-
await testDb.db.
|
|
99
|
+
await asRawClient(testDb.db).unsafe(
|
|
100
|
+
`ALTER TABLE migration_safety_test ALTER COLUMN key SET NOT NULL`,
|
|
101
|
+
);
|
|
98
102
|
|
|
99
103
|
// Beweis: information_schema zeigt die Spalte jetzt als NOT NULL.
|
|
100
|
-
const rows = await testDb.db.
|
|
101
|
-
|
|
104
|
+
const rows = await asRawClient(testDb.db).unsafe<{ is_nullable: string }>(
|
|
105
|
+
`SELECT is_nullable FROM information_schema.columns WHERE table_name = 'migration_safety_test' AND column_name = 'key'`,
|
|
102
106
|
);
|
|
103
107
|
expect(rows[0]?.is_nullable).toBe("NO");
|
|
104
108
|
});
|
package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
3
|
+
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
3
4
|
import {
|
|
4
5
|
createBooleanField,
|
|
5
6
|
createDateField,
|
|
@@ -9,8 +10,9 @@ import {
|
|
|
9
10
|
defineFeature,
|
|
10
11
|
} from "../../engine";
|
|
11
12
|
import type { FeatureDefinition } from "../../engine/types";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { unsafePushTables } from "../../stack";
|
|
14
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
15
|
+
import { buildEntityTable } from "../table-builder";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Integration tests for the schema migration workflow.
|
|
@@ -18,14 +20,15 @@ import { buildDrizzleTable } from "../table-builder";
|
|
|
18
20
|
*
|
|
19
21
|
* Each test simulates:
|
|
20
22
|
* 1. Developer defines/changes entities
|
|
21
|
-
* 2.
|
|
23
|
+
* 2. buildEntityTable creates Drizzle table objects
|
|
22
24
|
* 3. Schema is applied to a real database via unsafePushTables (drizzle-kit push)
|
|
23
25
|
* 4. We verify the DB state matches expectations
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
|
-
let testDb:
|
|
28
|
+
let testDb: BunTestDb;
|
|
27
29
|
|
|
28
30
|
beforeAll(async () => {
|
|
31
|
+
await ensureTemporalPolyfill();
|
|
29
32
|
testDb = await createTestDb();
|
|
30
33
|
});
|
|
31
34
|
|
|
@@ -38,7 +41,7 @@ async function applySchema(features: readonly FeatureDefinition[]): Promise<void
|
|
|
38
41
|
const tables: Record<string, unknown> = {};
|
|
39
42
|
for (const feature of features) {
|
|
40
43
|
for (const [entityName, entity] of Object.entries(feature.entities)) {
|
|
41
|
-
tables[entityName] =
|
|
44
|
+
tables[entityName] = buildEntityTable(entityName, entity);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
await unsafePushTables(testDb.db, tables);
|
|
@@ -48,12 +51,13 @@ async function applySchema(features: readonly FeatureDefinition[]): Promise<void
|
|
|
48
51
|
async function getTableColumns(
|
|
49
52
|
tableName: string,
|
|
50
53
|
): Promise<Map<string, { dataType: string; isNullable: boolean }>> {
|
|
51
|
-
const rows = await testDb.db.
|
|
54
|
+
const rows = await asRawClient(testDb.db).unsafe<{
|
|
52
55
|
column_name: string;
|
|
53
56
|
data_type: string;
|
|
54
57
|
is_nullable: string;
|
|
55
58
|
}>(
|
|
56
|
-
|
|
59
|
+
`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position`,
|
|
60
|
+
[tableName],
|
|
57
61
|
);
|
|
58
62
|
|
|
59
63
|
const result = new Map<string, { dataType: string; isNullable: boolean }>();
|
|
@@ -121,10 +125,8 @@ describe("schema migration workflows", () => {
|
|
|
121
125
|
});
|
|
122
126
|
await applySchema([feature]);
|
|
123
127
|
|
|
124
|
-
const indexRows = await testDb.db.
|
|
125
|
-
|
|
126
|
-
`SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'wf1b_articles' AND indexname = 'wf1b_articles_tenant_id_idx'`,
|
|
127
|
-
),
|
|
128
|
+
const indexRows = await asRawClient(testDb.db).unsafe<{ indexname: string; indexdef: string }>(
|
|
129
|
+
`SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'wf1b_articles' AND indexname = 'wf1b_articles_tenant_id_idx'`,
|
|
128
130
|
);
|
|
129
131
|
expect(indexRows.length).toBe(1);
|
|
130
132
|
expect(indexRows[0]?.indexdef).toContain("tenant_id");
|
|
@@ -136,7 +138,7 @@ describe("schema migration workflows", () => {
|
|
|
136
138
|
table: "wf2_users",
|
|
137
139
|
fields: { email: createTextField() },
|
|
138
140
|
});
|
|
139
|
-
await unsafePushTables(testDb.db, { user:
|
|
141
|
+
await unsafePushTables(testDb.db, { user: buildEntityTable("user", initialEntity) });
|
|
140
142
|
|
|
141
143
|
// Developer adds a new field
|
|
142
144
|
const updatedEntity = createEntity({
|
|
@@ -150,8 +152,8 @@ describe("schema migration workflows", () => {
|
|
|
150
152
|
// Push updated schema — drizzle-kit generates ALTER TABLE ADD COLUMN
|
|
151
153
|
await unsafePushTables(
|
|
152
154
|
testDb.db,
|
|
153
|
-
{ user:
|
|
154
|
-
{ user:
|
|
155
|
+
{ user: buildEntityTable("user", updatedEntity) },
|
|
156
|
+
{ user: buildEntityTable("user", initialEntity) },
|
|
155
157
|
);
|
|
156
158
|
|
|
157
159
|
const columns = await getTableColumns("wf2_users");
|
|
@@ -166,24 +168,25 @@ describe("schema migration workflows", () => {
|
|
|
166
168
|
table: "wf3_projects",
|
|
167
169
|
fields: { name: createTextField() },
|
|
168
170
|
});
|
|
169
|
-
const initialTable =
|
|
171
|
+
const initialTable = buildEntityTable("project", initialEntity);
|
|
170
172
|
await unsafePushTables(testDb.db, { project: initialTable });
|
|
171
173
|
|
|
172
174
|
// Insert a row first (to prove ADD COLUMN with default doesn't break existing rows)
|
|
173
|
-
await testDb.db
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
await insertOne(testDb.db, initialTable, {
|
|
176
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
177
|
+
name: "Test Project",
|
|
178
|
+
});
|
|
176
179
|
|
|
177
180
|
// Developer adds boolean field with default
|
|
178
181
|
const updatedEntity = createEntity({
|
|
179
182
|
table: "wf3_projects",
|
|
180
183
|
fields: { name: createTextField(), isArchived: createBooleanField({ default: false }) },
|
|
181
184
|
});
|
|
182
|
-
const updatedTable =
|
|
185
|
+
const updatedTable = buildEntityTable("project", updatedEntity);
|
|
183
186
|
await unsafePushTables(testDb.db, { project: updatedTable }, { project: initialTable });
|
|
184
187
|
|
|
185
188
|
// Existing row should have the default value
|
|
186
|
-
const rows = await testDb.db
|
|
189
|
+
const rows = await selectMany(testDb.db, updatedTable);
|
|
187
190
|
|
|
188
191
|
expect(rows[0]).toMatchObject({ name: "Test Project", isArchived: false });
|
|
189
192
|
});
|
|
@@ -199,12 +202,13 @@ describe("schema migration workflows", () => {
|
|
|
199
202
|
table: "wf3b_users",
|
|
200
203
|
fields: { email: createTextField({ required: true }) },
|
|
201
204
|
});
|
|
202
|
-
const initialTable =
|
|
205
|
+
const initialTable = buildEntityTable("user", initialEntity);
|
|
203
206
|
await unsafePushTables(testDb.db, { user: initialTable });
|
|
204
207
|
|
|
205
|
-
await testDb.db
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
await insertOne(testDb.db, initialTable, {
|
|
209
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
210
|
+
email: "x@y.z",
|
|
211
|
+
});
|
|
208
212
|
|
|
209
213
|
const updatedEntity = createEntity({
|
|
210
214
|
table: "wf3b_users",
|
|
@@ -213,10 +217,10 @@ describe("schema migration workflows", () => {
|
|
|
213
217
|
roles: createTextField({ required: true, default: "[]" }),
|
|
214
218
|
},
|
|
215
219
|
});
|
|
216
|
-
const updatedTable =
|
|
220
|
+
const updatedTable = buildEntityTable("user", updatedEntity);
|
|
217
221
|
await unsafePushTables(testDb.db, { user: updatedTable }, { user: initialTable });
|
|
218
222
|
|
|
219
|
-
const rows = await testDb.db
|
|
223
|
+
const rows = await selectMany(testDb.db, updatedTable);
|
|
220
224
|
expect(rows[0]).toMatchObject({ email: "x@y.z", roles: "[]" });
|
|
221
225
|
});
|
|
222
226
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { formatReport, isRawSqlAllowed, joinPath, scanRepo } from "../sql-inventory";
|
|
3
|
+
|
|
4
|
+
const cleanups: string[] = [];
|
|
5
|
+
|
|
6
|
+
afterEach(async () => {
|
|
7
|
+
for (const dir of cleanups) {
|
|
8
|
+
await Bun.spawn(["rm", "-rf", dir]).exited;
|
|
9
|
+
}
|
|
10
|
+
cleanups.length = 0;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function tempRepo(files: Record<string, string>): Promise<string> {
|
|
14
|
+
const root = joinPath(import.meta.dir, `.tmp-sql-inv-${crypto.randomUUID()}`);
|
|
15
|
+
cleanups.push(root);
|
|
16
|
+
await Promise.all(
|
|
17
|
+
Object.entries(files).map(([rel, content]) => Bun.write(joinPath(root, rel), content)),
|
|
18
|
+
);
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("sql-inventory", () => {
|
|
23
|
+
test("isRawSqlAllowed permits db/queries and bun-db/query", () => {
|
|
24
|
+
expect(isRawSqlAllowed("/repo/packages/framework/src/db/queries/event-store.ts")).toBe(true);
|
|
25
|
+
expect(isRawSqlAllowed("/repo/packages/framework/src/bun-db/query.ts")).toBe(true);
|
|
26
|
+
expect(
|
|
27
|
+
isRawSqlAllowed("/repo/packages/bundled-features/src/sessions/db/queries/cleanup.ts"),
|
|
28
|
+
).toBe(true);
|
|
29
|
+
expect(isRawSqlAllowed("/repo/samples/apps/marketing-demo/src/db/queries/seed-counts.ts")).toBe(
|
|
30
|
+
true,
|
|
31
|
+
);
|
|
32
|
+
expect(isRawSqlAllowed("/repo/bin/commands/schema.ts")).toBe(true);
|
|
33
|
+
expect(isRawSqlAllowed("/repo/scripts/codemod-drizzle-chain-to-bun-db.ts")).toBe(true);
|
|
34
|
+
expect(
|
|
35
|
+
isRawSqlAllowed("/repo/packages/bundled-features/src/sessions/handlers/cleanup.job.ts"),
|
|
36
|
+
).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("scanRepo classifies production vs test hits", async () => {
|
|
40
|
+
const root = await tempRepo({
|
|
41
|
+
"packages/framework/src/db/queries/demo.ts": `export async function x(db: unknown) {
|
|
42
|
+
return asRawClient(db).unsafe("SELECT 1");
|
|
43
|
+
}`,
|
|
44
|
+
"packages/framework/src/handlers/bad.ts": `export async function y(db: unknown) {
|
|
45
|
+
return asRawClient(db).unsafe("DELETE FROM read_users");
|
|
46
|
+
}`,
|
|
47
|
+
"packages/framework/src/__tests__/ok.integration.ts": `await asRawClient(db).unsafe("DELETE FROM read_users");`,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const report = await scanRepo(root);
|
|
51
|
+
expect(report.summary.byBucket.allowed).toBeGreaterThanOrEqual(1);
|
|
52
|
+
expect(report.summary.byBucket.tests).toBeGreaterThanOrEqual(1);
|
|
53
|
+
expect(report.summary.byBucket.disallowed).toBeGreaterThanOrEqual(1);
|
|
54
|
+
expect(formatReport(report)).toContain("sql inventory");
|
|
55
|
+
});
|
|
56
|
+
});
|