@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
|
@@ -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
|
|
package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts}
RENAMED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
// 3. The table column types survive round-trip (bigint → number via
|
|
9
9
|
// Drizzle's mode:"number", so arithmetic in assertions Just Works).
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { asRawClient, selectMany } from "../../db/query";
|
|
13
13
|
import type { SessionUser } from "../../engine";
|
|
14
14
|
import { createTestUser, setupTestStack, type TestStack, TestUsers } from "../../stack";
|
|
15
|
+
import { buildMultipartBody, patchFileInstanceofForBunTest } from "../../testing";
|
|
15
16
|
import {
|
|
16
17
|
createInMemoryFileProvider,
|
|
17
18
|
filesStorageTrackingFeature,
|
|
@@ -36,6 +37,7 @@ const SMALL = new Uint8Array([0x89, 0x50, 0x4e, 0x47, ...Array(16).fill(0)]); //
|
|
|
36
37
|
const LARGE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, ...Array(96).fill(0)]); // 100 bytes
|
|
37
38
|
|
|
38
39
|
beforeAll(async () => {
|
|
40
|
+
patchFileInstanceofForBunTest();
|
|
39
41
|
provider = createInMemoryFileProvider();
|
|
40
42
|
stack = await setupTestStack({
|
|
41
43
|
features: [filesStorageTrackingFeature],
|
|
@@ -54,8 +56,8 @@ beforeEach(async () => {
|
|
|
54
56
|
// each test starts from zero. kumiko_event_consumers registration is
|
|
55
57
|
// re-asserted below; truncating it forces ensureRegistered to seed the
|
|
56
58
|
// cursor at event.id = 0.
|
|
57
|
-
await stack.db.
|
|
58
|
-
|
|
59
|
+
await asRawClient(stack.db).unsafe(
|
|
60
|
+
`TRUNCATE kumiko_events, kumiko_event_consumers, file_refs, read_tenant_storage_usage RESTART IDENTITY CASCADE`,
|
|
59
61
|
);
|
|
60
62
|
await stack.eventDispatcher?.ensureRegistered();
|
|
61
63
|
});
|
|
@@ -64,23 +66,20 @@ async function upload(user: SessionUser, name: string, content: Uint8Array): Pro
|
|
|
64
66
|
const token = await stack.jwt.sign(user);
|
|
65
67
|
const formData = new FormData();
|
|
66
68
|
formData.append("file", new File([Buffer.from(content)], name, { type: "image/png" }));
|
|
69
|
+
const { body: multipartBody, contentType } = await buildMultipartBody(formData);
|
|
67
70
|
const res = await stack.app.request("/api/files", {
|
|
68
71
|
method: "POST",
|
|
69
|
-
headers: { Authorization: `Bearer ${token}
|
|
70
|
-
body:
|
|
72
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
73
|
+
body: multipartBody,
|
|
71
74
|
});
|
|
72
75
|
expect(res.status).toBe(201);
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
async function usageFor(tenantId: string): Promise<{ totalBytes: number; fileCount: number }> {
|
|
76
|
-
const [row] = await stack.db
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
})
|
|
81
|
-
.from(tenantStorageUsageTable)
|
|
82
|
-
.where(eq(tenantStorageUsageTable.tenantId, tenantId));
|
|
83
|
-
return row ?? { totalBytes: 0, fileCount: 0 };
|
|
79
|
+
const [row] = await selectMany(stack.db, tenantStorageUsageTable, { tenantId });
|
|
80
|
+
return row
|
|
81
|
+
? { totalBytes: Number(row["totalBytes"]) ?? 0, fileCount: Number(row["fileCount"]) ?? 0 }
|
|
82
|
+
: { totalBytes: 0, fileCount: 0 };
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
describe("tenant-storage-usage MSP", () => {
|
|
@@ -105,10 +104,7 @@ describe("tenant-storage-usage MSP", () => {
|
|
|
105
104
|
|
|
106
105
|
// Exactly one row per tenant — the UPSERT must not insert a second
|
|
107
106
|
// row for the second upload.
|
|
108
|
-
const rows = await stack.db
|
|
109
|
-
.select()
|
|
110
|
-
.from(tenantStorageUsageTable)
|
|
111
|
-
.where(eq(tenantStorageUsageTable.tenantId, admin.tenantId));
|
|
107
|
+
const rows = await selectMany(stack.db, tenantStorageUsageTable, { tenantId: admin.tenantId });
|
|
112
108
|
expect(rows).toHaveLength(1);
|
|
113
109
|
});
|
|
114
110
|
|
|
@@ -128,11 +124,10 @@ describe("tenant-storage-usage MSP", () => {
|
|
|
128
124
|
await upload(admin, "a.png", SMALL);
|
|
129
125
|
await stack.eventDispatcher?.runOnce();
|
|
130
126
|
|
|
131
|
-
const [first] = await stack.db
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
expect(first?.at).toBeInstanceOf(Temporal.Instant);
|
|
127
|
+
const [first] = await selectMany(stack.db, tenantStorageUsageTable, {
|
|
128
|
+
tenantId: admin.tenantId,
|
|
129
|
+
});
|
|
130
|
+
expect(first?.["lastUpdatedAt"]).toBeInstanceOf(Temporal.Instant);
|
|
136
131
|
|
|
137
132
|
// Postgres NOW() resolution is microseconds; a second upload a beat
|
|
138
133
|
// later must produce a strictly later timestamp (or at least not an
|
|
@@ -142,12 +137,13 @@ describe("tenant-storage-usage MSP", () => {
|
|
|
142
137
|
await upload(admin, "b.png", LARGE);
|
|
143
138
|
await stack.eventDispatcher?.runOnce();
|
|
144
139
|
|
|
145
|
-
const [second] = await stack.db
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
140
|
+
const [second] = await selectMany(stack.db, tenantStorageUsageTable, {
|
|
141
|
+
tenantId: admin.tenantId,
|
|
142
|
+
});
|
|
143
|
+
expect(second?.["lastUpdatedAt"]).toBeInstanceOf(Temporal.Instant);
|
|
144
|
+
if (!first?.["lastUpdatedAt"] || !second?.["lastUpdatedAt"]) throw new Error("missing rows");
|
|
145
|
+
expect(
|
|
146
|
+
Temporal.Instant.compare(second["lastUpdatedAt"], first["lastUpdatedAt"]),
|
|
147
|
+
).toBeGreaterThanOrEqual(0);
|
|
152
148
|
});
|
|
153
149
|
});
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
// AsyncIterable-source pinst die Streaming-Semantik (Caller streamt
|
|
13
13
|
// chunk-fuer-chunk, Provider niemals alles im Memory).
|
|
14
14
|
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
15
16
|
import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
16
17
|
import { tmpdir } from "node:os";
|
|
17
18
|
import { join } from "node:path";
|
|
18
|
-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
19
19
|
import { createInMemoryFileProvider } from "../in-memory-provider";
|
|
20
20
|
import { createLocalProvider } from "../local-provider";
|
|
21
21
|
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
// kann — kein "passt nur in unserer eigenen reverse-engineerten
|
|
12
12
|
// Welt".
|
|
13
13
|
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
14
15
|
import { spawn } from "node:child_process";
|
|
15
16
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
16
17
|
import { tmpdir } from "node:os";
|
|
17
18
|
import { join } from "node:path";
|
|
18
|
-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
19
19
|
import { getTemporal } from "../../time";
|
|
20
20
|
import { createZipStream, type ZipEntry } from "../zip-stream";
|
|
21
21
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import { instant, integer, table as pgTable, text, uuid } from "../db/dialect";
|
|
1
|
+
// sql now comes from native dialect
|
|
2
|
+
import { instant, integer, table as pgTable, sql, text, uuid } from "../db/dialect";
|
|
3
3
|
|
|
4
4
|
// `id` is a UUID (not serial): it doubles as the aggregate-id for the
|
|
5
5
|
// `fileRef` event stream — every upload appends exactly one
|
package/src/files/file-routes.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getUser } from "../api/auth-middleware";
|
|
5
|
-
import type { DbConnection } from "../db/connection";
|
|
5
|
+
import type { DbConnection, DbTx } from "../db/connection";
|
|
6
6
|
import type { EventDef } from "../engine/types";
|
|
7
7
|
import { isFileField, type Registry, type SessionUser, type TenantId } from "../engine/types";
|
|
8
8
|
import { append as appendEvent } from "../event-store/event-store";
|
|
@@ -172,8 +172,9 @@ export function createFileRoutes(options: FileRoutesOptions): Hono {
|
|
|
172
172
|
// Atomic: insert FileRef + append files:event:uploaded in one tx. Either
|
|
173
173
|
// both land or neither — no dangling FileRef without event, no event
|
|
174
174
|
// referencing a row that doesn't exist.
|
|
175
|
-
await db.
|
|
176
|
-
await
|
|
175
|
+
await db.begin(async (tx: DbTx) => {
|
|
176
|
+
const { insertOne } = await import("../bun-db/query");
|
|
177
|
+
await insertOne(tx, fileRefsTable, {
|
|
177
178
|
id: fileRefId,
|
|
178
179
|
tenantId: user.tenantId,
|
|
179
180
|
storageKey,
|
|
@@ -271,7 +272,7 @@ export function createFileRoutes(options: FileRoutesOptions): Hono {
|
|
|
271
272
|
// Upload. Umgekehrt liesse ein storage-success + db-fail eine Row mit
|
|
272
273
|
// permanent-broken Reference zurück — aus Sicht der API "Datei
|
|
273
274
|
// existiert" aber jeder Read 404t aus dem Provider.
|
|
274
|
-
await db
|
|
275
|
+
await deleteMany(db, fileRefsTable, { id: id });
|
|
275
276
|
await storageProvider.delete(fileRef.storageKey);
|
|
276
277
|
return c.json({ ok: true });
|
|
277
278
|
});
|
|
@@ -342,10 +343,7 @@ export function createFileRoutes(options: FileRoutesOptions): Hono {
|
|
|
342
343
|
});
|
|
343
344
|
|
|
344
345
|
async function loadFileForTenant(id: string, tenantId: TenantId): Promise<FileRef | null> {
|
|
345
|
-
const [row] = await db
|
|
346
|
-
.select()
|
|
347
|
-
.from(fileRefsTable)
|
|
348
|
-
.where(and(eq(fileRefsTable.id, id), eq(fileRefsTable.tenantId, tenantId)));
|
|
346
|
+
const [row] = await selectMany(db, fileRefsTable, { id, tenantId });
|
|
349
347
|
return (row as FileRef | undefined) ?? null; // @cast-boundary db-row
|
|
350
348
|
}
|
|
351
349
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// consumer-cursor row. Apps that want it pass filesStorageTrackingFeature
|
|
11
11
|
// into createApp / setupTestStack alongside their domain features.
|
|
12
12
|
|
|
13
|
-
import { sql } from "
|
|
14
|
-
import {
|
|
13
|
+
import { bigint, instant, integer, table as pgTable, sql, uuid } from "../db/dialect";
|
|
14
|
+
import { incrementCounter } from "../db/query";
|
|
15
15
|
import { defineFeature, typedPayload } from "../engine";
|
|
16
16
|
import { fileUploadedEvent } from "./file-routes";
|
|
17
17
|
|
|
@@ -39,21 +39,13 @@ export const filesStorageTrackingFeature = defineFeature("files-storage-tracking
|
|
|
39
39
|
// The SQL increment guarantees correctness under concurrent dispatcher
|
|
40
40
|
// runs (shouldn't happen with a single consumer, but the invariant is
|
|
41
41
|
// free and cheap — no reason to rely on serial delivery).
|
|
42
|
-
await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.onConflictDoUpdate({
|
|
50
|
-
target: tenantStorageUsageTable.tenantId,
|
|
51
|
-
set: {
|
|
52
|
-
totalBytes: sql`${tenantStorageUsageTable.totalBytes} + ${payload.size}`,
|
|
53
|
-
fileCount: sql`${tenantStorageUsageTable.fileCount} + 1`,
|
|
54
|
-
lastUpdatedAt: sql`NOW()`,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
42
|
+
await incrementCounter(
|
|
43
|
+
tx,
|
|
44
|
+
tenantStorageUsageTable,
|
|
45
|
+
{ tenantId: event.tenantId, totalBytes: payload.size, fileCount: 1 },
|
|
46
|
+
{ totalBytes: payload.size, fileCount: 1 },
|
|
47
|
+
{ set: { lastUpdatedAt: sql`now()` } },
|
|
48
|
+
);
|
|
57
49
|
},
|
|
58
50
|
},
|
|
59
51
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
1
2
|
import type { Hono } from "hono";
|
|
2
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { buildServer, type JwtHelper } from "../../api";
|
|
5
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
5
6
|
import { createRegistry, defineFeature, type SessionUser } from "../../engine";
|
|
6
|
-
import {
|
|
7
|
+
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
7
8
|
import { waitFor } from "../../testing";
|
|
9
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
8
10
|
import { createJobRunner, type JobRunner } from "../job-runner";
|
|
9
11
|
|
|
10
12
|
// --- Track job executions ---
|
|
@@ -64,7 +66,7 @@ const analyticsFeature = defineFeature("analytics", (r) => {
|
|
|
64
66
|
|
|
65
67
|
// --- Setup ---
|
|
66
68
|
|
|
67
|
-
let testDb:
|
|
69
|
+
let testDb: BunTestDb;
|
|
68
70
|
let testRedis: TestRedis;
|
|
69
71
|
let app: Hono;
|
|
70
72
|
let jwt: JwtHelper;
|
|
@@ -74,6 +76,7 @@ const adminUser = TestUsers.admin;
|
|
|
74
76
|
const JWT_SECRET = "event-trigger-test-secret-minimum-32-chars!!";
|
|
75
77
|
|
|
76
78
|
beforeAll(async () => {
|
|
79
|
+
await ensureTemporalPolyfill();
|
|
77
80
|
testDb = await createTestDb();
|
|
78
81
|
testRedis = await createTestRedis();
|
|
79
82
|
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
// Validator-Reject). Hier prüfen wir die runtime-dispatch-Pfade durch
|
|
7
7
|
// einen echten BullMQ-Worker.
|
|
8
8
|
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
10
|
import type { Hono } from "hono";
|
|
10
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { buildServer, type JwtHelper } from "../../api";
|
|
13
|
+
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
13
14
|
import { createRegistry, defineFeature, type SessionUser } from "../../engine";
|
|
14
|
-
import {
|
|
15
|
+
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
15
16
|
import { waitFor } from "../../testing";
|
|
17
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
16
18
|
import { createJobRunner, type JobRunner } from "../job-runner";
|
|
17
19
|
|
|
18
20
|
const jobExecutions: Array<{ trigger: string; payload: Record<string, unknown> }> = [];
|
|
@@ -48,7 +50,7 @@ const orderFeature = defineFeature("multi", (r) => {
|
|
|
48
50
|
);
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
let testDb:
|
|
53
|
+
let testDb: BunTestDb;
|
|
52
54
|
let testRedis: TestRedis;
|
|
53
55
|
let app: Hono;
|
|
54
56
|
let jwt: JwtHelper;
|
|
@@ -58,6 +60,7 @@ const adminUser = TestUsers.admin;
|
|
|
58
60
|
const JWT_SECRET = "multi-trigger-test-secret-minimum-32-chars!!";
|
|
59
61
|
|
|
60
62
|
beforeAll(async () => {
|
|
63
|
+
await ensureTemporalPolyfill();
|
|
61
64
|
testDb = await createTestDb();
|
|
62
65
|
testRedis = await createTestRedis();
|
|
63
66
|
|