@cosmicdrift/kumiko-framework 0.1.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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createConsoleProvider } from "../../observability";
|
|
3
|
+
import { mergeTraceFields } from "../pino-logger";
|
|
4
|
+
|
|
5
|
+
// The Pino trace-bridge hook is verified by directly exercising the helper
|
|
6
|
+
// that wrapPino uses. Going through pino's JSON output is flaky because
|
|
7
|
+
// pino buffers/async-writes; the helper is the actual contract, and the
|
|
8
|
+
// fact that wrapPino calls it on every log method is visible by inspection.
|
|
9
|
+
|
|
10
|
+
describe("mergeTraceFields", () => {
|
|
11
|
+
it("adds traceId/spanId when an active span exists", async () => {
|
|
12
|
+
const provider = createConsoleProvider({ writer: { log: () => {} } });
|
|
13
|
+
let captured: Record<string, unknown> | undefined;
|
|
14
|
+
await provider.tracer.withSpan("http.request", async () => {
|
|
15
|
+
captured = mergeTraceFields({ user: "bob" });
|
|
16
|
+
});
|
|
17
|
+
expect(captured).toMatchObject({
|
|
18
|
+
user: "bob",
|
|
19
|
+
traceId: expect.stringMatching(/^[0-9a-f]{32}$/),
|
|
20
|
+
spanId: expect.stringMatching(/^[0-9a-f]{16}$/),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns data unchanged when no active span", () => {
|
|
25
|
+
const result = mergeTraceFields({ foo: "bar" });
|
|
26
|
+
expect(result).toEqual({ foo: "bar" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns undefined when neither span nor data", () => {
|
|
30
|
+
expect(mergeTraceFields(undefined)).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("caller data overrides trace fields when conflicting", async () => {
|
|
34
|
+
const provider = createConsoleProvider({ writer: { log: () => {} } });
|
|
35
|
+
let captured: Record<string, unknown> | undefined;
|
|
36
|
+
await provider.tracer.withSpan("x", async () => {
|
|
37
|
+
captured = mergeTraceFields({ traceId: "override" });
|
|
38
|
+
});
|
|
39
|
+
expect(captured?.["traceId"]).toBe("override");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns just trace fields when data is undefined and span is active", async () => {
|
|
43
|
+
const provider = createConsoleProvider({ writer: { log: () => {} } });
|
|
44
|
+
let captured: Record<string, unknown> | undefined;
|
|
45
|
+
await provider.tracer.withSpan("y", async () => {
|
|
46
|
+
captured = mergeTraceFields(undefined);
|
|
47
|
+
});
|
|
48
|
+
expect(Object.keys(captured ?? {}).sort()).toEqual(["spanId", "traceId"]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pino, { type DestinationStream } from "pino";
|
|
2
|
+
import { observabilityContext } from "../observability";
|
|
3
|
+
import type { Logger } from "./types";
|
|
4
|
+
|
|
5
|
+
export type LoggerOptions = {
|
|
6
|
+
level?: "debug" | "info" | "warn" | "error";
|
|
7
|
+
pretty?: boolean;
|
|
8
|
+
// Optional custom sink — pino writes NDJSON to it instead of stdout. Used
|
|
9
|
+
// in tests that need to capture and parse log output.
|
|
10
|
+
destination?: DestinationStream;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createLogger(options: LoggerOptions = {}): Logger {
|
|
14
|
+
const level = options.level ?? (process.env["LOG_LEVEL"] as LoggerOptions["level"]) ?? "info";
|
|
15
|
+
const pretty = options.pretty ?? process.env["LOG_FORMAT"] === "pretty";
|
|
16
|
+
|
|
17
|
+
const pinoConfig = {
|
|
18
|
+
level,
|
|
19
|
+
...(pretty && !options.destination
|
|
20
|
+
? { transport: { target: "pino-pretty", options: { colorize: true } } }
|
|
21
|
+
: {}),
|
|
22
|
+
};
|
|
23
|
+
const pinoLogger = options.destination ? pino(pinoConfig, options.destination) : pino(pinoConfig);
|
|
24
|
+
|
|
25
|
+
return wrapPino(pinoLogger);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Pull traceId/spanId from the active observability context so every log
|
|
29
|
+
// line carries them when tracing is active. Empty IDs (NoopProvider) skip —
|
|
30
|
+
// nothing to correlate, no need to clutter output. Trace fields don't
|
|
31
|
+
// overwrite caller-provided data so explicit overrides still win. Exported
|
|
32
|
+
// for unit tests.
|
|
33
|
+
export function mergeTraceFields(
|
|
34
|
+
data: Record<string, unknown> | undefined,
|
|
35
|
+
): Record<string, unknown> | undefined {
|
|
36
|
+
const span = observabilityContext.getActiveSpan();
|
|
37
|
+
if (!span?.traceId) return data;
|
|
38
|
+
const traceFields = { traceId: span.traceId, spanId: span.spanId };
|
|
39
|
+
return data ? { ...traceFields, ...data } : traceFields;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function wrapPino(p: pino.Logger): Logger {
|
|
43
|
+
return {
|
|
44
|
+
info(msg, data) {
|
|
45
|
+
const merged = mergeTraceFields(data);
|
|
46
|
+
merged ? p.info(merged, msg) : p.info(msg);
|
|
47
|
+
},
|
|
48
|
+
warn(msg, data) {
|
|
49
|
+
const merged = mergeTraceFields(data);
|
|
50
|
+
merged ? p.warn(merged, msg) : p.warn(msg);
|
|
51
|
+
},
|
|
52
|
+
error(msg, data) {
|
|
53
|
+
const merged = mergeTraceFields(data);
|
|
54
|
+
merged ? p.error(merged, msg) : p.error(msg);
|
|
55
|
+
},
|
|
56
|
+
debug(msg, data) {
|
|
57
|
+
const merged = mergeTraceFields(data);
|
|
58
|
+
merged ? p.debug(merged, msg) : p.debug(msg);
|
|
59
|
+
},
|
|
60
|
+
child(context) {
|
|
61
|
+
return wrapPino(p.child(context));
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Logger = {
|
|
2
|
+
info(msg: string, data?: Record<string, unknown>): void;
|
|
3
|
+
warn(msg: string, data?: Record<string, unknown>): void;
|
|
4
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
5
|
+
debug(msg: string, data?: Record<string, unknown>): void;
|
|
6
|
+
child(context: Record<string, unknown>): Logger;
|
|
7
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Unit-Tests für compareSnapshots — der Diff-Algorithmus zwischen zwei
|
|
2
|
+
// Drizzle-Snapshots. Production-Behavior: bei Schema-Drift einer
|
|
3
|
+
// Projection-Tabelle muss der Detector die richtigen Tabellen-Namen
|
|
4
|
+
// melden, damit migrate apply den richtigen Rebuild triggert.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { compareSnapshots } from "../projection-detection";
|
|
8
|
+
import type { Snapshot, SnapshotTable } from "../schema-drift";
|
|
9
|
+
|
|
10
|
+
function snapshot(tables: Record<string, Partial<SnapshotTable>>): Snapshot {
|
|
11
|
+
const out: Record<string, SnapshotTable> = {};
|
|
12
|
+
for (const [key, partial] of Object.entries(tables)) {
|
|
13
|
+
out[key] = {
|
|
14
|
+
schema: partial.schema ?? "",
|
|
15
|
+
name: partial.name ?? key.replace(/^public\./, ""),
|
|
16
|
+
columns: partial.columns ?? {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { tables: out };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const userTable: SnapshotTable = {
|
|
23
|
+
schema: "",
|
|
24
|
+
name: "users",
|
|
25
|
+
columns: {
|
|
26
|
+
id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
|
|
27
|
+
email: { name: "email", type: "text", notNull: true },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("compareSnapshots", () => {
|
|
32
|
+
test("prev=null → all current tables marked as added", () => {
|
|
33
|
+
const current = snapshot({ "public.users": userTable });
|
|
34
|
+
const changes = compareSnapshots(null, current);
|
|
35
|
+
expect(changes).toHaveLength(1);
|
|
36
|
+
expect(changes[0]).toMatchObject({ tableName: "users", kind: "added" });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("identical snapshots → no changes", () => {
|
|
40
|
+
const s = snapshot({ "public.users": userTable });
|
|
41
|
+
expect(compareSnapshots(s, s)).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("table appears in current → kind=added", () => {
|
|
45
|
+
const prev = snapshot({});
|
|
46
|
+
const current = snapshot({ "public.users": userTable });
|
|
47
|
+
const changes = compareSnapshots(prev, current);
|
|
48
|
+
expect(changes).toEqual([{ fullName: "users", tableName: "users", kind: "added" }]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("table missing in current → kind=removed", () => {
|
|
52
|
+
const prev = snapshot({ "public.users": userTable });
|
|
53
|
+
const current = snapshot({});
|
|
54
|
+
const changes = compareSnapshots(prev, current);
|
|
55
|
+
expect(changes).toEqual([{ fullName: "users", tableName: "users", kind: "removed" }]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("column added → kind=modified", () => {
|
|
59
|
+
const prev = snapshot({ "public.users": userTable });
|
|
60
|
+
const current = snapshot({
|
|
61
|
+
"public.users": {
|
|
62
|
+
...userTable,
|
|
63
|
+
columns: { ...userTable.columns, name: { name: "name", type: "text" } },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const changes = compareSnapshots(prev, current);
|
|
67
|
+
expect(changes).toEqual([{ fullName: "users", tableName: "users", kind: "modified" }]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("column type changed → kind=modified", () => {
|
|
71
|
+
const prev = snapshot({ "public.users": userTable });
|
|
72
|
+
const current = snapshot({
|
|
73
|
+
"public.users": {
|
|
74
|
+
...userTable,
|
|
75
|
+
columns: {
|
|
76
|
+
...userTable.columns,
|
|
77
|
+
email: { name: "email", type: "varchar(255)", notNull: true },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
expect(compareSnapshots(prev, current)).toEqual([
|
|
82
|
+
{ fullName: "users", tableName: "users", kind: "modified" },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("notNull flipped → kind=modified", () => {
|
|
87
|
+
const prev = snapshot({ "public.users": userTable });
|
|
88
|
+
const current = snapshot({
|
|
89
|
+
"public.users": {
|
|
90
|
+
...userTable,
|
|
91
|
+
columns: { ...userTable.columns, email: { name: "email", type: "text", notNull: false } },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
expect(compareSnapshots(prev, current)).toEqual([
|
|
95
|
+
{ fullName: "users", tableName: "users", kind: "modified" },
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("default value changed → kind=modified", () => {
|
|
100
|
+
const prev = snapshot({
|
|
101
|
+
"public.users": {
|
|
102
|
+
...userTable,
|
|
103
|
+
columns: {
|
|
104
|
+
...userTable.columns,
|
|
105
|
+
status: { name: "status", type: "text", default: "'active'" },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const current = snapshot({
|
|
110
|
+
"public.users": {
|
|
111
|
+
...userTable,
|
|
112
|
+
columns: {
|
|
113
|
+
...userTable.columns,
|
|
114
|
+
status: { name: "status", type: "text", default: "'pending'" },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
expect(compareSnapshots(prev, current)).toEqual([
|
|
119
|
+
{ fullName: "users", tableName: "users", kind: "modified" },
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("schema-prefix in fullName when set", () => {
|
|
124
|
+
const prev = snapshot({});
|
|
125
|
+
const current = snapshot({
|
|
126
|
+
"auth.users": { ...userTable, schema: "auth" },
|
|
127
|
+
});
|
|
128
|
+
const changes = compareSnapshots(prev, current);
|
|
129
|
+
expect(changes[0]?.fullName).toBe("auth.users");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("multiple changes preserved with stable kind classification", () => {
|
|
133
|
+
const tableA: SnapshotTable = { ...userTable, name: "a" };
|
|
134
|
+
const tableB: SnapshotTable = { ...userTable, name: "b" };
|
|
135
|
+
const tableC: SnapshotTable = { ...userTable, name: "c" };
|
|
136
|
+
const prev = snapshot({
|
|
137
|
+
"public.a": tableA,
|
|
138
|
+
"public.b": tableB,
|
|
139
|
+
});
|
|
140
|
+
const current = snapshot({
|
|
141
|
+
"public.a": tableA, // unchanged
|
|
142
|
+
"public.c": tableC, // added
|
|
143
|
+
// b removed
|
|
144
|
+
});
|
|
145
|
+
const changes = compareSnapshots(prev, current);
|
|
146
|
+
expect(changes).toHaveLength(2);
|
|
147
|
+
expect(changes.find((c) => c.tableName === "c")?.kind).toBe("added");
|
|
148
|
+
expect(changes.find((c) => c.tableName === "b")?.kind).toBe("removed");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Integration-Test für detectDrift — vergleicht Journal vs.
|
|
2
|
+
// __drizzle_migrations und expected-Tables vs. Reality. Production-
|
|
3
|
+
// Behavior: assertSchemaCurrent ist der Boot-Gate, jeder False-Positive
|
|
4
|
+
// hier blockiert Container-Starts; jeder False-Negative lässt
|
|
5
|
+
// Schema-Drift unentdeckt durch.
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { sql } from "drizzle-orm";
|
|
11
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
12
|
+
import { createTestDb, type TestDb } from "../../stack";
|
|
13
|
+
import { detectDrift } from "../schema-drift";
|
|
14
|
+
|
|
15
|
+
let testDb: TestDb;
|
|
16
|
+
let migrationsDir: string;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
testDb = await createTestDb();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterAll(async () => {
|
|
23
|
+
await testDb.cleanup();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
migrationsDir = mkdtempSync(join(tmpdir(), "kumiko-drift-"));
|
|
28
|
+
mkdirSync(join(migrationsDir, "meta"), { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
rmSync(migrationsDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function writeJournal(entries: { idx: number; tag: string }[]): void {
|
|
36
|
+
const journal = {
|
|
37
|
+
version: "7",
|
|
38
|
+
dialect: "postgresql",
|
|
39
|
+
entries: entries.map((e) => ({
|
|
40
|
+
idx: e.idx,
|
|
41
|
+
version: "7",
|
|
42
|
+
when: 1700000000000 + e.idx,
|
|
43
|
+
tag: e.tag,
|
|
44
|
+
breakpoints: true,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
writeFileSync(join(migrationsDir, "meta/_journal.json"), JSON.stringify(journal));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type SnapshotColumn = {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly type: string;
|
|
53
|
+
readonly primaryKey?: boolean;
|
|
54
|
+
readonly notNull?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function writeSnapshot(
|
|
58
|
+
idx: number,
|
|
59
|
+
tables: Array<{ name: string; columns?: Record<string, SnapshotColumn> }>,
|
|
60
|
+
): void {
|
|
61
|
+
const out: Record<string, unknown> = {};
|
|
62
|
+
for (const t of tables) {
|
|
63
|
+
out[`public.${t.name}`] = {
|
|
64
|
+
schema: "",
|
|
65
|
+
name: t.name,
|
|
66
|
+
columns: t.columns ?? {
|
|
67
|
+
id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(migrationsDir, "meta", `${String(idx).padStart(4, "0")}_snapshot.json`),
|
|
73
|
+
JSON.stringify({ tables: out }),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeSnapshotSimple(idx: number, tableNames: string[]): void {
|
|
78
|
+
writeSnapshot(
|
|
79
|
+
idx,
|
|
80
|
+
tableNames.map((name) => ({ name })),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function ensureDrizzleMigrationsTable(): Promise<void> {
|
|
85
|
+
await testDb.db.execute(sql`CREATE SCHEMA IF NOT EXISTS drizzle`);
|
|
86
|
+
await testDb.db.execute(sql`
|
|
87
|
+
CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
|
|
88
|
+
id serial PRIMARY KEY,
|
|
89
|
+
hash text NOT NULL,
|
|
90
|
+
created_at bigint
|
|
91
|
+
)
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function dropDrizzleMigrationsTable(): Promise<void> {
|
|
96
|
+
await testDb.db.execute(sql`DROP TABLE IF EXISTS drizzle.__drizzle_migrations`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function insertAppliedMigration(hash: string): Promise<void> {
|
|
100
|
+
await testDb.db.execute(
|
|
101
|
+
sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at) VALUES (${hash}, ${Date.now()})`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe("detectDrift", () => {
|
|
106
|
+
beforeEach(async () => {
|
|
107
|
+
await dropDrizzleMigrationsTable();
|
|
108
|
+
// Cleanup test tables that might still exist from earlier runs
|
|
109
|
+
await testDb.db.execute(sql`DROP TABLE IF EXISTS drift_test_users`);
|
|
110
|
+
await testDb.db.execute(sql`DROP TABLE IF EXISTS drift_test_orders`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("frische DB ohne __drizzle_migrations + 1 Migration im Journal → 1 pending + table missing", async () => {
|
|
114
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
115
|
+
writeSnapshotSimple(0, ["drift_test_users"]);
|
|
116
|
+
|
|
117
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
118
|
+
expect(report.ok).toBe(false);
|
|
119
|
+
expect(report.pendingMigrations).toHaveLength(1);
|
|
120
|
+
expect(report.pendingMigrations[0]?.tag).toBe("0000_init");
|
|
121
|
+
expect(report.missingTables).toEqual(["drift_test_users"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("alle Migrations applied + alle Tabellen existieren → ok", async () => {
|
|
125
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
126
|
+
writeSnapshotSimple(0, ["drift_test_users"]);
|
|
127
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY)`);
|
|
128
|
+
await ensureDrizzleMigrationsTable();
|
|
129
|
+
await insertAppliedMigration("hash-0000");
|
|
130
|
+
|
|
131
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
132
|
+
expect(report.ok).toBe(true);
|
|
133
|
+
expect(report.pendingMigrations).toHaveLength(0);
|
|
134
|
+
expect(report.missingTables).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("partial applied: Journal hat 2, applied hat 1 → 1 pending", async () => {
|
|
138
|
+
writeJournal([
|
|
139
|
+
{ idx: 0, tag: "0000_init" },
|
|
140
|
+
{ idx: 1, tag: "0001_add_orders" },
|
|
141
|
+
]);
|
|
142
|
+
writeSnapshotSimple(1, ["drift_test_users", "drift_test_orders"]);
|
|
143
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY)`);
|
|
144
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_orders (id uuid PRIMARY KEY)`);
|
|
145
|
+
await ensureDrizzleMigrationsTable();
|
|
146
|
+
await insertAppliedMigration("hash-0000"); // nur eine applied
|
|
147
|
+
|
|
148
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
149
|
+
expect(report.ok).toBe(false);
|
|
150
|
+
expect(report.pendingMigrations).toHaveLength(1);
|
|
151
|
+
expect(report.pendingMigrations[0]?.tag).toBe("0001_add_orders");
|
|
152
|
+
expect(report.missingTables).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("alle Migrations applied aber Tabelle fehlt manuell → drift", async () => {
|
|
156
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
157
|
+
writeSnapshotSimple(0, ["drift_test_users", "drift_test_orders"]);
|
|
158
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY)`);
|
|
159
|
+
// drift_test_orders bewusst NICHT angelegt (simuliert manuellen DROP)
|
|
160
|
+
await ensureDrizzleMigrationsTable();
|
|
161
|
+
await insertAppliedMigration("hash-0000");
|
|
162
|
+
|
|
163
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
164
|
+
expect(report.ok).toBe(false);
|
|
165
|
+
expect(report.pendingMigrations).toHaveLength(0);
|
|
166
|
+
expect(report.missingTables).toEqual(["drift_test_orders"]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("Layer 3 — column-diff", () => {
|
|
170
|
+
test("snapshot column NOT NULL aber DB nullable → nullability-mismatch", async () => {
|
|
171
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
172
|
+
writeSnapshot(0, [
|
|
173
|
+
{
|
|
174
|
+
name: "drift_test_users",
|
|
175
|
+
columns: {
|
|
176
|
+
id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
|
|
177
|
+
email: { name: "email", type: "text", notNull: true },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
// DB hat email NULLABLE — drift.
|
|
182
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY, email text)`);
|
|
183
|
+
await ensureDrizzleMigrationsTable();
|
|
184
|
+
await insertAppliedMigration("hash-0000");
|
|
185
|
+
|
|
186
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
187
|
+
expect(report.ok).toBe(false);
|
|
188
|
+
expect(report.columnIssues).toHaveLength(1);
|
|
189
|
+
const issue = report.columnIssues[0];
|
|
190
|
+
expect(issue?.kind).toBe("nullability-mismatch");
|
|
191
|
+
expect(issue?.table).toBe("drift_test_users");
|
|
192
|
+
expect(issue?.column).toBe("email");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("snapshot column im DB nicht da → missing-column", async () => {
|
|
196
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
197
|
+
writeSnapshot(0, [
|
|
198
|
+
{
|
|
199
|
+
name: "drift_test_users",
|
|
200
|
+
columns: {
|
|
201
|
+
id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
|
|
202
|
+
email: { name: "email", type: "text", notNull: true },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
// DB hat KEINE email-Spalte.
|
|
207
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY)`);
|
|
208
|
+
await ensureDrizzleMigrationsTable();
|
|
209
|
+
await insertAppliedMigration("hash-0000");
|
|
210
|
+
|
|
211
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
212
|
+
expect(report.ok).toBe(false);
|
|
213
|
+
expect(report.columnIssues).toHaveLength(1);
|
|
214
|
+
expect(report.columnIssues[0]?.kind).toBe("missing-column");
|
|
215
|
+
expect(report.columnIssues[0]?.column).toBe("email");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("DB hat extra Spalte die nicht im Snapshot ist → extra-column", async () => {
|
|
219
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
220
|
+
writeSnapshot(0, [
|
|
221
|
+
{
|
|
222
|
+
name: "drift_test_users",
|
|
223
|
+
columns: {
|
|
224
|
+
id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
// DB hat zusätzliche Spalte (z.B. manueller ALTER TABLE in Prod).
|
|
229
|
+
await testDb.db.execute(
|
|
230
|
+
sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY, secret_legacy text)`,
|
|
231
|
+
);
|
|
232
|
+
await ensureDrizzleMigrationsTable();
|
|
233
|
+
await insertAppliedMigration("hash-0000");
|
|
234
|
+
|
|
235
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
236
|
+
expect(report.ok).toBe(false);
|
|
237
|
+
expect(report.columnIssues).toHaveLength(1);
|
|
238
|
+
expect(report.columnIssues[0]?.kind).toBe("extra-column");
|
|
239
|
+
expect(report.columnIssues[0]?.column).toBe("secret_legacy");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("snapshot type vs db type mismatch → type-mismatch", async () => {
|
|
243
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
244
|
+
writeSnapshot(0, [
|
|
245
|
+
{
|
|
246
|
+
name: "drift_test_users",
|
|
247
|
+
columns: {
|
|
248
|
+
id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
|
|
249
|
+
age: { name: "age", type: "integer" },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
// DB hat age als TEXT statt INTEGER.
|
|
254
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY, age text)`);
|
|
255
|
+
await ensureDrizzleMigrationsTable();
|
|
256
|
+
await insertAppliedMigration("hash-0000");
|
|
257
|
+
|
|
258
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
259
|
+
expect(report.ok).toBe(false);
|
|
260
|
+
const typeIssue = report.columnIssues.find((i) => i.kind === "type-mismatch");
|
|
261
|
+
expect(typeIssue).toBeDefined();
|
|
262
|
+
if (typeIssue && typeIssue.kind === "type-mismatch") {
|
|
263
|
+
expect(typeIssue.column).toBe("age");
|
|
264
|
+
expect(typeIssue.expected).toBe("integer");
|
|
265
|
+
expect(typeIssue.actual).toBe("text");
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("clean state: alle Spalten matchen Snapshot → ok + columnIssues=[]", async () => {
|
|
270
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
271
|
+
writeSnapshot(0, [
|
|
272
|
+
{
|
|
273
|
+
name: "drift_test_users",
|
|
274
|
+
columns: {
|
|
275
|
+
id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
|
|
276
|
+
email: { name: "email", type: "text", notNull: true },
|
|
277
|
+
age: { name: "age", type: "integer" },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
]);
|
|
281
|
+
await testDb.db.execute(sql`
|
|
282
|
+
CREATE TABLE drift_test_users (
|
|
283
|
+
id uuid PRIMARY KEY,
|
|
284
|
+
email text NOT NULL,
|
|
285
|
+
age integer
|
|
286
|
+
)
|
|
287
|
+
`);
|
|
288
|
+
await ensureDrizzleMigrationsTable();
|
|
289
|
+
await insertAppliedMigration("hash-0000");
|
|
290
|
+
|
|
291
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
292
|
+
expect(report.ok).toBe(true);
|
|
293
|
+
expect(report.columnIssues).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("public.__drizzle_migrations Fallback (Pre-0.20-Drizzle)", async () => {
|
|
298
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
299
|
+
writeSnapshotSimple(0, ["drift_test_users"]);
|
|
300
|
+
await testDb.db.execute(sql`CREATE TABLE drift_test_users (id uuid PRIMARY KEY)`);
|
|
301
|
+
// Legacy: Tabelle in public-Schema statt drizzle-Schema
|
|
302
|
+
await dropDrizzleMigrationsTable();
|
|
303
|
+
await testDb.db.execute(sql`
|
|
304
|
+
CREATE TABLE public.__drizzle_migrations (
|
|
305
|
+
id serial PRIMARY KEY,
|
|
306
|
+
hash text NOT NULL,
|
|
307
|
+
created_at bigint
|
|
308
|
+
)
|
|
309
|
+
`);
|
|
310
|
+
try {
|
|
311
|
+
await testDb.db.execute(
|
|
312
|
+
sql`INSERT INTO public.__drizzle_migrations (hash, created_at) VALUES ('hash-0000', ${Date.now()})`,
|
|
313
|
+
);
|
|
314
|
+
const report = await detectDrift(testDb.db, migrationsDir);
|
|
315
|
+
expect(report.ok).toBe(true);
|
|
316
|
+
} finally {
|
|
317
|
+
await testDb.db.execute(sql`DROP TABLE public.__drizzle_migrations`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|