@cosmicdrift/kumiko-framework 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
- package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
- package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
- package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
- package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
- package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
- package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
- package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
- package/src/api/__tests__/api.test.ts +1 -1
- package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
- package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
- package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/csrf-middleware.test.ts +1 -1
- package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
- package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
- package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
- package/src/api/__tests__/readiness.test.ts +1 -1
- package/src/api/__tests__/request-id-middleware.test.ts +1 -1
- package/src/api/__tests__/sse-broker.test.ts +12 -12
- package/src/api/__tests__/sse-route.test.ts +1 -1
- package/src/api/readiness.ts +2 -2
- package/src/auth/__tests__/roles.test.ts +2 -2
- package/src/bun-db/__tests__/PATTERN.md +73 -0
- package/src/bun-db/__tests__/_helpers.ts +103 -0
- package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
- package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
- package/src/bun-db/__tests__/bun-test-db.ts +19 -0
- package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
- package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
- package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
- package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
- package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
- package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
- package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
- package/src/bun-db/connection.ts +84 -0
- package/src/bun-db/index.ts +31 -0
- package/src/bun-db/query.ts +845 -0
- package/src/compliance/__tests__/duration-spec.test.ts +1 -1
- package/src/compliance/__tests__/profiles.test.ts +1 -1
- package/src/compliance/__tests__/sub-processors.test.ts +1 -1
- package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
- package/src/db/__tests__/big-int-field.test.ts +15 -14
- package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
- package/src/db/__tests__/connection-options.test.ts +1 -1
- package/src/db/__tests__/dialect-instant.test.ts +1 -1
- package/src/db/__tests__/encryption.test.ts +1 -1
- package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
- package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
- package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
- package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
- package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
- package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
- package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
- package/src/db/__tests__/sql-inventory.test.ts +56 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
- package/src/db/__tests__/table-builder-required.test.ts +20 -22
- package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
- package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
- package/src/db/api.ts +46 -0
- package/src/db/apply-entity-event.ts +45 -36
- package/src/db/assert-exists-in.ts +5 -16
- package/src/db/bun-provider.ts +37 -0
- package/src/db/config-seed.ts +4 -4
- package/src/db/connection.ts +14 -57
- package/src/db/cursor.ts +5 -56
- package/src/db/dialect.ts +472 -99
- package/src/db/eagerload.ts +5 -12
- package/src/db/entity-table-meta.ts +390 -0
- package/src/db/event-store-executor.ts +158 -100
- package/src/db/index.ts +33 -5
- package/src/db/migrate-generator.ts +350 -0
- package/src/db/migrate-runner.ts +206 -0
- package/src/db/postgres-provider.ts +25 -0
- package/src/db/queries/entity-read.ts +15 -0
- package/src/db/queries/es-ops.ts +17 -0
- package/src/db/queries/event-consumer.ts +170 -0
- package/src/db/queries/event-store-admin.ts +127 -0
- package/src/db/queries/event-store.ts +155 -0
- package/src/db/queries/projection-rebuild.ts +59 -0
- package/src/db/queries/raw-sql.ts +15 -0
- package/src/db/queries/schema-drift.ts +35 -0
- package/src/db/queries/seed-context.ts +58 -0
- package/src/db/queries/table-ops.ts +11 -0
- package/src/db/queries/test-stack.ts +56 -0
- package/src/db/query-api.ts +22 -0
- package/src/db/query.ts +30 -0
- package/src/db/reference-data.ts +19 -22
- package/src/db/render-ddl.ts +57 -0
- package/src/db/row-helpers.ts +3 -52
- package/src/db/schema-inspection.ts +17 -4
- package/src/db/sql-inventory.ts +208 -0
- package/src/db/table-builder.ts +48 -40
- package/src/db/tenant-db.ts +105 -326
- package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
- package/src/engine/__tests__/boot-validator.test.ts +4 -3
- package/src/engine/__tests__/build-app-schema.test.ts +1 -1
- package/src/engine/__tests__/build-target.test.ts +1 -1
- package/src/engine/__tests__/claim-keys.test.ts +1 -1
- package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
- package/src/engine/__tests__/config-helpers.test.ts +1 -1
- package/src/engine/__tests__/effective-features.test.ts +1 -1
- package/src/engine/__tests__/engine.test.ts +1 -1
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +3 -3
- package/src/engine/__tests__/extends-registrar.test.ts +4 -4
- package/src/engine/__tests__/factories-long-text.test.ts +1 -1
- package/src/engine/__tests__/factories-time.test.ts +1 -1
- package/src/engine/__tests__/field-predicates.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +1 -1
- package/src/engine/__tests__/identifiers.test.ts +1 -1
- package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/ownership.test.ts +10 -11
- package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
- package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
- package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
- package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +1 -1
- package/src/engine/__tests__/projection-helpers.test.ts +25 -17
- package/src/engine/__tests__/projection.test.ts +4 -4
- package/src/engine/__tests__/qualified-name.test.ts +1 -1
- package/src/engine/__tests__/raw-table.test.ts +9 -8
- package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
- package/src/engine/__tests__/run-in.test.ts +1 -1
- package/src/engine/__tests__/schema-builder.test.ts +1 -1
- package/src/engine/__tests__/screen.test.ts +1 -1
- package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
- package/src/engine/__tests__/state-machine.test.ts +1 -1
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
- package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
- package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
- package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
- package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
- package/src/engine/__tests__/steps-read.test.ts +34 -40
- package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
- package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
- package/src/engine/__tests__/steps-workflow.test.ts +7 -7
- package/src/engine/__tests__/system-user.test.ts +1 -1
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
- package/src/engine/__tests__/validation-hooks.test.ts +1 -1
- package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
- package/src/engine/boot-validator/entity-handler.ts +3 -3
- package/src/engine/boot-validator/ownership.ts +1 -1
- package/src/engine/define-feature.ts +1 -2
- package/src/engine/entity-handlers.ts +5 -5
- package/src/engine/factories.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
- package/src/engine/ownership.ts +113 -41
- package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
- package/src/engine/projection-helpers.ts +2 -11
- package/src/engine/registry.ts +2 -2
- package/src/engine/steps/read-find-many.ts +13 -13
- package/src/engine/steps/read-find-one.ts +7 -9
- package/src/engine/steps/unsafe-projection-delete.ts +4 -5
- package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
- package/src/engine/types/feature.ts +7 -2
- package/src/engine/types/fields.ts +4 -5
- package/src/engine/types/step.ts +10 -10
- package/src/engine/validate-projection-allowlist.ts +23 -3
- package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
- package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
- package/src/env/__tests__/compose-env-schema.test.ts +1 -1
- package/src/env/__tests__/dry-run.test.ts +1 -1
- package/src/errors/__tests__/classes.test.ts +1 -1
- package/src/errors/__tests__/write-failures.test.ts +1 -1
- package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
- package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
- package/src/es-ops/__tests__/runner.test.ts +29 -19
- package/src/es-ops/context.ts +9 -43
- package/src/es-ops/operations-schema.ts +2 -2
- package/src/es-ops/runner.ts +12 -26
- package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
- package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
- package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
- package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
- package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
- package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
- package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
- package/src/event-store/admin-api.ts +55 -83
- package/src/event-store/archive.ts +15 -39
- package/src/event-store/event-store.ts +92 -86
- package/src/event-store/events-schema.ts +2 -1
- package/src/event-store/index.ts +1 -0
- package/src/event-store/snapshot.ts +26 -24
- package/src/event-store/upcaster-dead-letter.ts +19 -18
- package/src/files/__tests__/content-disposition.test.ts +1 -1
- package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
- package/src/files/__tests__/file-handle.test.ts +1 -1
- package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
- package/src/files/__tests__/read-stream.test.ts +1 -1
- package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
- package/src/files/__tests__/write-stream.test.ts +1 -1
- package/src/files/__tests__/zip-stream.test.ts +1 -1
- package/src/files/file-ref-table.ts +2 -2
- package/src/files/file-routes.ts +7 -9
- package/src/files/storage-tracking.ts +9 -17
- package/src/i18n/__tests__/i18n.test.ts +1 -1
- package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
- package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
- package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
- package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
- package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
- package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
- package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
- package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
- package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
- package/src/migrations/projection-detection.ts +12 -1
- package/src/migrations/schema-drift.ts +7 -23
- package/src/observability/__tests__/console-provider.test.ts +1 -1
- package/src/observability/__tests__/metric-validator.test.ts +1 -1
- package/src/observability/__tests__/noop-provider.test.ts +1 -1
- package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
- package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-tracer.test.ts +1 -1
- package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
- package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
- package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
- package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
- package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
- package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
- package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
- package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
- package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
- package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
- package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
- package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
- package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
- package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
- package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
- package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
- package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
- package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
- package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
- package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
- package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
- package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
- package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- package/src/pipeline/dispatcher.ts +43 -48
- package/src/pipeline/event-consumer-state.ts +11 -2
- package/src/pipeline/event-dispatcher.ts +86 -146
- package/src/pipeline/event-retention.ts +14 -24
- package/src/pipeline/msp-rebuild.ts +54 -78
- package/src/pipeline/projection-rebuild.ts +65 -67
- package/src/pipeline/projection-state.ts +2 -2
- package/src/random/__tests__/generate.test.ts +13 -13
- package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
- package/src/redis/__tests__/redis-options.test.ts +1 -1
- package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
- package/src/search/__tests__/search-adapter.test.ts +1 -1
- package/src/secrets/__tests__/dek-cache.test.ts +1 -3
- package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/__tests__/leak-guard.test.ts +1 -1
- package/src/secrets/__tests__/rotation.test.ts +1 -1
- package/src/stack/db.ts +25 -48
- package/src/stack/push-entity-projection-tables.ts +2 -4
- package/src/stack/table-helpers.ts +98 -61
- package/src/stack/test-stack.ts +8 -7
- package/src/testing/__tests__/db-cleanup.test.ts +40 -0
- package/src/testing/__tests__/e2e-generator.test.ts +1 -1
- package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
- package/src/testing/db-cleanup.ts +44 -0
- package/src/testing/expect-error.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/multipart-helper.ts +94 -0
- package/src/testing/shared-entities.ts +5 -5
- package/src/time/__tests__/polyfill.test.ts +1 -1
- package/src/time/__tests__/tz-context.test.ts +1 -1
- package/src/utils/__tests__/assert.test.ts +1 -1
- package/src/utils/__tests__/env-parse.test.ts +1 -1
- package/CHANGELOG.md +0 -474
- package/src/db/__tests__/cursor.test.ts +0 -41
- package/src/db/__tests__/db-helpers.test.ts +0 -369
- package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
- package/src/db/__tests__/row-helpers.test.ts +0 -59
- package/src/engine/steps/_drizzle-boundary.ts +0 -19
- package/src/files/__tests__/file-field-column.integration.ts +0 -103
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Legacy re-export shim — query-api.ts hat früher drizzle gewrapped.
|
|
2
|
+
// Heute liegen die Helpers in src/bun-db/. Wir re-exportieren von dort
|
|
3
|
+
// damit existing imports `@cosmicdrift/kumiko-framework/db` weiterhin
|
|
4
|
+
// funktionieren während wir die Callers schrittweise auf den direkten
|
|
5
|
+
// bun-db-Import migrieren.
|
|
6
|
+
|
|
7
|
+
export type {
|
|
8
|
+
SelectOptions,
|
|
9
|
+
WhereObject,
|
|
10
|
+
WhereOperator,
|
|
11
|
+
WhereValue,
|
|
12
|
+
} from "../db/query";
|
|
13
|
+
export {
|
|
14
|
+
asRawClient,
|
|
15
|
+
deleteMany,
|
|
16
|
+
fetchOne,
|
|
17
|
+
insertMany,
|
|
18
|
+
insertOne,
|
|
19
|
+
selectMany,
|
|
20
|
+
transaction,
|
|
21
|
+
updateMany,
|
|
22
|
+
} from "../db/query";
|
package/src/db/query.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Provider-neutrale Query-API. Re-exported aus bun-db/query.
|
|
2
|
+
// Alle Consumer importieren von hier, nicht direkt aus bun-db/.
|
|
3
|
+
export {
|
|
4
|
+
type AnyDb,
|
|
5
|
+
asRawClient,
|
|
6
|
+
coerceRow,
|
|
7
|
+
countWhere,
|
|
8
|
+
type DeleteManyBatchedOptions,
|
|
9
|
+
type DeleteManyBatchedResult,
|
|
10
|
+
deleteMany,
|
|
11
|
+
deleteManyBatched,
|
|
12
|
+
extractTableInfo,
|
|
13
|
+
fetchOne,
|
|
14
|
+
type IncrementCounterOptions,
|
|
15
|
+
incrementCounter,
|
|
16
|
+
insertMany,
|
|
17
|
+
insertOne,
|
|
18
|
+
type OrderByClause,
|
|
19
|
+
type SelectOptions,
|
|
20
|
+
selectMany,
|
|
21
|
+
type TableInfo,
|
|
22
|
+
transaction,
|
|
23
|
+
type UpsertOnConflictOptions,
|
|
24
|
+
updateMany,
|
|
25
|
+
upsertByPk,
|
|
26
|
+
upsertOnConflict,
|
|
27
|
+
type WhereObject,
|
|
28
|
+
type WhereOperator,
|
|
29
|
+
type WhereValue,
|
|
30
|
+
} from "../bun-db/query";
|
package/src/db/reference-data.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchOne, insertOne, updateMany } from "../db/query";
|
|
2
2
|
import type { ReferenceDataDef } from "../engine/types";
|
|
3
3
|
import { SYSTEM_TENANT_ID } from "../engine/types";
|
|
4
4
|
import type { DbConnection, DbRow } from "./connection";
|
|
5
5
|
import type { TableColumns } from "./dialect";
|
|
6
|
-
import { toSnakeCase } from "./table-builder";
|
|
7
6
|
|
|
8
7
|
// biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic tables
|
|
9
8
|
type Table = TableColumns<any>;
|
|
10
9
|
|
|
10
|
+
const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
|
|
11
|
+
|
|
12
|
+
function hasColumn(table: Table, field: string): boolean {
|
|
13
|
+
const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
|
|
14
|
+
if (typeof cols !== "object" || cols === null) return false;
|
|
15
|
+
return field in (cols as Record<string, unknown>);
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Seed reference data at boot time.
|
|
13
20
|
* For each ReferenceDataDef: upsert rows (insert missing, update changed, never delete).
|
|
@@ -31,43 +38,33 @@ export async function seedReferenceData(
|
|
|
31
38
|
const firstKey = Object.keys(firstRow)[0];
|
|
32
39
|
if (!firstKey) continue;
|
|
33
40
|
const upsertKey = def.upsertKey ?? firstKey;
|
|
34
|
-
const snakeKey = toSnakeCase(upsertKey);
|
|
35
41
|
|
|
36
42
|
for (const row of def.data) {
|
|
37
43
|
const keyValue = row[upsertKey];
|
|
38
44
|
if (keyValue === undefined) continue;
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
const [existing] = await db
|
|
42
|
-
.select()
|
|
43
|
-
.from(table)
|
|
44
|
-
.where(eq(table[upsertKey] ?? table[snakeKey], keyValue))
|
|
45
|
-
.limit(1);
|
|
46
|
+
const existing = (await fetchOne(db, table, { [upsertKey]: keyValue })) as DbRow | undefined;
|
|
46
47
|
|
|
47
48
|
if (existing) {
|
|
48
|
-
// Update if any field changed
|
|
49
|
-
const existingData = existing as DbRow;
|
|
50
49
|
const changes: Record<string, unknown> = {};
|
|
51
50
|
for (const [field, value] of Object.entries(row)) {
|
|
52
51
|
if (field === upsertKey) continue;
|
|
53
|
-
if (
|
|
52
|
+
if (existing[field] !== value) {
|
|
54
53
|
changes[field] = value;
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
if (Object.keys(changes).length > 0) {
|
|
58
|
-
await db
|
|
59
|
-
.update(table)
|
|
60
|
-
.set(changes)
|
|
61
|
-
.where(eq(table[upsertKey] ?? table[snakeKey], keyValue));
|
|
57
|
+
await updateMany(db, table, changes, { [upsertKey]: keyValue });
|
|
62
58
|
updated++;
|
|
63
59
|
}
|
|
64
60
|
} else {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
// Only add framework columns if the table actually has them.
|
|
62
|
+
// Drizzle used to filter extra fields silently; bunInsertOne doesn't.
|
|
63
|
+
const values: Record<string, unknown> = { ...row };
|
|
64
|
+
if (hasColumn(table, "tenantId")) values["tenantId"] = SYSTEM_TENANT_ID;
|
|
65
|
+
if (hasColumn(table, "version")) values["version"] = 1;
|
|
66
|
+
if (hasColumn(table, "insertedAt")) values["insertedAt"] = Temporal.Now.instant();
|
|
67
|
+
await insertOne(db, table, values);
|
|
71
68
|
inserted++;
|
|
72
69
|
}
|
|
73
70
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Pure renderer: EntityTableMeta → SQL DDL statements.
|
|
2
|
+
// Wird vom Migrate-Generator (Phase 2 — CLI-Tool `kumiko migrate generate`)
|
|
3
|
+
// genutzt um initial-SQL-Files zu schreiben. Output ist Start-Form für
|
|
4
|
+
// User-Review — App-Author darf das SQL danach hand-editieren (extra-Index,
|
|
5
|
+
// partial-Index, BRIN, custom-clauses) bevor committed wird.
|
|
6
|
+
//
|
|
7
|
+
// NO-MAGIC-ON-DATA: dieser Renderer wird NIE zur App-Runtime aufgerufen.
|
|
8
|
+
// Nur Build-Step. Runner liest checked-in SQL, nicht Renderer-Output.
|
|
9
|
+
|
|
10
|
+
import { pgTypeToSqlType } from "./dialect";
|
|
11
|
+
import type { ColumnMeta, EntityTableMeta, IndexMeta } from "./entity-table-meta";
|
|
12
|
+
|
|
13
|
+
function quoteIdent(name: string): string {
|
|
14
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderColumn(col: ColumnMeta): string {
|
|
18
|
+
const parts: string[] = [quoteIdent(col.name), pgTypeToSqlType(col.pgType)];
|
|
19
|
+
if (col.identity) parts.push("GENERATED ALWAYS AS IDENTITY");
|
|
20
|
+
if (col.primaryKey) parts.push("PRIMARY KEY");
|
|
21
|
+
if (col.defaultSql !== undefined) parts.push(`DEFAULT ${col.defaultSql}`);
|
|
22
|
+
if (col.notNull && !col.primaryKey) parts.push("NOT NULL");
|
|
23
|
+
return parts.join(" ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderIndex(tableName: string, idx: IndexMeta): string {
|
|
27
|
+
const kind = idx.unique === true ? "UNIQUE INDEX" : "INDEX";
|
|
28
|
+
const colList = idx.columns.map(quoteIdent).join(", ");
|
|
29
|
+
if (idx.needsManualWhere === true) {
|
|
30
|
+
return [
|
|
31
|
+
`-- WARN: partial-index "${idx.name}" needs a WHERE clause that the`,
|
|
32
|
+
`-- generator can't render (entity uses drizzle sql\`…\` AST).`,
|
|
33
|
+
`-- Add the WHERE manually before applying:`,
|
|
34
|
+
`-- CREATE ${kind} IF NOT EXISTS ${quoteIdent(idx.name)} ON ${quoteIdent(tableName)} (${colList}) WHERE <your-condition>;`,
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
const where = idx.whereSql !== undefined ? ` WHERE ${idx.whereSql}` : "";
|
|
38
|
+
return `CREATE ${kind} IF NOT EXISTS ${quoteIdent(idx.name)} ON ${quoteIdent(tableName)} (${colList})${where};`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function renderTableDdl(meta: EntityTableMeta): readonly string[] {
|
|
42
|
+
const colLines = meta.columns.map(renderColumn);
|
|
43
|
+
const lines: string[] = [...colLines];
|
|
44
|
+
if (meta.compositePrimaryKey !== undefined) {
|
|
45
|
+
const pkCols = meta.compositePrimaryKey.columns.map(quoteIdent).join(",");
|
|
46
|
+
lines.push(`CONSTRAINT ${quoteIdent(meta.compositePrimaryKey.name)} PRIMARY KEY(${pkCols})`);
|
|
47
|
+
}
|
|
48
|
+
const create = `CREATE TABLE IF NOT EXISTS ${quoteIdent(meta.tableName)} (\n ${lines.join(",\n ")}\n);`;
|
|
49
|
+
const indexes = meta.indexes.map((idx) => renderIndex(meta.tableName, idx));
|
|
50
|
+
return [create, ...indexes];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderTablesDdl(metas: readonly EntityTableMeta[]): readonly string[] {
|
|
54
|
+
const stmts: string[] = [];
|
|
55
|
+
for (const m of metas) stmts.push(...renderTableDdl(m));
|
|
56
|
+
return stmts;
|
|
57
|
+
}
|
package/src/db/row-helpers.ts
CHANGED
|
@@ -1,53 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import type { DbRow } from "./connection";
|
|
3
|
-
import type { TableColumns } from "./dialect";
|
|
1
|
+
// Legacy re-export shim — fetchOne lebt jetzt in src/bun-db/query.ts.
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// Minimal DB surface fetchOne uses — structurally satisfied by raw DbRunner
|
|
9
|
-
// (connection / tx) AND TenantDb (tenant-scoped wrapper). Both expose the
|
|
10
|
-
// same `select().from().where().limit()` chain with compatible rows, so the
|
|
11
|
-
// helper types against the shared shape instead of a union that TS can't
|
|
12
|
-
// narrow cleanly.
|
|
13
|
-
type SelectChainDb = {
|
|
14
|
-
select: () => {
|
|
15
|
-
from: (table: AnyTable) => {
|
|
16
|
-
where: (cond: SQL | undefined) => {
|
|
17
|
-
limit: (n: number) => PromiseLike<readonly Record<string, unknown>[]>;
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// SELECT * FROM <table> WHERE <...conditions> LIMIT 1 → first row or undefined.
|
|
24
|
-
// Collapses the "const [row] = await db.select()...limit(1)" destructure
|
|
25
|
-
// that repeats in every detail-query-style handler and existence-check.
|
|
26
|
-
//
|
|
27
|
-
// Conditions are variadic and non-empty — the tuple `[SQL, ...SQL[]]` rejects
|
|
28
|
-
// `fetchOne(db, table)` (would silently pick any row) and `fetchOne(db, table,
|
|
29
|
-
// undefined)` (would do the same) at compile time. Multiple conditions are
|
|
30
|
-
// combined with AND.
|
|
31
|
-
//
|
|
32
|
-
// const existing = await fetchOne<{ id: number }>(db, userTable,
|
|
33
|
-
// eq(userTable.email, payload.email));
|
|
34
|
-
// if (existing) return writeFailure(new ConflictError({ ... }));
|
|
35
|
-
//
|
|
36
|
-
// const row = await fetchOne(db, membershipTable,
|
|
37
|
-
// eq(membershipTable.userId, userId),
|
|
38
|
-
// eq(membershipTable.tenantId, tenantId),
|
|
39
|
-
// );
|
|
40
|
-
//
|
|
41
|
-
// For dynamic condition arrays (length known only at runtime), spread
|
|
42
|
-
// explicitly: `fetchOne(db, table, first, ...rest)`. Raw `...arr` with
|
|
43
|
-
// `arr: SQL[]` won't type-check because TS can't prove the array is non-
|
|
44
|
-
// empty — a feature, not a bug.
|
|
45
|
-
export async function fetchOne<TRow = DbRow>(
|
|
46
|
-
db: SelectChainDb,
|
|
47
|
-
table: AnyTable,
|
|
48
|
-
...conditions: readonly [SQL, ...SQL[]]
|
|
49
|
-
): Promise<TRow | undefined> {
|
|
50
|
-
const where = conditions.length === 1 ? conditions[0] : and(...conditions);
|
|
51
|
-
const rows = await db.select().from(table).where(where).limit(1);
|
|
52
|
-
return rows[0] as TRow | undefined; // @cast-boundary db-row
|
|
53
|
-
}
|
|
3
|
+
export type { WhereObject } from "../db/query";
|
|
4
|
+
export { fetchOne } from "../db/query";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { sql } from "drizzle-orm";
|
|
2
1
|
import type { DbConnection, DbTx } from "./connection";
|
|
3
2
|
|
|
4
3
|
// True when `<fullyQualifiedName>` refers to an existing relation in the
|
|
@@ -18,8 +17,22 @@ export async function tableExists(
|
|
|
18
17
|
db: DbConnection | DbTx,
|
|
19
18
|
fullyQualifiedName: string,
|
|
20
19
|
): Promise<boolean> {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const dbAny = db as unknown as {
|
|
21
|
+
$client?: {
|
|
22
|
+
unsafe: (s: string, p?: readonly unknown[]) => Promise<readonly { exists: boolean }[]>;
|
|
23
|
+
};
|
|
24
|
+
session?: {
|
|
25
|
+
client?: {
|
|
26
|
+
unsafe: (s: string, p?: readonly unknown[]) => Promise<readonly { exists: boolean }[]>;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
unsafe?: (s: string, p?: readonly unknown[]) => Promise<readonly { exists: boolean }[]>;
|
|
30
|
+
};
|
|
31
|
+
const client = dbAny.$client ?? dbAny.session?.client ?? dbAny;
|
|
32
|
+
const rows = await (
|
|
33
|
+
client as {
|
|
34
|
+
unsafe: (s: string, p?: readonly unknown[]) => Promise<readonly { exists: boolean }[]>;
|
|
35
|
+
}
|
|
36
|
+
).unsafe(`SELECT to_regclass($1) IS NOT NULL AS exists`, [fullyQualifiedName]);
|
|
24
37
|
return rows[0]?.exists ?? false;
|
|
25
38
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw-SQL inventory — shared allowlist for `kumiko sql-inventory` and
|
|
3
|
+
* `guard-raw-sql` (Phase 5). Scans TypeScript sources for escape-hatch patterns.
|
|
4
|
+
*
|
|
5
|
+
* Bun-only I/O: Bun.Glob + Bun.file (no node:fs, no node:path).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** POSIX path join without Node path module. */
|
|
9
|
+
export function joinPath(base: string, ...segments: string[]): string {
|
|
10
|
+
return [base, ...segments]
|
|
11
|
+
.join("/")
|
|
12
|
+
.replace(/\/+/g, "/")
|
|
13
|
+
.replace(/\/\.\//g, "/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SqlInventoryKind = "unsafe" | "asRawClient" | "delete_from" | "execute";
|
|
17
|
+
|
|
18
|
+
export type SqlInventoryHit = {
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly line: number;
|
|
21
|
+
readonly kind: SqlInventoryKind;
|
|
22
|
+
readonly allowed: boolean;
|
|
23
|
+
readonly snippet: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SqlInventoryReport = {
|
|
27
|
+
readonly scannedAt: string;
|
|
28
|
+
readonly root: string;
|
|
29
|
+
readonly hits: readonly SqlInventoryHit[];
|
|
30
|
+
readonly summary: {
|
|
31
|
+
readonly total: number;
|
|
32
|
+
readonly disallowed: number;
|
|
33
|
+
readonly byKind: Readonly<Record<SqlInventoryKind, number>>;
|
|
34
|
+
readonly byBucket: {
|
|
35
|
+
readonly allowed: number;
|
|
36
|
+
readonly tests: number;
|
|
37
|
+
readonly disallowed: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Paths where `.unsafe()` / `asRawClient()` are permitted (Phase 5 guard). */
|
|
43
|
+
export const RAW_SQL_ALLOWLIST: ReadonlyArray<RegExp> = [
|
|
44
|
+
/\/packages\/framework\/src\/db\/queries\//,
|
|
45
|
+
/\/packages\/framework\/src\/db\/migrate-runner\.ts$/,
|
|
46
|
+
/\/packages\/framework\/src\/db\/schema-inspection\.ts$/,
|
|
47
|
+
/\/packages\/framework\/src\/db\/render-ddl\.ts$/,
|
|
48
|
+
/\/packages\/framework\/src\/db\/sql-inventory\.ts$/,
|
|
49
|
+
/\/packages\/framework\/src\/bun-db\/query\.ts$/,
|
|
50
|
+
/\/packages\/framework\/src\/testing\//,
|
|
51
|
+
/\/packages\/bundled-features\/src\/[^/]+\/db\/queries\//,
|
|
52
|
+
/\/packages\/framework\/src\/engine\/steps\/unsafe-projection-/,
|
|
53
|
+
/\/samples\/(apps|recipes)\/[^/]+\/src\/db\/queries\//,
|
|
54
|
+
/\/bin\/commands\//,
|
|
55
|
+
/\/scripts\/codemod-/,
|
|
56
|
+
/\/__tests__\//,
|
|
57
|
+
/\/scripts\/sql-inventory\.ts$/,
|
|
58
|
+
/\/bin\/_lib\//,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const SCAN_DIRS = ["packages", "samples", "scripts", "bin"] as const;
|
|
62
|
+
|
|
63
|
+
const SKIP_PATH_PARTS = ["/node_modules/", "/dist/", "/.kumiko/"] as const;
|
|
64
|
+
|
|
65
|
+
const PATTERNS: ReadonlyArray<{ readonly kind: SqlInventoryKind; readonly re: RegExp }> = [
|
|
66
|
+
{ kind: "unsafe", re: /\.unsafe\s*\(/ },
|
|
67
|
+
{ kind: "asRawClient", re: /asRawClient\s*\(/ },
|
|
68
|
+
{ kind: "delete_from", re: /DELETE\s+FROM/i },
|
|
69
|
+
{ kind: "execute", re: /\.execute\s*\(/ },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const TS_GLOB = new Bun.Glob("**/*.{ts,tsx}");
|
|
73
|
+
|
|
74
|
+
function normalizePathForMatch(filePath: string): string {
|
|
75
|
+
return filePath.startsWith("/") ? filePath : `/${filePath}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isRawSqlAllowed(filePath: string): boolean {
|
|
79
|
+
const normalized = normalizePathForMatch(filePath);
|
|
80
|
+
return RAW_SQL_ALLOWLIST.some((re) => re.test(normalized));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isTestPath(filePath: string): boolean {
|
|
84
|
+
return /\/__tests__\//.test(normalizePathForMatch(filePath));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function bucketFor(hit: SqlInventoryHit): "allowed" | "tests" | "disallowed" {
|
|
88
|
+
if (isTestPath(hit.file)) return "tests";
|
|
89
|
+
if (hit.allowed) return "allowed";
|
|
90
|
+
return "disallowed";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function shouldSkipRelativePath(rel: string): boolean {
|
|
94
|
+
return SKIP_PATH_PARTS.some((part) => rel.includes(part));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function directoryExists(path: string): boolean {
|
|
98
|
+
return Bun.spawnSync(["test", "-d", path]).exitCode === 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function collectTsFiles(repoRoot: string): Promise<string[]> {
|
|
102
|
+
const out: string[] = [];
|
|
103
|
+
for (const sub of SCAN_DIRS) {
|
|
104
|
+
const cwd = joinPath(repoRoot, sub);
|
|
105
|
+
if (!directoryExists(cwd)) continue;
|
|
106
|
+
for await (const rel of TS_GLOB.scan({ cwd, onlyFiles: true })) {
|
|
107
|
+
const normalized = rel.replace(/\0/g, "");
|
|
108
|
+
if (!normalized || shouldSkipRelativePath(normalized)) continue;
|
|
109
|
+
out.push(joinPath(sub, normalized));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scanFileText(relPath: string, text: string, hits: SqlInventoryHit[]): void {
|
|
116
|
+
const lines = text.split("\n");
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
const line = lines[i] ?? "";
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
if (
|
|
121
|
+
trimmed.startsWith("//") ||
|
|
122
|
+
trimmed.startsWith("*") ||
|
|
123
|
+
trimmed.startsWith("/**") ||
|
|
124
|
+
trimmed.startsWith("/*")
|
|
125
|
+
) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const { kind, re } of PATTERNS) {
|
|
129
|
+
if (!re.test(line)) continue;
|
|
130
|
+
hits.push({
|
|
131
|
+
file: relPath,
|
|
132
|
+
line: i + 1,
|
|
133
|
+
kind,
|
|
134
|
+
allowed: isRawSqlAllowed(relPath),
|
|
135
|
+
snippet: trimmed.slice(0, 120),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function scanRepo(repoRoot: string): Promise<SqlInventoryReport> {
|
|
142
|
+
const relFiles = await collectTsFiles(repoRoot);
|
|
143
|
+
const hits: SqlInventoryHit[] = [];
|
|
144
|
+
|
|
145
|
+
for (const rel of relFiles) {
|
|
146
|
+
const abs = joinPath(repoRoot, rel);
|
|
147
|
+
const text = await Bun.file(abs).text();
|
|
148
|
+
scanFileText(rel, text, hits);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const byKind: Record<SqlInventoryKind, number> = {
|
|
152
|
+
unsafe: 0,
|
|
153
|
+
asRawClient: 0,
|
|
154
|
+
delete_from: 0,
|
|
155
|
+
execute: 0,
|
|
156
|
+
};
|
|
157
|
+
let disallowed = 0;
|
|
158
|
+
const byBucket = { allowed: 0, tests: 0, disallowed: 0 };
|
|
159
|
+
for (const h of hits) {
|
|
160
|
+
byKind[h.kind]++;
|
|
161
|
+
const b = bucketFor(h);
|
|
162
|
+
byBucket[b]++;
|
|
163
|
+
if (b === "disallowed") disallowed++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
scannedAt: new Date().toISOString(),
|
|
168
|
+
root: repoRoot,
|
|
169
|
+
hits,
|
|
170
|
+
summary: {
|
|
171
|
+
total: hits.length,
|
|
172
|
+
disallowed,
|
|
173
|
+
byKind,
|
|
174
|
+
byBucket,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function formatReport(report: SqlInventoryReport): string {
|
|
180
|
+
const lines: string[] = [
|
|
181
|
+
"--- sql inventory ---",
|
|
182
|
+
` scanned: ${report.scannedAt}`,
|
|
183
|
+
` root: ${report.root}`,
|
|
184
|
+
` total: ${report.summary.total}`,
|
|
185
|
+
` allowed: ${report.summary.byBucket.allowed}`,
|
|
186
|
+
` tests: ${report.summary.byBucket.tests}`,
|
|
187
|
+
` disallowed:${report.summary.disallowed}`,
|
|
188
|
+
` unsafe: ${report.summary.byKind.unsafe}`,
|
|
189
|
+
` asRawClient:${report.summary.byKind.asRawClient}`,
|
|
190
|
+
` DELETE FROM strings: ${report.summary.byKind.delete_from}`,
|
|
191
|
+
` .execute: ${report.summary.byKind.execute}`,
|
|
192
|
+
"---",
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const bad = report.hits.filter((h) => bucketFor(h) === "disallowed");
|
|
196
|
+
if (bad.length === 0) {
|
|
197
|
+
lines.push(" (no disallowed production hits)");
|
|
198
|
+
} else {
|
|
199
|
+
lines.push(" disallowed (production):");
|
|
200
|
+
for (const h of bad.slice(0, 40)) {
|
|
201
|
+
lines.push(` ${h.kind.padEnd(12)} ${h.file}:${h.line} ${h.snippet}`);
|
|
202
|
+
}
|
|
203
|
+
if (bad.length > 40) {
|
|
204
|
+
lines.push(` … +${bad.length - 40} more`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return lines.join("\n");
|
|
208
|
+
}
|
package/src/db/table-builder.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { sql } from "drizzle-orm";
|
|
2
|
-
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
|
3
1
|
import type {
|
|
4
2
|
EntityDefinition,
|
|
5
3
|
EntityRelations,
|
|
@@ -10,29 +8,31 @@ import { assertUnreachable } from "../utils";
|
|
|
10
8
|
import {
|
|
11
9
|
bigint,
|
|
12
10
|
boolean,
|
|
11
|
+
type ColumnBuilder,
|
|
12
|
+
type ColumnHandle,
|
|
13
|
+
type IndexBuilderWithCols,
|
|
13
14
|
index,
|
|
14
15
|
instant,
|
|
15
16
|
integer,
|
|
16
17
|
jsonb,
|
|
17
18
|
moneyAmount,
|
|
18
19
|
table as pgTable,
|
|
20
|
+
type SqlExpression,
|
|
19
21
|
serial,
|
|
22
|
+
sql,
|
|
20
23
|
type TableColumns,
|
|
21
24
|
text,
|
|
22
25
|
uniqueIndex,
|
|
23
26
|
uuid,
|
|
24
27
|
} from "./dialect";
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
| ReturnType<typeof instant>
|
|
34
|
-
| ReturnType<typeof serial>
|
|
35
|
-
| ReturnType<typeof uuid>;
|
|
29
|
+
// Local AnyPgColumn alias — kept for legacy field-definition callers that
|
|
30
|
+
// still import this name as a type. ColumnHandle from the native dialect
|
|
31
|
+
// matches the same role (snake_case name + sql type accessor).
|
|
32
|
+
export type AnyPgColumn = ColumnHandle;
|
|
33
|
+
|
|
34
|
+
// biome-ignore lint/suspicious/noExplicitAny: ColumnBuilder is parameterised over value type; we erase here
|
|
35
|
+
type AnyColumnBuilder = ColumnBuilder<any>;
|
|
36
36
|
|
|
37
37
|
// Returns column(s) for a field. Most fields return a single entry,
|
|
38
38
|
// money returns two (amount + currency), files/images return none.
|
|
@@ -48,7 +48,7 @@ function fieldToColumns(
|
|
|
48
48
|
name: string,
|
|
49
49
|
field: FieldDefinition,
|
|
50
50
|
entity: EntityDefinition,
|
|
51
|
-
): Record<string,
|
|
51
|
+
): Record<string, AnyColumnBuilder> {
|
|
52
52
|
const snakeName = toSnakeCase(name);
|
|
53
53
|
|
|
54
54
|
switch (field.type) {
|
|
@@ -246,10 +246,13 @@ export function toTableName(entityName: string): string {
|
|
|
246
246
|
// fieldToColumns passen. Type-Tests gegen repräsentative Entities (siehe
|
|
247
247
|
// db/__tests__/drizzle-table-types.test.ts) catchen Drift.
|
|
248
248
|
|
|
249
|
-
// Single
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
// Single column handle with concrete data + nullability phantom. After the
|
|
250
|
+
// drizzle removal the runtime carries only the snake_case name + pg type
|
|
251
|
+
// (see ColumnHandle); the phantom-typed wrapper preserves the existing
|
|
252
|
+
// generic-inference call-sites without recreating drizzle's full column
|
|
253
|
+
// brand graph.
|
|
254
|
+
type Col<_T> = ColumnHandle & { readonly __notNull: true };
|
|
255
|
+
type NullCol<_T> = ColumnHandle & { readonly __notNull: false };
|
|
253
256
|
|
|
254
257
|
// Per-field column shape — matches `fieldToColumns`. Money +
|
|
255
258
|
// locatedTimestamp produce two-column pairs; files/images contribute no
|
|
@@ -345,7 +348,7 @@ type SoftDeleteColumnsType = {
|
|
|
345
348
|
readonly deletedById: NullCol<string>;
|
|
346
349
|
};
|
|
347
350
|
|
|
348
|
-
export type
|
|
351
|
+
export type EntityTable<E extends EntityDefinition = EntityDefinition> =
|
|
349
352
|
TableColumns<// biome-ignore lint/suspicious/noExplicitAny: drizzle's internal table-config stays generic; we layer typed columns on top via the intersection below.
|
|
350
353
|
any> &
|
|
351
354
|
BaseColumnsType<E> &
|
|
@@ -389,7 +392,7 @@ export function buildBaseColumns(softDelete: boolean, idType: "serial" | "uuid"
|
|
|
389
392
|
return base;
|
|
390
393
|
}
|
|
391
394
|
|
|
392
|
-
export type
|
|
395
|
+
export type BuildEntityTableOptions = {
|
|
393
396
|
readonly featureName?: string;
|
|
394
397
|
// Relations declared for this entity. When present, every belongsTo
|
|
395
398
|
// foreignKey gets an index — otherwise joins and `WHERE fk = ?` filters
|
|
@@ -398,13 +401,13 @@ export type BuildDrizzleTableOptions = {
|
|
|
398
401
|
readonly relations?: EntityRelations;
|
|
399
402
|
};
|
|
400
403
|
|
|
401
|
-
export function
|
|
404
|
+
export function buildEntityTable<E extends EntityDefinition>(
|
|
402
405
|
entityName: string,
|
|
403
406
|
entity: E,
|
|
404
|
-
options?:
|
|
405
|
-
):
|
|
407
|
+
options?: BuildEntityTableOptions,
|
|
408
|
+
): EntityTable<E> {
|
|
406
409
|
const baseColumns = buildBaseColumns(entity.softDelete ?? false, entity.idType ?? "uuid");
|
|
407
|
-
const fieldColumns: Record<string,
|
|
410
|
+
const fieldColumns: Record<string, AnyColumnBuilder> = {};
|
|
408
411
|
|
|
409
412
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
410
413
|
const cols = fieldToColumns(name, field, entity);
|
|
@@ -444,7 +447,7 @@ export function buildDrizzleTable<E extends EntityDefinition>(
|
|
|
444
447
|
}
|
|
445
448
|
}
|
|
446
449
|
|
|
447
|
-
// Cast back to
|
|
450
|
+
// Cast back to EntityTable<E>: drizzle-kit's pgTable returns a fully
|
|
448
451
|
// inferred PgTableWithColumns over the *exact* column-builder map we
|
|
449
452
|
// hand in. Our typed signature narrows that to the static names from
|
|
450
453
|
// EntityDefinition (kept in sync with fieldToColumns + buildBaseColumns).
|
|
@@ -457,14 +460,20 @@ export function buildDrizzleTable<E extends EntityDefinition>(
|
|
|
457
460
|
},
|
|
458
461
|
// Every multi-tenant query filters by tenant_id. Without this index, list
|
|
459
462
|
// queries scan the whole table across all tenants. Applies to every table
|
|
460
|
-
// built via
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
463
|
+
// built via buildEntityTable since every entity inherits tenantId.
|
|
464
|
+
(table) => {
|
|
465
|
+
const indexes: Record<string, IndexBuilderWithCols> = {};
|
|
466
|
+
const tHandle = table as unknown as Record<string, ColumnHandle>;
|
|
467
|
+
indexes[`${tableName}_tenant_id_idx`] = index(`${tableName}_tenant_id_idx`).on(
|
|
468
|
+
// biome-ignore lint/style/noNonNullAssertion: tenantId column always exists on entity tables
|
|
469
|
+
tHandle["tenantId"]!,
|
|
470
|
+
);
|
|
464
471
|
for (const fieldName of foreignKeyFields) {
|
|
465
|
-
const column =
|
|
472
|
+
const column = tHandle[fieldName];
|
|
466
473
|
if (column) {
|
|
467
|
-
indexes
|
|
474
|
+
indexes[`${tableName}_${toSnakeCase(fieldName)}_idx`] = index(
|
|
475
|
+
`${tableName}_${toSnakeCase(fieldName)}_idx`,
|
|
476
|
+
).on(column);
|
|
468
477
|
}
|
|
469
478
|
}
|
|
470
479
|
// entity.indexes = composite/unique-Indices die der Author explizit
|
|
@@ -473,23 +482,22 @@ export function buildDrizzleTable<E extends EntityDefinition>(
|
|
|
473
482
|
// — Override via index.name möglich.
|
|
474
483
|
for (const def of entity.indexes ?? []) {
|
|
475
484
|
const cols = def.columns
|
|
476
|
-
.map((fieldName) =>
|
|
477
|
-
.filter((col): col is
|
|
478
|
-
if (cols.length !== def.columns.length) continue;
|
|
485
|
+
.map((fieldName) => tHandle[fieldName])
|
|
486
|
+
.filter((col): col is ColumnHandle => col !== undefined);
|
|
487
|
+
if (cols.length !== def.columns.length) continue;
|
|
479
488
|
const suffix = def.unique === true ? "unique" : "idx";
|
|
480
489
|
const indexName =
|
|
481
490
|
def.name ?? `${tableName}_${def.columns.map((c) => toSnakeCase(c)).join("_")}_${suffix}`;
|
|
482
491
|
const builder = def.unique === true ? uniqueIndex(indexName) : index(indexName);
|
|
483
|
-
|
|
484
|
-
|
|
492
|
+
let chain = builder.on(...cols);
|
|
493
|
+
// entity.indexes[].where is now a SqlExpression (was drizzle SQL).
|
|
494
|
+
// Pass through to the IndexBuilderWithCols.where()-API.
|
|
485
495
|
if (def.where !== undefined) {
|
|
486
|
-
|
|
487
|
-
// `WHERE <condition>` ans Ende der `CREATE [UNIQUE] INDEX`-DDL.
|
|
488
|
-
chain = chain.where(def.where);
|
|
496
|
+
chain = chain.where(def.where as SqlExpression);
|
|
489
497
|
}
|
|
490
|
-
indexes
|
|
498
|
+
indexes[indexName] = chain;
|
|
491
499
|
}
|
|
492
500
|
return indexes;
|
|
493
501
|
},
|
|
494
|
-
) as unknown as
|
|
502
|
+
) as unknown as EntityTable<E>;
|
|
495
503
|
}
|