@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,134 @@
|
|
|
1
|
+
// Integration-Test für detectProjectionsToRebuild — die Brücke zwischen
|
|
2
|
+
// Snapshot-Diff (Welle 2) und ImplicitProjection (Sprint G). Beweist dass
|
|
3
|
+
// `kumiko migrate generate` das richtige Marker-File schreibt: ein
|
|
4
|
+
// Spalten-Add auf einer r.entity-Tabelle muss als
|
|
5
|
+
// `<feature>:projection:<entity>-entity` rebuild-Kandidat erkannt werden.
|
|
6
|
+
//
|
|
7
|
+
// Production-Behavior: ohne diese Brücke würden die Welle-2- und
|
|
8
|
+
// Sprint-G-Pieces nebeneinander leben aber sich nicht treffen — Marker
|
|
9
|
+
// wäre leer, kein Rebuild würde ausgelöst.
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
15
|
+
import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
|
|
16
|
+
import { createRegistry } from "../../engine/registry";
|
|
17
|
+
import { detectProjectionsToRebuild } from "../projection-detection";
|
|
18
|
+
|
|
19
|
+
let migrationsDir: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
migrationsDir = mkdtempSync(join(tmpdir(), "kumiko-detect-"));
|
|
23
|
+
mkdirSync(join(migrationsDir, "meta"), { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(migrationsDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function writeJournal(entries: { idx: number; tag: string }[]): void {
|
|
31
|
+
writeFileSync(
|
|
32
|
+
join(migrationsDir, "meta/_journal.json"),
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
version: "7",
|
|
35
|
+
dialect: "postgresql",
|
|
36
|
+
entries: entries.map((e) => ({
|
|
37
|
+
idx: e.idx,
|
|
38
|
+
version: "7",
|
|
39
|
+
when: 1700000000000 + e.idx,
|
|
40
|
+
tag: e.tag,
|
|
41
|
+
breakpoints: true,
|
|
42
|
+
})),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeSnapshot(idx: number, tableName: string, columnNames: string[]): void {
|
|
48
|
+
const columns: Record<string, unknown> = {};
|
|
49
|
+
for (const name of columnNames) {
|
|
50
|
+
columns[name] = { name, type: "text" };
|
|
51
|
+
}
|
|
52
|
+
// Plus base-columns die jede Entity-Tabelle hat, damit wir nicht mit
|
|
53
|
+
// dem Test-Compare versehentlich kompletten neue Tabellen markieren.
|
|
54
|
+
columns["id"] = { name: "id", type: "uuid", primaryKey: true, notNull: true };
|
|
55
|
+
columns["tenant_id"] = { name: "tenant_id", type: "uuid", notNull: true };
|
|
56
|
+
columns["version"] = { name: "version", type: "integer", default: 1, notNull: true };
|
|
57
|
+
writeFileSync(
|
|
58
|
+
join(migrationsDir, "meta", `${String(idx).padStart(4, "0")}_snapshot.json`),
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
tables: {
|
|
61
|
+
[`public.${tableName}`]: {
|
|
62
|
+
schema: "",
|
|
63
|
+
name: tableName,
|
|
64
|
+
columns,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const widgetEntity = createEntity({
|
|
72
|
+
table: "test_widgets",
|
|
73
|
+
fields: {
|
|
74
|
+
name: createTextField({ required: true }),
|
|
75
|
+
isEnabled: createBooleanField({ default: true }),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const widgetFeature = defineFeature("detecttest", (r) => {
|
|
80
|
+
r.entity("widget", widgetEntity);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("detectProjectionsToRebuild", () => {
|
|
84
|
+
test("Spalten-Add auf r.entity-Tabelle → ImplicitProjection als Rebuild-Kandidat", () => {
|
|
85
|
+
// Initial-Migration: 2 Spalten
|
|
86
|
+
writeSnapshot(0, "test_widgets", ["name", "is_enabled"]);
|
|
87
|
+
// Folge-Migration: 3 Spalten (description dazu)
|
|
88
|
+
writeSnapshot(1, "test_widgets", ["name", "is_enabled", "description"]);
|
|
89
|
+
writeJournal([
|
|
90
|
+
{ idx: 0, tag: "0000_init" },
|
|
91
|
+
{ idx: 1, tag: "0001_add_description" },
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const registry = createRegistry([widgetFeature]);
|
|
95
|
+
const projections = detectProjectionsToRebuild(registry, migrationsDir);
|
|
96
|
+
|
|
97
|
+
expect(projections).toEqual(["detecttest:projection:widget-entity"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("identische Snapshots → keine Rebuild-Kandidaten", () => {
|
|
101
|
+
writeSnapshot(0, "test_widgets", ["name", "is_enabled"]);
|
|
102
|
+
writeSnapshot(1, "test_widgets", ["name", "is_enabled"]);
|
|
103
|
+
writeJournal([
|
|
104
|
+
{ idx: 0, tag: "0000_init" },
|
|
105
|
+
{ idx: 1, tag: "0001_no_op" },
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const registry = createRegistry([widgetFeature]);
|
|
109
|
+
expect(detectProjectionsToRebuild(registry, migrationsDir)).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("Initial-Migration (nur ein Snapshot) → leer (keine historischen Events)", () => {
|
|
113
|
+
writeSnapshot(0, "test_widgets", ["name"]);
|
|
114
|
+
writeJournal([{ idx: 0, tag: "0000_init" }]);
|
|
115
|
+
|
|
116
|
+
const registry = createRegistry([widgetFeature]);
|
|
117
|
+
expect(detectProjectionsToRebuild(registry, migrationsDir)).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("Spalten-Add auf einer Tabelle die KEINE Projection ist → leer", () => {
|
|
121
|
+
// Tabelle "unrelated" ist nicht als Projection registriert (kein
|
|
122
|
+
// r.entity, keine r.projection). Schema-Change soll keinen
|
|
123
|
+
// Rebuild-Marker erzeugen.
|
|
124
|
+
writeSnapshot(0, "unrelated", ["a"]);
|
|
125
|
+
writeSnapshot(1, "unrelated", ["a", "b"]);
|
|
126
|
+
writeJournal([
|
|
127
|
+
{ idx: 0, tag: "0000_init" },
|
|
128
|
+
{ idx: 1, tag: "0001_add_b" },
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const registry = createRegistry([widgetFeature]);
|
|
132
|
+
expect(detectProjectionsToRebuild(registry, migrationsDir)).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Unit-Tests für die Marker-File-IO. Production-Behavior:
|
|
2
|
+
// - Generate-Step schreibt Marker mit kanonischer Struktur
|
|
3
|
+
// - Apply-Step liest Marker zurück (oder null wenn keiner)
|
|
4
|
+
// - schemaVersion-Mismatch wirft (verhindert dass alte Markers gegen
|
|
5
|
+
// neue Lese-Logik fahren)
|
|
6
|
+
|
|
7
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
11
|
+
import { readRebuildMarker, writeRebuildMarker } from "../rebuild-marker";
|
|
12
|
+
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = mkdtempSync(join(tmpdir(), "kumiko-marker-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("writeRebuildMarker / readRebuildMarker round-trip", () => {
|
|
24
|
+
test("schreibt File mit kanonischer Struktur", () => {
|
|
25
|
+
writeRebuildMarker(tmpDir, "0042_brave_taskmaster", [
|
|
26
|
+
"publicstatus:projection:incident-entity",
|
|
27
|
+
"publicstatus:projection:component-entity",
|
|
28
|
+
]);
|
|
29
|
+
const raw = readFileSync(join(tmpDir, "0042_brave_taskmaster__rebuild.json"), "utf-8");
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
expect(parsed).toEqual({
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
migrationTag: "0042_brave_taskmaster",
|
|
34
|
+
// sortiert — Reihenfolge der projections im Output ist deterministisch
|
|
35
|
+
projections: [
|
|
36
|
+
"publicstatus:projection:component-entity",
|
|
37
|
+
"publicstatus:projection:incident-entity",
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("read returns parsed marker", () => {
|
|
43
|
+
writeRebuildMarker(tmpDir, "0001_foo", ["app:projection:bar-entity"]);
|
|
44
|
+
const marker = readRebuildMarker(tmpDir, "0001_foo");
|
|
45
|
+
expect(marker?.migrationTag).toBe("0001_foo");
|
|
46
|
+
expect(marker?.projections).toEqual(["app:projection:bar-entity"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("leere Projection-Liste schreibt KEIN File (Noise-Reduktion)", () => {
|
|
50
|
+
writeRebuildMarker(tmpDir, "0003_only_index", []);
|
|
51
|
+
expect(readRebuildMarker(tmpDir, "0003_only_index")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("read returns null wenn File nicht existiert", () => {
|
|
55
|
+
expect(readRebuildMarker(tmpDir, "0099_never_written")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("schemaVersion-Mismatch wirft mit klarer Message", () => {
|
|
59
|
+
const path = join(tmpDir, "0042_future__rebuild.json");
|
|
60
|
+
writeFileSync(
|
|
61
|
+
path,
|
|
62
|
+
JSON.stringify({ schemaVersion: 999, migrationTag: "0042_future", projections: [] }),
|
|
63
|
+
);
|
|
64
|
+
expect(() => readRebuildMarker(tmpDir, "0042_future")).toThrow(/schemaVersion/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("korrupte JSON wirft (kein silent-null)", () => {
|
|
68
|
+
const path = join(tmpDir, "0050_corrupt__rebuild.json");
|
|
69
|
+
writeFileSync(path, "{ this is not json");
|
|
70
|
+
expect(() => readRebuildMarker(tmpDir, "0050_corrupt")).toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("Idempotenz: zweiter write überschreibt", () => {
|
|
74
|
+
writeRebuildMarker(tmpDir, "0010_x", ["a:projection:one-entity"]);
|
|
75
|
+
writeRebuildMarker(tmpDir, "0010_x", ["a:projection:two-entity"]);
|
|
76
|
+
const marker = readRebuildMarker(tmpDir, "0010_x");
|
|
77
|
+
expect(marker?.projections).toEqual(["a:projection:two-entity"]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildProjectionTableIndex,
|
|
3
|
+
type ChangedTable,
|
|
4
|
+
compareSnapshots,
|
|
5
|
+
detectProjectionsToRebuild,
|
|
6
|
+
latestMigrationTag,
|
|
7
|
+
projectionsFromChanges,
|
|
8
|
+
} from "./projection-detection";
|
|
9
|
+
export { type RebuildMarker, readRebuildMarker, writeRebuildMarker } from "./rebuild-marker";
|
|
10
|
+
export {
|
|
11
|
+
type AppliedMigration,
|
|
12
|
+
assertSchemaCurrent,
|
|
13
|
+
type ColumnIssue,
|
|
14
|
+
type ColumnSpec,
|
|
15
|
+
type DriftReport,
|
|
16
|
+
detectDrift,
|
|
17
|
+
formatDriftReport,
|
|
18
|
+
type Journal,
|
|
19
|
+
type JournalEntry,
|
|
20
|
+
loadAppliedMigrations,
|
|
21
|
+
loadJournal,
|
|
22
|
+
loadLatestSnapshot,
|
|
23
|
+
loadPreviousSnapshot,
|
|
24
|
+
loadSnapshot,
|
|
25
|
+
SchemaDriftError,
|
|
26
|
+
type Snapshot,
|
|
27
|
+
type SnapshotTable,
|
|
28
|
+
} from "./schema-drift";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Snapshot-Diff + Projection-Lookup für die Welle-2-Migration-Pipeline.
|
|
2
|
+
//
|
|
3
|
+
// Wenn `kumiko migrate generate` ein neues Drizzle-Snapshot-File erzeugt,
|
|
4
|
+
// vergleichen wir es mit dem vorherigen. Tabellen die schema-changes
|
|
5
|
+
// haben (Spalten dazu/weg, Spalten-Type-Änderung) sind Kandidaten für
|
|
6
|
+
// einen Projection-Rebuild — vorausgesetzt sie gehören zu einer
|
|
7
|
+
// registrierten Projection.
|
|
8
|
+
//
|
|
9
|
+
// Der Lookup geht über getTableName(projection.table) — die Drizzle-
|
|
10
|
+
// public-API für den physischen Tabellen-Namen einer pgTable-Definition.
|
|
11
|
+
// Damit muss niemand Tabellen-Namen doppelt pflegen (Truth liegt in der
|
|
12
|
+
// Projection-Definition).
|
|
13
|
+
|
|
14
|
+
import { getTableName } from "drizzle-orm";
|
|
15
|
+
import type { Registry } from "../engine/types/feature";
|
|
16
|
+
import {
|
|
17
|
+
type ColumnSpec,
|
|
18
|
+
loadJournal,
|
|
19
|
+
loadLatestSnapshot,
|
|
20
|
+
loadPreviousSnapshot,
|
|
21
|
+
type Snapshot,
|
|
22
|
+
} from "./schema-drift";
|
|
23
|
+
|
|
24
|
+
/** Welche Tabellen haben sich zwischen prev und current geändert?
|
|
25
|
+
* Reine Tabellen-Existenz: in current aber nicht in prev → "added".
|
|
26
|
+
* Spalten-Veränderungen: identische Tabelle aber Spalten unterscheiden. */
|
|
27
|
+
export type ChangedTable = {
|
|
28
|
+
readonly fullName: string; // "schema.name" oder einfach "name" wenn empty schema
|
|
29
|
+
readonly tableName: string; // nur "name" für tableName-Lookup
|
|
30
|
+
readonly kind: "added" | "modified" | "removed";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function compareSnapshots(
|
|
34
|
+
prev: Snapshot | null,
|
|
35
|
+
current: Snapshot,
|
|
36
|
+
): readonly ChangedTable[] {
|
|
37
|
+
const changes: ChangedTable[] = [];
|
|
38
|
+
const prevKeys = new Set(prev ? Object.keys(prev.tables) : []);
|
|
39
|
+
const currentKeys = new Set(Object.keys(current.tables));
|
|
40
|
+
|
|
41
|
+
for (const key of currentKeys) {
|
|
42
|
+
const cur = current.tables[key];
|
|
43
|
+
if (!cur) continue;
|
|
44
|
+
const fullName = cur.schema && cur.schema.length > 0 ? `${cur.schema}.${cur.name}` : cur.name;
|
|
45
|
+
if (!prevKeys.has(key)) {
|
|
46
|
+
changes.push({ fullName, tableName: cur.name, kind: "added" });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const prevTable = prev?.tables[key];
|
|
50
|
+
if (!prevTable) continue;
|
|
51
|
+
if (!sameColumns(prevTable.columns, cur.columns)) {
|
|
52
|
+
changes.push({ fullName, tableName: cur.name, kind: "modified" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const key of prevKeys) {
|
|
57
|
+
if (!currentKeys.has(key)) {
|
|
58
|
+
const prevTable = prev?.tables[key];
|
|
59
|
+
if (!prevTable) continue;
|
|
60
|
+
const fullName =
|
|
61
|
+
prevTable.schema && prevTable.schema.length > 0
|
|
62
|
+
? `${prevTable.schema}.${prevTable.name}`
|
|
63
|
+
: prevTable.name;
|
|
64
|
+
changes.push({ fullName, tableName: prevTable.name, kind: "removed" });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return changes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sameColumns(
|
|
72
|
+
a: Readonly<Record<string, ColumnSpec>>,
|
|
73
|
+
b: Readonly<Record<string, ColumnSpec>>,
|
|
74
|
+
): boolean {
|
|
75
|
+
const aKeys = Object.keys(a);
|
|
76
|
+
const bKeys = Object.keys(b);
|
|
77
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
78
|
+
for (const key of aKeys) {
|
|
79
|
+
const colA = a[key];
|
|
80
|
+
const colB = b[key];
|
|
81
|
+
if (!colA || !colB) return false;
|
|
82
|
+
if (colA.name !== colB.name) return false;
|
|
83
|
+
if (colA.type !== colB.type) return false;
|
|
84
|
+
if (Boolean(colA.notNull) !== Boolean(colB.notNull)) return false;
|
|
85
|
+
if (Boolean(colA.primaryKey) !== Boolean(colB.primaryKey)) return false;
|
|
86
|
+
// Default-Vergleich bewusst per JSON — Drizzle serialisiert default-
|
|
87
|
+
// expressions als String, das passt für CREATE TABLE-Zwecke.
|
|
88
|
+
if (JSON.stringify(colA.default) !== JSON.stringify(colB.default)) return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Index `tableName → projection-name` aus der Registry. Nur Projections
|
|
94
|
+
* mit table-Definition (single-stream + multi-stream-with-table) zählen.
|
|
95
|
+
* Side-effect-only MSPs (table omitted) haben keinen Rebuild-Sinn. */
|
|
96
|
+
export function buildProjectionTableIndex(registry: Registry): ReadonlyMap<string, string> {
|
|
97
|
+
const index = new Map<string, string>();
|
|
98
|
+
for (const [name, def] of registry.getAllProjections()) {
|
|
99
|
+
index.set(getTableName(def.table), name);
|
|
100
|
+
}
|
|
101
|
+
for (const [name, def] of registry.getAllMultiStreamProjections()) {
|
|
102
|
+
if (def.table) index.set(getTableName(def.table), name);
|
|
103
|
+
}
|
|
104
|
+
return index;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Aus einer Liste geänderter Tabellen die Projection-Namen extrahieren.
|
|
108
|
+
* "removed" ignoriert: gelöschte Projection-Tabellen → die Projection
|
|
109
|
+
* ist auch weg, kein Rebuild-Bedarf. "added" wird zurückgegeben — beim
|
|
110
|
+
* ersten Migrate aus einer leeren DB sind das keine echten Rebuilds
|
|
111
|
+
* (keine historischen Events), aber der Apply-Step filtert das selbst
|
|
112
|
+
* über event-count > 0 heraus. */
|
|
113
|
+
export function projectionsFromChanges(
|
|
114
|
+
changes: readonly ChangedTable[],
|
|
115
|
+
index: ReadonlyMap<string, string>,
|
|
116
|
+
): readonly string[] {
|
|
117
|
+
const names = new Set<string>();
|
|
118
|
+
for (const change of changes) {
|
|
119
|
+
if (change.kind === "removed") continue;
|
|
120
|
+
const projection = index.get(change.tableName);
|
|
121
|
+
if (projection) names.add(projection);
|
|
122
|
+
}
|
|
123
|
+
return [...names].sort();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Convenience: gibt für die letzte Migration zurück welche Projections
|
|
127
|
+
* rebuild brauchen würden. Empty wenn das gerade die erste Migration ist
|
|
128
|
+
* (kein vorheriger Snapshot, alle Tabellen "added", aber keine Events). */
|
|
129
|
+
export function detectProjectionsToRebuild(
|
|
130
|
+
registry: Registry,
|
|
131
|
+
migrationsDir: string,
|
|
132
|
+
): readonly string[] {
|
|
133
|
+
const prev = loadPreviousSnapshot(migrationsDir);
|
|
134
|
+
// Initial migration: nur "added"-Changes, keine historischen Events
|
|
135
|
+
// zum Replayen → kein Rebuild nötig.
|
|
136
|
+
if (prev === null) return [];
|
|
137
|
+
const current = loadLatestSnapshot(migrationsDir);
|
|
138
|
+
const changes = compareSnapshots(prev, current);
|
|
139
|
+
const index = buildProjectionTableIndex(registry);
|
|
140
|
+
return projectionsFromChanges(changes, index);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Tag des letzten journal-Eintrags — nutzen wir als Marker-File-Name. */
|
|
144
|
+
export function latestMigrationTag(migrationsDir: string): string {
|
|
145
|
+
const journal = loadJournal(migrationsDir);
|
|
146
|
+
const last = journal.entries[journal.entries.length - 1];
|
|
147
|
+
if (!last) throw new Error(`latestMigrationTag: empty journal in ${migrationsDir}`);
|
|
148
|
+
return last.tag;
|
|
149
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Rebuild-Marker-File: zur generate-Zeit schreibt der Migration-Generator
|
|
2
|
+
// ein Side-File `<tag>__rebuild.json` neben das SQL-File. Beim apply liest
|
|
3
|
+
// der Apply-Step die Marker für alle neu-applied Migrations und ruft
|
|
4
|
+
// rebuildProjection() für jede gelistete Projection.
|
|
5
|
+
//
|
|
6
|
+
// Format:
|
|
7
|
+
// {
|
|
8
|
+
// "schemaVersion": 1,
|
|
9
|
+
// "migrationTag": "0042_brave_taskmaster",
|
|
10
|
+
// "projections": ["publicstatus:projection:incident-state", ...]
|
|
11
|
+
// }
|
|
12
|
+
//
|
|
13
|
+
// Das File wird zum Migration-File committed und durchläuft Code-Review
|
|
14
|
+
// — die Projection-Rebuild-Liste ist damit sichtbar im PR.
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
import { parseJsonOrThrow } from "../utils/safe-json";
|
|
19
|
+
|
|
20
|
+
const MARKER_VERSION = 1 as const;
|
|
21
|
+
|
|
22
|
+
export type RebuildMarker = {
|
|
23
|
+
readonly schemaVersion: typeof MARKER_VERSION;
|
|
24
|
+
readonly migrationTag: string;
|
|
25
|
+
readonly projections: readonly string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function markerPath(migrationsDir: string, migrationTag: string): string {
|
|
29
|
+
return resolve(migrationsDir, `${migrationTag}__rebuild.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeRebuildMarker(
|
|
33
|
+
migrationsDir: string,
|
|
34
|
+
migrationTag: string,
|
|
35
|
+
projections: readonly string[],
|
|
36
|
+
): void {
|
|
37
|
+
// skip: leere Projections-Liste → kein Marker-File. Reduziert Noise
|
|
38
|
+
// bei Migrations die keine Projection berühren (Infra-Tabellen, pure
|
|
39
|
+
// Index-Adds). Caller braucht keinen Confirm — File-Existenz ist die
|
|
40
|
+
// Truth-Quelle.
|
|
41
|
+
if (projections.length === 0) return;
|
|
42
|
+
const marker: RebuildMarker = {
|
|
43
|
+
schemaVersion: MARKER_VERSION,
|
|
44
|
+
migrationTag,
|
|
45
|
+
projections: [...projections].sort(),
|
|
46
|
+
};
|
|
47
|
+
writeFileSync(markerPath(migrationsDir, migrationTag), `${JSON.stringify(marker, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readRebuildMarker(
|
|
51
|
+
migrationsDir: string,
|
|
52
|
+
migrationTag: string,
|
|
53
|
+
): RebuildMarker | null {
|
|
54
|
+
const path = markerPath(migrationsDir, migrationTag);
|
|
55
|
+
if (!existsSync(path)) return null;
|
|
56
|
+
const parsed = parseJsonOrThrow<RebuildMarker>(readFileSync(path, "utf-8"), `marker at ${path}`);
|
|
57
|
+
if (parsed.schemaVersion !== MARKER_VERSION) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`readRebuildMarker: ${path} hat schemaVersion ${parsed.schemaVersion}, ` +
|
|
60
|
+
`erwartet ${MARKER_VERSION}. Kumiko-Version-Mismatch?`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|