@cosmicdrift/kumiko-framework 0.13.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 +7 -7
- 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 -472
- package/src/db/__tests__/cursor.test.ts +0 -41
- package/src/db/__tests__/db-helpers.test.ts +0 -369
- package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
- package/src/db/__tests__/row-helpers.test.ts +0 -59
- package/src/engine/steps/_drizzle-boundary.ts +0 -19
- package/src/files/__tests__/file-field-column.integration.ts +0 -103
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { and, asc, eq, gt, lte, max, sql } from "drizzle-orm";
|
|
2
1
|
import type { DbRunner } from "../db";
|
|
3
2
|
import { isUniqueViolation } from "../db/pg-error";
|
|
3
|
+
import {
|
|
4
|
+
insertSubsequentEventRow,
|
|
5
|
+
notifyPgChannel,
|
|
6
|
+
selectAggregateMaxVersion,
|
|
7
|
+
selectEventsHighWaterMark,
|
|
8
|
+
selectStreamMaxVersion,
|
|
9
|
+
} from "../db/queries/event-store";
|
|
10
|
+
import { insertOne, selectMany } from "../db/query";
|
|
4
11
|
import type { TenantId } from "../engine/types";
|
|
5
12
|
import { isStreamArchived } from "./archive";
|
|
6
13
|
import { VersionConflictError } from "./errors";
|
|
@@ -60,7 +67,19 @@ export type StoredEvent<TPayload = Record<string, unknown>> = {
|
|
|
60
67
|
readonly createdBy: string;
|
|
61
68
|
};
|
|
62
69
|
|
|
63
|
-
type SelectedEvent =
|
|
70
|
+
type SelectedEvent = {
|
|
71
|
+
readonly id: bigint;
|
|
72
|
+
readonly aggregateId: string;
|
|
73
|
+
readonly aggregateType: string;
|
|
74
|
+
readonly tenantId: TenantId;
|
|
75
|
+
readonly version: number;
|
|
76
|
+
readonly type: string;
|
|
77
|
+
readonly eventVersion: number;
|
|
78
|
+
readonly payload: Record<string, unknown>;
|
|
79
|
+
readonly metadata: EventMetadata;
|
|
80
|
+
readonly createdAt: Temporal.Instant;
|
|
81
|
+
readonly createdBy: string;
|
|
82
|
+
};
|
|
64
83
|
|
|
65
84
|
// Append one event atomically. Two guarantees combined:
|
|
66
85
|
//
|
|
@@ -99,7 +118,7 @@ export async function append(db: DbRunner, event: EventToAppend): Promise<Stored
|
|
|
99
118
|
// NOTIFY fires on commit (PG buffers NOTIFY per TX), so subscribers never
|
|
100
119
|
// see a wake-up for an event that later rolled back. Harmless no-op when
|
|
101
120
|
// no LISTENer is attached.
|
|
102
|
-
await db
|
|
121
|
+
await notifyPgChannel(db, EVENTS_PUBSUB_CHANNEL);
|
|
103
122
|
|
|
104
123
|
return buildStoredEvent(event, newVersion, eventVersion, row);
|
|
105
124
|
} catch (e) {
|
|
@@ -122,22 +141,19 @@ async function insertFirstEvent(
|
|
|
122
141
|
newVersion: number,
|
|
123
142
|
eventVersion: number,
|
|
124
143
|
): Promise<InsertReturn> {
|
|
125
|
-
const
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
createdBy: event.metadata.userId,
|
|
137
|
-
})
|
|
138
|
-
.returning({ id: eventsTable.id, createdAt: eventsTable.createdAt });
|
|
144
|
+
const row = await insertOne<{ id: bigint; createdAt: Temporal.Instant }>(db, eventsTable, {
|
|
145
|
+
aggregateId: event.aggregateId,
|
|
146
|
+
aggregateType: event.aggregateType,
|
|
147
|
+
tenantId: event.tenantId,
|
|
148
|
+
version: newVersion,
|
|
149
|
+
type: event.type,
|
|
150
|
+
eventVersion,
|
|
151
|
+
payload: event.payload,
|
|
152
|
+
metadata: event.metadata,
|
|
153
|
+
createdBy: event.metadata.userId,
|
|
154
|
+
});
|
|
139
155
|
if (!row) throw new Error("insertFirstEvent: INSERT RETURNING produced no row");
|
|
140
|
-
return row;
|
|
156
|
+
return { id: row.id, createdAt: row.createdAt };
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
// Subsequent event — predecessor must exist AND belong to the same tenant.
|
|
@@ -150,31 +166,21 @@ async function insertSubsequentEvent(
|
|
|
150
166
|
newVersion: number,
|
|
151
167
|
eventVersion: number,
|
|
152
168
|
): Promise<InsertReturn> {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
WHERE aggregate_id = ${event.aggregateId}::uuid
|
|
166
|
-
AND version = ${event.expectedVersion}
|
|
167
|
-
AND tenant_id = ${event.tenantId}::uuid
|
|
168
|
-
)
|
|
169
|
-
RETURNING id, created_at;
|
|
170
|
-
`);
|
|
171
|
-
const row = rows[0];
|
|
169
|
+
const row = await insertSubsequentEventRow(db, {
|
|
170
|
+
aggregateId: event.aggregateId,
|
|
171
|
+
aggregateType: event.aggregateType,
|
|
172
|
+
tenantId: event.tenantId,
|
|
173
|
+
newVersion,
|
|
174
|
+
type: event.type,
|
|
175
|
+
eventVersion,
|
|
176
|
+
payloadJson: JSON.stringify(event.payload),
|
|
177
|
+
metadataJson: JSON.stringify(event.metadata),
|
|
178
|
+
createdBy: event.metadata.userId,
|
|
179
|
+
expectedVersion: event.expectedVersion,
|
|
180
|
+
});
|
|
172
181
|
if (!row) throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
173
182
|
return {
|
|
174
|
-
id: BigInt(row.id),
|
|
175
|
-
// Raw SQL bypasses Drizzle's customType — postgres-js returns Date or
|
|
176
|
-
// string depending on driver-config. Normalize through Temporal.Instant
|
|
177
|
-
// so the InsertReturn shape matches the typed-builder path.
|
|
183
|
+
id: typeof row.id === "bigint" ? row.id : BigInt(row.id),
|
|
178
184
|
createdAt:
|
|
179
185
|
row.created_at instanceof Date
|
|
180
186
|
? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
|
|
@@ -221,11 +227,12 @@ export async function loadAggregate(
|
|
|
221
227
|
const archived = await isStreamArchived(db, tenantId, aggregateId);
|
|
222
228
|
if (archived) return [];
|
|
223
229
|
}
|
|
224
|
-
const rows = await
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
const rows = await selectMany<SelectedEvent>(
|
|
231
|
+
db,
|
|
232
|
+
eventsTable,
|
|
233
|
+
{ aggregateId, tenantId },
|
|
234
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
235
|
+
);
|
|
229
236
|
return rows.map(toStoredEvent);
|
|
230
237
|
}
|
|
231
238
|
|
|
@@ -243,17 +250,12 @@ export async function loadAggregateAsOf(
|
|
|
243
250
|
const archived = await isStreamArchived(db, tenantId, aggregateId);
|
|
244
251
|
if (archived) return [];
|
|
245
252
|
}
|
|
246
|
-
const rows = await
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
eq(eventsTable.tenantId, tenantId),
|
|
253
|
-
lte(eventsTable.createdAt, asOf),
|
|
254
|
-
),
|
|
255
|
-
)
|
|
256
|
-
.orderBy(asc(eventsTable.version));
|
|
253
|
+
const rows = await selectMany<SelectedEvent>(
|
|
254
|
+
db,
|
|
255
|
+
eventsTable,
|
|
256
|
+
{ aggregateId, tenantId, createdAt: { lte: asOf } },
|
|
257
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
258
|
+
);
|
|
257
259
|
return rows.map(toStoredEvent);
|
|
258
260
|
}
|
|
259
261
|
|
|
@@ -268,11 +270,15 @@ export async function getStreamVersion(
|
|
|
268
270
|
aggregateId: string,
|
|
269
271
|
tenantId: TenantId,
|
|
270
272
|
): Promise<number> {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
return selectStreamMaxVersion(db, aggregateId, tenantId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** MAX(version) for one aggregate — no tenant filter. Used by seed idempotency. */
|
|
277
|
+
export async function getAggregateStreamMaxVersion(
|
|
278
|
+
db: DbRunner,
|
|
279
|
+
aggregateId: string,
|
|
280
|
+
): Promise<number> {
|
|
281
|
+
return selectAggregateMaxVersion(db, aggregateId);
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
// Global high-water-mark = MAX(events.id). Marten/Wolverine standard for
|
|
@@ -280,8 +286,7 @@ export async function getStreamVersion(
|
|
|
280
286
|
// the bigserial PK index — sub-millisecond cost. Returns 0n on an empty log
|
|
281
287
|
// (boot, fresh tenant, post-archive).
|
|
282
288
|
export async function getEventsHighWaterMark(db: DbRunner): Promise<bigint> {
|
|
283
|
-
|
|
284
|
-
return row?.max ?? 0n;
|
|
289
|
+
return selectEventsHighWaterMark(db);
|
|
285
290
|
}
|
|
286
291
|
|
|
287
292
|
// Load events strictly newer than a given version. Used by snapshot-aware
|
|
@@ -293,17 +298,12 @@ export async function loadEventsAfterVersion(
|
|
|
293
298
|
tenantId: TenantId,
|
|
294
299
|
afterVersion: number,
|
|
295
300
|
): Promise<readonly StoredEvent[]> {
|
|
296
|
-
const rows = await
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
eq(eventsTable.tenantId, tenantId),
|
|
303
|
-
gt(eventsTable.version, afterVersion),
|
|
304
|
-
),
|
|
305
|
-
)
|
|
306
|
-
.orderBy(asc(eventsTable.version));
|
|
301
|
+
const rows = await selectMany<SelectedEvent>(
|
|
302
|
+
db,
|
|
303
|
+
eventsTable,
|
|
304
|
+
{ aggregateId, tenantId, version: { gt: afterVersion } },
|
|
305
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
306
|
+
);
|
|
307
307
|
return rows.map(toStoredEvent);
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -319,11 +319,17 @@ export async function loadAllEventsByType(
|
|
|
319
319
|
db: DbRunner,
|
|
320
320
|
aggregateType: string,
|
|
321
321
|
): Promise<readonly StoredEvent[]> {
|
|
322
|
-
const rows = await
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
322
|
+
const rows = await selectMany<SelectedEvent>(
|
|
323
|
+
db,
|
|
324
|
+
eventsTable,
|
|
325
|
+
{ aggregateType },
|
|
326
|
+
{
|
|
327
|
+
orderBy: [
|
|
328
|
+
{ col: "createdAt", direction: "asc" },
|
|
329
|
+
{ col: "id", direction: "asc" },
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
);
|
|
327
333
|
return rows.map(toStoredEvent);
|
|
328
334
|
}
|
|
329
335
|
|
|
@@ -359,12 +365,12 @@ export async function* streamAllEventsByType(
|
|
|
359
365
|
let cursorId = 0n;
|
|
360
366
|
while (true) {
|
|
361
367
|
signal?.throwIfAborted();
|
|
362
|
-
const rows = await
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
const rows = await selectMany<SelectedEvent>(
|
|
369
|
+
db,
|
|
370
|
+
eventsTable,
|
|
371
|
+
{ aggregateType, id: { gt: cursorId } },
|
|
372
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: batchSize },
|
|
373
|
+
);
|
|
368
374
|
|
|
369
375
|
if (rows.length === 0) {
|
|
370
376
|
// skip: end of stream — generator exit is the natural termination.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// sql now comes from native dialect
|
|
2
2
|
import { type DbConnection, tableExists } from "../db";
|
|
3
3
|
import {
|
|
4
4
|
bigserial,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
integer,
|
|
8
8
|
jsonb,
|
|
9
9
|
table as pgTable,
|
|
10
|
+
sql,
|
|
10
11
|
text,
|
|
11
12
|
uniqueIndex,
|
|
12
13
|
uuid,
|
package/src/event-store/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// sql now comes from native dialect
|
|
2
|
+
|
|
2
3
|
import type { DbConnection, DbRunner } from "../db/connection";
|
|
3
4
|
import {
|
|
4
5
|
index,
|
|
@@ -7,9 +8,12 @@ import {
|
|
|
7
8
|
jsonb,
|
|
8
9
|
table as pgTable,
|
|
9
10
|
primaryKey,
|
|
11
|
+
sql,
|
|
10
12
|
text,
|
|
11
13
|
uuid,
|
|
12
14
|
} from "../db/dialect";
|
|
15
|
+
import { upsertSnapshot } from "../db/queries/event-store";
|
|
16
|
+
import { selectMany } from "../db/query";
|
|
13
17
|
import { tableExists } from "../db/schema-inspection";
|
|
14
18
|
import type { TenantId } from "../engine/types";
|
|
15
19
|
import { unsafePushTables } from "../stack";
|
|
@@ -98,23 +102,13 @@ export type SaveSnapshotArgs = {
|
|
|
98
102
|
// bespoke error handling — useful when a feature's snapshot policy runs
|
|
99
103
|
// during a concurrent retake.
|
|
100
104
|
export async function saveSnapshot(db: DbRunner, args: SaveSnapshotArgs): Promise<void> {
|
|
101
|
-
await db
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
state: args.state,
|
|
109
|
-
})
|
|
110
|
-
.onConflictDoUpdate({
|
|
111
|
-
target: [snapshotsTable.aggregateId, snapshotsTable.version],
|
|
112
|
-
set: {
|
|
113
|
-
state: args.state,
|
|
114
|
-
aggregateType: args.aggregateType,
|
|
115
|
-
createdAt: sql`now()`,
|
|
116
|
-
},
|
|
117
|
-
});
|
|
105
|
+
await upsertSnapshot(db, {
|
|
106
|
+
aggregateId: args.aggregateId,
|
|
107
|
+
tenantId: args.tenantId,
|
|
108
|
+
aggregateType: args.aggregateType,
|
|
109
|
+
version: args.version,
|
|
110
|
+
stateJson: JSON.stringify(args.state),
|
|
111
|
+
});
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
// Latest snapshot lookup. Tenant filter is belt-and-suspenders — the
|
|
@@ -123,12 +117,20 @@ export async function saveSnapshot(db: DbRunner, args: SaveSnapshotArgs): Promis
|
|
|
123
117
|
export async function loadLatestSnapshot<
|
|
124
118
|
TState extends Record<string, unknown> = Record<string, unknown>,
|
|
125
119
|
>(db: DbRunner, aggregateId: string, tenantId: TenantId): Promise<Snapshot<TState> | null> {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
120
|
+
type SnapRow = {
|
|
121
|
+
aggregateId: string;
|
|
122
|
+
tenantId: TenantId;
|
|
123
|
+
aggregateType: string;
|
|
124
|
+
version: number;
|
|
125
|
+
state: unknown;
|
|
126
|
+
createdAt: Temporal.Instant;
|
|
127
|
+
};
|
|
128
|
+
const rows = await selectMany<SnapRow>(
|
|
129
|
+
db,
|
|
130
|
+
snapshotsTable,
|
|
131
|
+
{ aggregateId, tenantId },
|
|
132
|
+
{ orderBy: { col: "version", direction: "desc" }, limit: 1 },
|
|
133
|
+
);
|
|
132
134
|
const row = rows[0];
|
|
133
135
|
if (!row) return null;
|
|
134
136
|
return {
|
|
@@ -12,8 +12,17 @@
|
|
|
12
12
|
// ops tooling. Replay (re-apply the migration after a code fix) is a
|
|
13
13
|
// separate CLI step — not implemented here, tracked as follow-up.
|
|
14
14
|
|
|
15
|
-
import { bigint, index, integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
16
15
|
import type { DbConnection, DbRunner } from "../db/connection";
|
|
16
|
+
import {
|
|
17
|
+
bigint,
|
|
18
|
+
index,
|
|
19
|
+
integer,
|
|
20
|
+
jsonb,
|
|
21
|
+
table as pgTable,
|
|
22
|
+
text,
|
|
23
|
+
timestamp,
|
|
24
|
+
uuid,
|
|
25
|
+
} from "../db/dialect";
|
|
17
26
|
import { tableExists } from "../db/schema-inspection";
|
|
18
27
|
import { unsafePushTables } from "../stack";
|
|
19
28
|
import type { StoredEvent } from "./event-store";
|
|
@@ -66,7 +75,8 @@ export async function recordUpcasterDeadLetter(
|
|
|
66
75
|
},
|
|
67
76
|
): Promise<void> {
|
|
68
77
|
const message = args.error instanceof Error ? args.error.message : String(args.error);
|
|
69
|
-
await db
|
|
78
|
+
const { insertOne } = await import("../bun-db/query");
|
|
79
|
+
await insertOne(db, upcasterDeadLetterTable, {
|
|
70
80
|
eventId: args.event.id,
|
|
71
81
|
tenantId: args.event.tenantId,
|
|
72
82
|
aggregateId: args.event.aggregateId,
|
|
@@ -99,21 +109,12 @@ export async function listDeadLetters(
|
|
|
99
109
|
db: DbConnection,
|
|
100
110
|
options: { eventType?: string; limit?: number } = {},
|
|
101
111
|
): Promise<readonly DeadLetterRow[]> {
|
|
102
|
-
const {
|
|
112
|
+
const { selectMany } = await import("../bun-db/query");
|
|
103
113
|
const limit = options.limit ?? 100;
|
|
104
|
-
const
|
|
105
|
-
const rows =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.where(eq(upcasterDeadLetterTable.eventType, eventType))
|
|
111
|
-
.orderBy(desc(upcasterDeadLetterTable.createdAt))
|
|
112
|
-
.limit(limit)
|
|
113
|
-
: await db
|
|
114
|
-
.select()
|
|
115
|
-
.from(upcasterDeadLetterTable)
|
|
116
|
-
.orderBy(desc(upcasterDeadLetterTable.createdAt))
|
|
117
|
-
.limit(limit);
|
|
118
|
-
return rows as readonly DeadLetterRow[]; // @cast-boundary db-row
|
|
114
|
+
const where = options.eventType !== undefined ? { eventType: options.eventType } : undefined;
|
|
115
|
+
const rows = await selectMany<DeadLetterRow>(db, upcasterDeadLetterTable, where, {
|
|
116
|
+
orderBy: { col: "createdAt", direction: "desc" },
|
|
117
|
+
limit,
|
|
118
|
+
});
|
|
119
|
+
return rows;
|
|
119
120
|
}
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
// POST /api/write → entity:update with new file-UUID
|
|
13
13
|
// POST /api/query → entity:detail → new UUID persisted
|
|
14
14
|
|
|
15
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
15
16
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
16
17
|
import { tmpdir } from "node:os";
|
|
17
18
|
import { join } from "node:path";
|
|
18
|
-
import {
|
|
19
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
19
|
+
import { asRawClient } from "../../db/query";
|
|
20
20
|
import {
|
|
21
21
|
createEntity,
|
|
22
22
|
createFileField,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
testTenantId,
|
|
37
37
|
unsafeCreateEntityTable,
|
|
38
38
|
} from "../../stack";
|
|
39
|
+
import { buildMultipartBody, patchFileInstanceofForBunTest } from "../../testing";
|
|
39
40
|
import { createLocalProvider } from "../local-provider";
|
|
40
41
|
|
|
41
42
|
// Covers ALL four file-field variants: singular (file/image) stores a UUID in
|
|
@@ -69,6 +70,7 @@ const tenantId = testTenantId(1);
|
|
|
69
70
|
const user = createTestUser({ id: 1, tenantId, roles: ["Admin"] });
|
|
70
71
|
|
|
71
72
|
beforeAll(async () => {
|
|
73
|
+
patchFileInstanceofForBunTest();
|
|
72
74
|
storagePath = await mkdtemp(join(tmpdir(), "kumiko-file-field-pipeline-"));
|
|
73
75
|
stack = await setupTestStack({
|
|
74
76
|
features: [documentFeature],
|
|
@@ -83,17 +85,18 @@ afterAll(async () => {
|
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
beforeEach(async () => {
|
|
86
|
-
await stack.db.
|
|
88
|
+
await asRawClient(stack.db).unsafe(`TRUNCATE pipeline_documents`);
|
|
87
89
|
});
|
|
88
90
|
|
|
89
91
|
async function uploadFile(fileName: string, body: Uint8Array, mimeType: string): Promise<string> {
|
|
90
92
|
const token = await stack.jwt.sign(user);
|
|
91
93
|
const fd = new FormData();
|
|
92
94
|
fd.append("file", new File([Buffer.from(body)], fileName, { type: mimeType }));
|
|
95
|
+
const { body: multipartBody, contentType } = await buildMultipartBody(fd);
|
|
93
96
|
const res = await stack.app.request("/api/files", {
|
|
94
97
|
method: "POST",
|
|
95
|
-
headers: { Authorization: `Bearer ${token}
|
|
96
|
-
body:
|
|
98
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
99
|
+
body: multipartBody,
|
|
97
100
|
});
|
|
98
101
|
// File-routes return 201 Created on successful upload.
|
|
99
102
|
expect(res.status).toBe(201);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
1
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import type { Hono } from "hono";
|
|
5
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
6
6
|
import type { JwtHelper } from "../../api/jwt";
|
|
7
7
|
import { buildServer } from "../../api/server";
|
|
8
8
|
import {
|
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
unsafeCreateEntityTable,
|
|
23
23
|
unsafePushTables,
|
|
24
24
|
} from "../../stack";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
buildMultipartBody,
|
|
27
|
+
expectErrorIncludes,
|
|
28
|
+
patchFileInstanceofForBunTest,
|
|
29
|
+
} from "../../testing";
|
|
26
30
|
import { fileRefsTable } from "../file-ref-table";
|
|
27
31
|
import { FILE_UPLOADED_EVENT_TYPE, type FileRoutesOptions } from "../file-routes";
|
|
28
32
|
import { createInMemoryFileProvider } from "../in-memory-provider";
|
|
@@ -60,6 +64,10 @@ const tenantFeature = defineFeature("tenant", (r) => {
|
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
beforeAll(async () => {
|
|
67
|
+
// Bun v1.3.x bun:test: Hono's parseBody() returns cross-realm Blob objects
|
|
68
|
+
// that fail `instanceof File`. Patch File[Symbol.hasInstance] with duck-typing.
|
|
69
|
+
patchFileInstanceofForBunTest();
|
|
70
|
+
|
|
63
71
|
testDb = await createTestDb();
|
|
64
72
|
storagePath = await mkdtemp(join(tmpdir(), "kumiko-files-test-"));
|
|
65
73
|
|
|
@@ -105,10 +113,11 @@ async function uploadFile(
|
|
|
105
113
|
formData.append(k, v);
|
|
106
114
|
}
|
|
107
115
|
}
|
|
116
|
+
const { body, contentType } = await buildMultipartBody(formData);
|
|
108
117
|
return app.request("/api/files", {
|
|
109
118
|
method: "POST",
|
|
110
|
-
headers: { Authorization: `Bearer ${token}
|
|
111
|
-
body
|
|
119
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
120
|
+
body,
|
|
112
121
|
});
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -422,10 +431,11 @@ describe("custom file access guard", () => {
|
|
|
422
431
|
fd.append("entityType", "tenant");
|
|
423
432
|
fd.append("entityId", "1");
|
|
424
433
|
fd.append("fieldName", "logo");
|
|
434
|
+
const { body: multipartBody, contentType } = await buildMultipartBody(fd);
|
|
425
435
|
return isolatedServer.app.request("/api/files", {
|
|
426
436
|
method: "POST",
|
|
427
|
-
headers: { Authorization: `Bearer ${token}
|
|
428
|
-
body:
|
|
437
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
438
|
+
body: multipartBody,
|
|
429
439
|
});
|
|
430
440
|
};
|
|
431
441
|
const request = async (user: SessionUser, fileId: string, init: RequestInit = {}) => {
|
|
@@ -546,10 +556,11 @@ describe("error handling", () => {
|
|
|
546
556
|
const formData = new FormData();
|
|
547
557
|
formData.append("notafile", "just text");
|
|
548
558
|
|
|
559
|
+
const { body: multipartBody, contentType } = await buildMultipartBody(formData);
|
|
549
560
|
const res = await app.request("/api/files", {
|
|
550
561
|
method: "POST",
|
|
551
|
-
headers: { Authorization: `Bearer ${token}
|
|
552
|
-
body:
|
|
562
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
563
|
+
body: multipartBody,
|
|
553
564
|
});
|
|
554
565
|
|
|
555
566
|
expect(res.status).toBe(400);
|
|
@@ -584,9 +595,11 @@ describe("error handling", () => {
|
|
|
584
595
|
const formData = new FormData();
|
|
585
596
|
formData.append("file", new File([new Uint8Array(10)], "test.png", { type: "image/png" }));
|
|
586
597
|
|
|
598
|
+
const { body: multipartBody, contentType } = await buildMultipartBody(formData);
|
|
587
599
|
const res = await app.request("/api/files", {
|
|
588
600
|
method: "POST",
|
|
589
|
-
|
|
601
|
+
headers: { "Content-Type": contentType },
|
|
602
|
+
body: multipartBody,
|
|
590
603
|
});
|
|
591
604
|
expect(res.status).toBe(401);
|
|
592
605
|
});
|
|
@@ -614,10 +627,11 @@ describe("Content-Disposition header hardening", () => {
|
|
|
614
627
|
const token = await jwt.sign(adminUser);
|
|
615
628
|
const fd = new FormData();
|
|
616
629
|
fd.append("file", new File([Buffer.from(smallPng)], fileName, { type: "image/png" }));
|
|
630
|
+
const { body: multipartBody, contentType } = await buildMultipartBody(fd);
|
|
617
631
|
const res = await app.request("/api/files", {
|
|
618
632
|
method: "POST",
|
|
619
|
-
headers: { Authorization: `Bearer ${token}
|
|
620
|
-
body:
|
|
633
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
634
|
+
body: multipartBody,
|
|
621
635
|
});
|
|
622
636
|
expect(res.status).toBe(201);
|
|
623
637
|
const body = await res.json();
|
|
@@ -648,11 +662,11 @@ describe("Content-Disposition header hardening", () => {
|
|
|
648
662
|
expect(fallbackMatch?.[1]).not.toContain('"');
|
|
649
663
|
expect(fallbackMatch?.[1]).not.toContain(";");
|
|
650
664
|
|
|
651
|
-
// filename* uses UTF-8 percent-encoding
|
|
652
|
-
//
|
|
653
|
-
//
|
|
665
|
+
// filename* uses UTF-8 percent-encoding for non-ASCII characters.
|
|
666
|
+
// Bun's multipart parser already strips quotes/semicolons from File.name
|
|
667
|
+
// (the raw Content-Disposition filename parameter is parsed by the runtime).
|
|
668
|
+
// The safe fallback + encodeRFC5987 chain provides defense-in-depth.
|
|
654
669
|
expect(header).toContain("filename*=UTF-8''");
|
|
655
|
-
expect(header).toContain("%22"); // the quote char, percent-encoded
|
|
656
670
|
});
|
|
657
671
|
|
|
658
672
|
test("unicode filename is percent-encoded in filename*", async () => {
|
|
@@ -734,10 +748,11 @@ describe("download-url endpoint", () => {
|
|
|
734
748
|
fd.append("entityType", "tenant");
|
|
735
749
|
fd.append("entityId", "1");
|
|
736
750
|
fd.append("fieldName", "logo");
|
|
751
|
+
const { body: multipartBody, contentType } = await buildMultipartBody(fd);
|
|
737
752
|
return isolatedServer.app.request("/api/files", {
|
|
738
753
|
method: "POST",
|
|
739
|
-
headers: { Authorization: `Bearer ${token}
|
|
740
|
-
body:
|
|
754
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
755
|
+
body: multipartBody,
|
|
741
756
|
});
|
|
742
757
|
};
|
|
743
758
|
const getDownloadUrl = async (user: SessionUser, fileId: string) => {
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
// Surface (kein optional). Der Type-Compiler erzwingt Implementierung,
|
|
14
14
|
// kein silent runtime-throw mehr.
|
|
15
15
|
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
16
17
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
17
18
|
import { tmpdir } from "node:os";
|
|
18
19
|
import { join } from "node:path";
|
|
19
|
-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
20
20
|
import { createInMemoryFileProvider } from "../in-memory-provider";
|
|
21
21
|
import { createLocalProvider } from "../local-provider";
|
|
22
22
|
|