@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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/AGENTS.md +9 -1
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +206 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +299 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +101 -0
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +61 -2
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
package/dist/schema-cli.js
CHANGED
|
@@ -7,212 +7,39 @@
|
|
|
7
7
|
// NO-MAGIC-ON-DATA: reads only checked-in artifacts (kumiko/schema.ts →
|
|
8
8
|
// ENTITY_METAS, kumiko/migrations/*.sql). Never auto-generates at runtime,
|
|
9
9
|
// never applies on app-boot — apply/baseline are explicit deploy-steps.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
19
|
-
import { join, resolve as resolvePath } from "node:path";
|
|
20
|
-
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
21
|
-
import { configureBlindIndexKey, configurePiiSubjectKms, resolveKmsWiringAsync, } from "./crypto/index.js";
|
|
22
|
-
import { assertUniqueTableNames, assertValidMigrationName, baselineMigrations, createDbConnection, diffReplayAgainstSnapshot, fetchAppliedMigrations, findCommentedDropTables, generateMigration, isRetiredFrameworkTable, loadMigrationsFromDir, loadSnapshotJson, readMigrationSqlTexts, rebuildTablesFromDiff, replayMigrationsDir, runMigrationsFromDir, tableExists, writeRebuildMarker, writeSnapshotJson, } from "./db/index.js";
|
|
23
|
-
import { isEntityTableMeta } from "./db/table-introspection.js";
|
|
24
|
-
import { validateBoot } from "./engine/boot-validator.js";
|
|
25
|
-
import { FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, } from "./engine/extension-names.js";
|
|
26
|
-
import { createRegistry } from "./engine/registry.js";
|
|
27
|
-
import { createEventsTable } from "./event-store/index.js";
|
|
28
|
-
import { queueRebuildsFromMarkers, reportPendingRebuildRun, runPendingRebuilds, } from "./migrations/index.js";
|
|
29
|
-
import { createEventConsumerStateTable, createProjectionStateTable } from "./pipeline/index.js";
|
|
10
|
+
import { resolve as resolvePath } from "node:path";
|
|
11
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
12
|
+
import { runApplyCommand } from "./schema-cli/apply-command.js";
|
|
13
|
+
import { runBaselineCommand } from "./schema-cli/baseline-command.js";
|
|
14
|
+
import { runGenerateCommand } from "./schema-cli/generate-command.js";
|
|
15
|
+
import { runStatusCommand } from "./schema-cli/status-command.js";
|
|
16
|
+
import { runValidateCommand } from "./schema-cli/validate-command.js";
|
|
30
17
|
import { ensureTemporalPolyfill } from "./time/index.js";
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
function validateBootOptionsError(detail) {
|
|
50
|
-
return new Error(`kumiko/schema.ts: \`VALIDATE_BOOT_OPTIONS\` ${detail}`);
|
|
51
|
-
}
|
|
52
|
-
function readOptionalOption(raw, key, isValid, expected) {
|
|
53
|
-
const value = Reflect.get(raw, key);
|
|
54
|
-
if (value === undefined)
|
|
55
|
-
return undefined;
|
|
56
|
-
if (!isValid(value))
|
|
57
|
-
throw validateBootOptionsError(`\`${key}\` must be ${expected}.`);
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
// Boundary check for `export const VALIDATE_BOOT_OPTIONS` in kumiko/schema.ts.
|
|
61
|
-
// Only the options that change what validateBoot reports are honoured; `env` and
|
|
62
|
-
// `entityFieldCipher` are runner-injected and deliberately ignored here.
|
|
63
|
-
function parseValidateBootOptions(raw) {
|
|
64
|
-
if (raw === undefined)
|
|
65
|
-
return {};
|
|
66
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
67
|
-
throw validateBootOptionsError("must be an object (ValidateBootOptions).");
|
|
68
|
-
}
|
|
69
|
-
const navAllowlist = readOptionalOption(raw, "navAllowlist", isStringSet, "a Set<string>");
|
|
70
|
-
const navAllowlistExempt = readOptionalOption(raw, "navAllowlistExempt", isStringSet, "a Set<string>");
|
|
71
|
-
const warnOnUniqueAccessRoles = readOptionalOption(raw, "warnOnUniqueAccessRoles", isBoolean, "a boolean");
|
|
72
|
-
const requiredLocales = readOptionalOption(raw, "requiredLocales", isNonEmptyStringArray, "an array of non-empty strings");
|
|
73
|
-
return {
|
|
74
|
-
...(requiredLocales !== undefined && { requiredLocales }),
|
|
75
|
-
...(navAllowlist !== undefined && { navAllowlist }),
|
|
76
|
-
...(navAllowlistExempt !== undefined && { navAllowlistExempt }),
|
|
77
|
-
...(warnOnUniqueAccessRoles !== undefined && { warnOnUniqueAccessRoles }),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
function tableNameHint(entry) {
|
|
81
|
-
if (isPlainObject(entry) && typeof entry["tableName"] === "string")
|
|
82
|
-
return entry["tableName"];
|
|
83
|
-
return "unknown";
|
|
84
|
-
}
|
|
85
|
-
// Every non-optional top-level property of FeatureDefinition, grouped by the runtime shape the
|
|
86
|
-
// boot validators rely on; a hand-built entry missing one would crash far from the cause.
|
|
87
|
-
const FEATURE_ARRAY_KEYS = [
|
|
88
|
-
"requires",
|
|
89
|
-
"optionalRequires",
|
|
90
|
-
"configSeeds",
|
|
91
|
-
"extensionUsages",
|
|
92
|
-
"extensionSelectors",
|
|
93
|
-
"bootChecks",
|
|
94
|
-
"referenceData",
|
|
95
|
-
"configReads",
|
|
96
|
-
"authClaimsHooks",
|
|
97
|
-
];
|
|
98
|
-
const FEATURE_SET_KEYS = [
|
|
99
|
-
"requiredProjections",
|
|
100
|
-
"requiredSteps",
|
|
101
|
-
"exposedApis",
|
|
102
|
-
"usedApis",
|
|
103
|
-
];
|
|
104
|
-
const FEATURE_RECORD_KEYS = [
|
|
105
|
-
"relations",
|
|
106
|
-
"writeHandlers",
|
|
107
|
-
"queryHandlers",
|
|
108
|
-
"streamHandlers",
|
|
109
|
-
"translations",
|
|
110
|
-
"configKeys",
|
|
111
|
-
"jobs",
|
|
112
|
-
"registrarExtensions",
|
|
113
|
-
"notifications",
|
|
114
|
-
"events",
|
|
115
|
-
"eventMigrations",
|
|
116
|
-
"handlerEntityMappings",
|
|
117
|
-
"metrics",
|
|
118
|
-
"secretKeys",
|
|
119
|
-
"secretNamespaces",
|
|
120
|
-
"projections",
|
|
121
|
-
"multiStreamProjections",
|
|
122
|
-
"claimKeys",
|
|
123
|
-
"screens",
|
|
124
|
-
"navs",
|
|
125
|
-
"workspaces",
|
|
126
|
-
"httpRoutes",
|
|
127
|
-
"webSocketRoutes",
|
|
128
|
-
"storeTables",
|
|
129
|
-
];
|
|
130
|
-
function isFeatureDefinitionShape(entry) {
|
|
131
|
-
if (!isPlainObject(entry))
|
|
132
|
-
return false;
|
|
133
|
-
return (typeof entry["name"] === "string" &&
|
|
134
|
-
typeof entry["systemScope"] === "boolean" &&
|
|
135
|
-
FEATURE_ARRAY_KEYS.every((key) => Array.isArray(entry[key])) &&
|
|
136
|
-
FEATURE_SET_KEYS.every((key) => entry[key] instanceof Set) &&
|
|
137
|
-
FEATURE_RECORD_KEYS.every((key) => isPlainObject(entry[key])));
|
|
138
|
-
}
|
|
139
|
-
// FEATURES comes from the app's schema module; one malformed entry would make
|
|
140
|
-
// validateBoot fail with an unrelated error far from the cause, so name the index here.
|
|
141
|
-
function featureDefinitionsOf(value) {
|
|
142
|
-
if (!Array.isArray(value))
|
|
143
|
-
return undefined;
|
|
144
|
-
const definitions = [];
|
|
145
|
-
for (const [index, entry] of value.entries()) {
|
|
146
|
-
if (!isFeatureDefinitionShape(entry)) {
|
|
147
|
-
throw new Error(`FEATURES[${index}] is not a FeatureDefinition (expected an object with a string \`name\`)`);
|
|
148
|
-
}
|
|
149
|
-
definitions.push(entry);
|
|
150
|
-
}
|
|
151
|
-
return definitions;
|
|
152
|
-
}
|
|
153
|
-
// Single load-path for kumiko/schema.ts (512/3) — `generate` and `validate`
|
|
154
|
-
// both need ENTITY_METAS (and validate additionally wants FEATURES); this is
|
|
155
|
-
// the one place that enforces "ENTITY_METAS must be an array" so the two
|
|
156
|
-
// commands can't drift on the check or its wording. Throws — callers that
|
|
157
|
-
// want a graceful exit code (validate) catch it themselves.
|
|
158
|
-
async function loadSchemaModFromApp(schemaFile) {
|
|
159
|
-
// bun imports TS directly — no spawn needed.
|
|
160
|
-
const mod = await import(__rewriteRelativeImportExtension(schemaFile));
|
|
161
|
-
const entityMetas = mod["ENTITY_METAS"];
|
|
162
|
-
if (!Array.isArray(entityMetas)) {
|
|
163
|
-
throw new Error(`Schema file ${schemaFile} muss \`export const ENTITY_METAS: EntityTableMeta[]\` haben.`);
|
|
164
|
-
}
|
|
165
|
-
const validMetas = [];
|
|
166
|
-
for (const [index, entry] of entityMetas.entries()) {
|
|
167
|
-
if (!isEntityTableMeta(entry)) {
|
|
168
|
-
throw new Error(`Schema file ${schemaFile}: ENTITY_METAS[${index}] is not an EntityTableMeta (table: ${tableNameHint(entry)}).`);
|
|
169
|
-
}
|
|
170
|
-
validMetas.push(entry);
|
|
171
|
-
}
|
|
172
|
-
assertUniqueTableNames(validMetas);
|
|
173
|
-
return {
|
|
174
|
-
entityMetas: validMetas,
|
|
175
|
-
features: mod["FEATURES"],
|
|
176
|
-
validateBootOptions: mod["VALIDATE_BOOT_OPTIONS"],
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
function nextSequenceNumber(migrationsDir) {
|
|
180
|
-
if (!existsSync(migrationsDir))
|
|
181
|
-
return 1;
|
|
182
|
-
const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
|
|
183
|
-
let max = 0;
|
|
184
|
-
for (const f of files) {
|
|
185
|
-
const m = f.match(/^(\d+)_/);
|
|
186
|
-
if (m) {
|
|
187
|
-
const n = Number(m[1]);
|
|
188
|
-
if (n > max)
|
|
189
|
-
max = n;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return max + 1;
|
|
193
|
-
}
|
|
194
|
-
// A table that a migration only drops as a commented DESTRUCTIVE marker and
|
|
195
|
-
// that the snapshot no longer has is gone on paper but still in the live DB.
|
|
196
|
-
async function reportLeftoverDroppedTables(db, migrationsDir, out) {
|
|
197
|
-
const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
|
|
198
|
-
const snapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
|
|
199
|
-
const leftovers = [];
|
|
200
|
-
for (const tableName of findCommentedDropTables(readMigrationSqlTexts(migrationsDir), snapshot)) {
|
|
201
|
-
if (await tableExists(db, tableName))
|
|
202
|
-
leftovers.push(tableName);
|
|
203
|
-
}
|
|
204
|
-
// skip: nothing left over, nothing to report
|
|
205
|
-
if (leftovers.length === 0)
|
|
206
|
-
return;
|
|
207
|
-
out.log(" Tables dropped only as a commented DESTRUCTIVE marker but still in the database:");
|
|
208
|
-
for (const tableName of leftovers) {
|
|
209
|
-
out.log(isRetiredFrameworkTable(tableName)
|
|
210
|
-
? ` ! ${tableName} — retired framework table: the next \`schema generate <name>\` creates the drop`
|
|
211
|
-
: ` ! ${tableName} — app table: after a backup, write a new migration with the drop`);
|
|
212
|
-
}
|
|
18
|
+
const SCHEMA_SUBCOMMANDS = ["generate", "validate", "apply", "baseline", "status"];
|
|
19
|
+
const SCHEMA_SUBCOMMAND_HANDLERS = {
|
|
20
|
+
generate: runGenerateCommand,
|
|
21
|
+
validate: runValidateCommand,
|
|
22
|
+
apply: runApplyCommand,
|
|
23
|
+
baseline: runBaselineCommand,
|
|
24
|
+
status: runStatusCommand,
|
|
25
|
+
};
|
|
26
|
+
const HELP_ARGS = ["--help", "-h", "help"];
|
|
27
|
+
function printSchemaCliUsage(out) {
|
|
28
|
+
out.log("");
|
|
29
|
+
out.log(" Subcommands:");
|
|
30
|
+
out.log(" generate <name> Write a new migration from the EntityTableMeta diff");
|
|
31
|
+
out.log(" validate Static CI gate (no DB): schema drift + validateBoot");
|
|
32
|
+
out.log(" apply Apply pending checked-in SQL files");
|
|
33
|
+
out.log(" baseline Mark checked-in migrations as applied (no SQL run)");
|
|
34
|
+
out.log(" status List applied vs pending");
|
|
213
35
|
out.log("");
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
function reportUnknownSubcommand(out, sub) {
|
|
39
|
+
out.err(` Unknown subcommand: "${sub}"`);
|
|
40
|
+
out.err(` Allowed: ${SCHEMA_SUBCOMMANDS.join(" | ")}`);
|
|
41
|
+
return 1;
|
|
214
42
|
}
|
|
215
|
-
const SCHEMA_SUBCOMMANDS = ["generate", "validate", "apply", "baseline", "status"];
|
|
216
43
|
/**
|
|
217
44
|
* Runs a schema-CLI subcommand. `appCwd` is the app workspace root (where
|
|
218
45
|
* `kumiko/schema.ts` + `kumiko/migrations/` live). Returns a process exit code.
|
|
@@ -232,406 +59,22 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
232
59
|
// migration, since the crashed process still records the migration as
|
|
233
60
|
// applied and the rebuild is never retried.
|
|
234
61
|
await ensureTemporalPolyfill();
|
|
235
|
-
// `--help`/`-h`/`help` fall through to the same "no subcommand" usage
|
|
236
|
-
// branch below (undefined) — a real request for help, not a typo that
|
|
237
|
-
// should fail loud like an unrecognized subcommand does.
|
|
238
62
|
const rawSub = argv[0];
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
assertValidMigrationName(name);
|
|
258
|
-
}
|
|
259
|
-
catch (e) {
|
|
260
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
261
|
-
out.err(` ${msg}`);
|
|
262
|
-
out.err(" Usage: schema generate <name>");
|
|
263
|
-
return 1;
|
|
264
|
-
}
|
|
265
|
-
// Trailing args (e.g. a typo'd `--dry-run` — there is no such flag) were
|
|
266
|
-
// silently ignored and the migration got written anyway. Reject them
|
|
267
|
-
// before any IO instead of pretending they did something.
|
|
268
|
-
if (argv.length > 2) {
|
|
269
|
-
out.err(` Unrecognized argument(s): ${argv.slice(2).join(" ")}`);
|
|
270
|
-
out.err(" Usage: schema generate <name>");
|
|
271
|
-
return 1;
|
|
272
|
-
}
|
|
273
|
-
if (!existsSync(schemaFile)) {
|
|
274
|
-
out.err(` ${schemaFile} fehlt.`);
|
|
275
|
-
out.err(" App-Convention: kumiko/schema.ts mit");
|
|
276
|
-
out.err(" export const ENTITY_METAS: EntityTableMeta[] = [...]");
|
|
277
|
-
return 1;
|
|
278
|
-
}
|
|
279
|
-
let metas;
|
|
280
|
-
try {
|
|
281
|
-
({ entityMetas: metas } = await loadSchemaModFromApp(schemaFile));
|
|
282
|
-
}
|
|
283
|
-
catch (e) {
|
|
284
|
-
out.err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
285
|
-
return 1;
|
|
286
|
-
}
|
|
287
|
-
const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
|
|
288
|
-
const prevSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
|
|
289
|
-
const result = generateMigration({
|
|
290
|
-
metas,
|
|
291
|
-
prevSnapshot,
|
|
292
|
-
name,
|
|
293
|
-
sequenceNumber: nextSequenceNumber(migrationsDir),
|
|
294
|
-
priorMigrationsSql: readMigrationSqlTexts(migrationsDir),
|
|
295
|
-
});
|
|
296
|
-
const isEmpty = result.diff.newTables.length === 0 &&
|
|
297
|
-
result.diff.changedTables.length === 0 &&
|
|
298
|
-
result.diff.droppedTables.length === 0 &&
|
|
299
|
-
result.retiredDrops.length === 0;
|
|
300
|
-
if (isEmpty) {
|
|
301
|
-
out.log(" No schema changes detected — kein neues Migration-File geschrieben.");
|
|
302
|
-
return 0;
|
|
303
|
-
}
|
|
304
|
-
if (!existsSync(migrationsDir))
|
|
305
|
-
mkdirSync(migrationsDir, { recursive: true });
|
|
306
|
-
writeFileSync(join(migrationsDir, result.filename), result.sqlContent);
|
|
307
|
-
writeSnapshotJson(snapshotPath, result.snapshot);
|
|
308
|
-
// Rebuild markers are written only for incremental migrations — the initial
|
|
309
|
-
// migration (prevSnapshot===null) only creates tables, so there are no historical
|
|
310
|
-
// events to replay.
|
|
311
|
-
const rebuildTables = prevSnapshot === null ? [] : rebuildTablesFromDiff(result.diff);
|
|
312
|
-
writeRebuildMarker(migrationsDir, result.filename, rebuildTables);
|
|
313
|
-
out.log("");
|
|
314
|
-
out.log(` ✓ ${result.filename}`);
|
|
315
|
-
out.log(` new tables: ${result.diff.newTables.length}, changed: ${result.diff.changedTables.length}, dropped: ${result.diff.droppedTables.length}, retired framework tables dropped: ${result.retiredDrops.length}`);
|
|
316
|
-
if (rebuildTables.length > 0) {
|
|
317
|
-
out.log(` rebuild-marker: ${result.filename.replace(/\.sql$/, ".rebuild.json")} (${rebuildTables.length} table(s))`);
|
|
318
|
-
}
|
|
319
|
-
out.log("");
|
|
320
|
-
out.log(" Review + ggf. hand-edit + git add + commit. Apply via: schema apply");
|
|
321
|
-
out.log("");
|
|
322
|
-
return 0;
|
|
323
|
-
}
|
|
324
|
-
case "validate": {
|
|
325
|
-
// Static, DB-free boot-blocking checks for CI — catches "this won't boot"
|
|
326
|
-
// before deploy. Three layers, no database:
|
|
327
|
-
// 1. schema drift: would `generate` write a migration? (= an entity was
|
|
328
|
-
// added/changed but never generated → missing table → prod 500)
|
|
329
|
-
// 2. boot validity: validateBoot over the composed FEATURES (QN/screen/
|
|
330
|
-
// nav/role refs). Runs only if kumiko/schema.ts exports FEATURES;
|
|
331
|
-
// honours its optional VALIDATE_BOOT_OPTIONS export.
|
|
332
|
-
// 3. migration-content drift: replay every committed *.sql file and
|
|
333
|
-
// diff the result against .snapshot.json — catches a migration
|
|
334
|
-
// file whose SQL body doesn't match what its own snapshot entry
|
|
335
|
-
// claims (e.g. an accidental copy-paste from an earlier file).
|
|
336
|
-
// Layer 1 alone misses this: it only compares ENTITY_METAS to the
|
|
337
|
-
// snapshot, never the snapshot to the SQL that's supposed to have
|
|
338
|
-
// produced it.
|
|
339
|
-
// The DB-level gate (assertKumikoSchemaCurrent) stays at boot/deploy.
|
|
340
|
-
if (!existsSync(schemaFile)) {
|
|
341
|
-
out.err(` ${schemaFile} fehlt.`);
|
|
342
|
-
out.err(" App-Convention: kumiko/schema.ts mit");
|
|
343
|
-
out.err(" export const ENTITY_METAS: EntityTableMeta[] = [...]");
|
|
344
|
-
return 1;
|
|
345
|
-
}
|
|
346
|
-
let schemaMod;
|
|
347
|
-
try {
|
|
348
|
-
schemaMod = await loadSchemaModFromApp(schemaFile);
|
|
349
|
-
}
|
|
350
|
-
catch (e) {
|
|
351
|
-
out.err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
352
|
-
return 1;
|
|
353
|
-
}
|
|
354
|
-
let ok = true;
|
|
355
|
-
// 1. Schema drift — compute the diff, never write.
|
|
356
|
-
const metas = schemaMod.entityMetas;
|
|
357
|
-
const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
|
|
358
|
-
const prevSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
|
|
359
|
-
const drift = generateMigration({
|
|
360
|
-
metas,
|
|
361
|
-
prevSnapshot,
|
|
362
|
-
name: "validate",
|
|
363
|
-
sequenceNumber: nextSequenceNumber(migrationsDir),
|
|
364
|
-
priorMigrationsSql: readMigrationSqlTexts(migrationsDir),
|
|
365
|
-
});
|
|
366
|
-
const pendingTables = [
|
|
367
|
-
...drift.diff.newTables.map((t) => t.tableName),
|
|
368
|
-
...drift.diff.changedTables.map((t) => t.tableName),
|
|
369
|
-
...drift.diff.droppedTables,
|
|
370
|
-
...drift.retiredDrops.map((t) => t.tableName),
|
|
371
|
-
];
|
|
372
|
-
if (pendingTables.length === 0) {
|
|
373
|
-
out.log(" ✓ schema: migrations match the entity definitions");
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
ok = false;
|
|
377
|
-
out.err(" ✗ schema drift: entity definitions are ahead of kumiko/migrations.");
|
|
378
|
-
out.err(` pending — new: ${drift.diff.newTables.length}, changed: ${drift.diff.changedTables.length}, dropped: ${drift.diff.droppedTables.length}`);
|
|
379
|
-
out.err(` tables: ${pendingTables.join(", ")}`);
|
|
380
|
-
out.err(" Fix: `kumiko-schema generate <name>`, then commit the migration.");
|
|
381
|
-
}
|
|
382
|
-
// 2. Boot validity — needs the composed feature set.
|
|
383
|
-
let bootFeatures;
|
|
384
|
-
let bootFeaturesError;
|
|
385
|
-
try {
|
|
386
|
-
bootFeatures = featureDefinitionsOf(schemaMod.features);
|
|
387
|
-
}
|
|
388
|
-
catch (e) {
|
|
389
|
-
bootFeaturesError = e instanceof Error ? e.message : String(e);
|
|
390
|
-
}
|
|
391
|
-
if (bootFeaturesError !== undefined) {
|
|
392
|
-
ok = false;
|
|
393
|
-
out.err(` ✗ boot: ${bootFeaturesError}`);
|
|
394
|
-
}
|
|
395
|
-
else if (bootFeatures) {
|
|
396
|
-
try {
|
|
397
|
-
validateBoot(bootFeatures, {
|
|
398
|
-
...parseValidateBootOptions(schemaMod.validateBootOptions),
|
|
399
|
-
env: withFileStoragePresence(process.env),
|
|
400
|
-
});
|
|
401
|
-
out.log(" ✓ boot: feature configuration is valid");
|
|
402
|
-
}
|
|
403
|
-
catch (e) {
|
|
404
|
-
ok = false;
|
|
405
|
-
out.err(` ✗ boot: ${e instanceof Error ? e.message : String(e)}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
out.log(" · boot: skipped — add `export const FEATURES = composeFeatures(APP_FEATURES, { includeBundled: true })` to kumiko/schema.ts to enable validateBoot.");
|
|
410
|
-
}
|
|
411
|
-
// 3. Migration-content drift — replay the committed *.sql files and
|
|
412
|
-
// diff the reconstructed schema against .snapshot.json.
|
|
413
|
-
if (existsSync(migrationsDir) && prevSnapshot !== null) {
|
|
414
|
-
try {
|
|
415
|
-
const replayed = replayMigrationsDir(migrationsDir);
|
|
416
|
-
const mismatches = diffReplayAgainstSnapshot(replayed, prevSnapshot);
|
|
417
|
-
if (mismatches.length === 0) {
|
|
418
|
-
out.log(" ✓ migrations: table/column names match .snapshot.json");
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
ok = false;
|
|
422
|
-
out.err(" ✗ migration-content drift: committed *.sql files don't produce .snapshot.json.");
|
|
423
|
-
for (const m of mismatches) {
|
|
424
|
-
out.err(` ${m.tableName} (${m.kind}): ${m.detail}`);
|
|
425
|
-
}
|
|
426
|
-
if (mismatches.some((m) => m.kind === "unexpected-table")) {
|
|
427
|
-
out.err(" Fix (unexpected-table): build a meta via `defineUnmanagedTable()` " +
|
|
428
|
-
"from `@cosmicdrift/kumiko-framework/db`, then `r.storeTable(meta, { reason: ... })` " +
|
|
429
|
-
"inside a feature — this adds it to ENTITY_METAS immediately; the .snapshot.json " +
|
|
430
|
-
"only picks it up on the NEXT `kumiko-schema generate` run, which you still need " +
|
|
431
|
-
"to run and commit. `table()` returns a query handle, not a storeTable()-compatible " +
|
|
432
|
-
"meta — don't pass its result to storeTable(). See the bundled `jobs` feature's " +
|
|
433
|
-
"job-run-log store table for the pattern.");
|
|
434
|
-
}
|
|
435
|
-
if (mismatches.some((m) => m.kind !== "unexpected-table")) {
|
|
436
|
-
out.err(" Fix (missing-table/column-drift): a migration file's body doesn't match " +
|
|
437
|
-
"what it (or the snapshot) claims — hand-fix the file, or ship a corrective " +
|
|
438
|
-
"migration if it's already applied in prod.");
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
catch (e) {
|
|
443
|
-
// replayMigrationsDir fail-loud's on a table-DDL statement it can't
|
|
444
|
-
// parse — that must surface as a normal ✗ line like every other
|
|
445
|
-
// check here, not a raw stack trace that kills the process before
|
|
446
|
-
// any later check (or the Fix: help) ever runs.
|
|
447
|
-
ok = false;
|
|
448
|
-
out.err(` ✗ migration-content: ${e instanceof Error ? e.message : String(e)}`);
|
|
449
|
-
out.err(" Fix: a statement in a migration file isn't recognizable to the replay parser — extend the parser's recognized patterns, or reword the migration to one it understands.");
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return ok ? 0 : 1;
|
|
453
|
-
}
|
|
454
|
-
case "apply": {
|
|
455
|
-
const dbUrl = process.env["DATABASE_URL"];
|
|
456
|
-
if (!dbUrl) {
|
|
457
|
-
out.err(" DATABASE_URL not set.");
|
|
458
|
-
return 1;
|
|
459
|
-
}
|
|
460
|
-
if (!existsSync(migrationsDir)) {
|
|
461
|
-
out.err(` ${migrationsDir} fehlt — erst schema generate <name>.`);
|
|
462
|
-
return 1;
|
|
463
|
-
}
|
|
464
|
-
const { db, close } = createDbConnection({ url: dbUrl });
|
|
465
|
-
let wiring;
|
|
466
|
-
try {
|
|
467
|
-
const result = await runMigrationsFromDir(db, migrationsDir);
|
|
468
|
-
// Framework infrastructure tables (event store + pipeline state) are not captured by
|
|
469
|
-
// `generate`, which handles only entity read tables. Existing databases have them
|
|
470
|
-
// from the legacy-drizzle foundation; without them, a greenfield database (the
|
|
471
|
-
// first app without a cutover) would lack kumiko_events and runProdApp would fail
|
|
472
|
-
// during boot. All three are idempotent (tableExists-gated), so they are no-ops for
|
|
473
|
-
// existing databases.
|
|
474
|
-
await createEventsTable(db);
|
|
475
|
-
await createEventConsumerStateTable(db);
|
|
476
|
-
await createProjectionStateTable(db);
|
|
477
|
-
out.log("");
|
|
478
|
-
if (result.applied.length === 0) {
|
|
479
|
-
out.log(` ✓ All ${result.skipped.length} migrations already applied.`);
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
out.log(` ✓ Applied ${result.applied.length}:`);
|
|
483
|
-
for (const id of result.applied)
|
|
484
|
-
out.log(` + ${id}`);
|
|
485
|
-
if (result.skipped.length > 0)
|
|
486
|
-
out.log(` (${result.skipped.length} already applied)`);
|
|
487
|
-
}
|
|
488
|
-
out.log("");
|
|
489
|
-
// Projection-rebuild for tables a freshly applied migration changed
|
|
490
|
-
// (marker NNNN_<name>.rebuild.json from `generate`). Needs the composed
|
|
491
|
-
// feature set → only when the caller passed `features` (the app bin);
|
|
492
|
-
// the dev CLI omits it and applies migrations only. Runs unconditionally
|
|
493
|
-
// (not gated on result.applied.length > 0): a persistent queue
|
|
494
|
-
// (kumiko_pending_rebuilds) survives a failed/crashed rebuild from an
|
|
495
|
-
// earlier apply, so a later apply with zero new migrations still must
|
|
496
|
-
// retry it — otherwise a failed rebuild is silently never retried,
|
|
497
|
-
// since the migration itself is already tracked applied.
|
|
498
|
-
if (options.features) {
|
|
499
|
-
// A rebuild replays applyEntityEvent, which computes bidx columns from
|
|
500
|
-
// configureBlindIndexKey/configurePiiSubjectKms — both boot-injected by
|
|
501
|
-
// run{Prod,Dev}App but never by this standalone CLI (the migrate-db
|
|
502
|
-
// initContainer). Without wiring them here, a rebuild would silently
|
|
503
|
-
// null out bidx columns and, for ciphertext, throw only once it hits
|
|
504
|
-
// the row.
|
|
505
|
-
wiring = await resolveKmsWiringAsync(process.env, {
|
|
506
|
-
logPrefix: "[kumiko schema apply]",
|
|
507
|
-
log: out.log,
|
|
508
|
-
slots: options.kmsSlots,
|
|
509
|
-
});
|
|
510
|
-
if ("kms" in wiring) {
|
|
511
|
-
configurePiiSubjectKms(wiring.kms);
|
|
512
|
-
configureBlindIndexKey(wiring.blindIndexKey);
|
|
513
|
-
}
|
|
514
|
-
const thisRunTables = await queueRebuildsFromMarkers(db, {
|
|
515
|
-
migrationsDir,
|
|
516
|
-
appliedIds: result.applied,
|
|
517
|
-
});
|
|
518
|
-
const registry = createRegistry(options.features);
|
|
519
|
-
const rebuildRun = await runPendingRebuilds(db, registry, { thisRunTables });
|
|
520
|
-
reportPendingRebuildRun(rebuildRun, out.log);
|
|
521
|
-
}
|
|
522
|
-
return 0;
|
|
523
|
-
}
|
|
524
|
-
catch (e) {
|
|
525
|
-
out.err("");
|
|
526
|
-
out.err(` ✗ ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`);
|
|
527
|
-
out.err("");
|
|
528
|
-
return 1;
|
|
529
|
-
}
|
|
530
|
-
finally {
|
|
531
|
-
await wiring?.close();
|
|
532
|
-
await close();
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
case "baseline": {
|
|
536
|
-
// Adopt an existing DB: mark all checked-in migrations as applied WITHOUT
|
|
537
|
-
// running their SQL (prod tables already exist — cutover from the legacy
|
|
538
|
-
// drizzle system). Afterwards the boot-gate is drift-free.
|
|
539
|
-
const dbUrl = process.env["DATABASE_URL"];
|
|
540
|
-
if (!dbUrl) {
|
|
541
|
-
out.err(" DATABASE_URL not set.");
|
|
542
|
-
return 1;
|
|
543
|
-
}
|
|
544
|
-
if (!existsSync(migrationsDir)) {
|
|
545
|
-
out.err(` ${migrationsDir} fehlt — erst schema generate <name>.`);
|
|
546
|
-
return 1;
|
|
547
|
-
}
|
|
548
|
-
const { db, close } = createDbConnection({ url: dbUrl });
|
|
549
|
-
try {
|
|
550
|
-
const result = await baselineMigrations(db, loadMigrationsFromDir(migrationsDir));
|
|
551
|
-
out.log("");
|
|
552
|
-
out.log(` ✓ Marked ${result.marked.length} migration(s) as applied (no SQL run):`);
|
|
553
|
-
for (const id of result.marked)
|
|
554
|
-
out.log(` + ${id}`);
|
|
555
|
-
if (result.alreadyTracked.length > 0) {
|
|
556
|
-
out.log(` (${result.alreadyTracked.length} already tracked)`);
|
|
557
|
-
}
|
|
558
|
-
out.log("");
|
|
559
|
-
return 0;
|
|
560
|
-
}
|
|
561
|
-
catch (e) {
|
|
562
|
-
out.err(` ✗ ${e instanceof Error ? e.message : String(e)}`);
|
|
563
|
-
return 1;
|
|
564
|
-
}
|
|
565
|
-
finally {
|
|
566
|
-
await close();
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
case "status": {
|
|
570
|
-
const dbUrl = process.env["DATABASE_URL"];
|
|
571
|
-
if (!dbUrl) {
|
|
572
|
-
out.err(" DATABASE_URL not set.");
|
|
573
|
-
return 1;
|
|
574
|
-
}
|
|
575
|
-
if (!existsSync(migrationsDir)) {
|
|
576
|
-
out.log(" Kein kumiko/migrations/ — App ist noch auf dem alten drizzle-Pfad.");
|
|
577
|
-
return 0;
|
|
578
|
-
}
|
|
579
|
-
const local = loadMigrationsFromDir(migrationsDir);
|
|
580
|
-
const { db, close } = createDbConnection({ url: dbUrl });
|
|
581
|
-
try {
|
|
582
|
-
// On a fresh database where `kumiko schema apply` has never run, the tracking table is
|
|
583
|
-
// absent, which means "nothing applied". Connection and permission errors must not
|
|
584
|
-
// be swallowed (false pending results would hide the problem) — tableExists checks
|
|
585
|
-
// only for existence, while all other errors propagate.
|
|
586
|
-
const trackingExists = await tableExists(db, "_kumiko_migrations");
|
|
587
|
-
const applied = trackingExists
|
|
588
|
-
? new Set((await fetchAppliedMigrations(db)).map((a) => a.id))
|
|
589
|
-
: new Set();
|
|
590
|
-
out.log("");
|
|
591
|
-
out.log(` ${local.length} migrations in ${migrationsDir}:`);
|
|
592
|
-
for (const m of local)
|
|
593
|
-
out.log(` ${applied.has(m.id) ? "✓" : " "} ${m.id}`);
|
|
594
|
-
const pending = local.filter((m) => !applied.has(m.id)).length;
|
|
595
|
-
out.log("");
|
|
596
|
-
out.log(` ${applied.size} applied, ${pending} pending.`);
|
|
597
|
-
out.log("");
|
|
598
|
-
await reportLeftoverDroppedTables(db, migrationsDir, out);
|
|
599
|
-
return pending === 0 ? 0 : 1;
|
|
600
|
-
}
|
|
601
|
-
catch (e) {
|
|
602
|
-
// Message only, never dbUrl: it can carry the password.
|
|
603
|
-
out.err(` ✗ schema status failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
604
|
-
out.err(' Check that DATABASE_URL is reachable and the database exists (the scaffold\'s docker-compose creates "app").');
|
|
605
|
-
return 1;
|
|
606
|
-
}
|
|
607
|
-
finally {
|
|
608
|
-
try {
|
|
609
|
-
await close();
|
|
610
|
-
}
|
|
611
|
-
catch {
|
|
612
|
-
// The connection already failed and was reported above; a failing close must not throw again.
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
default: {
|
|
617
|
-
// No subcommand (`kumiko schema`) is a help request → usage + exit 0.
|
|
618
|
-
// An unrecognized one (typo, e.g. `schema cutover`) must fail loud —
|
|
619
|
-
// silently exiting 0 here let a deploy's migrate step print help and
|
|
620
|
-
// move on without ever running a migration.
|
|
621
|
-
if (sub !== undefined) {
|
|
622
|
-
out.err(` Unbekanntes Subcommand: "${sub}"`);
|
|
623
|
-
out.err(` Erlaubt: ${SCHEMA_SUBCOMMANDS.join(" | ")}`);
|
|
624
|
-
return 1;
|
|
625
|
-
}
|
|
626
|
-
out.log("");
|
|
627
|
-
out.log(" Subcommands:");
|
|
628
|
-
out.log(" generate <name> Schreibe neue Migration aus EntityTableMeta-Diff");
|
|
629
|
-
out.log(" validate Static CI-Gate (kein DB): schema-drift + validateBoot");
|
|
630
|
-
out.log(" apply Applied pending checked-in SQL-Files");
|
|
631
|
-
out.log(" baseline Markiere checked-in Migrations als applied (kein SQL-Run)");
|
|
632
|
-
out.log(" status Liste applied vs pending");
|
|
633
|
-
out.log("");
|
|
634
|
-
return 0;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
63
|
+
// No subcommand (`kumiko schema`) is a help request → usage + exit 0.
|
|
64
|
+
// `--help`/`-h`/`help` are the same — a real request for help, not a typo
|
|
65
|
+
// that should fail loud like an unrecognized subcommand does.
|
|
66
|
+
if (rawSub === undefined || isOneOf(HELP_ARGS, rawSub))
|
|
67
|
+
return printSchemaCliUsage(out);
|
|
68
|
+
// An unrecognized subcommand (typo, e.g. `schema cutover`) must fail loud —
|
|
69
|
+
// silently exiting 0 here let a deploy's migrate step print help and
|
|
70
|
+
// move on without ever running a migration.
|
|
71
|
+
if (!isOneOf(SCHEMA_SUBCOMMANDS, rawSub))
|
|
72
|
+
return reportUnknownSubcommand(out, rawSub);
|
|
73
|
+
return SCHEMA_SUBCOMMAND_HANDLERS[rawSub]({
|
|
74
|
+
argv,
|
|
75
|
+
out,
|
|
76
|
+
options,
|
|
77
|
+
schemaFile: resolvePath(appCwd, "kumiko/schema.ts"),
|
|
78
|
+
migrationsDir: resolvePath(appCwd, "kumiko/migrations"),
|
|
79
|
+
});
|
|
637
80
|
}
|