@cosmicdrift/kumiko-framework 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
- package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
- package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
- package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
- package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
- package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
- package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
- package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
- package/src/api/__tests__/api.test.ts +1 -1
- package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
- package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
- package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/csrf-middleware.test.ts +1 -1
- package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
- package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
- package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
- package/src/api/__tests__/readiness.test.ts +1 -1
- package/src/api/__tests__/request-id-middleware.test.ts +1 -1
- package/src/api/__tests__/sse-broker.test.ts +12 -12
- package/src/api/__tests__/sse-route.test.ts +1 -1
- package/src/api/auth-routes.ts +2 -5
- package/src/api/readiness.ts +2 -2
- package/src/auth/__tests__/roles.test.ts +2 -2
- package/src/bun-db/__tests__/PATTERN.md +73 -0
- package/src/bun-db/__tests__/_helpers.ts +103 -0
- package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
- package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
- package/src/bun-db/__tests__/bun-test-db.ts +19 -0
- package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
- package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
- package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
- package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
- package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
- package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
- package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
- package/src/bun-db/connection.ts +84 -0
- package/src/bun-db/index.ts +31 -0
- package/src/bun-db/query.ts +842 -0
- package/src/compliance/__tests__/duration-spec.test.ts +1 -1
- package/src/compliance/__tests__/profiles.test.ts +1 -1
- package/src/compliance/__tests__/sub-processors.test.ts +1 -1
- package/src/compliance/profiles.ts +1 -4
- package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
- package/src/db/__tests__/big-int-field.test.ts +15 -14
- package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
- package/src/db/__tests__/connection-options.test.ts +1 -1
- package/src/db/__tests__/cursor.test.ts +8 -32
- package/src/db/__tests__/dialect-instant.test.ts +1 -1
- package/src/db/__tests__/encryption.test.ts +1 -1
- package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
- package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
- package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
- package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/migrate-generator.test.ts +71 -0
- package/src/db/__tests__/migrate-runner.test.ts +19 -0
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
- package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
- package/src/db/__tests__/pg-error.test.ts +43 -0
- package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
- package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
- package/src/db/__tests__/sql-inventory.test.ts +56 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
- package/src/db/__tests__/table-builder-required.test.ts +20 -22
- package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
- package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
- package/src/db/api.ts +46 -0
- package/src/db/apply-entity-event.ts +45 -36
- package/src/db/assert-exists-in.ts +5 -16
- package/src/db/bun-provider.ts +37 -0
- package/src/db/config-seed.ts +4 -4
- package/src/db/connection.ts +14 -57
- package/src/db/cursor.ts +5 -56
- package/src/db/dialect.ts +472 -99
- package/src/db/eagerload.ts +5 -12
- package/src/db/entity-table-meta.ts +390 -0
- package/src/db/event-store-executor.ts +158 -100
- package/src/db/index.ts +33 -5
- package/src/db/migrate-generator.ts +350 -0
- package/src/db/migrate-runner.ts +206 -0
- package/src/db/postgres-provider.ts +25 -0
- package/src/db/queries/entity-read.ts +15 -0
- package/src/db/queries/es-ops.ts +17 -0
- package/src/db/queries/event-consumer.ts +170 -0
- package/src/db/queries/event-store-admin.ts +127 -0
- package/src/db/queries/event-store.ts +155 -0
- package/src/db/queries/projection-rebuild.ts +59 -0
- package/src/db/queries/raw-sql.ts +15 -0
- package/src/db/queries/schema-drift.ts +35 -0
- package/src/db/queries/seed-context.ts +58 -0
- package/src/db/queries/table-ops.ts +11 -0
- package/src/db/queries/test-stack.ts +56 -0
- package/src/db/query-api.ts +22 -0
- package/src/db/query.ts +30 -0
- package/src/db/reference-data.ts +19 -22
- package/src/db/render-ddl.ts +57 -0
- package/src/db/row-helpers.ts +3 -52
- package/src/db/schema-inspection.ts +17 -4
- package/src/db/sql-inventory.ts +208 -0
- package/src/db/table-builder.ts +54 -46
- package/src/db/tenant-db.ts +105 -326
- package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
- package/src/engine/__tests__/boot-validator.test.ts +4 -3
- package/src/engine/__tests__/build-app-schema.test.ts +1 -1
- package/src/engine/__tests__/build-target.test.ts +1 -1
- package/src/engine/__tests__/claim-keys.test.ts +1 -1
- package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
- package/src/engine/__tests__/config-helpers.test.ts +1 -1
- package/src/engine/__tests__/duration-utils.test.ts +16 -0
- package/src/engine/__tests__/effective-features.test.ts +1 -1
- package/src/engine/__tests__/engine.test.ts +1 -1
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +3 -3
- package/src/engine/__tests__/extends-registrar.test.ts +4 -4
- package/src/engine/__tests__/factories-long-text.test.ts +1 -1
- package/src/engine/__tests__/factories-time.test.ts +1 -1
- package/src/engine/__tests__/field-access.test.ts +38 -0
- package/src/engine/__tests__/field-predicates.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +1 -1
- package/src/engine/__tests__/identifiers.test.ts +1 -1
- package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/no-return-guard.test.ts +17 -0
- package/src/engine/__tests__/ownership.test.ts +10 -11
- package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
- package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
- package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
- package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +1 -1
- package/src/engine/__tests__/projection-helpers.test.ts +25 -17
- package/src/engine/__tests__/projection.test.ts +4 -4
- package/src/engine/__tests__/qualified-name.test.ts +1 -1
- package/src/engine/__tests__/raw-table.test.ts +9 -8
- package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
- package/src/engine/__tests__/run-in.test.ts +1 -1
- package/src/engine/__tests__/schema-builder.test.ts +1 -1
- package/src/engine/__tests__/screen.test.ts +1 -1
- package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
- package/src/engine/__tests__/state-machine.test.ts +1 -1
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
- package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
- package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
- package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
- package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
- package/src/engine/__tests__/steps-read.test.ts +34 -40
- package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
- package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
- package/src/engine/__tests__/steps-workflow.test.ts +7 -7
- package/src/engine/__tests__/system-user.test.ts +1 -1
- package/src/engine/__tests__/unmanaged-table.test.ts +98 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
- package/src/engine/__tests__/validation-hooks.test.ts +1 -1
- package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
- package/src/engine/boot-validator/entity-handler.ts +3 -3
- package/src/engine/boot-validator/ownership.ts +1 -1
- package/src/engine/define-feature.ts +37 -2
- package/src/engine/entity-handlers.ts +5 -5
- package/src/engine/factories.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
- package/src/engine/feature-ast/extractors/shared.ts +2 -3
- package/src/engine/ownership.ts +113 -41
- package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
- package/src/engine/projection-helpers.ts +2 -11
- package/src/engine/registry.ts +21 -2
- package/src/engine/steps/read-find-many.ts +13 -13
- package/src/engine/steps/read-find-one.ts +7 -9
- package/src/engine/steps/unsafe-projection-delete.ts +4 -5
- package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
- package/src/engine/types/feature.ts +47 -2
- package/src/engine/types/fields.ts +4 -5
- package/src/engine/types/index.ts +2 -0
- package/src/engine/types/step.ts +10 -10
- package/src/engine/validate-projection-allowlist.ts +23 -3
- package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
- package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
- package/src/env/__tests__/compose-env-schema.test.ts +1 -1
- package/src/env/__tests__/dry-run.test.ts +1 -1
- package/src/errors/__tests__/classes.test.ts +1 -1
- package/src/errors/__tests__/error-helpers.test.ts +44 -0
- package/src/errors/__tests__/field-issue-compat.test.ts +16 -0
- package/src/errors/__tests__/write-failures.test.ts +1 -1
- package/src/errors/classes.ts +5 -19
- package/src/errors/field-issue.ts +11 -0
- package/src/errors/index.ts +1 -0
- package/src/errors/zod-bridge.ts +3 -2
- package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
- package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
- package/src/es-ops/__tests__/runner.test.ts +29 -19
- package/src/es-ops/context.ts +11 -56
- package/src/es-ops/operations-schema.ts +2 -2
- package/src/es-ops/runner.ts +12 -26
- package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
- package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
- package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
- package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
- package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
- package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
- package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
- package/src/event-store/admin-api.ts +55 -83
- package/src/event-store/archive.ts +15 -39
- package/src/event-store/event-store.ts +92 -86
- package/src/event-store/events-schema.ts +2 -1
- package/src/event-store/index.ts +1 -0
- package/src/event-store/snapshot.ts +26 -24
- package/src/event-store/upcaster-dead-letter.ts +19 -18
- package/src/files/__tests__/content-disposition.test.ts +1 -1
- package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
- package/src/files/__tests__/file-handle.test.ts +1 -1
- package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
- package/src/files/__tests__/read-stream.test.ts +1 -1
- package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
- package/src/files/__tests__/write-stream.test.ts +1 -1
- package/src/files/__tests__/zip-stream.test.ts +1 -1
- package/src/files/file-ref-table.ts +2 -2
- package/src/files/file-routes.ts +7 -9
- package/src/files/storage-tracking.ts +9 -17
- package/src/i18n/__tests__/i18n.test.ts +1 -1
- package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
- package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
- package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
- package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
- package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
- package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
- package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
- package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
- package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
- package/src/migrations/projection-detection.ts +12 -1
- package/src/migrations/schema-drift.ts +7 -23
- package/src/observability/__tests__/console-provider.test.ts +1 -1
- package/src/observability/__tests__/metric-validator.test.ts +1 -1
- package/src/observability/__tests__/noop-provider.test.ts +1 -1
- package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
- package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-tracer.test.ts +1 -1
- package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
- package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
- package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
- package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
- package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
- package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
- package/src/pipeline/__tests__/dispatcher-utils.test.ts +107 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
- package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
- package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
- package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
- package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
- package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
- package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
- package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
- package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
- package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
- package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
- package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
- package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
- package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
- package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
- package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
- package/src/pipeline/__tests__/redis-keys.test.ts +12 -0
- package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- package/src/pipeline/dispatcher-utils.ts +8 -7
- package/src/pipeline/dispatcher.ts +43 -48
- package/src/pipeline/event-consumer-state.ts +11 -2
- package/src/pipeline/event-dispatcher.ts +86 -146
- package/src/pipeline/event-retention.ts +14 -24
- package/src/pipeline/msp-rebuild.ts +54 -78
- package/src/pipeline/projection-rebuild.ts +65 -67
- package/src/pipeline/projection-state.ts +2 -2
- package/src/random/__tests__/generate.test.ts +13 -13
- package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
- package/src/redis/__tests__/redis-options.test.ts +1 -1
- package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
- package/src/search/__tests__/search-adapter.test.ts +1 -1
- package/src/secrets/__tests__/dek-cache.test.ts +1 -3
- package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/__tests__/leak-guard.test.ts +1 -1
- package/src/secrets/__tests__/rotation.test.ts +1 -1
- package/src/stack/db.ts +25 -48
- package/src/stack/push-entity-projection-tables.ts +2 -4
- package/src/stack/table-helpers.ts +98 -61
- package/src/stack/test-stack.ts +10 -9
- package/src/testing/__tests__/db-cleanup.test.ts +40 -0
- package/src/testing/__tests__/e2e-generator.test.ts +1 -1
- package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
- package/src/testing/db-cleanup.ts +44 -0
- package/src/testing/expect-error.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/multipart-helper.ts +94 -0
- package/src/testing/shared-entities.ts +5 -5
- package/src/time/__tests__/polyfill.test.ts +1 -1
- package/src/time/__tests__/tz-context.test.ts +1 -1
- package/src/utils/__tests__/assert.test.ts +1 -1
- package/src/utils/__tests__/case.test.ts +16 -0
- package/src/utils/__tests__/env-parse.test.ts +1 -1
- package/src/utils/__tests__/is-plain-object.test.ts +16 -0
- package/src/utils/__tests__/parse-string-array-json.test.ts +16 -0
- package/src/utils/__tests__/safe-json.test.ts +22 -0
- package/src/utils/case.ts +6 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/is-plain-object.ts +4 -0
- package/src/utils/parse-string-array-json.ts +14 -0
- package/CHANGELOG.md +0 -474
- package/src/db/__tests__/db-helpers.test.ts +0 -369
- package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
- package/src/db/__tests__/row-helpers.test.ts +0 -59
- package/src/engine/steps/_drizzle-boundary.ts +0 -19
- package/src/files/__tests__/file-field-column.integration.ts +0 -103
package/src/stack/db.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Test-DB Factory: CREATE DATABASE → connect → createEventsTable.
|
|
2
|
+
// Provider-agnostic via createConnection (DB_PROVIDER env).
|
|
3
|
+
// postgres-js = default. DB_PROVIDER=bun = Bun.SQL (experimentell).
|
|
4
|
+
|
|
5
|
+
import { createConnection } from "../db/api";
|
|
6
|
+
import { createDatabase, databaseExists, dropDatabaseIfExists } from "../db/queries/test-stack";
|
|
7
|
+
import { ensureTemporalPolyfill } from "../time/polyfill";
|
|
3
8
|
import { generateId } from "../utils";
|
|
4
9
|
|
|
5
10
|
function requireEnv(name: string): string {
|
|
@@ -13,88 +18,60 @@ function requireEnv(name: string): string {
|
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export type TestDb = {
|
|
16
|
-
db:
|
|
17
|
-
client:
|
|
21
|
+
db: unknown;
|
|
22
|
+
client: unknown;
|
|
18
23
|
dbName: string;
|
|
19
24
|
cleanup: () => Promise<void>;
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
export type CreateTestDbOptions = {
|
|
23
|
-
/** Override TEST_DATABASE_URL. Rare — mostly for tests that want a
|
|
24
|
-
* non-default Postgres (e.g. a read-replica probe). */
|
|
25
28
|
readonly baseUrl?: string;
|
|
26
|
-
/** Use a specific DB name instead of the default
|
|
27
|
-
* `kumiko_test_<8chars>`. Combined with `persistent: true`, lets a
|
|
28
|
-
* dev server keep state across restarts. Must be a legal Postgres
|
|
29
|
-
* identifier — the caller is responsible for matching the usual
|
|
30
|
-
* [a-z_0-9]+ shape. */
|
|
31
29
|
readonly dbName?: string;
|
|
32
|
-
/** When true, cleanup() is a no-op and the DB survives. Also
|
|
33
|
-
* changes CREATE DATABASE to IF-NOT-EXISTS semantics so restarts
|
|
34
|
-
* reuse the same storage. Default false (test contract: fresh DB
|
|
35
|
-
* per call, dropped on cleanup). */
|
|
36
30
|
readonly persistent?: boolean;
|
|
37
31
|
};
|
|
38
32
|
|
|
39
33
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* call `createTestDb()` with no args; only dev-server and niche tests
|
|
43
|
-
* need the options form.
|
|
34
|
+
* Provider-agnostische Test-DB. createConnection liest DB_PROVIDER.
|
|
35
|
+
* Für Bun.SQL: DB_PROVIDER=bun setzen (experimentell — siehe db/bun-provider.ts).
|
|
44
36
|
*/
|
|
45
37
|
export async function createTestDb(arg?: string | CreateTestDbOptions): Promise<TestDb> {
|
|
38
|
+
await ensureTemporalPolyfill();
|
|
46
39
|
const opts: CreateTestDbOptions = typeof arg === "string" ? { baseUrl: arg } : (arg ?? {});
|
|
47
40
|
const url = opts.baseUrl ?? requireEnv("TEST_DATABASE_URL");
|
|
48
|
-
// slice(-8) — the last 8 hex chars of a UUIDv7 are pure random (the
|
|
49
|
-
// front 48 bits are a timestamp, which would collide across workers
|
|
50
|
-
// that start within the same millisecond).
|
|
51
41
|
const dbName = opts.dbName ?? `kumiko_test_${generateId().slice(-8)}`;
|
|
52
42
|
const adminUrl = url.replace(/\/[^/]+$/, "/postgres");
|
|
53
43
|
|
|
54
|
-
const
|
|
44
|
+
const admin = await createConnection(adminUrl, { maxConnections: 1 });
|
|
55
45
|
try {
|
|
56
46
|
if (opts.persistent) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const existing = await adminClient<{ exists: boolean }[]>`
|
|
60
|
-
SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = ${dbName}) AS exists
|
|
61
|
-
`;
|
|
62
|
-
if (!existing[0]?.exists) {
|
|
63
|
-
await adminClient.unsafe(`CREATE DATABASE "${dbName}"`);
|
|
47
|
+
if (!(await databaseExists(admin.db, dbName))) {
|
|
48
|
+
await createDatabase(admin.db, dbName);
|
|
64
49
|
}
|
|
65
50
|
} else {
|
|
66
|
-
await
|
|
51
|
+
await createDatabase(admin.db, dbName);
|
|
67
52
|
}
|
|
68
53
|
} finally {
|
|
69
|
-
await
|
|
54
|
+
await admin.close();
|
|
70
55
|
}
|
|
71
56
|
|
|
72
57
|
const testUrl = url.replace(/\/[^/]+$/, `/${dbName}`);
|
|
73
|
-
const
|
|
74
|
-
const db = drizzle(client);
|
|
58
|
+
const conn = await createConnection(testUrl);
|
|
75
59
|
|
|
76
|
-
// Every ES-entity writes events; auto-create the events table so tests that
|
|
77
|
-
// go straight to createTestDb (not setupTestStack) also work out of the box.
|
|
78
|
-
// In persistent mode this is idempotent: createEventsTable emits IF NOT
|
|
79
|
-
// EXISTS so a second boot is a no-op.
|
|
80
60
|
const { createEventsTable } = await import("../event-store");
|
|
81
|
-
await createEventsTable(db);
|
|
61
|
+
await createEventsTable(conn.db);
|
|
82
62
|
|
|
83
63
|
return {
|
|
84
|
-
db,
|
|
85
|
-
client,
|
|
64
|
+
db: conn.db,
|
|
65
|
+
client: conn.client,
|
|
86
66
|
dbName,
|
|
87
67
|
cleanup: async () => {
|
|
88
|
-
await
|
|
89
|
-
// Persistent mode: dev-server owns the DB lifecycle — don't drop
|
|
90
|
-
// on process exit. `yarn kumiko clean-test-dbs` is the escape
|
|
91
|
-
// hatch when you really want to start over.
|
|
68
|
+
await conn.close();
|
|
92
69
|
if (!opts.persistent) {
|
|
93
|
-
const
|
|
70
|
+
const admin2 = await createConnection(adminUrl, { maxConnections: 1 });
|
|
94
71
|
try {
|
|
95
|
-
await
|
|
72
|
+
await dropDatabaseIfExists(admin2.db, dbName);
|
|
96
73
|
} finally {
|
|
97
|
-
await
|
|
74
|
+
await admin2.close();
|
|
98
75
|
}
|
|
99
76
|
}
|
|
100
77
|
},
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getTableName } from "drizzle-orm";
|
|
2
1
|
import { tableExists } from "../db/schema-inspection";
|
|
3
2
|
import type { Registry } from "../engine/types";
|
|
4
3
|
import { unsafePushTables } from "./table-helpers";
|
|
@@ -35,9 +34,8 @@ export async function pushEntityProjectionTables(
|
|
|
35
34
|
if (!proj.isImplicit) continue;
|
|
36
35
|
if (seen.has(proj.table)) continue;
|
|
37
36
|
seen.add(proj.table);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const physical = getTableName(proj.table as Parameters<typeof getTableName>[0]);
|
|
37
|
+
const tableRec = proj.table as unknown as Record<symbol, unknown>;
|
|
38
|
+
const physical = tableRec[Symbol.for("kumiko:schema:Name")] as string;
|
|
41
39
|
if (await tableExists(stack.db, `public.${physical}`)) {
|
|
42
40
|
logInfo(`[kumiko-stack] table ${physical} already exists — skipping create`);
|
|
43
41
|
continue;
|
|
@@ -1,42 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import
|
|
1
|
+
import type { DbConnection } from "../db/connection";
|
|
2
|
+
import { pgTypeToSqlType } from "../db/dialect";
|
|
3
|
+
import type { ColumnMeta, EntityTableMeta } from "../db/entity-table-meta";
|
|
4
|
+
import {
|
|
5
|
+
alterTableAddColumn,
|
|
6
|
+
createIndexIfNotExists,
|
|
7
|
+
executeDdlStatement,
|
|
8
|
+
truncateTablesRestartIdentity,
|
|
9
|
+
} from "../db/queries/test-stack";
|
|
10
|
+
import { renderTableDdl } from "../db/render-ddl";
|
|
5
11
|
import { tableExists } from "../db/schema-inspection";
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
12
|
+
import { buildEntityTable, toTableName } from "../db/table-builder";
|
|
13
|
+
import type { EventDispatcher } from "../pipeline";
|
|
14
|
+
|
|
15
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
16
|
+
function tableNameOf(table: unknown): string {
|
|
17
|
+
if (typeof table !== "object" || table === null) {
|
|
18
|
+
throw new Error("table-helpers: table is not a SchemaTable object");
|
|
19
|
+
}
|
|
20
|
+
const rec = table as Record<string | symbol, unknown>;
|
|
21
|
+
if (typeof rec[KUMIKO_NAME_SYMBOL] === "string") return rec[KUMIKO_NAME_SYMBOL] as string;
|
|
22
|
+
if (typeof (rec as { tableName?: unknown }).tableName === "string") {
|
|
23
|
+
return (rec as { tableName: string }).tableName;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("table-helpers: table has no name");
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
/**
|
|
10
|
-
* Bypass: creates
|
|
11
|
-
*
|
|
12
|
-
* via `r.entity(...)
|
|
13
|
-
* for free — this helper is reserved for framework-internal meta-tables
|
|
14
|
-
* (event-store, snapshots, projection-state) and test setup.
|
|
15
|
-
*
|
|
16
|
-
* Strict: raises a postgres `relation already exists` (42P07) error if
|
|
17
|
-
* the table is already there. Use `unsafeEnsureEntityTable` for the
|
|
18
|
-
* idempotent boot-path variant.
|
|
29
|
+
* Bypass: creates an entity-table directly without going through the
|
|
30
|
+
* full registry. Reserved for framework-internal meta-tables and
|
|
31
|
+
* test setup — apps declare data via `r.entity(...)`.
|
|
19
32
|
*/
|
|
20
33
|
export async function unsafeCreateEntityTable(
|
|
21
|
-
db:
|
|
34
|
+
db: DbConnection,
|
|
22
35
|
entity: import("../engine/types").EntityDefinition,
|
|
23
36
|
entityName?: string,
|
|
24
37
|
): Promise<void> {
|
|
25
|
-
const table =
|
|
38
|
+
const table = buildEntityTable(entityName ?? "entity", entity);
|
|
26
39
|
await unsafePushTables(db, { [entityName ?? "entity"]: table });
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
/**
|
|
30
|
-
* Bypass (idempotent): same caveat as `unsafeCreateEntityTable` —
|
|
31
|
-
* apps declare data via `r.entity(...)`. Checks whether the entity's
|
|
32
|
-
* table already exists and skips creation if so. Schema-drift is *not*
|
|
33
|
-
* detected: if the table is there but has the wrong columns, that's
|
|
34
|
-
* the caller's problem (the dev-server contract is "drop the DB by
|
|
35
|
-
* hand when you change the schema"). Tests should use
|
|
36
|
-
* `unsafeCreateEntityTable` instead, since they rely on fresh DBs.
|
|
37
|
-
*/
|
|
38
42
|
export async function unsafeEnsureEntityTable(
|
|
39
|
-
db:
|
|
43
|
+
db: DbConnection,
|
|
40
44
|
entity: import("../engine/types").EntityDefinition,
|
|
41
45
|
entityName?: string,
|
|
42
46
|
): Promise<boolean> {
|
|
@@ -46,54 +50,88 @@ export async function unsafeEnsureEntityTable(
|
|
|
46
50
|
return true;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
// Tables produced by the native dialect already carry EntityTableMeta-shape
|
|
54
|
+
// (source/columns/indexes). renderTableDdl converts that to CREATE TABLE +
|
|
55
|
+
// CREATE INDEX statements executed via db/queries/test-stack.
|
|
56
|
+
function tableToMeta(table: unknown): EntityTableMeta {
|
|
57
|
+
if (
|
|
58
|
+
typeof table === "object" &&
|
|
59
|
+
table !== null &&
|
|
60
|
+
"tableName" in table &&
|
|
61
|
+
"columns" in table &&
|
|
62
|
+
"indexes" in table &&
|
|
63
|
+
"source" in table
|
|
64
|
+
) {
|
|
65
|
+
return table as EntityTableMeta;
|
|
66
|
+
}
|
|
67
|
+
throw new Error("unsafePushTables: argument is not a SchemaTable / EntityTableMeta");
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
/**
|
|
50
|
-
* Bypass: pushes
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* Reserved for framework-internal meta-tables (event-store, projections,
|
|
54
|
-
* consumer-state) and test setup — apps declare data via `r.entity(...)`.
|
|
71
|
+
* Bypass: pushes table definitions to the database directly. Produces
|
|
72
|
+
* CREATE TABLE IF NOT EXISTS + CREATE INDEX statements via renderTableDdl
|
|
73
|
+
* and executes them via db/queries/test-stack. Idempotent re-runs are safe.
|
|
55
74
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
75
|
+
* Reserved for framework-internal meta-tables + test setup. App-defined
|
|
76
|
+
* entities go through `kumiko schema apply` (committed SQL files).
|
|
58
77
|
*/
|
|
59
78
|
export async function unsafePushTables(
|
|
60
|
-
db:
|
|
79
|
+
db: DbConnection,
|
|
61
80
|
tables: Record<string, unknown>,
|
|
62
81
|
prevTables?: Record<string, unknown>,
|
|
63
82
|
): Promise<void> {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
const prevMetas = new Map<string, EntityTableMeta>();
|
|
84
|
+
if (prevTables) {
|
|
85
|
+
for (const [key, table] of Object.entries(prevTables)) {
|
|
86
|
+
const meta = tableToMeta(table);
|
|
87
|
+
prevMetas.set(key, meta);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const [key, table] of Object.entries(tables)) {
|
|
92
|
+
const meta = tableToMeta(table);
|
|
93
|
+
const prev = prevMetas.get(key);
|
|
94
|
+
|
|
95
|
+
if (prev) {
|
|
96
|
+
const prevCols = new Set(prev.columns.map((c) => c.name));
|
|
97
|
+
for (const col of meta.columns) {
|
|
98
|
+
if (!prevCols.has(col.name)) {
|
|
99
|
+
const type = renderColumnType(col);
|
|
100
|
+
const notNull = col.notNull && !col.primaryKey ? " NOT NULL" : "";
|
|
101
|
+
const defaultClause = col.defaultSql !== undefined ? ` DEFAULT ${col.defaultSql}` : "";
|
|
102
|
+
await alterTableAddColumn(db, meta.tableName, col.name, type, defaultClause, notNull);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const prevIdxNames = new Set(prev.indexes.map((i) => i.name));
|
|
106
|
+
for (const idx of meta.indexes) {
|
|
107
|
+
if (!prevIdxNames.has(idx.name)) {
|
|
108
|
+
const kind = idx.unique ? "UNIQUE INDEX" : "INDEX";
|
|
109
|
+
const colList = idx.columns.map((c) => `"${c}"`).join(", ");
|
|
110
|
+
await createIndexIfNotExists(db, kind, idx.name, meta.tableName, colList);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const statements = renderTableDdl(meta);
|
|
115
|
+
for (const stmt of statements) {
|
|
116
|
+
await executeDdlStatement(db, stmt);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
69
119
|
}
|
|
70
120
|
}
|
|
71
121
|
|
|
122
|
+
function renderColumnType(col: ColumnMeta): string {
|
|
123
|
+
return pgTypeToSqlType(col.pgType);
|
|
124
|
+
}
|
|
125
|
+
|
|
72
126
|
/**
|
|
73
127
|
* Wipes event store + framework-state + the given feature read-models in
|
|
74
128
|
* one TRUNCATE, then re-registers the event-consumer state rows. Used in
|
|
75
129
|
* test beforeEach-hooks to return the stack to a clean slate without
|
|
76
130
|
* rebuilding it.
|
|
77
|
-
*
|
|
78
|
-
* Fixed list of framework tables (kumiko_events, kumiko_event_consumers,
|
|
79
|
-
* kumiko_archived_streams, kumiko_snapshots, kumiko_projections) is always
|
|
80
|
-
* included — any event-sourced test setup needs those cleared. The
|
|
81
|
-
* `extraTables` arg covers the feature's own read-model tables that would
|
|
82
|
-
* otherwise accumulate rows across tests.
|
|
83
|
-
*
|
|
84
|
-
* Accepts either a Drizzle PgTable (for locally-defined tables: getTableName
|
|
85
|
-
* extracts the SQL name) or a plain string (for SQL names whose Drizzle
|
|
86
|
-
* reference lives in another module and importing it for the TRUNCATE
|
|
87
|
-
* alone would be overkill). Both round-trip to the same TRUNCATE list.
|
|
88
|
-
*
|
|
89
|
-
* Pre-existing code duplicates this block 30+ times, each with its own
|
|
90
|
-
* list of extras. The helper collapses that to a one-liner per test and
|
|
91
|
-
* lets a future change to the framework-table set (e.g. adding a new
|
|
92
|
-
* consumer-state table) ripple through without touching every suite.
|
|
93
131
|
*/
|
|
94
132
|
export async function resetEventStore(
|
|
95
|
-
stack:
|
|
96
|
-
extraTables: readonly (
|
|
133
|
+
stack: { db: unknown; eventDispatcher?: EventDispatcher },
|
|
134
|
+
extraTables: readonly (unknown | string)[] = [],
|
|
97
135
|
): Promise<void> {
|
|
98
136
|
const frameworkTables = [
|
|
99
137
|
"kumiko_events",
|
|
@@ -102,9 +140,8 @@ export async function resetEventStore(
|
|
|
102
140
|
"kumiko_snapshots",
|
|
103
141
|
"kumiko_projections",
|
|
104
142
|
];
|
|
105
|
-
const extraNames = extraTables.map((t) => (typeof t === "string" ? t :
|
|
106
|
-
|
|
107
|
-
await stack.db.execute(sql.raw(`TRUNCATE ${allTables.join(", ")} RESTART IDENTITY CASCADE`));
|
|
143
|
+
const extraNames = extraTables.map((t) => (typeof t === "string" ? t : tableNameOf(t)));
|
|
144
|
+
await truncateTablesRestartIdentity(stack.db, [...frameworkTables, ...extraNames]);
|
|
108
145
|
if (stack.eventDispatcher) {
|
|
109
146
|
await stack.eventDispatcher.ensureRegistered();
|
|
110
147
|
}
|
package/src/stack/test-stack.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { AuthRoutesConfig } from "../api/auth-routes";
|
|
|
3
3
|
import type { JwtHelper } from "../api/jwt";
|
|
4
4
|
import { buildServer } from "../api/server";
|
|
5
5
|
import { createSseBroker } from "../api/sse-broker";
|
|
6
|
-
import type {
|
|
6
|
+
import type { PgClient } from "../db/connection";
|
|
7
|
+
import { extractTableInfo } from "../db/query";
|
|
7
8
|
import { createRegistry } from "../engine/registry";
|
|
8
9
|
import type { FeatureDefinition, Registry, TenantId } from "../engine/types";
|
|
9
10
|
import { createArchivedStreamsTable, createEventsTable } from "../event-store";
|
|
@@ -23,9 +24,8 @@ export type TestStack = {
|
|
|
23
24
|
app: Hono;
|
|
24
25
|
jwt: JwtHelper;
|
|
25
26
|
registry: Registry;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
db: DbConnection;
|
|
27
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-provider connection
|
|
28
|
+
db: any;
|
|
29
29
|
redis: TestRedis;
|
|
30
30
|
search: SearchAdapter;
|
|
31
31
|
events: EventCollector;
|
|
@@ -58,7 +58,8 @@ export type TestStackOptions = {
|
|
|
58
58
|
| Record<string, unknown>
|
|
59
59
|
| ((deps: {
|
|
60
60
|
registry: Registry;
|
|
61
|
-
|
|
61
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-provider connection
|
|
62
|
+
db: any;
|
|
62
63
|
sseBroker: import("../api/sse-broker").SseBroker;
|
|
63
64
|
redis: import("ioredis").default;
|
|
64
65
|
}) => Record<string, unknown>);
|
|
@@ -110,7 +111,8 @@ export type TestStackOptions = {
|
|
|
110
111
|
| import("../api/server").ServerOptions["anonymousAccess"]
|
|
111
112
|
| ((deps: {
|
|
112
113
|
registry: Registry;
|
|
113
|
-
|
|
114
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-provider connection
|
|
115
|
+
db: any;
|
|
114
116
|
sseBroker: import("../api/sse-broker").SseBroker;
|
|
115
117
|
redis: import("ioredis").default;
|
|
116
118
|
}) => import("../api/server").ServerOptions["anonymousAccess"]);
|
|
@@ -206,10 +208,9 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
206
208
|
// exist; drizzle-kit's diff machinery would otherwise emit CREATE for
|
|
207
209
|
// them again.
|
|
208
210
|
const { tableExists } = await import("../db/schema-inspection");
|
|
209
|
-
const { getTableName } = await import("drizzle-orm");
|
|
210
211
|
const missing: Record<string, unknown> = {};
|
|
211
212
|
for (const [key, tbl] of Object.entries(projectionTables)) {
|
|
212
|
-
const physical =
|
|
213
|
+
const physical = extractTableInfo(tbl).name;
|
|
213
214
|
if (await tableExists(testDb.db, `public.${physical}`)) continue;
|
|
214
215
|
missing[key] = tbl;
|
|
215
216
|
}
|
|
@@ -294,7 +295,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
294
295
|
// post-commit latency (Sprint E.4).
|
|
295
296
|
eventDispatcher: {
|
|
296
297
|
pollIntervalMs: 50,
|
|
297
|
-
pgClient: testDb.client,
|
|
298
|
+
pgClient: testDb.client as PgClient | undefined,
|
|
298
299
|
systemConsumers: {
|
|
299
300
|
sse: enabledHooks.includes("sse"),
|
|
300
301
|
search: enabledHooks.includes("search"),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { clearTables } from "../db-cleanup";
|
|
3
|
+
|
|
4
|
+
describe("db-cleanup", () => {
|
|
5
|
+
test("clearTables issues DELETE without WHERE per table via deleteMany", async () => {
|
|
6
|
+
const sqlLog: string[] = [];
|
|
7
|
+
const mockDb = {
|
|
8
|
+
unsafe: async (sql: string) => {
|
|
9
|
+
sqlLog.push(sql);
|
|
10
|
+
return [];
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
await clearTables(mockDb, ["read_users", "kumiko_events"]);
|
|
15
|
+
|
|
16
|
+
expect(sqlLog).toHaveLength(2);
|
|
17
|
+
expect(sqlLog[0]).toBe('DELETE FROM "read_users"');
|
|
18
|
+
expect(sqlLog[1]).toBe('DELETE FROM "kumiko_events"');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("clearTables accepts EntityTableMeta-shaped tables", async () => {
|
|
22
|
+
const sqlLog: string[] = [];
|
|
23
|
+
const mockDb = {
|
|
24
|
+
unsafe: async (sql: string) => {
|
|
25
|
+
sqlLog.push(sql);
|
|
26
|
+
return [];
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const userTable = {
|
|
31
|
+
source: "managed" as const,
|
|
32
|
+
tableName: "read_users",
|
|
33
|
+
columns: [{ name: "id", pgType: "uuid", notNull: true, primaryKey: true }],
|
|
34
|
+
indexes: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await clearTables(mockDb, [userTable]);
|
|
38
|
+
expect(sqlLog[0]).toBe('DELETE FROM "read_users"');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { asRawClient } from "../../db/query";
|
|
3
3
|
import type { EntityDefinition } from "../../engine/types";
|
|
4
4
|
import {
|
|
5
5
|
createTestDb,
|
|
@@ -34,8 +34,8 @@ describe("unsafeEnsureEntityTable", () => {
|
|
|
34
34
|
test("legt die Tabelle beim ersten Aufruf an (returnt true)", async () => {
|
|
35
35
|
const created = await unsafeEnsureEntityTable(db.db, tenantEntity, "probe");
|
|
36
36
|
expect(created).toBe(true);
|
|
37
|
-
const rows = await db.db.
|
|
38
|
-
|
|
37
|
+
const rows = await asRawClient(db.db).unsafe<{ exists: boolean }>(
|
|
38
|
+
`SELECT to_regclass('public.ensure_entity_table_probe') IS NOT NULL AS exists`,
|
|
39
39
|
);
|
|
40
40
|
expect(rows[0]?.exists).toBe(true);
|
|
41
41
|
});
|
|
@@ -45,15 +45,8 @@ describe("unsafeEnsureEntityTable", () => {
|
|
|
45
45
|
expect(created).toBe(false);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
test("unsafeCreateEntityTable
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
// DrizzleQueryError; der echte Code steckt in .cause. Sicherstellt,
|
|
52
|
-
// dass unsafeEnsureEntityTable nicht versehentlich das strict-Verhalten
|
|
53
|
-
// verändert.
|
|
54
|
-
await expect(unsafeCreateEntityTable(db.db, tenantEntity, "probe")).rejects.toSatisfy((err) => {
|
|
55
|
-
const cause = (err as { cause?: { code?: string } }).cause;
|
|
56
|
-
return cause?.code === "42P07";
|
|
57
|
-
});
|
|
48
|
+
test("unsafeCreateEntityTable ist idempotent — zweiter Push wirft nicht (CREATE IF NOT EXISTS)", async () => {
|
|
49
|
+
// CREATE TABLE IF NOT EXISTS — idempotent by design.
|
|
50
|
+
await expect(unsafeCreateEntityTable(db.db, tenantEntity, "probe")).resolves.toBeUndefined();
|
|
58
51
|
});
|
|
59
52
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration-test DB cleanup — replaces copy-pasted `DELETE FROM …` in
|
|
3
|
+
* beforeEach hooks. All table clears go through typed `deleteMany` (empty
|
|
4
|
+
* where = full table wipe). Raw SQL stays out of test files.
|
|
5
|
+
*/
|
|
6
|
+
import { type AnyDb, deleteMany } from "../db/query";
|
|
7
|
+
|
|
8
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
9
|
+
const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
|
|
10
|
+
|
|
11
|
+
/** EntityTableMeta, drizzle pgTable, or plain table name string. */
|
|
12
|
+
export type ClearableTable = string | { readonly tableName?: string } | unknown;
|
|
13
|
+
|
|
14
|
+
function tableFromName(name: string): unknown {
|
|
15
|
+
return {
|
|
16
|
+
[KUMIKO_NAME_SYMBOL]: name,
|
|
17
|
+
[KUMIKO_COLUMNS_SYMBOL]: {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveClearableTable(table: ClearableTable): unknown {
|
|
22
|
+
if (typeof table === "string") return tableFromName(table);
|
|
23
|
+
if (
|
|
24
|
+
typeof table === "object" &&
|
|
25
|
+
table !== null &&
|
|
26
|
+
"tableName" in table &&
|
|
27
|
+
typeof (table as { tableName?: unknown }).tableName === "string"
|
|
28
|
+
) {
|
|
29
|
+
return table;
|
|
30
|
+
}
|
|
31
|
+
return table;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Delete all rows from each table (order preserved — FK-sensitive callers order explicitly). */
|
|
35
|
+
export async function clearTables(db: AnyDb, tables: readonly ClearableTable[]): Promise<void> {
|
|
36
|
+
for (const table of tables) {
|
|
37
|
+
await deleteMany(db, resolveClearableTable(table), {});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Alias — same as clearTables, reads better in beforeEach. */
|
|
42
|
+
export async function resetTestTables(db: AnyDb, tables: readonly ClearableTable[]): Promise<void> {
|
|
43
|
+
await clearTables(db, tables);
|
|
44
|
+
}
|
package/src/testing/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
export { rolesOf } from "./access-assertions";
|
|
7
7
|
export { expectError, expectSuccess } from "./assertions";
|
|
8
|
+
export { type ClearableTable, clearTables, resetTestTables } from "./db-cleanup";
|
|
8
9
|
export {
|
|
9
10
|
type E2EGeneratorOptions,
|
|
10
11
|
type E2ETestSpec,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
type ParsedSetCookie,
|
|
22
23
|
} from "./http-cookies";
|
|
23
24
|
export { createLateBoundHolder, type LateBoundHolder } from "./late-bound";
|
|
25
|
+
export { buildMultipartBody, patchFileInstanceofForBunTest } from "./multipart-helper";
|
|
24
26
|
export {
|
|
25
27
|
createMutableMasterKeyProvider,
|
|
26
28
|
type MutableMasterKeyProvider,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Workarounds for Bun v1.3.x bun:test limitations with multipart/form-data.
|
|
2
|
+
//
|
|
3
|
+
// Two bugs combine to break file upload tests in bun:test:
|
|
4
|
+
//
|
|
5
|
+
// 1. Content-Type omission: both app.request({body: formData}) and
|
|
6
|
+
// fetch(url, {body: formData}) stringify FormData via .toString() instead
|
|
7
|
+
// of serializing it as multipart, so no Content-Type header is set.
|
|
8
|
+
// Fix: serialize FormData manually via buildMultipartBody().
|
|
9
|
+
//
|
|
10
|
+
// 2. Cross-realm instanceof: Hono's multipart parser creates Blob objects from
|
|
11
|
+
// a different JS realm than the test globals. In bun:test this means
|
|
12
|
+
// `parsedValue instanceof File` is always false even when the value has all
|
|
13
|
+
// File properties. Fix: patchFilInstanceofForBunTest().
|
|
14
|
+
//
|
|
15
|
+
// Both fixes are test-only — production code and real HTTP clients are unaffected.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Serializes a FormData instance to multipart/form-data bytes.
|
|
19
|
+
*
|
|
20
|
+
* Returns the encoded body and the Content-Type header value (including the
|
|
21
|
+
* generated boundary). Pass both directly to app.request or fetch.
|
|
22
|
+
*/
|
|
23
|
+
export async function buildMultipartBody(
|
|
24
|
+
fd: FormData,
|
|
25
|
+
): Promise<{ body: BodyInit; contentType: string }> {
|
|
26
|
+
const boundary = `KumikoBnd${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
|
|
27
|
+
const enc = new TextEncoder();
|
|
28
|
+
const parts: Uint8Array[] = [];
|
|
29
|
+
|
|
30
|
+
for (const [name, value] of fd.entries()) {
|
|
31
|
+
const v = value as unknown as {
|
|
32
|
+
name?: string;
|
|
33
|
+
type?: string;
|
|
34
|
+
arrayBuffer?: () => Promise<ArrayBuffer>;
|
|
35
|
+
};
|
|
36
|
+
if (typeof v === "object" && v !== null && typeof v.arrayBuffer === "function") {
|
|
37
|
+
parts.push(
|
|
38
|
+
enc.encode(
|
|
39
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${name}"; filename="${v.name ?? "blob"}"\r\nContent-Type: ${v.type || "application/octet-stream"}\r\n\r\n`,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
parts.push(new Uint8Array(await v.arrayBuffer()));
|
|
43
|
+
parts.push(enc.encode("\r\n"));
|
|
44
|
+
} else {
|
|
45
|
+
parts.push(
|
|
46
|
+
enc.encode(
|
|
47
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${String(value)}\r\n`,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
parts.push(enc.encode(`--${boundary}--\r\n`));
|
|
53
|
+
|
|
54
|
+
const total = parts.reduce((s, p) => s + p.length, 0);
|
|
55
|
+
const buf = new Uint8Array(total);
|
|
56
|
+
let off = 0;
|
|
57
|
+
for (const p of parts) {
|
|
58
|
+
buf.set(p, off);
|
|
59
|
+
off += p.length;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
body: buf as unknown as BodyInit,
|
|
63
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Patches File[Symbol.hasInstance] so that cross-realm Blob objects returned
|
|
69
|
+
* by Hono's parseBody() pass `instanceof File` checks in bun:test.
|
|
70
|
+
*
|
|
71
|
+
* In bun:test the multipart parser runs in a different JS realm than the test
|
|
72
|
+
* globals, so the Blob/File constructors differ. The patch replaces the
|
|
73
|
+
* prototype-chain check with a duck-type check: an object with string `.name`,
|
|
74
|
+
* number `.size`, and function `.arrayBuffer` is treated as a File.
|
|
75
|
+
*
|
|
76
|
+
* Safe to call multiple times (idempotent via the `_patched` marker).
|
|
77
|
+
*/
|
|
78
|
+
export function patchFileInstanceofForBunTest(): void {
|
|
79
|
+
// skip: idempotent re-patch — already installed, nothing to do
|
|
80
|
+
if ((File as unknown as { _kumikoPatched?: boolean })._kumikoPatched) return;
|
|
81
|
+
Object.defineProperty(File, Symbol.hasInstance, {
|
|
82
|
+
value(instance: unknown): boolean {
|
|
83
|
+
if (typeof instance !== "object" || instance === null) return false;
|
|
84
|
+
const f = instance as Record<string, unknown>;
|
|
85
|
+
return (
|
|
86
|
+
typeof f["name"] === "string" &&
|
|
87
|
+
typeof f["size"] === "number" &&
|
|
88
|
+
typeof f["arrayBuffer"] === "function"
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
configurable: true,
|
|
92
|
+
});
|
|
93
|
+
(File as unknown as { _kumikoPatched?: boolean })._kumikoPatched = true;
|
|
94
|
+
}
|