@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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// JSONB-Edge-Cases — wo Drift zwischen bun.sql und postgres-js entstanden ist.
|
|
2
|
+
// Bun.SQL kann arrays/objects nicht direkt als jsonb binden; query.ts
|
|
3
|
+
// macht JSON.stringify + ::jsonb cast (siehe query.ts:301-306). Diese
|
|
4
|
+
// Tests beweisen dass dabei nichts subtil verloren geht — egal wie
|
|
5
|
+
// komplex/edge das jsonb-Value ist.
|
|
6
|
+
|
|
7
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
8
|
+
import { fetchOne, insertOne, updateMany } from "../query";
|
|
9
|
+
import { closeDb, withTable } from "./_helpers";
|
|
10
|
+
|
|
11
|
+
afterAll(async () => {
|
|
12
|
+
await closeDb();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Hilfsfunktion: jsonb-Spalte mit pgType + Default. NotNull damit der
|
|
16
|
+
// expliziter Test ist (NULL-jsonb verdeckt sonst Bind-Bugs).
|
|
17
|
+
const jsonbCol = (defaultSql: string) => [
|
|
18
|
+
{ name: "data", pgType: "jsonb" as const, notNull: true, defaultSql },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async function roundtripObject(db: unknown, meta: unknown, value: object): Promise<unknown> {
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: tests pass any shape
|
|
23
|
+
const ins = (await insertOne<any>(db as never, meta as never, { data: value }))!;
|
|
24
|
+
const row = await fetchOne<{ data: unknown }>(db as never, meta as never, { id: ins.id });
|
|
25
|
+
return row?.data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("jsonb — primitive shapes", () => {
|
|
29
|
+
test("nested object 3 levels deep", async () => {
|
|
30
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
31
|
+
const v = { level1: { level2: { level3: "deep" } } };
|
|
32
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("array of strings", async () => {
|
|
37
|
+
await withTable(jsonbCol("'[]'::jsonb"), async ({ db, meta }) => {
|
|
38
|
+
const v = ["a", "b", "c"];
|
|
39
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("array of numbers (ints + floats)", async () => {
|
|
44
|
+
await withTable(jsonbCol("'[]'::jsonb"), async ({ db, meta }) => {
|
|
45
|
+
const v = [1, 2.5, -3, 0, 1e10];
|
|
46
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("array of booleans", async () => {
|
|
51
|
+
await withTable(jsonbCol("'[]'::jsonb"), async ({ db, meta }) => {
|
|
52
|
+
expect(await roundtripObject(db, meta, [true, false, true])).toEqual([true, false, true]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("array of objects (rows-pattern)", async () => {
|
|
57
|
+
await withTable(jsonbCol("'[]'::jsonb"), async ({ db, meta }) => {
|
|
58
|
+
const v = [
|
|
59
|
+
{ id: "1", label: "Alpha" },
|
|
60
|
+
{ id: "2", label: "Beta" },
|
|
61
|
+
];
|
|
62
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("mixed array (string|number|bool|null)", async () => {
|
|
67
|
+
await withTable(jsonbCol("'[]'::jsonb"), async ({ db, meta }) => {
|
|
68
|
+
const v = ["x", 1, true, null, 0];
|
|
69
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("jsonb — strings inside", () => {
|
|
75
|
+
test("string with double quotes (JSON-escape edge)", async () => {
|
|
76
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
77
|
+
const v = { note: 'He said "hello" loudly' };
|
|
78
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("string with backslashes (Windows path)", async () => {
|
|
83
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
84
|
+
const v = { path: "C:\\Users\\marc\\Documents" };
|
|
85
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("string with newlines + tabs", async () => {
|
|
90
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
91
|
+
const v = { body: "line1\nline2\tcol2" };
|
|
92
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("string with Unicode + emoji", async () => {
|
|
97
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
98
|
+
const v = { name: "Müller", flag: "🇩🇪", chinese: "你好" };
|
|
99
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("Stripe-style IDs", async () => {
|
|
104
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
105
|
+
const v = {
|
|
106
|
+
subscription: "sub_1Nq1qY2eZvKYlo2C0",
|
|
107
|
+
event: "evt_1Nq1qY2eZvKYlo2C0",
|
|
108
|
+
customer: "cus_1234567890",
|
|
109
|
+
};
|
|
110
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("string with single quotes (SQL-injection-shape)", async () => {
|
|
115
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
116
|
+
const v = { malicious: "'; DROP TABLE users; --" };
|
|
117
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("empty string + null distinction inside object", async () => {
|
|
122
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
123
|
+
const v = { empty: "", missing: null };
|
|
124
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("jsonb — numbers + special", () => {
|
|
130
|
+
test("bigint-grenzen als number (in JSON)", async () => {
|
|
131
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
132
|
+
// JSON nicht bigint-fähig, aber 2^53-1 ist max safe integer in JS
|
|
133
|
+
const v = { large: 9007199254740991, negativeLarge: -9007199254740991 };
|
|
134
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("floats with precision", async () => {
|
|
139
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
140
|
+
const v = { pi: Number.parseFloat("3.141592653589793"), small: 0.0001 };
|
|
141
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("ISO date strings stay strings (NOT auto-parsed to Date)", async () => {
|
|
146
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
147
|
+
// Wichtig: bun-db darf String-Werte in jsonb nicht zu Date coercen.
|
|
148
|
+
const v = { iso: "2026-05-24T08:30:00.000Z" };
|
|
149
|
+
const out = (await roundtripObject(db, meta, v)) as { iso: unknown };
|
|
150
|
+
expect(typeof out.iso).toBe("string");
|
|
151
|
+
expect(out.iso).toBe(v.iso);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("jsonb — booleans + null", () => {
|
|
157
|
+
test("boolean field inside object", async () => {
|
|
158
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
159
|
+
const v = { active: true, deleted: false };
|
|
160
|
+
const out = (await roundtripObject(db, meta, v)) as { active: unknown; deleted: unknown };
|
|
161
|
+
// typeof-Check verhindert "true"-string-statt-bool-Drift
|
|
162
|
+
expect(typeof out.active).toBe("boolean");
|
|
163
|
+
expect(typeof out.deleted).toBe("boolean");
|
|
164
|
+
expect(out.active).toBe(true);
|
|
165
|
+
expect(out.deleted).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("null-value in object", async () => {
|
|
170
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
171
|
+
const v = { missing: null, present: "x" };
|
|
172
|
+
const out = (await roundtripObject(db, meta, v)) as { missing: unknown };
|
|
173
|
+
expect(out.missing).toBeNull();
|
|
174
|
+
expect(out).toEqual(v);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("jsonb — large + deeply nested", () => {
|
|
180
|
+
test("large object (~10KB)", async () => {
|
|
181
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
182
|
+
const v: Record<string, string> = {};
|
|
183
|
+
for (let i = 0; i < 100; i++) v[`k${i}`] = `value-${i}-${"x".repeat(50)}`;
|
|
184
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("deeply nested (10 levels)", async () => {
|
|
189
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
190
|
+
let v: object = { leaf: "bottom" };
|
|
191
|
+
for (let i = 0; i < 10; i++) v = { [`level${i}`]: v };
|
|
192
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("array of 50 objects", async () => {
|
|
197
|
+
await withTable(jsonbCol("'[]'::jsonb"), async ({ db, meta }) => {
|
|
198
|
+
const v = Array.from({ length: 50 }, (_, i) => ({
|
|
199
|
+
idx: i,
|
|
200
|
+
label: `row-${i}`,
|
|
201
|
+
flag: i % 2 === 0,
|
|
202
|
+
}));
|
|
203
|
+
expect(await roundtripObject(db, meta, v)).toEqual(v);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("jsonb — UPDATE roundtrip (separate from INSERT)", () => {
|
|
209
|
+
test("UPDATE replaces jsonb value entirely (not merge)", async () => {
|
|
210
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
211
|
+
const ins = (await insertOne<{ id: string }>(db, meta, { data: { a: 1, b: 2 } }))!;
|
|
212
|
+
await updateMany(db, meta, { data: { c: 3 } }, { id: ins.id });
|
|
213
|
+
const row = await fetchOne<{ data: unknown }>(db, meta, { id: ins.id });
|
|
214
|
+
expect(row?.data).toEqual({ c: 3 });
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("UPDATE with array value replaces full array", async () => {
|
|
219
|
+
await withTable(jsonbCol("'[]'::jsonb"), async ({ db, meta }) => {
|
|
220
|
+
const ins = (await insertOne<{ id: string }>(db, meta, { data: [1, 2, 3] }))!;
|
|
221
|
+
await updateMany(db, meta, { data: ["a", "b"] }, { id: ins.id });
|
|
222
|
+
const row = await fetchOne<{ data: unknown }>(db, meta, { id: ins.id });
|
|
223
|
+
expect(row?.data).toEqual(["a", "b"]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("UPDATE with empty {} → leeres Objekt überschrieben", async () => {
|
|
228
|
+
await withTable(jsonbCol("'{}'::jsonb"), async ({ db, meta }) => {
|
|
229
|
+
const ins = (await insertOne<{ id: string }>(db, meta, { data: { foo: "bar" } }))!;
|
|
230
|
+
await updateMany(db, meta, { data: {} }, { id: ins.id });
|
|
231
|
+
const row = await fetchOne<{ data: unknown }>(db, meta, { id: ins.id });
|
|
232
|
+
expect(row?.data).toEqual({});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Smoke-Test: prüft dass _helpers.ts gegen die echte Test-DB funktioniert.
|
|
2
|
+
// Wenn dieser File grün ist, kann sql-matrix.integration.ts dranbauen.
|
|
3
|
+
|
|
4
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
5
|
+
import { deleteMany, fetchOne, insertOne, selectMany } from "../query";
|
|
6
|
+
import { closeDb, withTable } from "./_helpers";
|
|
7
|
+
|
|
8
|
+
afterAll(async () => {
|
|
9
|
+
await closeDb();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("_helpers smoke", () => {
|
|
13
|
+
test("withTable creates + drops a table, insert+fetch roundtrip", async () => {
|
|
14
|
+
await withTable([{ name: "label", pgType: "text", notNull: true }], async ({ db, meta }) => {
|
|
15
|
+
const inserted = await insertOne<{ id: string; label: string }>(db, meta, {
|
|
16
|
+
label: "hello",
|
|
17
|
+
});
|
|
18
|
+
expect(inserted?.label).toBe("hello");
|
|
19
|
+
expect(inserted?.id).toMatch(/^[0-9a-f-]{36}$/);
|
|
20
|
+
|
|
21
|
+
const fetched = await fetchOne<{ id: string; label: string }>(db, meta, {
|
|
22
|
+
id: inserted!.id,
|
|
23
|
+
});
|
|
24
|
+
expect(fetched?.label).toBe("hello");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("selectMany returns all rows, deleteMany removes by where", async () => {
|
|
29
|
+
await withTable([{ name: "label", pgType: "text", notNull: true }], async ({ db, meta }) => {
|
|
30
|
+
await insertOne(db, meta, { label: "a" });
|
|
31
|
+
await insertOne(db, meta, { label: "b" });
|
|
32
|
+
await insertOne(db, meta, { label: "c" });
|
|
33
|
+
|
|
34
|
+
const all = await selectMany<{ label: string }>(db, meta);
|
|
35
|
+
expect(all.length).toBe(3);
|
|
36
|
+
expect(all.map((r) => r.label).sort()).toEqual(["a", "b", "c"]);
|
|
37
|
+
|
|
38
|
+
await deleteMany(db, meta, { label: "b" });
|
|
39
|
+
const remaining = await selectMany<{ label: string }>(db, meta);
|
|
40
|
+
expect(remaining.map((r) => r.label).sort()).toEqual(["a", "c"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// sql-methods.integration.ts — verhaltens-Beweis für alle bun-db query-API-Methoden.
|
|
2
|
+
// Abgedeckt: selectMany, fetchOne, insertMany, updateMany, deleteMany, transaction.
|
|
3
|
+
//
|
|
4
|
+
// Known limitation: nested transactions (transaction() inside another transaction())
|
|
5
|
+
// werden von Bun.sql NICHT unterstützt — "cannot call begin inside a transaction use
|
|
6
|
+
// savepoint() instead". Test weggelassen, da query.ts keine savepoint()-Abstraktion
|
|
7
|
+
// hat. transaction() kann nur von einem Top-Level-db-Handle aufgerufen werden.
|
|
8
|
+
//
|
|
9
|
+
// deleteMany({}) ohne WHERE-Clause: query.ts rendert keinen WHERE-Clause wenn
|
|
10
|
+
// where-object leer ist → löscht ALLE Zeilen. Das ist ein Footgun; das Verhalten
|
|
11
|
+
// ist hier explizit dokumentiert und getestet.
|
|
12
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
13
|
+
import {
|
|
14
|
+
deleteMany,
|
|
15
|
+
fetchOne,
|
|
16
|
+
insertMany,
|
|
17
|
+
insertOne,
|
|
18
|
+
selectMany,
|
|
19
|
+
transaction,
|
|
20
|
+
updateMany,
|
|
21
|
+
} from "../query";
|
|
22
|
+
import { closeDb, withTable } from "./_helpers";
|
|
23
|
+
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await closeDb();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const textCols = [{ name: "val", pgType: "text" as const, notNull: true }] as const;
|
|
29
|
+
|
|
30
|
+
describe("selectMany", () => {
|
|
31
|
+
test("leere Tabelle returnt []", async () => {
|
|
32
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
33
|
+
const rows = await selectMany(db, meta);
|
|
34
|
+
expect(rows).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("ohne where gibt alle Zeilen zurück", async () => {
|
|
39
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
40
|
+
await insertOne(db, meta, { val: "a" });
|
|
41
|
+
await insertOne(db, meta, { val: "b" });
|
|
42
|
+
await insertOne(db, meta, { val: "c" });
|
|
43
|
+
const rows = await selectMany(db, meta);
|
|
44
|
+
expect(rows.length).toBe(3);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("mit where filtert korrekt", async () => {
|
|
49
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
50
|
+
await insertOne(db, meta, { val: "match" });
|
|
51
|
+
await insertOne(db, meta, { val: "no-match" });
|
|
52
|
+
const rows = await selectMany(db, meta, { val: "match" });
|
|
53
|
+
expect(rows.length).toBe(1);
|
|
54
|
+
expect((rows[0] as { val: string }).val).toBe("match");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("orderBy asc", async () => {
|
|
59
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
60
|
+
await insertMany(db, meta, [{ val: "c" }, { val: "a" }, { val: "b" }]);
|
|
61
|
+
const rows = (await selectMany(
|
|
62
|
+
db,
|
|
63
|
+
meta,
|
|
64
|
+
{},
|
|
65
|
+
{ orderBy: { col: "val", direction: "asc" } },
|
|
66
|
+
)) as readonly { val: string }[];
|
|
67
|
+
expect(rows.map((r) => r.val)).toEqual(["a", "b", "c"]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("orderBy desc", async () => {
|
|
72
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
73
|
+
await insertMany(db, meta, [{ val: "a" }, { val: "c" }, { val: "b" }]);
|
|
74
|
+
const rows = (await selectMany(
|
|
75
|
+
db,
|
|
76
|
+
meta,
|
|
77
|
+
{},
|
|
78
|
+
{ orderBy: { col: "val", direction: "desc" } },
|
|
79
|
+
)) as readonly { val: string }[];
|
|
80
|
+
expect(rows.map((r) => r.val)).toEqual(["c", "b", "a"]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("limit begrenzt Anzahl Ergebnisse", async () => {
|
|
85
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
86
|
+
await insertMany(db, meta, [{ val: "x" }, { val: "y" }, { val: "z" }]);
|
|
87
|
+
const rows = await selectMany(db, meta, {}, { limit: 2 });
|
|
88
|
+
expect(rows.length).toBe(2);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("fetchOne", () => {
|
|
94
|
+
test("gefundene Zeile zurückgeben", async () => {
|
|
95
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
96
|
+
const inserted = await insertOne<{ id: string; val: string }>(db, meta, { val: "find-me" });
|
|
97
|
+
const row = await fetchOne<{ id: string; val: string }>(db, meta, { id: inserted!.id });
|
|
98
|
+
expect(row).toBeDefined();
|
|
99
|
+
expect(row!.val).toBe("find-me");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("nicht-gefunden gibt undefined zurück", async () => {
|
|
104
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
105
|
+
const row = await fetchOne(db, meta, { id: "00000000-0000-4000-8000-000000000001" });
|
|
106
|
+
expect(row).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("insertMany", () => {
|
|
112
|
+
test("0 rows = no-op, gibt [] zurück", async () => {
|
|
113
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
114
|
+
const result = await insertMany(db, meta, []);
|
|
115
|
+
expect(result).toEqual([]);
|
|
116
|
+
const rows = await selectMany(db, meta);
|
|
117
|
+
expect(rows.length).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("1 row", async () => {
|
|
122
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
123
|
+
const result = await insertMany<{ id: string; val: string }>(db, meta, [{ val: "single" }]);
|
|
124
|
+
expect(result.length).toBe(1);
|
|
125
|
+
expect(result[0]!.val).toBe("single");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("5 rows auf einmal", async () => {
|
|
130
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
131
|
+
const input = Array.from({ length: 5 }, (_, i) => ({ val: `row-${i}` }));
|
|
132
|
+
const result = await insertMany<{ id: string; val: string }>(db, meta, input);
|
|
133
|
+
expect(result.length).toBe(5);
|
|
134
|
+
const all = await selectMany(db, meta);
|
|
135
|
+
expect(all.length).toBe(5);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("updateMany", () => {
|
|
141
|
+
test("zero match gibt [] zurück + keine Änderung", async () => {
|
|
142
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
143
|
+
await insertOne(db, meta, { val: "unchanged" });
|
|
144
|
+
const result = await updateMany(
|
|
145
|
+
db,
|
|
146
|
+
meta,
|
|
147
|
+
{ val: "new" },
|
|
148
|
+
{ id: "00000000-0000-4000-8000-000000000001" },
|
|
149
|
+
);
|
|
150
|
+
expect(result).toEqual([]);
|
|
151
|
+
const rows = await selectMany<{ val: string }>(db, meta);
|
|
152
|
+
expect(rows[0]!.val).toBe("unchanged");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("single match updatet genau eine Zeile", async () => {
|
|
157
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
158
|
+
const ins = await insertOne<{ id: string }>(db, meta, { val: "before" });
|
|
159
|
+
await updateMany(db, meta, { val: "after" }, { id: ins!.id });
|
|
160
|
+
const row = await fetchOne<{ val: string }>(db, meta, { id: ins!.id });
|
|
161
|
+
expect(row!.val).toBe("after");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("multi-row update ändert alle matches", async () => {
|
|
166
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
167
|
+
await insertMany(db, meta, [{ val: "old" }, { val: "old" }, { val: "keep" }]);
|
|
168
|
+
const result = await updateMany(db, meta, { val: "updated" }, { val: "old" });
|
|
169
|
+
expect(result.length).toBe(2);
|
|
170
|
+
const all = await selectMany<{ val: string }>(db, meta, { val: "updated" });
|
|
171
|
+
expect(all.length).toBe(2);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("deleteMany", () => {
|
|
177
|
+
test("zero match = keine Änderung", async () => {
|
|
178
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
179
|
+
await insertOne(db, meta, { val: "stay" });
|
|
180
|
+
await deleteMany(db, meta, { id: "00000000-0000-4000-8000-000000000001" });
|
|
181
|
+
const rows = await selectMany(db, meta);
|
|
182
|
+
expect(rows.length).toBe(1);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("single delete entfernt genau eine Zeile (verifiziert mit selectMany)", async () => {
|
|
187
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
188
|
+
const ins = await insertOne<{ id: string }>(db, meta, { val: "delete-me" });
|
|
189
|
+
await insertOne(db, meta, { val: "keep" });
|
|
190
|
+
await deleteMany(db, meta, { id: ins!.id });
|
|
191
|
+
const rows = await selectMany(db, meta);
|
|
192
|
+
expect(rows.length).toBe(1);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("deleteMany({}) löscht alle Zeilen (FOOTGUN: kein WHERE-Clause)", async () => {
|
|
197
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
198
|
+
await insertMany(db, meta, [{ val: "a" }, { val: "b" }, { val: "c" }]);
|
|
199
|
+
await deleteMany(db, meta, {});
|
|
200
|
+
const rows = await selectMany(db, meta);
|
|
201
|
+
expect(rows.length).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("transaction", () => {
|
|
207
|
+
test("commit: writes sind danach in DB sichtbar", async () => {
|
|
208
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
209
|
+
await transaction(db, async (tx) => {
|
|
210
|
+
await insertOne(tx, meta, { val: "committed" });
|
|
211
|
+
});
|
|
212
|
+
const rows = await selectMany<{ val: string }>(db, meta);
|
|
213
|
+
expect(rows.length).toBe(1);
|
|
214
|
+
expect(rows[0]!.val).toBe("committed");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("rollback bei throw: writes sind WEG (verifiziert mit selectMany)", async () => {
|
|
219
|
+
await withTable(textCols, async ({ db, meta }) => {
|
|
220
|
+
await expect(
|
|
221
|
+
transaction(db, async (tx) => {
|
|
222
|
+
await insertOne(tx, meta, { val: "should-vanish" });
|
|
223
|
+
throw new Error("forced rollback");
|
|
224
|
+
}),
|
|
225
|
+
).rejects.toThrow("forced rollback");
|
|
226
|
+
// Verifizieren dass die Zeile tatsächlich nicht da ist
|
|
227
|
+
const rows = await selectMany(db, meta);
|
|
228
|
+
expect(rows.length).toBe(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// where-patterns.integration.ts — vollständige WhereObject + WhereOperator-Matrix.
|
|
2
|
+
//
|
|
3
|
+
// WhereObject aus query.ts unterstützt:
|
|
4
|
+
// - primitive equality: string, number, boolean, uuid
|
|
5
|
+
// - null → IS NULL
|
|
6
|
+
// - bare array → IN (shortcut)
|
|
7
|
+
// - WhereOperator: { in: [...] }, { ne: x }, { gt/gte/lt/lte }, { like: str }
|
|
8
|
+
// - multi-field AND-Kombination
|
|
9
|
+
//
|
|
10
|
+
// JSONB top-level equality: wird getestet (JSON.stringify + ::jsonb cast in prepareValue).
|
|
11
|
+
// Deep-path-queries (->>'key') sind out-of-scope für bun-db's WhereObject.
|
|
12
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
13
|
+
import { insertMany, selectMany } from "../query";
|
|
14
|
+
import { closeDb, withTable } from "./_helpers";
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await closeDb();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const strNumCols = [
|
|
21
|
+
{ name: "label", pgType: "text" as const, notNull: true },
|
|
22
|
+
{ name: "score", pgType: "integer" as const, notNull: true },
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const boolCols = [{ name: "active", pgType: "boolean" as const, notNull: false }] as const;
|
|
26
|
+
|
|
27
|
+
const refCols = [{ name: "ref", pgType: "uuid" as const, notNull: false }] as const;
|
|
28
|
+
|
|
29
|
+
describe("where — primitive equality", () => {
|
|
30
|
+
test("string equality", async () => {
|
|
31
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
32
|
+
await insertMany(db, meta, [
|
|
33
|
+
{ label: "alpha", score: 1 },
|
|
34
|
+
{ label: "beta", score: 2 },
|
|
35
|
+
]);
|
|
36
|
+
const rows = await selectMany<{ label: string }>(db, meta, { label: "alpha" });
|
|
37
|
+
expect(rows.length).toBe(1);
|
|
38
|
+
expect(rows[0]!.label).toBe("alpha");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("number equality", async () => {
|
|
43
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
44
|
+
await insertMany(db, meta, [
|
|
45
|
+
{ label: "a", score: 42 },
|
|
46
|
+
{ label: "b", score: 99 },
|
|
47
|
+
]);
|
|
48
|
+
const rows = await selectMany<{ score: number }>(db, meta, { score: 42 });
|
|
49
|
+
expect(rows.length).toBe(1);
|
|
50
|
+
expect(rows[0]!.score).toBe(42);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("boolean equality (true)", async () => {
|
|
55
|
+
await withTable(boolCols, async ({ db, meta }) => {
|
|
56
|
+
await insertMany(db, meta, [{ active: true }, { active: false }, { active: true }]);
|
|
57
|
+
const rows = await selectMany(db, meta, { active: true });
|
|
58
|
+
expect(rows.length).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("boolean equality (false)", async () => {
|
|
63
|
+
await withTable(boolCols, async ({ db, meta }) => {
|
|
64
|
+
await insertMany(db, meta, [{ active: true }, { active: false }]);
|
|
65
|
+
const rows = await selectMany(db, meta, { active: false });
|
|
66
|
+
expect(rows.length).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("uuid equality", async () => {
|
|
71
|
+
await withTable(refCols, async ({ db, meta }) => {
|
|
72
|
+
const target = "00000000-0000-4000-8000-000000000042";
|
|
73
|
+
const other = "00000000-0000-4000-8000-000000000099";
|
|
74
|
+
await insertMany(db, meta, [{ ref: target }, { ref: other }]);
|
|
75
|
+
const rows = await selectMany<{ ref: string }>(db, meta, { ref: target });
|
|
76
|
+
expect(rows.length).toBe(1);
|
|
77
|
+
expect(rows[0]!.ref).toBe(target);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("where — null / IS NULL", () => {
|
|
83
|
+
test("where: { col: null } → IS NULL", async () => {
|
|
84
|
+
await withTable(boolCols, async ({ db, meta }) => {
|
|
85
|
+
await insertMany(db, meta, [{ active: null }, { active: true }]);
|
|
86
|
+
const rows = await selectMany(db, meta, { active: null });
|
|
87
|
+
expect(rows.length).toBe(1);
|
|
88
|
+
expect((rows[0] as { active: unknown }).active).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("where — WhereOperator", () => {
|
|
94
|
+
test("{ in: [...] } → IN-Clause multi-match", async () => {
|
|
95
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
96
|
+
await insertMany(db, meta, [
|
|
97
|
+
{ label: "a", score: 1 },
|
|
98
|
+
{ label: "b", score: 2 },
|
|
99
|
+
{ label: "c", score: 3 },
|
|
100
|
+
]);
|
|
101
|
+
const rows = await selectMany(db, meta, { label: { in: ["a", "c"] } });
|
|
102
|
+
expect(rows.length).toBe(2);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("bare array als IN-Shortcut", async () => {
|
|
107
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
108
|
+
await insertMany(db, meta, [
|
|
109
|
+
{ label: "x", score: 10 },
|
|
110
|
+
{ label: "y", score: 20 },
|
|
111
|
+
{ label: "z", score: 30 },
|
|
112
|
+
]);
|
|
113
|
+
// Array als WhereValue direkt ist valider IN-Shortcut (buildWhereClause L.2)
|
|
114
|
+
const rows = await selectMany(db, meta, { label: ["x", "z"] as unknown as string });
|
|
115
|
+
expect(rows.length).toBe(2);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("{ ne: x } → NOT EQUAL", async () => {
|
|
120
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
121
|
+
await insertMany(db, meta, [
|
|
122
|
+
{ label: "keep", score: 1 },
|
|
123
|
+
{ label: "exclude", score: 2 },
|
|
124
|
+
]);
|
|
125
|
+
const rows = await selectMany<{ label: string }>(db, meta, { label: { ne: "exclude" } });
|
|
126
|
+
expect(rows.length).toBe(1);
|
|
127
|
+
expect(rows[0]!.label).toBe("keep");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("{ gt: x } → greater than", async () => {
|
|
132
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
133
|
+
await insertMany(db, meta, [
|
|
134
|
+
{ label: "low", score: 5 },
|
|
135
|
+
{ label: "mid", score: 50 },
|
|
136
|
+
{ label: "high", score: 100 },
|
|
137
|
+
]);
|
|
138
|
+
const rows = await selectMany(db, meta, { score: { gt: 10 } });
|
|
139
|
+
expect(rows.length).toBe(2);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("{ lte: x } → less-or-equal", async () => {
|
|
144
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
145
|
+
await insertMany(db, meta, [
|
|
146
|
+
{ label: "a", score: 1 },
|
|
147
|
+
{ label: "b", score: 5 },
|
|
148
|
+
{ label: "c", score: 10 },
|
|
149
|
+
]);
|
|
150
|
+
const rows = await selectMany(db, meta, { score: { lte: 5 } });
|
|
151
|
+
expect(rows.length).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("{ like: pattern } → LIKE-Match", async () => {
|
|
156
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
157
|
+
await insertMany(db, meta, [
|
|
158
|
+
{ label: "hello-world", score: 1 },
|
|
159
|
+
{ label: "hello-bun", score: 2 },
|
|
160
|
+
{ label: "goodbye", score: 3 },
|
|
161
|
+
]);
|
|
162
|
+
const rows = await selectMany(db, meta, { label: { like: "hello%" } });
|
|
163
|
+
expect(rows.length).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("where — multi-field AND", () => {
|
|
169
|
+
test("zwei Felder kombiniert (UND-Semantik)", async () => {
|
|
170
|
+
await withTable(strNumCols, async ({ db, meta }) => {
|
|
171
|
+
await insertMany(db, meta, [
|
|
172
|
+
{ label: "target", score: 7 },
|
|
173
|
+
{ label: "target", score: 99 },
|
|
174
|
+
{ label: "other", score: 7 },
|
|
175
|
+
]);
|
|
176
|
+
const rows = await selectMany<{ label: string; score: number }>(db, meta, {
|
|
177
|
+
label: "target",
|
|
178
|
+
score: 7,
|
|
179
|
+
});
|
|
180
|
+
expect(rows.length).toBe(1);
|
|
181
|
+
expect(rows[0]!.label).toBe("target");
|
|
182
|
+
expect(rows[0]!.score).toBe(7);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|