@cosmicdrift/kumiko-framework 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
- package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
- package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
- package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
- package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
- package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
- package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
- package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
- package/src/api/__tests__/api.test.ts +1 -1
- package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
- package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
- package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/csrf-middleware.test.ts +1 -1
- package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
- package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
- package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
- package/src/api/__tests__/readiness.test.ts +1 -1
- package/src/api/__tests__/request-id-middleware.test.ts +1 -1
- package/src/api/__tests__/sse-broker.test.ts +12 -12
- package/src/api/__tests__/sse-route.test.ts +1 -1
- package/src/api/auth-routes.ts +2 -5
- package/src/api/readiness.ts +2 -2
- package/src/auth/__tests__/roles.test.ts +2 -2
- package/src/bun-db/__tests__/PATTERN.md +73 -0
- package/src/bun-db/__tests__/_helpers.ts +103 -0
- package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
- package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
- package/src/bun-db/__tests__/bun-test-db.ts +19 -0
- package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
- package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
- package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
- package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
- package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
- package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
- package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
- package/src/bun-db/connection.ts +84 -0
- package/src/bun-db/index.ts +31 -0
- package/src/bun-db/query.ts +842 -0
- package/src/compliance/__tests__/duration-spec.test.ts +1 -1
- package/src/compliance/__tests__/profiles.test.ts +1 -1
- package/src/compliance/__tests__/sub-processors.test.ts +1 -1
- package/src/compliance/profiles.ts +1 -4
- package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
- package/src/db/__tests__/big-int-field.test.ts +15 -14
- package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
- package/src/db/__tests__/connection-options.test.ts +1 -1
- package/src/db/__tests__/cursor.test.ts +8 -32
- package/src/db/__tests__/dialect-instant.test.ts +1 -1
- package/src/db/__tests__/encryption.test.ts +1 -1
- package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
- package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
- package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
- package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/migrate-generator.test.ts +71 -0
- package/src/db/__tests__/migrate-runner.test.ts +19 -0
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
- package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
- package/src/db/__tests__/pg-error.test.ts +43 -0
- package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
- package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
- package/src/db/__tests__/sql-inventory.test.ts +56 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
- package/src/db/__tests__/table-builder-required.test.ts +20 -22
- package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
- package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
- package/src/db/api.ts +46 -0
- package/src/db/apply-entity-event.ts +45 -36
- package/src/db/assert-exists-in.ts +5 -16
- package/src/db/bun-provider.ts +37 -0
- package/src/db/config-seed.ts +4 -4
- package/src/db/connection.ts +14 -57
- package/src/db/cursor.ts +5 -56
- package/src/db/dialect.ts +472 -99
- package/src/db/eagerload.ts +5 -12
- package/src/db/entity-table-meta.ts +390 -0
- package/src/db/event-store-executor.ts +158 -100
- package/src/db/index.ts +33 -5
- package/src/db/migrate-generator.ts +350 -0
- package/src/db/migrate-runner.ts +206 -0
- package/src/db/postgres-provider.ts +25 -0
- package/src/db/queries/entity-read.ts +15 -0
- package/src/db/queries/es-ops.ts +17 -0
- package/src/db/queries/event-consumer.ts +170 -0
- package/src/db/queries/event-store-admin.ts +127 -0
- package/src/db/queries/event-store.ts +155 -0
- package/src/db/queries/projection-rebuild.ts +59 -0
- package/src/db/queries/raw-sql.ts +15 -0
- package/src/db/queries/schema-drift.ts +35 -0
- package/src/db/queries/seed-context.ts +58 -0
- package/src/db/queries/table-ops.ts +11 -0
- package/src/db/queries/test-stack.ts +56 -0
- package/src/db/query-api.ts +22 -0
- package/src/db/query.ts +30 -0
- package/src/db/reference-data.ts +19 -22
- package/src/db/render-ddl.ts +57 -0
- package/src/db/row-helpers.ts +3 -52
- package/src/db/schema-inspection.ts +17 -4
- package/src/db/sql-inventory.ts +208 -0
- package/src/db/table-builder.ts +54 -46
- package/src/db/tenant-db.ts +105 -326
- package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
- package/src/engine/__tests__/boot-validator.test.ts +4 -3
- package/src/engine/__tests__/build-app-schema.test.ts +1 -1
- package/src/engine/__tests__/build-target.test.ts +1 -1
- package/src/engine/__tests__/claim-keys.test.ts +1 -1
- package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
- package/src/engine/__tests__/config-helpers.test.ts +1 -1
- package/src/engine/__tests__/duration-utils.test.ts +16 -0
- package/src/engine/__tests__/effective-features.test.ts +1 -1
- package/src/engine/__tests__/engine.test.ts +1 -1
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +3 -3
- package/src/engine/__tests__/extends-registrar.test.ts +4 -4
- package/src/engine/__tests__/factories-long-text.test.ts +1 -1
- package/src/engine/__tests__/factories-time.test.ts +1 -1
- package/src/engine/__tests__/field-access.test.ts +38 -0
- package/src/engine/__tests__/field-predicates.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +1 -1
- package/src/engine/__tests__/identifiers.test.ts +1 -1
- package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/no-return-guard.test.ts +17 -0
- package/src/engine/__tests__/ownership.test.ts +10 -11
- package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
- package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
- package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
- package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +1 -1
- package/src/engine/__tests__/projection-helpers.test.ts +25 -17
- package/src/engine/__tests__/projection.test.ts +4 -4
- package/src/engine/__tests__/qualified-name.test.ts +1 -1
- package/src/engine/__tests__/raw-table.test.ts +9 -8
- package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
- package/src/engine/__tests__/run-in.test.ts +1 -1
- package/src/engine/__tests__/schema-builder.test.ts +1 -1
- package/src/engine/__tests__/screen.test.ts +1 -1
- package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
- package/src/engine/__tests__/state-machine.test.ts +1 -1
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
- package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
- package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
- package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
- package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
- package/src/engine/__tests__/steps-read.test.ts +34 -40
- package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
- package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
- package/src/engine/__tests__/steps-workflow.test.ts +7 -7
- package/src/engine/__tests__/system-user.test.ts +1 -1
- package/src/engine/__tests__/unmanaged-table.test.ts +98 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
- package/src/engine/__tests__/validation-hooks.test.ts +1 -1
- package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
- package/src/engine/boot-validator/entity-handler.ts +3 -3
- package/src/engine/boot-validator/ownership.ts +1 -1
- package/src/engine/define-feature.ts +37 -2
- package/src/engine/entity-handlers.ts +5 -5
- package/src/engine/factories.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
- package/src/engine/feature-ast/extractors/shared.ts +2 -3
- package/src/engine/ownership.ts +113 -41
- package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
- package/src/engine/projection-helpers.ts +2 -11
- package/src/engine/registry.ts +21 -2
- package/src/engine/steps/read-find-many.ts +13 -13
- package/src/engine/steps/read-find-one.ts +7 -9
- package/src/engine/steps/unsafe-projection-delete.ts +4 -5
- package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
- package/src/engine/types/feature.ts +47 -2
- package/src/engine/types/fields.ts +4 -5
- package/src/engine/types/index.ts +2 -0
- package/src/engine/types/step.ts +10 -10
- package/src/engine/validate-projection-allowlist.ts +23 -3
- package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
- package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
- package/src/env/__tests__/compose-env-schema.test.ts +1 -1
- package/src/env/__tests__/dry-run.test.ts +1 -1
- package/src/errors/__tests__/classes.test.ts +1 -1
- package/src/errors/__tests__/error-helpers.test.ts +44 -0
- package/src/errors/__tests__/field-issue-compat.test.ts +16 -0
- package/src/errors/__tests__/write-failures.test.ts +1 -1
- package/src/errors/classes.ts +5 -19
- package/src/errors/field-issue.ts +11 -0
- package/src/errors/index.ts +1 -0
- package/src/errors/zod-bridge.ts +3 -2
- package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
- package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
- package/src/es-ops/__tests__/runner.test.ts +29 -19
- package/src/es-ops/context.ts +11 -56
- package/src/es-ops/operations-schema.ts +2 -2
- package/src/es-ops/runner.ts +12 -26
- package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
- package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
- package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
- package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
- package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
- package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
- package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
- package/src/event-store/admin-api.ts +55 -83
- package/src/event-store/archive.ts +15 -39
- package/src/event-store/event-store.ts +92 -86
- package/src/event-store/events-schema.ts +2 -1
- package/src/event-store/index.ts +1 -0
- package/src/event-store/snapshot.ts +26 -24
- package/src/event-store/upcaster-dead-letter.ts +19 -18
- package/src/files/__tests__/content-disposition.test.ts +1 -1
- package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
- package/src/files/__tests__/file-handle.test.ts +1 -1
- package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
- package/src/files/__tests__/read-stream.test.ts +1 -1
- package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
- package/src/files/__tests__/write-stream.test.ts +1 -1
- package/src/files/__tests__/zip-stream.test.ts +1 -1
- package/src/files/file-ref-table.ts +2 -2
- package/src/files/file-routes.ts +7 -9
- package/src/files/storage-tracking.ts +9 -17
- package/src/i18n/__tests__/i18n.test.ts +1 -1
- package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
- package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
- package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
- package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
- package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
- package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
- package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
- package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
- package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
- package/src/migrations/projection-detection.ts +12 -1
- package/src/migrations/schema-drift.ts +7 -23
- package/src/observability/__tests__/console-provider.test.ts +1 -1
- package/src/observability/__tests__/metric-validator.test.ts +1 -1
- package/src/observability/__tests__/noop-provider.test.ts +1 -1
- package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
- package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-tracer.test.ts +1 -1
- package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
- package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
- package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
- package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
- package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
- package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
- package/src/pipeline/__tests__/dispatcher-utils.test.ts +107 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
- package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
- package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
- package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
- package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
- package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
- package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
- package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
- package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
- package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
- package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
- package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
- package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
- package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
- package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
- package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
- package/src/pipeline/__tests__/redis-keys.test.ts +12 -0
- package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- package/src/pipeline/dispatcher-utils.ts +8 -7
- package/src/pipeline/dispatcher.ts +43 -48
- package/src/pipeline/event-consumer-state.ts +11 -2
- package/src/pipeline/event-dispatcher.ts +86 -146
- package/src/pipeline/event-retention.ts +14 -24
- package/src/pipeline/msp-rebuild.ts +54 -78
- package/src/pipeline/projection-rebuild.ts +65 -67
- package/src/pipeline/projection-state.ts +2 -2
- package/src/random/__tests__/generate.test.ts +13 -13
- package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
- package/src/redis/__tests__/redis-options.test.ts +1 -1
- package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
- package/src/search/__tests__/search-adapter.test.ts +1 -1
- package/src/secrets/__tests__/dek-cache.test.ts +1 -3
- package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/__tests__/leak-guard.test.ts +1 -1
- package/src/secrets/__tests__/rotation.test.ts +1 -1
- package/src/stack/db.ts +25 -48
- package/src/stack/push-entity-projection-tables.ts +2 -4
- package/src/stack/table-helpers.ts +98 -61
- package/src/stack/test-stack.ts +10 -9
- package/src/testing/__tests__/db-cleanup.test.ts +40 -0
- package/src/testing/__tests__/e2e-generator.test.ts +1 -1
- package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
- package/src/testing/db-cleanup.ts +44 -0
- package/src/testing/expect-error.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/multipart-helper.ts +94 -0
- package/src/testing/shared-entities.ts +5 -5
- package/src/time/__tests__/polyfill.test.ts +1 -1
- package/src/time/__tests__/tz-context.test.ts +1 -1
- package/src/utils/__tests__/assert.test.ts +1 -1
- package/src/utils/__tests__/case.test.ts +16 -0
- package/src/utils/__tests__/env-parse.test.ts +1 -1
- package/src/utils/__tests__/is-plain-object.test.ts +16 -0
- package/src/utils/__tests__/parse-string-array-json.test.ts +16 -0
- package/src/utils/__tests__/safe-json.test.ts +22 -0
- package/src/utils/case.ts +6 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/is-plain-object.ts +4 -0
- package/src/utils/parse-string-array-json.ts +14 -0
- package/CHANGELOG.md +0 -474
- package/src/db/__tests__/db-helpers.test.ts +0 -369
- package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
- package/src/db/__tests__/row-helpers.test.ts +0 -59
- package/src/engine/steps/_drizzle-boundary.ts +0 -19
- package/src/files/__tests__/file-field-column.integration.ts +0 -103
|
@@ -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
|
+
}
|