@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
package/src/engine/ownership.ts
CHANGED
|
@@ -12,14 +12,22 @@
|
|
|
12
12
|
// { from: "claim:<featureQn>",
|
|
13
13
|
// column?: "..." } → row[column ?? claim.shortName] === user.claims[claim.qn]
|
|
14
14
|
// (string[] claim → inArray)
|
|
15
|
-
// { where: (user,
|
|
15
|
+
// { where: (user, ctx) => SqlFragment } → escape hatch, raw parameterised SQL
|
|
16
16
|
//
|
|
17
17
|
// Construction: use the `from(ref, column?)` helper. It returns a FromRule
|
|
18
18
|
// ready to drop into an access map.
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { toSnakeCase } from "../db/table-builder";
|
|
21
21
|
import type { SessionUser } from "./types";
|
|
22
22
|
|
|
23
|
+
// Parameterised SQL fragment — produced by buildOwnershipClause + by the
|
|
24
|
+
// WhereRule escape-hatch. Caller weaves `sqlText` into a larger statement,
|
|
25
|
+
// renumbering placeholders if needed (FragmentBuilder below).
|
|
26
|
+
export type SqlFragment = {
|
|
27
|
+
readonly sqlText: string;
|
|
28
|
+
readonly params: readonly unknown[];
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
// Reference spec supported by `from()`:
|
|
24
32
|
// "user:id" → user.id
|
|
25
33
|
// "user:tenantId" → user.tenantId (rarely needed — TenantDb scopes anyway)
|
|
@@ -49,9 +57,18 @@ export type FromRule = {
|
|
|
49
57
|
readonly column: string;
|
|
50
58
|
};
|
|
51
59
|
|
|
60
|
+
// Context passed to a WhereRule escape-hatch. The author returns a SqlFragment
|
|
61
|
+
// whose placeholders start at `paramStart` ($N, $N+1, ...); the framework
|
|
62
|
+
// concatenates the fragment into the larger query.
|
|
63
|
+
export type WhereRuleContext<TTable = unknown> = {
|
|
64
|
+
readonly table: TTable;
|
|
65
|
+
readonly tableName: string;
|
|
66
|
+
readonly paramStart: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
52
69
|
export type WhereRule<TTable = unknown> = {
|
|
53
70
|
readonly kind: "where";
|
|
54
|
-
readonly where: (user: SessionUser,
|
|
71
|
+
readonly where: (user: SessionUser, ctx: WhereRuleContext<TTable>) => SqlFragment;
|
|
55
72
|
};
|
|
56
73
|
|
|
57
74
|
// "all" collapses to a primitive so map authors can write `Admin: "all"`
|
|
@@ -244,51 +261,94 @@ export function userCanCreateFieldRow(
|
|
|
244
261
|
// "empty" → user has a role mapped but no rule accepts any row (missing
|
|
245
262
|
// claim, empty array, role not in map). Skip the DB call entirely
|
|
246
263
|
// — returning [] is equivalent and avoids a pointless roundtrip.
|
|
247
|
-
// "sql" → apply
|
|
264
|
+
// "sql" → apply the parameterised fragment as an AND on the query.
|
|
265
|
+
// Caller is responsible for renumbering placeholders when
|
|
266
|
+
// concatenating with other fragments (see `shiftParams` below).
|
|
248
267
|
//
|
|
249
268
|
// "empty" vs. "pass" is the critical distinction for a safe default:
|
|
250
|
-
// undefined/pass = allow, empty = deny-by-construction.
|
|
251
|
-
// the exact leak direction advisor flagged; the disjoint type prevents it.
|
|
269
|
+
// undefined/pass = allow, empty = deny-by-construction.
|
|
252
270
|
export type OwnershipClause =
|
|
253
271
|
| { readonly kind: "pass" }
|
|
254
272
|
| { readonly kind: "empty" }
|
|
255
|
-
| { readonly kind: "sql"; readonly
|
|
273
|
+
| { readonly kind: "sql"; readonly sqlText: string; readonly params: readonly unknown[] };
|
|
256
274
|
|
|
257
275
|
const PASS_CLAUSE: OwnershipClause = { kind: "pass" };
|
|
258
276
|
const EMPTY_CLAUSE: OwnershipClause = { kind: "empty" };
|
|
259
277
|
|
|
260
|
-
|
|
261
|
-
|
|
278
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
279
|
+
const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
|
|
280
|
+
|
|
281
|
+
function tableNameOf(table: unknown): string {
|
|
282
|
+
if (table !== null && typeof table === "object") {
|
|
283
|
+
const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
|
|
284
|
+
if (typeof sym === "string") return sym;
|
|
285
|
+
}
|
|
286
|
+
return "<unknown>";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Resolve a JS-field name on the table to its underlying SQL column name.
|
|
290
|
+
// Drizzle tables carry the mapping under Symbol.for("kumiko:schema:Columns");
|
|
291
|
+
// we read it without importing drizzle-orm at runtime.
|
|
292
|
+
function columnSqlName(table: unknown, field: string): string | null {
|
|
293
|
+
if (table === null || typeof table !== "object") return null;
|
|
294
|
+
const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
|
|
295
|
+
if (cols && typeof cols === "object") {
|
|
296
|
+
const col = (cols as Record<string, unknown>)[field];
|
|
297
|
+
if (col && typeof col === "object") {
|
|
298
|
+
const nameVal = (col as Record<string, unknown>)["name"];
|
|
299
|
+
if (typeof nameVal === "string") return nameVal;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Field may already be the SQL column name on plain objects (tests, etc.).
|
|
303
|
+
if ((table as Record<string, unknown>)[field] !== undefined) {
|
|
304
|
+
return toSnakeCase(field);
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function quoteIdent(name: string): string {
|
|
310
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Shift `$N` placeholder numbers in an embedded fragment so they line up
|
|
314
|
+
// with the outer query's param array.
|
|
315
|
+
export function shiftParams(fragment: SqlFragment, shift: number): SqlFragment {
|
|
316
|
+
if (shift === 0) return fragment;
|
|
317
|
+
const sqlText = fragment.sqlText.replace(/\$(\d+)/g, (_, num) => `$${Number(num) + shift}`);
|
|
318
|
+
return { sqlText, params: fragment.params };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Build an ownership clause for entity-level READ access. Caller weaves
|
|
322
|
+
// the result into a raw-SQL WHERE (see event-store-executor list/getById).
|
|
262
323
|
//
|
|
263
|
-
// `table` is the
|
|
264
|
-
//
|
|
265
|
-
//
|
|
324
|
+
// `table` is the (drizzle or compatible) table object; we extract column
|
|
325
|
+
// SQL names via the kumiko:schema:Columns symbol. Unknown column on a from-rule
|
|
326
|
+
// is a boot-time misconfiguration; at request time we treat it as empty
|
|
327
|
+
// (safe default) rather than passing silently.
|
|
266
328
|
export function buildOwnershipClause(
|
|
267
329
|
user: SessionUser,
|
|
268
330
|
accessMap: OwnershipMap | undefined,
|
|
269
|
-
|
|
270
|
-
|
|
331
|
+
table: unknown,
|
|
332
|
+
paramStart = 1,
|
|
271
333
|
): OwnershipClause {
|
|
272
334
|
if (!accessMap || Object.keys(accessMap).length === 0) return PASS_CLAUSE;
|
|
273
335
|
|
|
274
|
-
const clauses:
|
|
336
|
+
const clauses: SqlFragment[] = [];
|
|
275
337
|
let anyRoleMatched = false;
|
|
276
338
|
let everyRuleCollapsedToEmpty = true;
|
|
339
|
+
let nextParamIdx = paramStart;
|
|
277
340
|
|
|
278
341
|
for (const role of user.roles) {
|
|
279
342
|
const rule = accessMap[role];
|
|
280
343
|
if (!rule) continue;
|
|
281
344
|
anyRoleMatched = true;
|
|
282
|
-
// "all" = no filter at all for this role; short-circuit.
|
|
283
345
|
if (rule === "all") return PASS_CLAUSE;
|
|
284
|
-
const resolved =
|
|
346
|
+
const resolved = ruleToFragment(rule, user, table, nextParamIdx);
|
|
285
347
|
if (resolved.kind === "sql") {
|
|
286
|
-
clauses.push(resolved.
|
|
348
|
+
clauses.push({ sqlText: resolved.sqlText, params: resolved.params });
|
|
349
|
+
nextParamIdx += resolved.params.length;
|
|
287
350
|
everyRuleCollapsedToEmpty = false;
|
|
288
351
|
}
|
|
289
|
-
// "empty" contribution from one role doesn't short-circuit: another
|
|
290
|
-
// role might still contribute an OR-branch. But if ALL branches are
|
|
291
|
-
// empty, the result is empty.
|
|
292
352
|
}
|
|
293
353
|
|
|
294
354
|
if (!anyRoleMatched) return EMPTY_CLAUSE;
|
|
@@ -296,42 +356,54 @@ export function buildOwnershipClause(
|
|
|
296
356
|
if (clauses.length === 1) {
|
|
297
357
|
const only = clauses[0];
|
|
298
358
|
if (!only) return EMPTY_CLAUSE;
|
|
299
|
-
return { kind: "sql",
|
|
359
|
+
return { kind: "sql", sqlText: `(${only.sqlText})`, params: only.params };
|
|
300
360
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
return { kind: "sql",
|
|
361
|
+
const sqlText = clauses.map((c) => `(${c.sqlText})`).join(" OR ");
|
|
362
|
+
const params: unknown[] = [];
|
|
363
|
+
for (const c of clauses) for (const p of c.params) params.push(p);
|
|
364
|
+
return { kind: "sql", sqlText: `(${sqlText})`, params };
|
|
305
365
|
}
|
|
306
366
|
|
|
307
|
-
type
|
|
367
|
+
type RuleFragmentResult =
|
|
368
|
+
| { readonly kind: "empty" }
|
|
369
|
+
| { readonly kind: "sql"; readonly sqlText: string; readonly params: readonly unknown[] };
|
|
308
370
|
|
|
309
|
-
function
|
|
371
|
+
function ruleToFragment(
|
|
310
372
|
rule: OwnershipRule,
|
|
311
373
|
user: SessionUser,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
):
|
|
374
|
+
table: unknown,
|
|
375
|
+
paramStart: number,
|
|
376
|
+
): RuleFragmentResult {
|
|
315
377
|
if (rule === "all") {
|
|
316
|
-
|
|
317
|
-
// fallback.
|
|
318
|
-
return { kind: "sql", sql: sql`true` };
|
|
378
|
+
return { kind: "sql", sqlText: "TRUE", params: [] };
|
|
319
379
|
}
|
|
320
380
|
if (rule.kind === "where") {
|
|
321
|
-
|
|
381
|
+
const frag = rule.where(user, {
|
|
382
|
+
table,
|
|
383
|
+
tableName: tableNameOf(table),
|
|
384
|
+
paramStart,
|
|
385
|
+
});
|
|
386
|
+
return { kind: "sql", sqlText: frag.sqlText, params: frag.params };
|
|
322
387
|
}
|
|
323
388
|
// FromRule
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
// time we treat as empty (fail-closed).
|
|
327
|
-
if (!column) return { kind: "empty" };
|
|
389
|
+
const colName = columnSqlName(table, rule.column);
|
|
390
|
+
if (!colName) return { kind: "empty" };
|
|
328
391
|
|
|
329
392
|
const value = resolveUserValue(rule, user);
|
|
330
393
|
if (value === undefined || value === null) return { kind: "empty" };
|
|
331
394
|
|
|
332
395
|
if (Array.isArray(value)) {
|
|
333
396
|
if (value.length === 0) return { kind: "empty" };
|
|
334
|
-
|
|
397
|
+
const placeholders = value.map((_, i) => `$${paramStart + i}`).join(", ");
|
|
398
|
+
return {
|
|
399
|
+
kind: "sql",
|
|
400
|
+
sqlText: `${quoteIdent(colName)} IN (${placeholders})`,
|
|
401
|
+
params: value,
|
|
402
|
+
};
|
|
335
403
|
}
|
|
336
|
-
return {
|
|
404
|
+
return {
|
|
405
|
+
kind: "sql",
|
|
406
|
+
sqlText: `${quoteIdent(colName)} = $${paramStart}`,
|
|
407
|
+
params: [value],
|
|
408
|
+
};
|
|
337
409
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// sample pattern (sanity check that entity-fields-editor points at
|
|
8
8
|
// `definition.fields`, not `fields`).
|
|
9
9
|
|
|
10
|
-
import { describe, expect, expectTypeOf, test } from "
|
|
10
|
+
import { describe, expect, expectTypeOf, test } from "bun:test";
|
|
11
11
|
import {
|
|
12
12
|
type FeaturePattern,
|
|
13
13
|
type FeaturePatternKind,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
// All FeaturePatternKind discriminator values, hand-listed so the test
|
|
25
25
|
// fails CI when a new pattern is added without a library entry. Match
|
|
26
26
|
// against the FeaturePattern union via type-test below.
|
|
27
|
-
const ALL_KINDS:
|
|
27
|
+
const ALL_KINDS: FeaturePatternKind[] = [
|
|
28
28
|
"requires",
|
|
29
29
|
"optionalRequires",
|
|
30
30
|
"readsConfig",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
2
1
|
import type { DbRunner } from "../db/connection";
|
|
2
|
+
import { updateMany } from "../db/query";
|
|
3
3
|
import type { StoredEvent } from "../event-store/event-store";
|
|
4
4
|
import type { MultiStreamApplyContext } from "../pipeline/multi-stream-apply-context";
|
|
5
5
|
import type { MultiStreamApplyFn, ProjectionTable, SingleStreamApplyFn } from "./types/projection";
|
|
@@ -71,15 +71,6 @@ export function setFields(
|
|
|
71
71
|
}
|
|
72
72
|
return async (event, tx) => {
|
|
73
73
|
const values = typeof fields === "function" ? fields(event) : fields;
|
|
74
|
-
|
|
75
|
-
// does not know user table shapes). Drizzle's tx.update().set() is
|
|
76
|
-
// strict about the concrete row, so we feed it the erased value; the
|
|
77
|
-
// type-safety guarantee for `values` lives at the setFields call-site.
|
|
78
|
-
// biome-ignore lint/suspicious/noExplicitAny: see note above.
|
|
79
|
-
const set = values as any; // @cast-boundary engine-bridge
|
|
80
|
-
await tx
|
|
81
|
-
.update(table)
|
|
82
|
-
.set(set)
|
|
83
|
-
.where(eq(idCol as never, event.aggregateId)); // @cast-boundary db-operator
|
|
74
|
+
await updateMany(tx, table, values, { id: event.aggregateId });
|
|
84
75
|
};
|
|
85
76
|
}
|
package/src/engine/registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applyEntityEvent } from "../db/apply-entity-event";
|
|
2
|
-
import {
|
|
2
|
+
import { buildEntityTable } from "../db/table-builder";
|
|
3
3
|
import { buildMetricName, validateMetricName } from "../observability";
|
|
4
4
|
import { type QnType, qualifyEntityName } from "./qualified-name";
|
|
5
5
|
import type {
|
|
@@ -67,7 +67,7 @@ function buildImplicitProjection(
|
|
|
67
67
|
qualify: typeof qualifyEntityName,
|
|
68
68
|
): ProjectionDefinition {
|
|
69
69
|
const name = qualify(featureName, "projection", `${entityName}${IMPLICIT_PROJECTION_SUFFIX}`);
|
|
70
|
-
const drizzleTable =
|
|
70
|
+
const drizzleTable = buildEntityTable(entityName, entity);
|
|
71
71
|
// applyEntityEvent gibt ApplyResult zurück; SingleStreamApplyFn erwartet
|
|
72
72
|
// Promise<void>. Im rebuild-Pfad ist die Row irrelevant — wir discarden.
|
|
73
73
|
const handler = async (
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
// r.step.read.findMany — load multiple rows from a projection table.
|
|
2
2
|
//
|
|
3
|
-
// Sibling to read.findOne — same tenant-filter caveat (caller-owned)
|
|
4
|
-
//
|
|
5
|
-
// landed under steps.<name>.
|
|
3
|
+
// Sibling to read.findOne — same tenant-filter caveat (caller-owned).
|
|
4
|
+
// Resolves to a row-array (possibly empty), landed under steps.<name>.
|
|
6
5
|
//
|
|
7
6
|
// Optional `limit` — defaults to no-limit (caller-chosen, NOT a
|
|
8
7
|
// guard-rail). Most legitimate uses iterate via r.step.forEach (M.1.6)
|
|
9
8
|
// over the result, where unbounded arrays would be the bug. Set
|
|
10
9
|
// `limit` explicitly when the row-count could grow without bound.
|
|
11
10
|
|
|
12
|
-
import
|
|
11
|
+
import { selectMany, type WhereObject } from "../../db/query";
|
|
13
12
|
import { defineStep } from "../define-step";
|
|
14
13
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
15
|
-
import { asQueryTarget } from "./_drizzle-boundary";
|
|
16
14
|
import { resolveOptional } from "./_resolver-utils";
|
|
17
15
|
|
|
18
16
|
type ReadFindManyArgs = {
|
|
19
17
|
readonly name: string;
|
|
20
|
-
readonly table:
|
|
21
|
-
readonly where?: StepResolver<
|
|
18
|
+
readonly table: unknown;
|
|
19
|
+
readonly where?: StepResolver<WhereObject | undefined>;
|
|
22
20
|
readonly limit?: number;
|
|
23
21
|
};
|
|
24
22
|
|
|
@@ -28,10 +26,12 @@ defineStep<ReadFindManyArgs, readonly Record<string, unknown>[]>({
|
|
|
28
26
|
resultKey: (args) => args.name,
|
|
29
27
|
run: async (args, ctx: PipelineCtx) => {
|
|
30
28
|
const where = resolveOptional(args.where, ctx);
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const rows = await selectMany(
|
|
30
|
+
ctx.db.raw,
|
|
31
|
+
args.table,
|
|
32
|
+
where,
|
|
33
|
+
args.limit !== undefined ? { limit: args.limit } : undefined,
|
|
34
|
+
);
|
|
35
35
|
return rows as readonly Record<string, unknown>[];
|
|
36
36
|
},
|
|
37
37
|
});
|
|
@@ -39,8 +39,8 @@ defineStep<ReadFindManyArgs, readonly Record<string, unknown>[]>({
|
|
|
39
39
|
export function buildReadFindManyStep(
|
|
40
40
|
name: string,
|
|
41
41
|
opts: {
|
|
42
|
-
readonly table:
|
|
43
|
-
readonly where?: StepResolver<
|
|
42
|
+
readonly table: unknown;
|
|
43
|
+
readonly where?: StepResolver<WhereObject | undefined>;
|
|
44
44
|
readonly limit?: number;
|
|
45
45
|
},
|
|
46
46
|
): StepInstance {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// r.step.read.findOne — load a single row from a projection table.
|
|
2
2
|
//
|
|
3
|
-
// Thin wrapper on
|
|
3
|
+
// Thin wrapper on selectMany(db, table, where, { limit: 1 }) (bun-db).
|
|
4
4
|
// Resolves to the first row or null. Tenant-isolation: the caller's
|
|
5
5
|
// `where` clause is responsible for any tenantId filter — read.findOne
|
|
6
6
|
// does NOT auto-inject one (different from ctx.queryProjection which
|
|
@@ -20,16 +20,15 @@
|
|
|
20
20
|
// fine for "find by uuid", a footgun for "find by tenantId". No
|
|
21
21
|
// runtime check; reviewer responsibility.
|
|
22
22
|
|
|
23
|
-
import
|
|
23
|
+
import { selectMany, type WhereObject } from "../../db/query";
|
|
24
24
|
import { defineStep } from "../define-step";
|
|
25
25
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
26
|
-
import { asQueryTarget } from "./_drizzle-boundary";
|
|
27
26
|
import { resolveRequired } from "./_resolver-utils";
|
|
28
27
|
|
|
29
28
|
type ReadFindOneArgs = {
|
|
30
29
|
readonly name: string;
|
|
31
|
-
readonly table:
|
|
32
|
-
readonly where: StepResolver<
|
|
30
|
+
readonly table: unknown;
|
|
31
|
+
readonly where: StepResolver<WhereObject | undefined>;
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
|
|
@@ -38,8 +37,7 @@ defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
|
|
|
38
37
|
resultKey: (args) => args.name,
|
|
39
38
|
run: async (args, ctx: PipelineCtx) => {
|
|
40
39
|
const where = resolveRequired(args.where, ctx);
|
|
41
|
-
const
|
|
42
|
-
const rows = where === undefined ? await query.limit(1) : await query.where(where).limit(1);
|
|
40
|
+
const rows = await selectMany(ctx.db.raw, args.table, where, { limit: 1 });
|
|
43
41
|
return (rows[0] as Record<string, unknown> | undefined) ?? null;
|
|
44
42
|
},
|
|
45
43
|
});
|
|
@@ -47,8 +45,8 @@ defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
|
|
|
47
45
|
export function buildReadFindOneStep(
|
|
48
46
|
name: string,
|
|
49
47
|
opts: {
|
|
50
|
-
readonly table:
|
|
51
|
-
readonly where: StepResolver<
|
|
48
|
+
readonly table: unknown;
|
|
49
|
+
readonly where: StepResolver<WhereObject | undefined>;
|
|
52
50
|
},
|
|
53
51
|
): StepInstance {
|
|
54
52
|
return {
|
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
// must commit in the same TX as the aggregate-mutation that triggered
|
|
17
17
|
// it (stronger consistency than an async projection). Reviewer judges.
|
|
18
18
|
|
|
19
|
-
import
|
|
19
|
+
import { deleteMany, type WhereObject } from "../../db/query";
|
|
20
20
|
import { defineStep } from "../define-step";
|
|
21
21
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
22
|
-
import { asQueryTarget } from "./_drizzle-boundary";
|
|
23
22
|
import { resolveRequired } from "./_resolver-utils";
|
|
24
23
|
|
|
25
24
|
// `where` is REQUIRED — table-wide DELETE without a clause is a TRUNCATE
|
|
@@ -28,8 +27,8 @@ import { resolveRequired } from "./_resolver-utils";
|
|
|
28
27
|
// `r.step.unsafeProjectionTruncate` step rather than loosening this
|
|
29
28
|
// type to `SQL | undefined`.
|
|
30
29
|
type UnsafeProjectionDeleteArgs = {
|
|
31
|
-
readonly table:
|
|
32
|
-
readonly where: StepResolver<
|
|
30
|
+
readonly table: unknown;
|
|
31
|
+
readonly where: StepResolver<WhereObject>;
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
defineStep<UnsafeProjectionDeleteArgs, void>({
|
|
@@ -37,7 +36,7 @@ defineStep<UnsafeProjectionDeleteArgs, void>({
|
|
|
37
36
|
defaultFailureStrategy: "throw",
|
|
38
37
|
run: async (args, ctx: PipelineCtx) => {
|
|
39
38
|
const where = resolveRequired(args.where, ctx);
|
|
40
|
-
await ctx.db.
|
|
39
|
+
await deleteMany(ctx.db.raw, args.table, where);
|
|
41
40
|
},
|
|
42
41
|
});
|
|
43
42
|
|
|
@@ -11,56 +11,88 @@
|
|
|
11
11
|
// rejected by boot-validation — domain mutation MUST go through
|
|
12
12
|
// r.step.aggregate.*.
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import type { PgColumn } from "drizzle-orm/pg-core";
|
|
14
|
+
import { executeRawQuery } from "../../db/queries/raw-sql";
|
|
16
15
|
import { defineStep } from "../define-step";
|
|
17
16
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
18
|
-
import { asQueryTarget } from "./_drizzle-boundary";
|
|
19
17
|
import { resolveRequired } from "./_resolver-utils";
|
|
20
18
|
|
|
21
19
|
type UnsafeProjectionUpsertArgs = {
|
|
22
|
-
readonly table:
|
|
20
|
+
readonly table: unknown;
|
|
23
21
|
readonly on: readonly string[];
|
|
24
22
|
readonly row: StepResolver<Record<string, unknown>>;
|
|
25
23
|
};
|
|
26
24
|
|
|
25
|
+
// @cast-boundary drizzle-bridge — reads table name + column snake_case
|
|
26
|
+
// names from drizzle Symbol-based metadata without importing drizzle-orm.
|
|
27
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
28
|
+
const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
|
|
29
|
+
|
|
30
|
+
function resolveTableName(table: unknown): string {
|
|
31
|
+
if (typeof table !== "object" || table === null) {
|
|
32
|
+
throw new Error("unsafeProjectionUpsert: table is not an object");
|
|
33
|
+
}
|
|
34
|
+
const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
|
|
35
|
+
if (typeof name !== "string") {
|
|
36
|
+
throw new Error("unsafeProjectionUpsert: table has no kumiko:schema:Name symbol");
|
|
37
|
+
}
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveColumnName(table: unknown, field: string): string {
|
|
42
|
+
if (typeof table !== "object" || table === null) return field;
|
|
43
|
+
const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
|
|
44
|
+
if (typeof cols !== "object" || cols === null) return field;
|
|
45
|
+
const col = (cols as Record<string, unknown>)[field];
|
|
46
|
+
if (typeof col === "object" && col !== null) {
|
|
47
|
+
const nameVal = (col as Record<string, unknown>)["name"];
|
|
48
|
+
if (typeof nameVal === "string") return nameVal;
|
|
49
|
+
}
|
|
50
|
+
return field;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function quoteIdent(name: string): string {
|
|
54
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
defineStep<UnsafeProjectionUpsertArgs, void>({
|
|
28
58
|
kind: "unsafeProjectionUpsert",
|
|
29
59
|
defaultFailureStrategy: "throw",
|
|
30
60
|
run: async (args, ctx: PipelineCtx) => {
|
|
31
61
|
const resolvedRow = resolveRequired(args.row, ctx);
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
throw new Error(`unsafeProjectionUpsert: column "${key}" not found on target table`);
|
|
63
|
+
// Validate conflict-key columns exist in the row.
|
|
64
|
+
for (const key of args.on) {
|
|
65
|
+
if (!(key in resolvedRow)) {
|
|
66
|
+
throw new Error(`unsafeProjectionUpsert: column "${key}" not found in row`);
|
|
38
67
|
}
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tableName = resolveTableName(args.table);
|
|
71
|
+
const entries = Object.entries(resolvedRow);
|
|
72
|
+
const params: unknown[] = [];
|
|
41
73
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
const colNames = entries.map(([k]) => quoteIdent(resolveColumnName(args.table, k)));
|
|
75
|
+
const placeholders = entries.map((_, i) => `$${i + 1}`);
|
|
76
|
+
for (const [, v] of entries) params.push(v);
|
|
77
|
+
|
|
78
|
+
const conflictCols = args.on
|
|
79
|
+
.map((k) => quoteIdent(resolveColumnName(args.table, k)))
|
|
80
|
+
.join(", ");
|
|
81
|
+
|
|
82
|
+
// SET clause excludes conflict-key columns.
|
|
83
|
+
const setClauses: string[] = [];
|
|
84
|
+
let paramIdx = entries.length + 1;
|
|
85
|
+
for (const [k, v] of entries) {
|
|
86
|
+
if (args.on.includes(k)) continue;
|
|
87
|
+
setClauses.push(`${quoteIdent(resolveColumnName(args.table, k))} = $${paramIdx++}`);
|
|
88
|
+
params.push(v);
|
|
47
89
|
}
|
|
48
90
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// `as never` (not `as any`) — never is contravariantly assignable to
|
|
55
|
-
// every drizzle Insert-shape; explicit "this bypass cannot be made
|
|
56
|
-
// type-safe without lifting <TTable extends Table>" marker.
|
|
57
|
-
await ctx.db
|
|
58
|
-
.insert(asQueryTarget(args.table))
|
|
59
|
-
.values(resolvedRow as never)
|
|
60
|
-
.onConflictDoUpdate({
|
|
61
|
-
target: conflictTargets as unknown as PgColumn[],
|
|
62
|
-
set: updateSet as never,
|
|
63
|
-
});
|
|
91
|
+
const sqlText =
|
|
92
|
+
`INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
|
|
93
|
+
`ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
|
|
94
|
+
|
|
95
|
+
await executeRawQuery(ctx.db.raw, sqlText, params);
|
|
64
96
|
},
|
|
65
97
|
});
|
|
66
98
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { PgTable } from "drizzle-orm/pg-core";
|
|
2
1
|
import type { ZodType, z } from "zod";
|
|
2
|
+
|
|
3
|
+
// PgTable historically came from drizzle-orm/pg-core; the native dialect
|
|
4
|
+
// no longer carries drizzle internal class types. Every caller really
|
|
5
|
+
// needs "an opaque table-object with Symbol-based introspection".
|
|
6
|
+
type PgTable = unknown;
|
|
7
|
+
|
|
3
8
|
import type { QueryHandlerDefinition, WriteHandlerDefinition } from "../define-handler";
|
|
4
9
|
import type {
|
|
5
10
|
ConfigKeyDefinition,
|
|
@@ -176,7 +181,7 @@ export type FeatureDefinition = {
|
|
|
176
181
|
// F3 search-payload-extension — per-entity contributors that add flat fields
|
|
177
182
|
// to the search-index payload during indexing. Keyed by entityName. Wrapped
|
|
178
183
|
// in OwnedFn for feature-toggle filtering (consistent with postQuery-Hooks).
|
|
179
|
-
readonly searchPayloadExtensions
|
|
184
|
+
readonly searchPayloadExtensions?: Readonly<
|
|
180
185
|
Record<string, readonly OwnedFn<SearchPayloadContributorFn>[]>
|
|
181
186
|
>;
|
|
182
187
|
readonly configKeys: Readonly<Record<string, ConfigKeyDefinition>>;
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// accepted at the type layer during migration: features that pass an
|
|
6
6
|
// array are auto-normalized to { [role]: "all" } at registry build.
|
|
7
7
|
// Long-term: string[] disappears.
|
|
8
|
-
import type { SQL } from "drizzle-orm";
|
|
9
8
|
import type { OwnershipMap } from "../ownership";
|
|
10
9
|
|
|
11
10
|
export type FieldAccess = {
|
|
@@ -475,7 +474,7 @@ export function isFileField(field: FieldDefinition | undefined): field is AnyFil
|
|
|
475
474
|
export type TransitionMap = Readonly<Record<string, readonly string[]>>;
|
|
476
475
|
|
|
477
476
|
/** Composite-Index auf einer Entity. Spalten werden via field-Name
|
|
478
|
-
* referenziert (camelCase).
|
|
477
|
+
* referenziert (camelCase). buildEntityTable mapped sie auf snake_case-
|
|
479
478
|
* Spaltennamen und benennt den Index nach Convention:
|
|
480
479
|
*
|
|
481
480
|
* <table>_<col1>_<col2>_idx (non-unique)
|
|
@@ -484,7 +483,7 @@ export type TransitionMap = Readonly<Record<string, readonly string[]>>;
|
|
|
484
483
|
* Eine `name`-Override ist erlaubt — Convention-Bruch in Bestandscode
|
|
485
484
|
* vermeidet Migration-Churn beim Refactor.
|
|
486
485
|
*
|
|
487
|
-
* Single-column indices über `tenantId` sind redundant (
|
|
486
|
+
* Single-column indices über `tenantId` sind redundant (buildEntityTable
|
|
488
487
|
* legt die immer automatisch an); die Boot-Validation warnt. */
|
|
489
488
|
export type EntityIndexDef = {
|
|
490
489
|
readonly columns: readonly [string, ...string[]];
|
|
@@ -503,7 +502,7 @@ export type EntityIndexDef = {
|
|
|
503
502
|
* man z.B. fuer scharfe BTREE-Indexes nur auf einer Status-Teilmenge
|
|
504
503
|
* statt voller Tabelle).
|
|
505
504
|
*/
|
|
506
|
-
readonly where?:
|
|
505
|
+
readonly where?: unknown;
|
|
507
506
|
};
|
|
508
507
|
|
|
509
508
|
export type FieldsMap = Readonly<Record<string, FieldDefinition>>;
|
|
@@ -517,7 +516,7 @@ export type EntityDefinition<F extends FieldsMap = FieldsMap> = {
|
|
|
517
516
|
/** Allowed state transitions per field. Boot validates against select options. */
|
|
518
517
|
readonly transitions?: Readonly<Record<string, TransitionMap>>;
|
|
519
518
|
/** Composite-Indices über mehrere Felder. Single-column FK-Indices und
|
|
520
|
-
* der tenant_id-Index werden weiterhin automatisch von
|
|
519
|
+
* der tenant_id-Index werden weiterhin automatisch von buildEntityTable
|
|
521
520
|
* angelegt — diese Liste ist nur für Custom-Indices die der Author
|
|
522
521
|
* explizit deklariert (z.B. `{ unique: true, columns: ["key", "tenantId", "userId"] }`). */
|
|
523
522
|
readonly indexes?: readonly EntityIndexDef[];
|