@voltro/cli 0.32.0 → 0.34.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/CHANGELOG.md +2006 -0
- package/THIRD-PARTY-NOTICES.md +1 -29
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-BICVGnEC.js +2 -0
- package/dist/{apiBuild-C4uEbs3s.js → apiBuild-DZF_f0_I.js} +46 -46
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CE7Ca9gm.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Dv8r9tGO.js +231 -0
- package/dist/checkCommand-YZDmkAGd.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-CNXKlcvD.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-DC6w2tNZ.js +30 -0
- package/dist/codemodRunner-DRRqXR74.js +5243 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-gutsz-Ac.js +808 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-3YG8p-UA.js +25 -0
- package/dist/dataCommand-qL0r7fPO.js +535 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/dbCommand-B6X0FZbq.js +1621 -0
- package/dist/dbCommand-CpdKLeQq.js +2 -0
- package/dist/{dev-5ficNnvF.js → dev-CerMd0mW.js} +3155 -3110
- package/dist/dev-CoG-ZPx8.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-BvqGBwNG.js +2 -0
- package/dist/{checkCommand-dm7OHtPt.js → doctorCommand-CnDXQxfa.js} +520 -1278
- package/dist/dormancyCommand-Dszo57d6.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
- package/dist/envCommand-DPgdV1Bq.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-DHpkgjgH.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
- package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
- package/dist/generateCommand-oibemh97.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-BJw9nLUR.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
- package/dist/inspect-C_T_WGvl.js +2 -0
- package/dist/inspectCmd-Bppy-GGw.js +224 -0
- package/dist/inspectFetch-Cm8_wVvp.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-CCca3KRZ.js +260 -0
- package/dist/manifestBuild-ChsKAhmn.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-CBwOt_iV.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CRxalXTs.js +91 -0
- package/dist/scheduleCmd--jksTrf6.js +69 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-B_5mHQS2.js → sdkgen-CYJscZC7.js} +111 -209
- package/dist/seedRunner-TFHHiToI.js +329 -0
- package/dist/serveCommand-B_isw7q4.js +1647 -0
- package/dist/serveCommand-DOvbgRnQ.js +2 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-9LiUOfES.js +1087 -0
- package/dist/start-B-9Nsp-S.js +3 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-CkEZQrtt.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-Bkptutss.js +585 -0
- package/dist/updateCommand-us1_hdIC.js +2 -0
- package/dist/{inspectMetrics-BqO4E9G0.js → webDev-CBYvPqQr.js} +1006 -1567
- package/dist/webDev-Cg-fFiyd2.js +2 -0
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-CID96Rga.js +267 -0
- package/dist/workflowsCmd-D1VTmLMY.js +608 -0
- package/package.json +193 -18
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +22 -5
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +265 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +794 -50
- package/templates/agent-docs/data.md +550 -11
- package/templates/agent-docs/database/migrations.md +174 -25
- package/templates/agent-docs/database/misc.md +193 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +5 -2
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +156 -4
- package/templates/agent-docs/introduction.md +87 -16
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +95 -20
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +161 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +1 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +157 -6
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +200 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +14 -1
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +449 -2
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +1722 -84
- package/templates/agent-docs/workflows.md +130 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-OJEjtwcn.js +0 -2
- package/dist/checkCommand-CwMrzAgV.js +0 -2
- package/dist/commands-C0nEePif.js +0 -11457
- package/dist/dbCommand-By__Ev0R.js +0 -2
- package/dist/dbCommand-ifOMafuG.js +0 -1311
- package/dist/dev-rc3fwPSZ.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-PxMmn96o.js +0 -1578
- package/dist/start-D1-8eKrO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
import { n as e, r as t } from "./nodeEnvironment-cGFAj1J8.js";
|
|
2
|
+
import { t as n } from "./cliError-BmdYnghb.js";
|
|
3
|
+
import { n as r } from "./appModuleLoader-C9r9mxZt.js";
|
|
4
|
+
import { a as i, i as a, n as o, t as s } from "./cliArgs-D4p8n7EE.js";
|
|
5
|
+
import { r as c } from "./dialectDriver-CgXnDfec.js";
|
|
6
|
+
import { a as l } from "./connectionConfig-UFlIEiys.js";
|
|
7
|
+
import { i as u, n as d, r as f, s as p, t as m } from "./seedRunner-TFHHiToI.js";
|
|
8
|
+
import { n as h, o as g, y as _ } from "./frameworkTableAssembly-DkkP6BgC.js";
|
|
9
|
+
import { b as v, x as y } from "./fileConventions-Cof68_BL.js";
|
|
10
|
+
import { basename as b, join as x, relative as S, resolve as C } from "node:path";
|
|
11
|
+
import { promises as w, writeSync as ee } from "node:fs";
|
|
12
|
+
import { pathToFileURL as T } from "node:url";
|
|
13
|
+
import { createLogger as te } from "@voltro/logger";
|
|
14
|
+
import { Effect as E } from "effect";
|
|
15
|
+
import { SqlClient as D } from "@effect/sql";
|
|
16
|
+
import { DEFAULT_CDC_CHANNEL as ne, applyPlan as re, applySchema as O, assessRollingDeployGate as k, branchRehearsalExitCode as ie, declaredSnapshot as A, destructiveScope as ae, fingerprintSchema as oe, formatBranchRehearsal as se, ignoreTablesFromEnv as j, introspectSchema as ce, pendingFileMigrationIds as le, planMigrations as M, recordUpToDate as ue, rehearseMigrationOnBranch as de, rollbackFileBasedMigration as fe, rollingDeployUnsafeOps as pe, runFileBasedMigrations as me, shortFingerprint as N, squashMigrationPlans as he, unblockLossy as ge } from "@voltro/database/sql";
|
|
17
|
+
import { actorsTable as _e, branchNamespaceName as ve, frameworkTables as P, isEncrypted as F, isMigrationDefinition as ye, makeNamespaceBranchExecutor as be, provisionBranch as xe, quoteIdent as Se, registerCoreTables as Ce, resolveBranchMechanism as we, teardownBranch as Te } from "@voltro/database";
|
|
18
|
+
import { InMemoryDataStore as Ee, makeFieldCipher as De } from "@voltro/runtime";
|
|
19
|
+
import { createHash as Oe } from "node:crypto";
|
|
20
|
+
//#region src/adoptPlan.ts
|
|
21
|
+
var ke = (e) => typeof e != "string", Ae = (e, t, n, r) => {
|
|
22
|
+
let i = [], a = new Map(t.map((e) => [e.name, e])), o = new Map(n.map((e) => [e.name, e])), s = new Set(e.leaveUnset ?? []);
|
|
23
|
+
r > 0 && i.push({
|
|
24
|
+
kind: "target-not-empty",
|
|
25
|
+
detail: `'${e.into}' already holds ${r} row(s) — adopt MOVES rows into a table, it does not merge into one`,
|
|
26
|
+
fatal: !0
|
|
27
|
+
});
|
|
28
|
+
let c = [], l = /* @__PURE__ */ new Set();
|
|
29
|
+
for (let [t, n] of Object.entries(e.map)) {
|
|
30
|
+
if (!o.has(t)) {
|
|
31
|
+
i.push({
|
|
32
|
+
kind: "unknown-target-column",
|
|
33
|
+
detail: `'${e.into}' has no column '${t}'`,
|
|
34
|
+
fatal: !0
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ke(n)) {
|
|
39
|
+
c.push({
|
|
40
|
+
target: t,
|
|
41
|
+
source: n.expr,
|
|
42
|
+
raw: !0
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!a.has(n)) {
|
|
47
|
+
i.push({
|
|
48
|
+
kind: "unknown-source-column",
|
|
49
|
+
detail: `'${e.from}' has no column '${n}' (mapped to '${t}')`,
|
|
50
|
+
fatal: !0
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
l.add(n), c.push({
|
|
55
|
+
target: t,
|
|
56
|
+
source: n,
|
|
57
|
+
raw: !1
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (let t of n) t.name in e.map || s.has(t.name) || t.nullable || t.hasDefault || i.push({
|
|
61
|
+
kind: "required-target-unmapped",
|
|
62
|
+
detail: `'${e.into}.${t.name}' is NOT NULL with no default and nothing maps to it`,
|
|
63
|
+
fatal: !0
|
|
64
|
+
});
|
|
65
|
+
for (let n of t) l.has(n.name) || i.push({
|
|
66
|
+
kind: "source-column-unmapped",
|
|
67
|
+
detail: `'${e.from}.${n.name}' is not carried across — its values are lost when the table is dropped`,
|
|
68
|
+
fatal: !1
|
|
69
|
+
});
|
|
70
|
+
let u = t.find((e) => e.idPrefix !== void 0), d = n.find((e) => e.idPrefix !== void 0), f = u !== void 0 && d !== void 0 && u.idPrefix !== d.idPrefix;
|
|
71
|
+
return f && i.push({
|
|
72
|
+
kind: "id-prefix-differs",
|
|
73
|
+
detail: `id prefix '${u.idPrefix}' → '${d.idPrefix}': every row gets a NEW id, so every reference to '${e.from}' elsewhere must be rewritten from the translation table — including ids inside JSON columns`,
|
|
74
|
+
fatal: !1
|
|
75
|
+
}), {
|
|
76
|
+
problems: i,
|
|
77
|
+
assignments: c.sort((e, t) => e.target.localeCompare(t.target)),
|
|
78
|
+
remintIds: f,
|
|
79
|
+
fatal: i.some((e) => e.fatal)
|
|
80
|
+
};
|
|
81
|
+
}, je = (e, t) => [
|
|
82
|
+
`back up '${e.from}' to a restorable dump`,
|
|
83
|
+
`copy rows: INSERT INTO ${e.into} (${t.assignments.map((e) => e.target).join(", ")}) SELECT … FROM ${e.from}`,
|
|
84
|
+
...t.remintIds ? ["record old id → new id in a translation table", `rewrite every reference to '${e.from}', JSON columns included`] : [],
|
|
85
|
+
"verify: row counts match and a sample round-trips",
|
|
86
|
+
`drop '${e.from}' — LAST, and only after the verify`
|
|
87
|
+
], Me = (e, t, n) => {
|
|
88
|
+
let r = [`adopt · ${e.from} (${n} row(s)) → ${e.into}`, ""];
|
|
89
|
+
for (let e of t.assignments) r.push(` ${e.target.padEnd(24)} ← ${e.raw ? `${e.source} (raw)` : e.source}`);
|
|
90
|
+
t.problems.length > 0 && r.push("");
|
|
91
|
+
for (let e of t.problems) r.push(` ${e.fatal ? "✗" : "⚠"} ${e.detail}`);
|
|
92
|
+
if (r.push(""), t.fatal) return r.push(" REFUSED — fix the problems above. Nothing was written."), r;
|
|
93
|
+
r.push(" would run, in this order:");
|
|
94
|
+
for (let [n, i] of je(e, t).entries()) r.push(` ${n + 1}. ${i}`);
|
|
95
|
+
return r;
|
|
96
|
+
}, Ne = (e) => e === "mysql" || e === "mariadb" ? (e) => `\`${e}\`` : e === "mssql" ? (e) => `[${e}]` : (e) => `"${e}"`, Pe = (e) => `${e}__adopt_snapshot`, Fe = (e, t, n, r, i) => {
|
|
97
|
+
let a = Ne(n), o = Pe(e.from), s = t.assignments.map((e) => e.target), c = t.assignments.map((e) => e.raw ? e.source : a(e.source));
|
|
98
|
+
return i !== void 0 && (s.unshift(r), c.unshift(i)), {
|
|
99
|
+
snapshot: n === "mssql" ? `SELECT * INTO ${a(o)} FROM ${a(e.from)}` : `CREATE TABLE ${a(o)} AS SELECT * FROM ${a(e.from)}`,
|
|
100
|
+
copy: `INSERT INTO ${a(e.into)} (${s.map(a).join(", ")}) SELECT ${c.join(", ")} FROM ${a(e.from)}`,
|
|
101
|
+
translations: i === void 0 ? [] : [`CREATE TABLE ${a(`${o}__ids`)} (old_id TEXT, new_id TEXT)`],
|
|
102
|
+
verifySource: `SELECT COUNT(*) AS n FROM ${a(e.from)}`,
|
|
103
|
+
verifyTarget: `SELECT COUNT(*) AS n FROM ${a(e.into)}`,
|
|
104
|
+
drop: `DROP TABLE ${a(e.from)}`,
|
|
105
|
+
dropSnapshot: `DROP TABLE ${a(o)}`
|
|
106
|
+
};
|
|
107
|
+
}, Ie = (e) => {
|
|
108
|
+
let t = e[0];
|
|
109
|
+
if (t === void 0) return 0;
|
|
110
|
+
let n = t.n ?? t.N ?? Object.values(t)[0];
|
|
111
|
+
return Number(n ?? 0);
|
|
112
|
+
}, Le = async (e, t, n, r, i, a, o = {}) => {
|
|
113
|
+
if (t.fatal) throw Error("adopt: the plan was refused — nothing was run");
|
|
114
|
+
let s = Fe(e, t, n, r, i), c = Pe(e.from), l = Ie(await a(s.verifySource));
|
|
115
|
+
await a(s.snapshot);
|
|
116
|
+
for (let e of s.translations) await a(e);
|
|
117
|
+
await a(s.copy);
|
|
118
|
+
let u = Ie(await a(s.verifyTarget));
|
|
119
|
+
return u === l ? o.keepSource === !0 ? {
|
|
120
|
+
copied: u,
|
|
121
|
+
sourceRows: l,
|
|
122
|
+
verified: !0,
|
|
123
|
+
snapshotTable: c,
|
|
124
|
+
sourceDropped: !1
|
|
125
|
+
} : (await a(s.drop), {
|
|
126
|
+
copied: u,
|
|
127
|
+
sourceRows: l,
|
|
128
|
+
verified: !0,
|
|
129
|
+
snapshotTable: c,
|
|
130
|
+
sourceDropped: !0
|
|
131
|
+
}) : {
|
|
132
|
+
copied: u,
|
|
133
|
+
sourceRows: l,
|
|
134
|
+
verified: !1,
|
|
135
|
+
snapshotTable: c,
|
|
136
|
+
sourceDropped: !1
|
|
137
|
+
};
|
|
138
|
+
}, I = [
|
|
139
|
+
"token",
|
|
140
|
+
"secret",
|
|
141
|
+
"password",
|
|
142
|
+
"apikey",
|
|
143
|
+
"api_key",
|
|
144
|
+
"credential",
|
|
145
|
+
"privatekey",
|
|
146
|
+
"private_key"
|
|
147
|
+
], Re = [
|
|
148
|
+
{
|
|
149
|
+
table: "_voltro_audit_log",
|
|
150
|
+
column: "subject"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
table: "_voltro_audit_log",
|
|
154
|
+
column: "actor"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
table: "_voltro_audit_log",
|
|
158
|
+
column: "scope"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
table: "_voltro_audit_log",
|
|
162
|
+
column: "metadata"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
table: "_voltro_audit_log",
|
|
166
|
+
column: "input"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
table: "_voltro_audit_log",
|
|
170
|
+
column: "outcome"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
table: "_voltro_audit_log",
|
|
174
|
+
column: "subjectId"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
table: "_voltro_row_history",
|
|
178
|
+
column: "data"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
table: "_voltro_row_history",
|
|
182
|
+
column: "actor"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
table: "_voltro_row_history",
|
|
186
|
+
column: "scope"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
table: "_voltro_row_history",
|
|
190
|
+
column: "subjectId"
|
|
191
|
+
}
|
|
192
|
+
], ze = (e, t) => {
|
|
193
|
+
switch (t) {
|
|
194
|
+
case "postgres": return `LOWER(CAST(${e} AS text))`;
|
|
195
|
+
case "mysql":
|
|
196
|
+
case "mariadb": return `LOWER(CAST(${e} AS CHAR))`;
|
|
197
|
+
case "mssql": return `LOWER(CAST(${e} AS NVARCHAR(MAX)))`;
|
|
198
|
+
case "sqlite":
|
|
199
|
+
case "turso": return `LOWER(CAST(${e} AS TEXT))`;
|
|
200
|
+
}
|
|
201
|
+
}, Be = (e, t, n = I) => n.map((n) => `${ze(e, t)} LIKE '%${n}%'`).join("\n OR "), Ve = ["_omitted", "__redacted"], He = (e, t) => {
|
|
202
|
+
let n = e.toLowerCase();
|
|
203
|
+
return t.some((e) => n.includes(e));
|
|
204
|
+
}, Ue = (e, t = []) => {
|
|
205
|
+
if (Array.isArray(e)) {
|
|
206
|
+
for (let n of e) Ue(n, t);
|
|
207
|
+
return t;
|
|
208
|
+
}
|
|
209
|
+
if (typeof e == "object" && e) for (let [n, r] of Object.entries(e)) t.push(n), Ue(r, t);
|
|
210
|
+
return t;
|
|
211
|
+
}, We = (e) => Array.isArray(e) ? e.map(We) : typeof e == "object" && e ? Object.fromEntries(Object.entries(e).map(([e, t]) => [e, Ve.includes(e) ? null : We(t)])) : e, Ge = (e, t = I) => {
|
|
212
|
+
let n;
|
|
213
|
+
try {
|
|
214
|
+
n = JSON.parse(e);
|
|
215
|
+
} catch {
|
|
216
|
+
return {
|
|
217
|
+
asKey: !1,
|
|
218
|
+
redactionMarkerOnly: !1,
|
|
219
|
+
undecidable: !0
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
asKey: Ue(n).some((e) => He(e, t)),
|
|
224
|
+
redactionMarkerOnly: !He(JSON.stringify(We(n)) ?? "", t),
|
|
225
|
+
undecidable: !1
|
|
226
|
+
};
|
|
227
|
+
}, Ke = (e) => ({
|
|
228
|
+
examined: e.length,
|
|
229
|
+
asKey: e.filter((e) => e.asKey).length,
|
|
230
|
+
redactionMarkerOnly: e.filter((e) => e.redactionMarkerOnly && !e.asKey).length,
|
|
231
|
+
undecidable: e.filter((e) => e.undecidable).length
|
|
232
|
+
}), L = (e) => {
|
|
233
|
+
let t = e.classification;
|
|
234
|
+
return t === void 0 || e.hits === void 0 || e.hits === 0 ? !1 : t.examined === e.hits && t.undecidable === 0 && t.asKey === 0 && t.redactionMarkerOnly === e.hits;
|
|
235
|
+
}, qe = (e, t, n, r) => {
|
|
236
|
+
let i = `WHERE ${Be(t, n)}`, a = ze(t, n).replace(/^LOWER\((.*)\)$/s, "$1");
|
|
237
|
+
return n === "mssql" ? `SELECT TOP (${r}) ${a} AS v FROM ${e}\n ${i}` : `SELECT ${a} AS v FROM ${e}\n ${i}\n LIMIT ${r}`;
|
|
238
|
+
}, Je = async (e, t, n, r, i) => (await E.runPromise(e.unsafe(qe(t, n, r, i)))).map((e) => {
|
|
239
|
+
let t = Object.values(e)[0];
|
|
240
|
+
return typeof t == "string" ? t : JSON.stringify(t ?? null);
|
|
241
|
+
}), Ye = async (e, t) => {
|
|
242
|
+
let n = await E.runPromise(e.unsafe(t));
|
|
243
|
+
return Number(Object.values(n[0] ?? { n: 0 })[0] ?? 0);
|
|
244
|
+
}, Xe = async (e, t, n, r) => {
|
|
245
|
+
let i = [];
|
|
246
|
+
for (let a of n) {
|
|
247
|
+
let n = r(a.table), o;
|
|
248
|
+
try {
|
|
249
|
+
o = await Ye(e, `SELECT COUNT(*) AS n FROM ${n}`);
|
|
250
|
+
} catch {
|
|
251
|
+
i.push({
|
|
252
|
+
table: a.table,
|
|
253
|
+
column: a.column,
|
|
254
|
+
scanned: void 0,
|
|
255
|
+
hits: void 0,
|
|
256
|
+
unavailable: "table"
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
let s = await Ye(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${Be(r(a.column), t)}`), c = [];
|
|
262
|
+
if (s > 0) for (let i of I) try {
|
|
263
|
+
let o = await Ye(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${Be(r(a.column), t, [i])}`);
|
|
264
|
+
o > 0 && c.push({
|
|
265
|
+
needle: i,
|
|
266
|
+
rows: o
|
|
267
|
+
});
|
|
268
|
+
} catch {}
|
|
269
|
+
let l;
|
|
270
|
+
if (s > 0) try {
|
|
271
|
+
l = Ke((await Je(e, n, r(a.column), t, 500)).map((e) => Ge(e)));
|
|
272
|
+
} catch {}
|
|
273
|
+
i.push({
|
|
274
|
+
table: a.table,
|
|
275
|
+
column: a.column,
|
|
276
|
+
scanned: o,
|
|
277
|
+
hits: s,
|
|
278
|
+
matchedNeedles: c,
|
|
279
|
+
...l === void 0 ? {} : { classification: l }
|
|
280
|
+
});
|
|
281
|
+
} catch {
|
|
282
|
+
i.push({
|
|
283
|
+
table: a.table,
|
|
284
|
+
column: a.column,
|
|
285
|
+
scanned: o,
|
|
286
|
+
hits: void 0,
|
|
287
|
+
unavailable: "column"
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return i;
|
|
292
|
+
}, Ze = (e, t) => {
|
|
293
|
+
let n = I.length, r = [`credential scan · dialect ${t}`, ""], i = /* @__PURE__ */ new Set();
|
|
294
|
+
for (let t of e) {
|
|
295
|
+
if (t.unavailable === "column") {
|
|
296
|
+
r.push(` · ${t.table}.${t.column} — column is not present on this deployment (${t.scanned} row(s) exist but this column was not scanned)`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (t.scanned === void 0) {
|
|
300
|
+
i.has(t.table) || (i.add(t.table), r.push(` · ${t.table} — table not present (nothing scanned)`));
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (t.scanned === 0) {
|
|
304
|
+
r.push(` · ${t.table} — table is EMPTY (0 rows scanned; this is not a clean bill of health)`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
let e = L(t), n = (t.hits ?? 0) === 0 ? "✓" : e ? "~" : "✗";
|
|
308
|
+
r.push(` ${n} ${t.table}.${t.column} — ${t.hits} of ${t.scanned} row(s) contain a credential-shaped NAME (anywhere in the value)`), (t.matchedNeedles?.length ?? 0) > 0 && r.push(` matched (rows per needle, may overlap): ${t.matchedNeedles.map((e) => `${e.needle} (${e.rows})`).join(", ")}`);
|
|
309
|
+
let a = t.classification;
|
|
310
|
+
a !== void 0 && (r.push(` examined ${a.examined} of ${t.hits} matched row(s)${a.examined < (t.hits ?? 0) ? " (capped at 500)" : ""}: ${a.asKey} as a JSON KEY, ${a.redactionMarkerOnly} only inside a redaction marker${a.undecidable > 0 ? `, ${a.undecidable} not parseable as JSON` : ""}`), e && r.push(` EXPLAINED — every match is \`${Ve.join("` / `")}\`, the framework's own`, " record that a `.sensitive()` column was left OUT. Nothing to purge, nothing to rotate."));
|
|
311
|
+
}
|
|
312
|
+
let a = e.reduce((e, t) => e + (L(t) ? 0 : t.hits ?? 0), 0), o = e.reduce((e, t) => e + (L(t) ? t.hits ?? 0 : 0), 0);
|
|
313
|
+
return r.push(""), o > 0 && r.push(` ${o} row(s) matched only the framework's own redaction markers and are NOT findings.`), a > 0 ? r.push(` ${a} row(s) carry something credential-shaped.`, " Purge them AND rotate the credentials — assume anything written to a log", " table has been read. Then stop putting the credential on the Subject:", " `connectionCredentials(...)` keeps it in the framework vault.") : e.every((e) => e.scanned === void 0 || e.scanned === 0 || e.unavailable !== void 0) ? r.push(" NOTHING WAS SCANNED. This run proves nothing about your data.") : r.push(" No credential-shaped keys found in the rows that exist."), r.push("", ` These are NAME heuristics (${n} of them), not proof. A credential in a`, " column whose key is not credential-shaped — `keyValue`, `pat`, `authz` —", " is invisible here. A clean run means \"no credential-shaped NAME found\",", " which is not the same as \"no credential\".", "", " Run this on EVERY environment. A dev database is not a sample of production."), r.join("\n");
|
|
314
|
+
}, Qe = (e) => e.some((e) => (e.hits ?? 0) > 0 && !L(e)) ? 1 : e.every((e) => e.unavailable !== void 0 || e.scanned === void 0 || e.scanned === 0) ? 2 : 0, $e = /\.migration\.ts$/, R = async (e, t) => {
|
|
315
|
+
let n = [];
|
|
316
|
+
for (let r of e) {
|
|
317
|
+
let e;
|
|
318
|
+
try {
|
|
319
|
+
e = await import(T(r).href);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
t.warn("migration: failed to import", { file: r }, e);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
let i = (e.default && ye(e.default) ? e.default : void 0) ?? Object.values(e).find(ye);
|
|
325
|
+
if (!i) {
|
|
326
|
+
t.warn("migration: no defineMigration export", { file: r });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
i.id !== b(r).replace(/\.migration\.ts$/, "") && t.warn("migration: id does not match filename — id wins for ordering, but rename the file for clarity", {
|
|
330
|
+
file: r,
|
|
331
|
+
idInFile: i.id,
|
|
332
|
+
idFromFile: b(r).replace(/\.migration\.ts$/, "")
|
|
333
|
+
});
|
|
334
|
+
let a = await w.readFile(r, "utf8").catch(() => ""), o = Oe("sha256").update(a).digest("hex").slice(0, 16);
|
|
335
|
+
n.push({
|
|
336
|
+
file: r,
|
|
337
|
+
definition: i,
|
|
338
|
+
hash: o
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return [...n].sort((e, t) => e.definition.id < t.definition.id ? -1 : 1);
|
|
342
|
+
}, et = (e) => {
|
|
343
|
+
let t = (process.env.DB_DIALECT ?? "postgres").toLowerCase(), n = t === "memory" ? "postgres" : t;
|
|
344
|
+
return E.runPromise(O(P, n).pipe(E.provide(e)));
|
|
345
|
+
}, tt = (e) => E.runPromise(E.gen(function* () {
|
|
346
|
+
let e = yield* (yield* D.SqlClient)`SELECT id, status, hash, "appliedAt" FROM "_voltro_migrations"`.pipe(E.orElseSucceed(() => []));
|
|
347
|
+
return new Map(e.map((e) => [e.id, e]));
|
|
348
|
+
}).pipe(E.provide(e))), z = (e, t) => E.runPromise(E.gen(function* () {
|
|
349
|
+
yield* (yield* D.SqlClient)`
|
|
350
|
+
INSERT INTO "_voltro_migrations"
|
|
351
|
+
(id, name, hash, status, "appliedAt", "durationMs", "errorMessage", "errorStack")
|
|
352
|
+
VALUES
|
|
353
|
+
(${t.id}, ${t.name}, ${t.hash}, ${t.status}, ${t.appliedAt}, ${t.durationMs}, ${t.errorMessage}, ${t.errorStack})
|
|
354
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
355
|
+
name = EXCLUDED.name,
|
|
356
|
+
hash = EXCLUDED.hash,
|
|
357
|
+
status = EXCLUDED.status,
|
|
358
|
+
"appliedAt" = EXCLUDED."appliedAt",
|
|
359
|
+
"durationMs" = EXCLUDED."durationMs",
|
|
360
|
+
"errorMessage"= EXCLUDED."errorMessage",
|
|
361
|
+
"errorStack" = EXCLUDED."errorStack",
|
|
362
|
+
"updatedAt" = now()
|
|
363
|
+
`;
|
|
364
|
+
}).pipe(E.provide(e))), nt = async (e, t, n) => {
|
|
365
|
+
if (e.length === 0) return {
|
|
366
|
+
applied: [],
|
|
367
|
+
failed: null,
|
|
368
|
+
skipped: []
|
|
369
|
+
};
|
|
370
|
+
await et(t);
|
|
371
|
+
let r = await tt(t), i = [], a = [], o = null;
|
|
372
|
+
for (let s of e) {
|
|
373
|
+
let e = r.get(s.definition.id);
|
|
374
|
+
if (e && e.status === "applied") {
|
|
375
|
+
e.hash !== s.hash && n.warn("migration: applied migration has a different hash on disk", {
|
|
376
|
+
id: s.definition.id,
|
|
377
|
+
appliedHash: e.hash,
|
|
378
|
+
fileHash: s.hash
|
|
379
|
+
}), a.push({
|
|
380
|
+
migrationId: s.definition.id,
|
|
381
|
+
status: "skipped",
|
|
382
|
+
durationMs: 0,
|
|
383
|
+
steps: []
|
|
384
|
+
});
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
n.info("migration: applying", {
|
|
388
|
+
id: s.definition.id,
|
|
389
|
+
name: s.definition.name
|
|
390
|
+
});
|
|
391
|
+
let c = Date.now(), l = [];
|
|
392
|
+
await z(t, {
|
|
393
|
+
id: s.definition.id,
|
|
394
|
+
name: s.definition.name,
|
|
395
|
+
hash: s.hash,
|
|
396
|
+
status: "running",
|
|
397
|
+
appliedAt: null,
|
|
398
|
+
durationMs: null,
|
|
399
|
+
errorMessage: null,
|
|
400
|
+
errorStack: null
|
|
401
|
+
});
|
|
402
|
+
let u = !1;
|
|
403
|
+
for (let e of s.definition.steps) {
|
|
404
|
+
let n = Date.now();
|
|
405
|
+
try {
|
|
406
|
+
await E.runPromise(e.apply.pipe(E.provide(t))), l.push({
|
|
407
|
+
stepName: e.name,
|
|
408
|
+
status: "applied",
|
|
409
|
+
durationMs: Date.now() - n
|
|
410
|
+
});
|
|
411
|
+
} catch (r) {
|
|
412
|
+
let a = r, d = {
|
|
413
|
+
stepName: e.name,
|
|
414
|
+
status: "failed",
|
|
415
|
+
durationMs: Date.now() - n,
|
|
416
|
+
error: {
|
|
417
|
+
message: a.message,
|
|
418
|
+
...a.stack ? { stack: a.stack } : {}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
l.push(d), u = !0;
|
|
422
|
+
let f = {
|
|
423
|
+
migrationId: s.definition.id,
|
|
424
|
+
status: "failed",
|
|
425
|
+
durationMs: Date.now() - c,
|
|
426
|
+
steps: l,
|
|
427
|
+
error: {
|
|
428
|
+
stepName: e.name,
|
|
429
|
+
message: a.message,
|
|
430
|
+
...a.stack ? { stack: a.stack } : {}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
o = f, i.push(f), await z(t, {
|
|
434
|
+
id: s.definition.id,
|
|
435
|
+
name: s.definition.name,
|
|
436
|
+
hash: s.hash,
|
|
437
|
+
status: "failed",
|
|
438
|
+
appliedAt: null,
|
|
439
|
+
durationMs: Date.now() - c,
|
|
440
|
+
errorMessage: a.message,
|
|
441
|
+
errorStack: a.stack ?? null
|
|
442
|
+
});
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (u) break;
|
|
447
|
+
i.push({
|
|
448
|
+
migrationId: s.definition.id,
|
|
449
|
+
status: "applied",
|
|
450
|
+
durationMs: Date.now() - c,
|
|
451
|
+
steps: l
|
|
452
|
+
}), await z(t, {
|
|
453
|
+
id: s.definition.id,
|
|
454
|
+
name: s.definition.name,
|
|
455
|
+
hash: s.hash,
|
|
456
|
+
status: "applied",
|
|
457
|
+
appliedAt: /* @__PURE__ */ new Date(),
|
|
458
|
+
durationMs: Date.now() - c,
|
|
459
|
+
errorMessage: null,
|
|
460
|
+
errorStack: null
|
|
461
|
+
}), n.info("migration: applied", {
|
|
462
|
+
id: s.definition.id,
|
|
463
|
+
durationMs: Date.now() - c,
|
|
464
|
+
stepCount: l.length
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
applied: i.filter((e) => e.status === "applied"),
|
|
469
|
+
failed: o,
|
|
470
|
+
skipped: a
|
|
471
|
+
};
|
|
472
|
+
}, rt = async (e, t, n, r = {}) => {
|
|
473
|
+
await et(t);
|
|
474
|
+
let i = await tt(t), a = e.filter((e) => i.get(e.definition.id)?.status === "applied");
|
|
475
|
+
if (a.length === 0) return n.info("migration: nothing to roll back"), {
|
|
476
|
+
applied: [],
|
|
477
|
+
failed: null,
|
|
478
|
+
skipped: []
|
|
479
|
+
};
|
|
480
|
+
let o = [];
|
|
481
|
+
for (let e = a.length - 1; e >= 0; e--) {
|
|
482
|
+
let t = a[e];
|
|
483
|
+
if (o.push(t), !r.to && o.length === 1 || r.to && t.definition.id === r.to) break;
|
|
484
|
+
}
|
|
485
|
+
let s = [], c = null;
|
|
486
|
+
for (let e of o) {
|
|
487
|
+
n.info("migration: rolling back", { id: e.definition.id });
|
|
488
|
+
let r = Date.now(), i = [], a = !1;
|
|
489
|
+
for (let n = e.definition.steps.length - 1; n >= 0; n--) {
|
|
490
|
+
let o = e.definition.steps[n], s = Date.now();
|
|
491
|
+
try {
|
|
492
|
+
await E.runPromise(o.undo.pipe(E.provide(t))), i.push({
|
|
493
|
+
stepName: o.name,
|
|
494
|
+
status: "applied",
|
|
495
|
+
durationMs: Date.now() - s
|
|
496
|
+
});
|
|
497
|
+
} catch (t) {
|
|
498
|
+
let n = t;
|
|
499
|
+
i.push({
|
|
500
|
+
stepName: o.name,
|
|
501
|
+
status: "failed",
|
|
502
|
+
durationMs: Date.now() - s,
|
|
503
|
+
error: {
|
|
504
|
+
message: n.message,
|
|
505
|
+
...n.stack ? { stack: n.stack } : {}
|
|
506
|
+
}
|
|
507
|
+
}), a = !0, c = {
|
|
508
|
+
migrationId: e.definition.id,
|
|
509
|
+
status: "failed",
|
|
510
|
+
durationMs: Date.now() - r,
|
|
511
|
+
steps: i,
|
|
512
|
+
error: {
|
|
513
|
+
stepName: o.name,
|
|
514
|
+
message: n.message,
|
|
515
|
+
...n.stack ? { stack: n.stack } : {}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (a) break;
|
|
522
|
+
s.push({
|
|
523
|
+
migrationId: e.definition.id,
|
|
524
|
+
status: "applied",
|
|
525
|
+
durationMs: Date.now() - r,
|
|
526
|
+
steps: i
|
|
527
|
+
}), await z(t, {
|
|
528
|
+
id: e.definition.id,
|
|
529
|
+
name: e.definition.name,
|
|
530
|
+
hash: e.hash,
|
|
531
|
+
status: "reverted",
|
|
532
|
+
appliedAt: null,
|
|
533
|
+
durationMs: Date.now() - r,
|
|
534
|
+
errorMessage: null,
|
|
535
|
+
errorStack: null
|
|
536
|
+
}), n.info("migration: rolled back", { id: e.definition.id });
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
applied: s,
|
|
540
|
+
failed: c,
|
|
541
|
+
skipped: []
|
|
542
|
+
};
|
|
543
|
+
}, it = async (e, t) => {
|
|
544
|
+
await et(t);
|
|
545
|
+
let n = await tt(t);
|
|
546
|
+
return e.map((e) => {
|
|
547
|
+
let t = n.get(e.definition.id);
|
|
548
|
+
return t ? {
|
|
549
|
+
id: e.definition.id,
|
|
550
|
+
name: e.definition.name,
|
|
551
|
+
status: t.status === "running" ? "failed" : t.status,
|
|
552
|
+
appliedAt: t.appliedAt,
|
|
553
|
+
hashMatches: t.hash === e.hash
|
|
554
|
+
} : {
|
|
555
|
+
id: e.definition.id,
|
|
556
|
+
name: e.definition.name,
|
|
557
|
+
status: "pending",
|
|
558
|
+
appliedAt: null,
|
|
559
|
+
hashMatches: !0
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
}, at = (e) => {
|
|
563
|
+
if (!e.failed) return "";
|
|
564
|
+
let t = e.failed, n = [
|
|
565
|
+
`✗ Migration failed: ${t.migrationId} / step '${t.error?.stepName ?? "?"}'`,
|
|
566
|
+
` Reason: ${t.error?.message ?? "unknown"}`,
|
|
567
|
+
"",
|
|
568
|
+
"Next steps:",
|
|
569
|
+
" voltro db status # full migration timeline",
|
|
570
|
+
" voltro db rollback # revert to last good state",
|
|
571
|
+
" voltro db migrate # retry after fixing the cause",
|
|
572
|
+
""
|
|
573
|
+
];
|
|
574
|
+
return e.applied.length > 0 && n.unshift("", `(${e.applied.length} migration(s) applied successfully before the failure.)`), n.join("\n");
|
|
575
|
+
}, ot = (e) => {
|
|
576
|
+
let t = e.split(".");
|
|
577
|
+
if (t.length !== 2 || !t[0] || !t[1]) throw Error(`voltro db encrypt-column: expected <table>.<column>, got "${e}"`);
|
|
578
|
+
return {
|
|
579
|
+
table: t[0],
|
|
580
|
+
column: t[1]
|
|
581
|
+
};
|
|
582
|
+
}, st = (e) => 49 + 4 * Math.ceil(e / 3), ct = (e, t) => {
|
|
583
|
+
let n = st(e);
|
|
584
|
+
return {
|
|
585
|
+
longestPlaintextBytes: e,
|
|
586
|
+
longestCiphertext: n,
|
|
587
|
+
declaredMaxLength: t,
|
|
588
|
+
fits: t === void 0 || n <= t
|
|
589
|
+
};
|
|
590
|
+
}, lt = (e, t) => `${e.table}.${e.column} is too narrow: its longest value is ${t.longestPlaintextBytes} byte(s), which encrypts to ${t.longestCiphertext} characters, and the column is declared as ${t.declaredMaxLength}. Widen it first (\`.maxLength(${t.longestCiphertext})\` or more, then \`voltro db apply\`) and run this again. Nothing was written.`, ut = (e, t) => {
|
|
591
|
+
let n = t ? "would encrypt" : "encrypted";
|
|
592
|
+
return [
|
|
593
|
+
` ${e.table}.${e.column}`,
|
|
594
|
+
` scanned ${e.scanned}`,
|
|
595
|
+
` already encrypted ${e.alreadyEncrypted} (skipped — a re-run is safe)`,
|
|
596
|
+
` null / empty ${e.empty}`,
|
|
597
|
+
` ${n.padEnd(18)} ${e.encrypted}`
|
|
598
|
+
].join("\n");
|
|
599
|
+
}, dt = (e, t, n) => {
|
|
600
|
+
if (e == null) return;
|
|
601
|
+
let r = typeof e == "string" ? e : String(e);
|
|
602
|
+
if (r === "" || F(r)) return;
|
|
603
|
+
let i = t.encrypt(r), a;
|
|
604
|
+
try {
|
|
605
|
+
a = t.decrypt(i);
|
|
606
|
+
} catch (e) {
|
|
607
|
+
throw Error(`${n.table}.${n.column}: the cipher produced a value it cannot read back (${e.message ?? String(e)}). Nothing was written.`);
|
|
608
|
+
}
|
|
609
|
+
if (a !== r) throw Error(`${n.table}.${n.column}: round-trip mismatch — the cipher did not return what it was given. Nothing was written.`);
|
|
610
|
+
return i;
|
|
611
|
+
}, ft = (e) => Buffer.byteLength(e, "utf8"), pt = (e, t) => {
|
|
612
|
+
if (e !== void 0) try {
|
|
613
|
+
return t.decrypt(e), !0;
|
|
614
|
+
} catch {
|
|
615
|
+
return !1;
|
|
616
|
+
}
|
|
617
|
+
}, mt = (e, t) => `${e.table}.${e.column} already holds values encrypted with a DIFFERENT key than ${t} — the active key cannot decrypt them. Continuing would encrypt the remaining rows under a second key, and the column would then be readable with neither key alone. Nothing was written. Point ${t} at the key the existing rows were written with.`, ht = (e) => [
|
|
618
|
+
"",
|
|
619
|
+
" NEXT — this converted the rows that exist. New writes are still plaintext",
|
|
620
|
+
" until the column carries the modifier:",
|
|
621
|
+
"",
|
|
622
|
+
...e.map((e) => ` ${e.column}: text().encrypted() // ${e.table}`),
|
|
623
|
+
"",
|
|
624
|
+
" Order does not matter: a read returns a non-ciphertext value unchanged, so",
|
|
625
|
+
" the column may hold a mix while you deploy. Run this again afterwards to",
|
|
626
|
+
" catch anything written in between — it skips what is already encrypted."
|
|
627
|
+
].join("\n"), B = te({ scope: "voltro:db" }), gt = /* @__PURE__ */ new Set([
|
|
628
|
+
"node_modules",
|
|
629
|
+
"dist",
|
|
630
|
+
"build",
|
|
631
|
+
".turbo",
|
|
632
|
+
".vite",
|
|
633
|
+
".next",
|
|
634
|
+
".git"
|
|
635
|
+
]), V = async (e, t) => {
|
|
636
|
+
let n = [], r = async (e) => {
|
|
637
|
+
let i = await w.readdir(e, { withFileTypes: !0 }).catch(() => []);
|
|
638
|
+
for (let a of i) {
|
|
639
|
+
if (gt.has(a.name)) continue;
|
|
640
|
+
let i = x(e, a.name);
|
|
641
|
+
a.isDirectory() ? await r(i) : a.isFile() && t.test(a.name) && n.push(i);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
return await r(e), n;
|
|
645
|
+
}, _t = [
|
|
646
|
+
"--against",
|
|
647
|
+
"--batch",
|
|
648
|
+
"--before",
|
|
649
|
+
"--id",
|
|
650
|
+
"--key-env",
|
|
651
|
+
"--lifecycle",
|
|
652
|
+
"--limit",
|
|
653
|
+
"--name",
|
|
654
|
+
"--note",
|
|
655
|
+
"--online-after",
|
|
656
|
+
"--plan",
|
|
657
|
+
"--pr",
|
|
658
|
+
"--prefer",
|
|
659
|
+
"--root",
|
|
660
|
+
"--seed",
|
|
661
|
+
"--store",
|
|
662
|
+
"--to",
|
|
663
|
+
"--token"
|
|
664
|
+
], H = (e) => C(s(e, _t) ?? process.cwd()), U = () => l("migration"), vt = (e) => {
|
|
665
|
+
let t = e;
|
|
666
|
+
if (typeof t != "object" || !t) return !1;
|
|
667
|
+
if (t.code === "ERR_OUT_OF_RANGE" || t.cause?.code === "ERR_OUT_OF_RANGE") return !0;
|
|
668
|
+
let n = `${t.message ?? ""} ${t.cause?.message ?? ""} ${t.stack ?? ""}`;
|
|
669
|
+
return /ERR_OUT_OF_RANGE|out of range/i.test(n) && /pg-protocol|parseDataRow|buffer-reader|BufferReader/i.test(n);
|
|
670
|
+
}, yt = "Live-DB introspection got a corrupt / misaligned response from the database. This happens when `voltro db plan` / `apply` / `drift` / `migrate` runs through a connection POOLER (Supabase Supavisor / PgBouncer) on a LARGE schema — the pooler mis-frames a big catalog response. The introspection already batches by table to keep responses small; if a batch still trips it:\n 1. Lower the batch: set `VOLTRO_INTROSPECT_BATCH` to a smaller number of tables (default 20 — try 5 or 1). This keeps `db plan` on the normal pooler, no direct connection needed.\n 2. Or use a DIRECT (non-pooler) connection: set `DB_DIRECT_URL` (or `DB_MIGRATE_URL`) to the direct database URL — the runtime keeps `DB_URL`, so only the migration path is affected. (Supabase: direct = `db.<project-ref>.supabase.co` [IPv6-only on newer projects]; pooler = `aws-0-<region>.pooler.supabase.com`.)", W = () => {
|
|
671
|
+
let e = (process.env.DB_DIALECT ?? "postgres").toLowerCase();
|
|
672
|
+
if (e === "memory") throw new n("voltro db: subcommands require a real dialect, not DB_DIALECT=memory.");
|
|
673
|
+
return e;
|
|
674
|
+
}, G = (e) => Se(e, W()), K = (e) => e === "mysql" || e === "mariadb" ? "table_schema = DATABASE()" : e === "mssql" ? "table_schema = SCHEMA_NAME()" : "table_schema = 'public'", q = async () => (await c(W())).makeSqlLayer(U()), bt = async (e) => {
|
|
675
|
+
let t = H(e), n = e.includes("--dry-run"), r = await R(await V(t, $e), B);
|
|
676
|
+
if (r.length === 0) return B.info("no migrations found", { root: t }), 0;
|
|
677
|
+
if (n) {
|
|
678
|
+
let e = /* @__PURE__ */ new Set();
|
|
679
|
+
try {
|
|
680
|
+
let t = await it(r, await q());
|
|
681
|
+
e = new Set(t.filter((e) => e.status === "applied").map((e) => e.id));
|
|
682
|
+
} catch {
|
|
683
|
+
B.warn("migrate --dry-run: could not read the migration ledger — showing ALL as pending");
|
|
684
|
+
}
|
|
685
|
+
let t = r.filter((t) => !e.has(t.definition.id));
|
|
686
|
+
if (t.length === 0) return B.info("migrate --dry-run: nothing pending — every migration is applied"), 0;
|
|
687
|
+
B.info(`migrate --dry-run: ${t.length} pending (nothing applied)`);
|
|
688
|
+
for (let e of t) console.log(` pending ${e.definition.id}`);
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
let i = await nt(r, await q(), B);
|
|
692
|
+
return i.failed ? (process.stderr.write(at(i) + "\n"), 1) : (B.info("migrate complete", {
|
|
693
|
+
applied: i.applied.length,
|
|
694
|
+
skipped: i.skipped.length
|
|
695
|
+
}), 0);
|
|
696
|
+
}, xt = async (e) => {
|
|
697
|
+
let t = H(e), n = o(e, "--to"), r = await R(await V(t, $e), B);
|
|
698
|
+
if (r.length === 0) return B.info("no migrations found", { root: t }), 0;
|
|
699
|
+
let i = await rt(r, await q(), B, n ? { to: n } : {});
|
|
700
|
+
return i.failed ? (process.stderr.write(at(i) + "\n"), 1) : (B.info("rollback complete", { reverted: i.applied.length }), 0);
|
|
701
|
+
}, St = async (e) => {
|
|
702
|
+
let t = await R(await V(H(e), $e), B);
|
|
703
|
+
if (t.length === 0) return console.log("no migrations found"), 0;
|
|
704
|
+
let n = await it(t, await q());
|
|
705
|
+
for (let e of n) {
|
|
706
|
+
let t = e.status === "applied" ? "✓" : e.status === "failed" ? "✗" : e.status === "reverted" ? "↩" : "·", n = e.hashMatches ? "" : " (hash drift!)", r = e.appliedAt ? ` @ ${e.appliedAt.toISOString()}` : "";
|
|
707
|
+
console.log(` ${t} ${e.id} [${e.status}] ${e.name}${r}${n}`);
|
|
708
|
+
}
|
|
709
|
+
return 0;
|
|
710
|
+
}, Ct = async (e) => {
|
|
711
|
+
let t = (o(e, "--store") ?? process.env.DB_DIALECT ?? "postgres").toLowerCase();
|
|
712
|
+
if (t === "memory") return B.warn("voltro db seed: using in-memory store (explicit opt-in via --store memory / DB_DIALECT=memory)"), B.warn(" rows written by this run will NOT persist beyond the process. set DB_DIALECT=postgres or unset to seed the real DB."), {
|
|
713
|
+
store: new Ee(),
|
|
714
|
+
dispose: async () => {},
|
|
715
|
+
dialectId: "memory"
|
|
716
|
+
};
|
|
717
|
+
let n = await c(t);
|
|
718
|
+
B.info(`voltro db seed: target store=${n.id} — rows will persist`);
|
|
719
|
+
let r = await n.makeStore({
|
|
720
|
+
sqlLayer: n.makeSqlLayer(U()),
|
|
721
|
+
changeStrategy: "inline"
|
|
722
|
+
}), i = r;
|
|
723
|
+
return {
|
|
724
|
+
store: r,
|
|
725
|
+
dispose: async () => {
|
|
726
|
+
typeof i.close == "function" && await i.close(5e3);
|
|
727
|
+
},
|
|
728
|
+
dialectId: n.id
|
|
729
|
+
};
|
|
730
|
+
}, wt = async (e) => {
|
|
731
|
+
let t = H(e), n = o(e, "--id"), r = o(e, "--lifecycle"), i = await d(await V(t, y), B);
|
|
732
|
+
if (i.length === 0) return B.info("no seeds found", { root: t }), 0;
|
|
733
|
+
let { store: a, dispose: s, dialectId: c } = await Ct(e);
|
|
734
|
+
try {
|
|
735
|
+
return n ? +((await u(i, n, a, B)).status === "failed") : +((await f((r ? i.filter((e) => e.definition.lifecycle === r) : i.filter((e) => e.definition.lifecycle === "boot")).map((e) => ({
|
|
736
|
+
...e,
|
|
737
|
+
definition: {
|
|
738
|
+
...e.definition,
|
|
739
|
+
lifecycle: "boot"
|
|
740
|
+
}
|
|
741
|
+
})), a, B, { force: !0 })).filter((e) => e.status === "failed").length > 0);
|
|
742
|
+
} finally {
|
|
743
|
+
await s().catch((e) => {
|
|
744
|
+
B.warn("voltro db seed: store dispose failed", { error: String(e) });
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}, Tt = (e) => typeof e == "object" && !!e && "tableName" in e && "fields" in e && typeof e.tableName == "string", Et = async (e) => {
|
|
748
|
+
let t = await V(e, v), n = [];
|
|
749
|
+
for (let r of t) {
|
|
750
|
+
let t;
|
|
751
|
+
try {
|
|
752
|
+
t = await import(T(r).href);
|
|
753
|
+
} catch (t) {
|
|
754
|
+
B.warn("db: failed to import schema file", {
|
|
755
|
+
file: S(e, r),
|
|
756
|
+
error: String(t)
|
|
757
|
+
});
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
for (let e of Object.values(t)) Tt(e) && n.push(e);
|
|
761
|
+
}
|
|
762
|
+
return n;
|
|
763
|
+
}, Dt = (e) => {
|
|
764
|
+
let t = new Map(e.map((e) => [e.tableName, e]));
|
|
765
|
+
t.has("actors") || t.set("actors", _e);
|
|
766
|
+
let n = t.get("actors"), r = t.get("tenants");
|
|
767
|
+
return Ce({
|
|
768
|
+
actors: () => n,
|
|
769
|
+
...r === void 0 ? {} : { tenants: () => r }
|
|
770
|
+
}), [...t.values()];
|
|
771
|
+
}, Ot = (e) => {
|
|
772
|
+
let t = /* @__PURE__ */ new Map();
|
|
773
|
+
for (let n of e) {
|
|
774
|
+
let e = n.tableName;
|
|
775
|
+
e !== void 0 && !t.has(e) && t.set(e, n);
|
|
776
|
+
}
|
|
777
|
+
return [...t.values()];
|
|
778
|
+
}, kt = (e) => {
|
|
779
|
+
let t = ae(process.env.VOLTRO_DESTRUCTIVE_OK);
|
|
780
|
+
if (t === void 0 || !e.operations.filter((e) => e.blocked).every((e) => e.classification === "lossy")) return {
|
|
781
|
+
plan: e,
|
|
782
|
+
acknowledged: !1
|
|
783
|
+
};
|
|
784
|
+
let n = ge(e, t);
|
|
785
|
+
return {
|
|
786
|
+
plan: n,
|
|
787
|
+
acknowledged: !n.operations.some((e) => e.blocked)
|
|
788
|
+
};
|
|
789
|
+
}, J = async (e, t) => E.runPromise(t.pipe(E.provide(e))), Y = async (e) => {
|
|
790
|
+
try {
|
|
791
|
+
return await J(e, D.SqlClient.pipe(E.flatMap(ce)));
|
|
792
|
+
} catch (e) {
|
|
793
|
+
let t = String(e?.message ?? e);
|
|
794
|
+
if (vt(e) || /connection error/i.test(t)) {
|
|
795
|
+
let n = /* @__PURE__ */ Error(`${yt}\n\nOriginal error: ${t}`);
|
|
796
|
+
throw n.cause = e, n;
|
|
797
|
+
}
|
|
798
|
+
throw e;
|
|
799
|
+
}
|
|
800
|
+
}, At = {
|
|
801
|
+
safe: "\x1B[32m✓\x1B[0m",
|
|
802
|
+
"needs-default": "\x1B[32m⊕\x1B[0m",
|
|
803
|
+
"needs-backfill": "\x1B[33m⊕\x1B[0m",
|
|
804
|
+
"needs-rename-annotation": "\x1B[33m⟲\x1B[0m",
|
|
805
|
+
lossy: "\x1B[31m✗\x1B[0m",
|
|
806
|
+
"online-required": "\x1B[36m◷\x1B[0m",
|
|
807
|
+
"multi-step": "\x1B[35m⇉\x1B[0m"
|
|
808
|
+
}, jt = (e) => {
|
|
809
|
+
let t = e.op, n;
|
|
810
|
+
switch (t.kind) {
|
|
811
|
+
case "create-table":
|
|
812
|
+
n = `CREATE TABLE ${t.table} (${t.columns.length} cols)`;
|
|
813
|
+
break;
|
|
814
|
+
case "drop-table":
|
|
815
|
+
n = `DROP TABLE ${t.table}`;
|
|
816
|
+
break;
|
|
817
|
+
case "add-column":
|
|
818
|
+
n = `ALTER TABLE ${t.table} ADD COLUMN ${t.column.name} ${t.column.type}${t.column.nullable ? "" : " NOT NULL"}`;
|
|
819
|
+
break;
|
|
820
|
+
case "drop-column":
|
|
821
|
+
n = `ALTER TABLE ${t.table} DROP COLUMN ${t.column}`;
|
|
822
|
+
break;
|
|
823
|
+
case "rename-column":
|
|
824
|
+
n = `ALTER TABLE ${t.table} RENAME COLUMN ${t.from} TO ${t.to}`;
|
|
825
|
+
break;
|
|
826
|
+
case "alter-column-nullability":
|
|
827
|
+
n = `ALTER TABLE ${t.table} ALTER COLUMN ${t.column} ${t.toNullable ? "DROP NOT NULL" : "SET NOT NULL"}`;
|
|
828
|
+
break;
|
|
829
|
+
case "alter-column-type":
|
|
830
|
+
n = `ALTER TABLE ${t.table} ALTER COLUMN ${t.column} TYPE ${t.to}`;
|
|
831
|
+
break;
|
|
832
|
+
case "alter-column-default":
|
|
833
|
+
n = `ALTER TABLE ${t.table} ALTER COLUMN ${t.column} ${t.hasDefault ? `SET DEFAULT ${JSON.stringify(t.defaultValue)}` : "DROP DEFAULT"}`;
|
|
834
|
+
break;
|
|
835
|
+
case "add-index":
|
|
836
|
+
n = `CREATE INDEX ${t.index.name} ON ${t.table} (${t.index.columns.join(", ")})`;
|
|
837
|
+
break;
|
|
838
|
+
case "drop-index":
|
|
839
|
+
n = `DROP INDEX ${t.index}`;
|
|
840
|
+
break;
|
|
841
|
+
case "rename-index":
|
|
842
|
+
n = `ALTER INDEX ${t.from} RENAME TO ${t.to}`;
|
|
843
|
+
break;
|
|
844
|
+
case "rename-table":
|
|
845
|
+
n = `ALTER TABLE ${t.from} RENAME TO ${t.to}`;
|
|
846
|
+
break;
|
|
847
|
+
case "add-unique":
|
|
848
|
+
n = `ALTER TABLE ${t.table} ADD UNIQUE (${t.column})`;
|
|
849
|
+
break;
|
|
850
|
+
case "drop-unique":
|
|
851
|
+
n = `ALTER TABLE ${t.table} DROP UNIQUE (${t.column})`;
|
|
852
|
+
break;
|
|
853
|
+
case "add-unique-composite":
|
|
854
|
+
n = `ALTER TABLE ${t.table} ADD CONSTRAINT ${t.name} UNIQUE (${t.fields.join(", ")})`;
|
|
855
|
+
break;
|
|
856
|
+
case "drop-unique-composite":
|
|
857
|
+
n = `ALTER TABLE ${t.table} DROP CONSTRAINT ${t.name}`;
|
|
858
|
+
break;
|
|
859
|
+
case "add-foreign-key":
|
|
860
|
+
n = `ALTER TABLE ${t.table} ADD FK ${t.column} → ${t.targetTable}.${t.targetColumn}`;
|
|
861
|
+
break;
|
|
862
|
+
case "drop-foreign-key":
|
|
863
|
+
n = `ALTER TABLE ${t.table} DROP FK ${t.column}`;
|
|
864
|
+
break;
|
|
865
|
+
case "add-check":
|
|
866
|
+
n = `ALTER TABLE ${t.table} ADD CHECK (${t.column} IN (${t.values.join(", ")}))`;
|
|
867
|
+
break;
|
|
868
|
+
case "drop-check":
|
|
869
|
+
n = `ALTER TABLE ${t.table} DROP CHECK ${t.column}`;
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
let r = At[e.classification], i = e.reason ? ` [2m# ${e.reason}[0m` : "", a = e.blocked ? `\n [31m! ${e.blocked.fix}[0m` : "";
|
|
873
|
+
return ` ${r} ${n}${i}${a}`;
|
|
874
|
+
}, Mt = async (e, t) => {
|
|
875
|
+
let n = e.operations.filter((e) => e.classification === "needs-default" && e.op.kind === "add-column");
|
|
876
|
+
if (n.length === 0) return;
|
|
877
|
+
let r = [];
|
|
878
|
+
for (let e of n) {
|
|
879
|
+
let n = e.op, i = n.table, a = n.column?.name;
|
|
880
|
+
if (i === void 0 || a === void 0) continue;
|
|
881
|
+
let o = 0;
|
|
882
|
+
try {
|
|
883
|
+
let e = await J(t, D.SqlClient.pipe(E.flatMap((e) => e.unsafe(`SELECT COUNT(*) AS n FROM ${Se(i, W())}`))));
|
|
884
|
+
o = Number(Object.values(e[0] ?? { n: 0 })[0] ?? 0);
|
|
885
|
+
} catch {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
o !== 0 && r.push(` ${o.toLocaleString("en-US")} existing row(s) in ${i} will get the default for ${a}`);
|
|
889
|
+
}
|
|
890
|
+
if (r.length !== 0) {
|
|
891
|
+
console.log("\n existing rows that a default will fill:");
|
|
892
|
+
for (let e of r) console.log(e);
|
|
893
|
+
console.log(" (the shape is correct either way — this is the VALUE a state diff cannot know)");
|
|
894
|
+
}
|
|
895
|
+
}, Nt = async (e, t) => {
|
|
896
|
+
if (W() !== "postgres") return;
|
|
897
|
+
let n = ne, r;
|
|
898
|
+
try {
|
|
899
|
+
r = await J(t, D.SqlClient.pipe(E.flatMap((t) => p(t, e, n))));
|
|
900
|
+
} catch {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
let { drift: i, statements: a } = r;
|
|
904
|
+
if (a.length === 0) return;
|
|
905
|
+
B.info(i.missing.length + i.stale.length > 0 ? `db apply: installing change triggers on ${i.missing.length} table(s)` + (i.stale.length > 0 ? `, removing ${i.stale.length} stale one(s)` : "") + (i.functionOutdated ? ", replacing an outdated notify function" : "") : "db apply: replacing an outdated change-notify function (no table drift)");
|
|
906
|
+
let o = 0;
|
|
907
|
+
for (let e of a) await J(t, D.SqlClient.pipe(E.flatMap((t) => t.unsafe(e)))), o += 1;
|
|
908
|
+
B.info(`db apply: change triggers converged (${o} statement(s))`, {
|
|
909
|
+
installed: i.missing.length,
|
|
910
|
+
removed: i.stale.length,
|
|
911
|
+
functionReplaced: i.functionOutdated
|
|
912
|
+
});
|
|
913
|
+
}, X = (e) => {
|
|
914
|
+
let t = e.summary, n = e.operations.length;
|
|
915
|
+
if (console.log(`\nschema diff: ${n} operation${n === 1 ? "" : "s"}, ${t.blocked} blocked\n`), n === 0) {
|
|
916
|
+
console.log(" (schema is up to date)\n");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
for (let t of e.operations) console.log(jt(t));
|
|
920
|
+
console.log(), console.log(` safe: ${t.safe} needs-default: ${t.needsDefault} needs-backfill: ${t.needsBackfill}`), console.log(` rename: ${t.needsRenameAnnotation} lossy: ${t.lossy} blocked: ${t.blocked}`), console.log(` fingerprint: ${e.fromFingerprint.slice(0, 16)} → ${e.toFingerprint.slice(0, 16)}\n`), Pt(e);
|
|
921
|
+
}, Pt = (e) => {
|
|
922
|
+
let t = pe(e.operations);
|
|
923
|
+
if (t.length !== 0) {
|
|
924
|
+
console.log(` ⚠ ${t.length} operation(s) UNSAFE under a rolling deploy`), console.log(" (old + new instances overlap → old code breaks against the new schema):");
|
|
925
|
+
for (let e of t) console.log(` • ${e.op.op.kind}: ${e.reason}`), console.log(` → ${e.remedy}`);
|
|
926
|
+
console.log(" Deploy with a maintenance window / scale-to-zero, or split into"), console.log(" expand → deploy → contract. See docs: deployment/production-hardening.\n");
|
|
927
|
+
}
|
|
928
|
+
}, Ft = (e, t) => {
|
|
929
|
+
let n = k({
|
|
930
|
+
enabled: process.env.VOLTRO_ROLLING_DEPLOY === "1",
|
|
931
|
+
forced: t.includes("--force"),
|
|
932
|
+
unsafeCount: pe(e.operations).length
|
|
933
|
+
});
|
|
934
|
+
return n.refuse ? (B.error(`db apply: ${n.message}`), !0) : (n.warnForced && B.warn(`db apply: ${n.message}`), !1);
|
|
935
|
+
}, It = (e) => {
|
|
936
|
+
let t = o(e, "--online-after") ?? process.env.VOLTRO_ONLINE_THRESHOLD;
|
|
937
|
+
if (t === void 0) return;
|
|
938
|
+
let n = Number(t);
|
|
939
|
+
if (!Number.isInteger(n) || n < 0) throw Error(`--online-after / VOLTRO_ONLINE_THRESHOLD must be a non-negative integer, got "${t}"`);
|
|
940
|
+
return n;
|
|
941
|
+
}, Z = async (e) => {
|
|
942
|
+
let t = await Et(e);
|
|
943
|
+
if (t.length === 0) return;
|
|
944
|
+
let n = await h({ root: e });
|
|
945
|
+
return Dt(Ot([
|
|
946
|
+
...t,
|
|
947
|
+
...P,
|
|
948
|
+
...n
|
|
949
|
+
]));
|
|
950
|
+
}, Q = async (e, t, n) => {
|
|
951
|
+
let r;
|
|
952
|
+
try {
|
|
953
|
+
await J(n, _(await g(e), (e, t) => {
|
|
954
|
+
B.info(`db apply: ${e}`, t ?? {});
|
|
955
|
+
}));
|
|
956
|
+
} catch (e) {
|
|
957
|
+
r = e;
|
|
958
|
+
}
|
|
959
|
+
return await Nt(t, n), r === void 0 ? 0 : (B.error("db apply: a plugin migration FAILED — the schema was applied, the plugin steps were not", { error: r instanceof Error ? r.message : String(r) }), 1);
|
|
960
|
+
}, Lt = async (e, t) => {
|
|
961
|
+
let n = { close: async () => {} }, r = await V(e, y);
|
|
962
|
+
if (r.length === 0) return n;
|
|
963
|
+
let i = await d(r, B), a = i.filter((e) => e.definition.lifecycle === "onSchemaChange");
|
|
964
|
+
if (a.length === 0) return n;
|
|
965
|
+
let { store: o, dispose: s } = await Ct(t), c = m(i, o, B.child("seeds"));
|
|
966
|
+
return B.info(`db apply: ${a.length} onSchemaChange seed(s) armed for this apply`, { ids: a.map((e) => e.definition.id) }), { close: async () => {
|
|
967
|
+
c(), await s().catch((e) => {
|
|
968
|
+
B.warn("db apply: seed store dispose failed", { error: String(e) });
|
|
969
|
+
});
|
|
970
|
+
} };
|
|
971
|
+
}, Rt = async (e) => {
|
|
972
|
+
let t = It(e), n = H(e), r = await Z(n);
|
|
973
|
+
if (r === void 0) return B.warn("db plan: no schema files found", {
|
|
974
|
+
root: n,
|
|
975
|
+
hint: "looked for *.entity.ts / *.schema.ts / schema.ts"
|
|
976
|
+
}), 1;
|
|
977
|
+
let i = o(e, "--against");
|
|
978
|
+
if (i) {
|
|
979
|
+
let n = i, a = o(e, "--token") ?? process.env.VOLTRO_INSPECT_TOKEN, s = { accept: "application/json" };
|
|
980
|
+
a && (s.authorization = `Bearer ${a}`);
|
|
981
|
+
try {
|
|
982
|
+
let e = n.endsWith("/_voltro/inspect/migrations") ? n : `${n.replace(/\/+$/, "")}/_voltro/inspect/migrations`, i = await fetch(e, { headers: s });
|
|
983
|
+
if (!i.ok) return B.error(`db plan --against: remote returned ${i.status}`, { url: e }), 1;
|
|
984
|
+
let a = (await i.json()).drift?.liveSnapshot;
|
|
985
|
+
if (!a) return B.error("db plan --against: remote inspect payload missing drift.liveSnapshot", { url: e }), 1;
|
|
986
|
+
let o = M({
|
|
987
|
+
declared: r,
|
|
988
|
+
live: a,
|
|
989
|
+
dialect: W(),
|
|
990
|
+
...j(),
|
|
991
|
+
...t === void 0 ? {} : { onlineAfter: t }
|
|
992
|
+
});
|
|
993
|
+
return console.log(`\n[--against ${e}] — diff vs remote live schema:\n`), X(o), o.summary.blocked > 0 ? 2 : 0;
|
|
994
|
+
} catch (e) {
|
|
995
|
+
return B.error(`db plan --against: ${e.message}`), 1;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
let a = M({
|
|
999
|
+
declared: r,
|
|
1000
|
+
live: await Y(await q()),
|
|
1001
|
+
dialect: W(),
|
|
1002
|
+
...j(),
|
|
1003
|
+
...t === void 0 ? {} : { onlineAfter: t }
|
|
1004
|
+
});
|
|
1005
|
+
return e.includes("--json") ? (ee(1, `${JSON.stringify(a, null, 2)}\n`), a.summary.blocked > 0 ? 2 : 0) : (X(a), a.summary.blocked > 0 ? 2 : 0);
|
|
1006
|
+
}, $ = t, zt = async (e, t, n) => {
|
|
1007
|
+
let r;
|
|
1008
|
+
try {
|
|
1009
|
+
let t = await w.readFile(C(e), "utf8");
|
|
1010
|
+
r = JSON.parse(t);
|
|
1011
|
+
} catch (t) {
|
|
1012
|
+
return B.error(`db apply --plan: cannot read/parse plan file '${e}': ${t.message}`), 1;
|
|
1013
|
+
}
|
|
1014
|
+
if (!Array.isArray(r?.operations) || typeof r?.fromFingerprint != "string" || typeof r?.toFingerprint != "string") return B.error(`db apply --plan: '${e}' is not a valid plan file (expected the output of \`voltro db plan --json\`).`), 1;
|
|
1015
|
+
let i = await Z(n);
|
|
1016
|
+
if (i === void 0) return B.warn("db apply --plan: no schema files found", { root: n }), 1;
|
|
1017
|
+
let a = await q(), s = await J(a, D.SqlClient.pipe(E.flatMap((e) => le(e, n)))).catch(() => []);
|
|
1018
|
+
if (s.length > 0) return B.error(`db apply --plan: refusing — ${s.length} pending file-based migration(s).`), console.error(` ${s.join("\n ")}`), console.error(""), console.error(" These perform the changes a state diff cannot infer, so they change the shape"), console.error(" this plan was computed against. Apply them and regenerate the plan:"), console.error(""), console.error(" voltro db files ."), console.error(" voltro db plan --json > plan.json"), console.error(" voltro db apply --plan plan.json"), 1;
|
|
1019
|
+
let c = M({
|
|
1020
|
+
declared: i,
|
|
1021
|
+
live: await Y(a),
|
|
1022
|
+
dialect: W(),
|
|
1023
|
+
...j()
|
|
1024
|
+
});
|
|
1025
|
+
if (c.fromFingerprint !== r.fromFingerprint) return B.error("db apply --plan: refusing — the live schema has drifted since this plan was generated."), console.error(` plan.from=${r.fromFingerprint.slice(0, 16)} live=${c.fromFingerprint.slice(0, 16)}`), console.error(" Regenerate against the current live schema: `voltro db plan --json > plan.json`"), 2;
|
|
1026
|
+
if (c.toFingerprint !== r.toFingerprint) return B.error("db apply --plan: refusing — the declared schema differs from what this plan targets."), console.error(` plan.to=${r.toFingerprint.slice(0, 16)} declared=${c.toFingerprint.slice(0, 16)}`), console.error(" The schema files changed since the plan was generated. Regenerate the plan."), 2;
|
|
1027
|
+
let l = kt(c), u = l.plan;
|
|
1028
|
+
if (u.summary.blocked > 0 && !l.acknowledged) return X(u), B.error(`db apply --plan: refusing — ${c.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (B.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), B.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
|
|
1029
|
+
if (l.acknowledged && B.warn("db apply --plan: proceeding with acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)"), Ft(u, t)) return 2;
|
|
1030
|
+
if (u.operations.length === 0) {
|
|
1031
|
+
let e = await J(a, D.SqlClient.pipe(E.flatMap((e) => ue(e, u, {
|
|
1032
|
+
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:plan",
|
|
1033
|
+
environment: $(),
|
|
1034
|
+
source: "file"
|
|
1035
|
+
}))));
|
|
1036
|
+
return B.info(e === "recorded" ? `db apply --plan: schema is up to date — no DDL to apply, recorded fingerprint=${u.toFingerprint.slice(0, 16)} (change triggers are reconciled separately below)` : "db apply --plan: schema is up to date — no DDL to apply (change triggers are reconciled separately below)"), Q(n, i, a);
|
|
1037
|
+
}
|
|
1038
|
+
X(u);
|
|
1039
|
+
let d = o(t, "--note"), f = await J(a, D.SqlClient.pipe(E.flatMap((e) => re(e, u, {
|
|
1040
|
+
declared: i,
|
|
1041
|
+
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:plan",
|
|
1042
|
+
environment: $(),
|
|
1043
|
+
source: "file",
|
|
1044
|
+
...d ? { notes: d } : {},
|
|
1045
|
+
replan: () => E.promise(() => Y(a)).pipe(E.map((e) => M({
|
|
1046
|
+
declared: i,
|
|
1047
|
+
live: e,
|
|
1048
|
+
dialect: W(),
|
|
1049
|
+
...j()
|
|
1050
|
+
})))
|
|
1051
|
+
}))));
|
|
1052
|
+
return B.info(`db apply --plan: applied ${f.appliedOps} op(s) in ${f.durationMs}ms`, {
|
|
1053
|
+
id: f.id,
|
|
1054
|
+
fingerprint: f.fingerprint.slice(0, 16)
|
|
1055
|
+
}), Q(n, i, a);
|
|
1056
|
+
}, Bt = async (e) => {
|
|
1057
|
+
let t = H(e), n = await Lt(t, e);
|
|
1058
|
+
try {
|
|
1059
|
+
return await Vt(e, t);
|
|
1060
|
+
} finally {
|
|
1061
|
+
await n.close();
|
|
1062
|
+
}
|
|
1063
|
+
}, Vt = async (t, n) => {
|
|
1064
|
+
let r = It(t), i = o(t, "--plan");
|
|
1065
|
+
if (i) return zt(i, t, n);
|
|
1066
|
+
if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return B.error(`voltro db apply: refusing — auto-apply on prod is not allowed.${e()}`), console.error("In production, schema changes go through `voltro db plan --json > plan.json`"), console.error("then `voltro db apply --plan plan.json` as an explicit step in your deploy pipeline."), 3;
|
|
1067
|
+
let a = await Z(n);
|
|
1068
|
+
if (a === void 0) return B.warn("db apply: no schema files found", { root: n }), 1;
|
|
1069
|
+
let s = await q(), c = await rn(n, s);
|
|
1070
|
+
if ("failed" in c) return B.error("db apply: a file-based migration FAILED — the schema diff was NOT run"), console.error(c.failed), console.error("Fix the migration and re-run. `voltro db files` runs them alone."), 1;
|
|
1071
|
+
let l = M({
|
|
1072
|
+
declared: a,
|
|
1073
|
+
live: await Y(s),
|
|
1074
|
+
dialect: W(),
|
|
1075
|
+
...j(),
|
|
1076
|
+
...r === void 0 ? {} : { onlineAfter: r }
|
|
1077
|
+
});
|
|
1078
|
+
X(l), await Mt(l, s);
|
|
1079
|
+
let u = kt(l), d = u.plan;
|
|
1080
|
+
if (d.summary.blocked > 0 && !u.acknowledged) return B.error(`db apply: refusing — ${d.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (B.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), B.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
|
|
1081
|
+
if (u.acknowledged && B.warn(`db apply: proceeding with ${l.summary.blocked} acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)`), Ft(d, t)) return 2;
|
|
1082
|
+
if (d.operations.length === 0) {
|
|
1083
|
+
B.info("db apply: schema is up to date — no DDL to apply (change triggers are reconciled separately below)");
|
|
1084
|
+
let e = await J(s, D.SqlClient.pipe(E.flatMap((e) => ue(e, d, {
|
|
1085
|
+
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:apply",
|
|
1086
|
+
environment: $(),
|
|
1087
|
+
source: "auto-diff"
|
|
1088
|
+
}))));
|
|
1089
|
+
return e === "recorded" && B.info(`db apply: recorded fingerprint=${d.toFingerprint.slice(0, 16)} (0 operations)`), await Qt(s), o(t, "--note") !== void 0 && e !== "recorded" && (B.warn("db apply: --note was ignored — this schema is already the latest recorded one, so there is no new history row to attach it to."), console.error("To record that the CURRENT live schema is the accepted baseline, use `voltro db drift --accept`.")), Q(n, a, s);
|
|
1090
|
+
}
|
|
1091
|
+
let f = o(t, "--note"), p = await J(s, D.SqlClient.pipe(E.flatMap((e) => re(e, d, {
|
|
1092
|
+
declared: a,
|
|
1093
|
+
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user",
|
|
1094
|
+
environment: $(),
|
|
1095
|
+
source: "auto-diff",
|
|
1096
|
+
...f ? { notes: f } : {},
|
|
1097
|
+
replan: () => E.promise(() => Y(s)).pipe(E.map((e) => M({
|
|
1098
|
+
declared: a,
|
|
1099
|
+
live: e,
|
|
1100
|
+
dialect: W(),
|
|
1101
|
+
...j(),
|
|
1102
|
+
...r === void 0 ? {} : { onlineAfter: r }
|
|
1103
|
+
})))
|
|
1104
|
+
}))));
|
|
1105
|
+
return B.info(`db apply: applied ${p.appliedOps} op(s) in ${p.durationMs}ms`, {
|
|
1106
|
+
id: p.id,
|
|
1107
|
+
fingerprint: p.fingerprint.slice(0, 16)
|
|
1108
|
+
}), Q(n, a, s);
|
|
1109
|
+
}, Ht = (e) => {
|
|
1110
|
+
let t = [];
|
|
1111
|
+
for (let n of e.tables) for (let e of n.columns) {
|
|
1112
|
+
let r = e.references;
|
|
1113
|
+
r !== void 0 && t.push({
|
|
1114
|
+
table: n.name,
|
|
1115
|
+
column: e.name,
|
|
1116
|
+
targetTable: r.table,
|
|
1117
|
+
targetColumn: r.column,
|
|
1118
|
+
...r.onDelete === void 0 ? {} : { onDelete: r.onDelete }
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
return t;
|
|
1122
|
+
}, Ut = (e) => {
|
|
1123
|
+
let t = [];
|
|
1124
|
+
for (let n of e.tables) for (let e of n.indexes) e.expression !== !0 && e.name !== `${n.name}_pkey` && e.columns.length !== 0 && t.push({
|
|
1125
|
+
table: n.name,
|
|
1126
|
+
columns: e.columns,
|
|
1127
|
+
unique: e.unique,
|
|
1128
|
+
name: e.name
|
|
1129
|
+
});
|
|
1130
|
+
return t;
|
|
1131
|
+
}, Wt = (e, t) => {
|
|
1132
|
+
let n = (e) => JSON.stringify(e.op), r = new Set(e.operations.map(n)), i = new Set(t.operations.map(n)), a = [...t.operations.filter((e) => !r.has(n(e))), ...e.operations.filter((e) => !i.has(n(e)))];
|
|
1133
|
+
return {
|
|
1134
|
+
...t,
|
|
1135
|
+
operations: a,
|
|
1136
|
+
summary: {
|
|
1137
|
+
...t.summary,
|
|
1138
|
+
blocked: 0
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
}, Gt = (e) => {
|
|
1142
|
+
if (e === void 0) return "empty";
|
|
1143
|
+
if (e === "empty" || e === "copy") return e;
|
|
1144
|
+
throw e === "fresh" ? new n("voltro db branch: --seed fresh is not implemented — the branch lifecycle has no executor step that runs boot seeds, so the branch would come up EMPTY while this reported it as seeded. Use --seed copy (the parent's rows) or --seed empty (schema only).") : new n(`voltro db branch: --seed must be one of empty|copy, got "${e}"`);
|
|
1145
|
+
}, Kt = (e) => {
|
|
1146
|
+
if (e !== void 0) {
|
|
1147
|
+
if (e === "namespace" || e === "neon-cow") return e;
|
|
1148
|
+
throw new n(`voltro db branch: --prefer must be one of namespace|neon-cow, got "${e}"`);
|
|
1149
|
+
}
|
|
1150
|
+
}, qt = async (e) => {
|
|
1151
|
+
let t = H(e), r = Gt(o(e, "--seed")), i = Kt(o(e, "--prefer")), a = e.includes("--keep"), s = e.includes("--json"), l = It(e), u = W();
|
|
1152
|
+
if (u !== "postgres") return B.error(`voltro db branch: not supported on '${u}'.`), console.error(" The branch PLAN is dialect-agnostic; the shipped namespace EXECUTOR is Postgres-only"), console.error(" (CREATE SCHEMA / CREATE TABLE … LIKE … INCLUDING ALL / \"-quoted identifiers)."), console.error(" MySQL, MariaDB, SQLite and SQL Server need their own BranchExecutor — see"), console.error(" makeNamespaceBranchExecutor in @voltro/database."), 1;
|
|
1153
|
+
let d = await Z(t);
|
|
1154
|
+
if (d === void 0) return B.warn("db branch: no schema files found", {
|
|
1155
|
+
root: t,
|
|
1156
|
+
hint: "looked for *.entity.ts / *.schema.ts / schema.ts"
|
|
1157
|
+
}), 1;
|
|
1158
|
+
let f = U(), p = we({
|
|
1159
|
+
seed: r,
|
|
1160
|
+
...f.url === void 0 ? {} : { dbUrl: f.url },
|
|
1161
|
+
...i === void 0 ? {} : { prefer: i }
|
|
1162
|
+
});
|
|
1163
|
+
if (p === "neon-cow") return B.error("voltro db branch: this connection resolves to the Neon copy-on-write mechanism, which the CLI cannot execute."), console.error(" A Neon CoW branch is a single call to Neon's branch REST API and needs a Neon API token;"), console.error(" the framework ships the PLAN, and the cloud control plane executes it."), console.error(" Re-run with `--prefer namespace` to rehearse on a schema-per-branch copy instead"), console.error(` (portable, no vendor API — it copies ${r === "copy" ? "every row" : "the schema only"}).`), 1;
|
|
1164
|
+
let m = f.schema ?? "public", h = o(e, "--name") ?? b(t), g = o(e, "--pr"), _ = g === void 0 ? 0 : Number(g);
|
|
1165
|
+
if (!Number.isInteger(_) || _ < 0) throw new n(`voltro db branch: --pr must be a non-negative integer, got "${String(g)}"`);
|
|
1166
|
+
let v = ve(h, _), y = await c(u), x = y.makeSqlLayer(f), S = y.makeSqlLayer({
|
|
1167
|
+
...f,
|
|
1168
|
+
schema: v
|
|
1169
|
+
}), C = await Y(x), w = M({
|
|
1170
|
+
declared: d,
|
|
1171
|
+
live: C,
|
|
1172
|
+
dialect: u,
|
|
1173
|
+
...j(),
|
|
1174
|
+
...l === void 0 ? {} : { onlineAfter: l }
|
|
1175
|
+
}), T = C.tables.map((e) => e.name), te = Ht(C), ne = Ut(C), O = be({ run: (e) => J(x, D.SqlClient.pipe(E.flatMap((t) => t.unsafe(e)), E.asVoid)) }), k = async () => M({
|
|
1176
|
+
declared: d,
|
|
1177
|
+
live: await Y(S),
|
|
1178
|
+
dialect: u,
|
|
1179
|
+
...j(),
|
|
1180
|
+
...l === void 0 ? {} : { onlineAfter: l }
|
|
1181
|
+
}), A = await de({
|
|
1182
|
+
branchId: v,
|
|
1183
|
+
mechanism: p,
|
|
1184
|
+
branchedTables: T.length,
|
|
1185
|
+
replayedForeignKeys: te.length,
|
|
1186
|
+
keep: a,
|
|
1187
|
+
log: (e) => {
|
|
1188
|
+
s || B.info(`db branch: ${e}`);
|
|
1189
|
+
},
|
|
1190
|
+
provision: async () => {
|
|
1191
|
+
await xe({
|
|
1192
|
+
appSlug: h,
|
|
1193
|
+
prNumber: _,
|
|
1194
|
+
seed: r,
|
|
1195
|
+
tableNames: T,
|
|
1196
|
+
parentNamespace: m,
|
|
1197
|
+
foreignKeys: te,
|
|
1198
|
+
indexNames: ne,
|
|
1199
|
+
prefer: "namespace",
|
|
1200
|
+
...f.url === void 0 ? {} : { dbUrl: f.url }
|
|
1201
|
+
}, O);
|
|
1202
|
+
},
|
|
1203
|
+
fidelityPlan: async () => Wt(w, await k()),
|
|
1204
|
+
plan: k,
|
|
1205
|
+
apply: async (e) => {
|
|
1206
|
+
await J(S, D.SqlClient.pipe(E.flatMap((t) => re(t, e, {
|
|
1207
|
+
declared: d,
|
|
1208
|
+
appliedBy: "cli:db-branch",
|
|
1209
|
+
environment: $(),
|
|
1210
|
+
source: "auto-diff",
|
|
1211
|
+
notes: `migration rehearsal on branch ${v}`,
|
|
1212
|
+
replan: () => E.promise(k)
|
|
1213
|
+
}))));
|
|
1214
|
+
},
|
|
1215
|
+
replan: async () => await k(),
|
|
1216
|
+
teardown: async () => {
|
|
1217
|
+
await Te(v, "namespace", O);
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
return s ? ee(1, `${JSON.stringify(A, null, 2)}\n`) : console.log(`\n${se(A)}\n`), ie(A);
|
|
1221
|
+
}, Jt = async (e) => {
|
|
1222
|
+
let t = Math.max(1, Number(o(e, "--limit") ?? "20")), n = await J(await q(), D.SqlClient.pipe(E.flatMap((e) => e`
|
|
1223
|
+
SELECT ${e("id")}, ${e("fingerprint")}, ${e("operations")},
|
|
1224
|
+
${e("appliedBy")}, ${e("environment")}, ${e("source")},
|
|
1225
|
+
${e("durationMs")}, ${e("notes")}, ${e("appliedAt")}
|
|
1226
|
+
FROM ${e("_voltro_migration_plans")}
|
|
1227
|
+
ORDER BY ${e("appliedAt")} DESC
|
|
1228
|
+
LIMIT ${t}
|
|
1229
|
+
`)));
|
|
1230
|
+
if (n.length === 0) return B.info("db plans: no planner-based migrations applied yet"), console.log("Run `voltro db apply` after a schema change to start a plan history."), 0;
|
|
1231
|
+
console.log(`\n${n.length} plan${n.length === 1 ? "" : "s"} (newest first):\n`);
|
|
1232
|
+
for (let e of n) {
|
|
1233
|
+
let t = 0;
|
|
1234
|
+
try {
|
|
1235
|
+
t = JSON.parse($t(e.operations)).length;
|
|
1236
|
+
} catch {}
|
|
1237
|
+
console.log(` ${e.id} fp=${N(e.fingerprint)} env=${e.environment} src=${e.source} ${t} op(s) ${e.durationMs}ms ${en(e.appliedAt)} by=${e.appliedBy}`), e.notes && console.log(` [2mnote: ${e.notes}[0m`);
|
|
1238
|
+
}
|
|
1239
|
+
return console.log(), 0;
|
|
1240
|
+
}, Yt = async (e) => {
|
|
1241
|
+
let t = e[0];
|
|
1242
|
+
if (!t) return console.error("voltro db restore-snapshot <plan-id>"), console.error(" Renames soft-dropped columns/tables from a plan back to their original names."), console.error(" Requires the plan to have been applied with VOLTRO_SOFT_DROP=1."), 2;
|
|
1243
|
+
let n = await q(), r = await J(n, D.SqlClient.pipe(E.flatMap((e) => e`SELECT ${e("id")}, ${e("operations")}
|
|
1244
|
+
FROM ${e("_voltro_migration_plans")} WHERE ${e("id")} = ${t}`)));
|
|
1245
|
+
if (r.length === 0) return console.error(`restore-snapshot: plan '${t}' not found`), 1;
|
|
1246
|
+
let i;
|
|
1247
|
+
try {
|
|
1248
|
+
i = JSON.parse($t(r[0].operations));
|
|
1249
|
+
} catch {
|
|
1250
|
+
return console.error(`restore-snapshot: plan '${t}' has corrupted operations JSON`), 1;
|
|
1251
|
+
}
|
|
1252
|
+
let a = [];
|
|
1253
|
+
for (let e of i) e.op?.kind === "drop-column" && e.op.table && e.op.column && a.push({
|
|
1254
|
+
table: e.op.table,
|
|
1255
|
+
column: e.op.column
|
|
1256
|
+
});
|
|
1257
|
+
let o = [];
|
|
1258
|
+
for (let e of i) e.op?.kind === "drop-table" && e.op.table && o.push({ table: e.op.table });
|
|
1259
|
+
if (a.length === 0 && o.length === 0) return console.log(`restore-snapshot: plan '${t}' has no drop-column / drop-table ops to restore`), 0;
|
|
1260
|
+
let s = 0;
|
|
1261
|
+
for (let e of a) {
|
|
1262
|
+
let t = await J(n, D.SqlClient.pipe(E.flatMap((t) => t`
|
|
1263
|
+
SELECT column_name FROM information_schema.columns
|
|
1264
|
+
WHERE ${t.unsafe(K(W()))}
|
|
1265
|
+
AND table_name = ${e.table}
|
|
1266
|
+
AND column_name LIKE ${e.column + "__dropped_%"}
|
|
1267
|
+
ORDER BY column_name DESC LIMIT 1
|
|
1268
|
+
`)));
|
|
1269
|
+
if (t.length === 0) {
|
|
1270
|
+
console.warn(`restore-snapshot: no snapshot found for ${e.table}.${e.column} — was the plan applied with VOLTRO_SOFT_DROP=1?`);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
let r = t[0].column_name;
|
|
1274
|
+
await J(n, D.SqlClient.pipe(E.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table)} RENAME COLUMN ${G(r)} TO ${G(e.column)}`)))), s++, console.log(` ↩ restored ${e.table}.${e.column} (from ${r})`);
|
|
1275
|
+
}
|
|
1276
|
+
let c = 0;
|
|
1277
|
+
for (let e of o) {
|
|
1278
|
+
let t = await J(n, D.SqlClient.pipe(E.flatMap((t) => t`
|
|
1279
|
+
SELECT table_name FROM information_schema.tables
|
|
1280
|
+
WHERE ${t.unsafe(K(W()))}
|
|
1281
|
+
AND table_name LIKE ${e.table + "__dropped_%"}
|
|
1282
|
+
ORDER BY table_name DESC LIMIT 1
|
|
1283
|
+
`)));
|
|
1284
|
+
if (t.length === 0) {
|
|
1285
|
+
console.warn(`restore-snapshot: no snapshot found for table ${e.table} — was the plan applied with VOLTRO_SOFT_DROP=1?`);
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
let r = t[0].table_name;
|
|
1289
|
+
await J(n, D.SqlClient.pipe(E.flatMap((t) => t.unsafe(`ALTER TABLE ${G(r)} RENAME TO ${G(e.table)}`)))), c++, console.log(` ↩ restored table ${e.table} (from ${r})`);
|
|
1290
|
+
}
|
|
1291
|
+
return console.log(`\n✓ restored ${s} column(s) + ${c} table(s) from plan '${t}'\n`), 0;
|
|
1292
|
+
}, Xt = async (e) => {
|
|
1293
|
+
let t = o(e, "--before");
|
|
1294
|
+
if (!t) return console.error("voltro db gc-snapshots --before <date> [--dry-run]"), console.error(" Permanently drops soft-dropped columns/tables (<name>__dropped_<ts>) older than <date>."), console.error(" <date> is ISO — e.g. 2026-01-01 or 2026-01-01T00:00:00Z. NOT reversible."), 2;
|
|
1295
|
+
let n = t.replace(/[^0-9]/g, "");
|
|
1296
|
+
if (n.length < 8) return console.error(`gc-snapshots: --before must be at least a full date (YYYY-MM-DD), got "${t}"`), 2;
|
|
1297
|
+
let r = (n + "00000000000000").slice(0, 14), i = e.includes("--dry-run"), a = await q(), s = await J(a, D.SqlClient.pipe(E.flatMap((e) => e`
|
|
1298
|
+
SELECT table_name, column_name FROM information_schema.columns
|
|
1299
|
+
WHERE ${e.unsafe(K(W()))} AND column_name LIKE ${"%__dropped_%"}
|
|
1300
|
+
`))), c = await J(a, D.SqlClient.pipe(E.flatMap((e) => e`
|
|
1301
|
+
SELECT table_name FROM information_schema.tables
|
|
1302
|
+
WHERE ${e.unsafe(K(W()))} AND table_name LIKE ${"%__dropped_%"}
|
|
1303
|
+
`))), l = (e) => {
|
|
1304
|
+
let t = /__dropped_(\d{14})$/.exec(e);
|
|
1305
|
+
return t ? t[1] : null;
|
|
1306
|
+
}, u = 0;
|
|
1307
|
+
for (let e of s) {
|
|
1308
|
+
let t = l(e.column_name);
|
|
1309
|
+
t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop column" : "dropping column"} ${e.table_name}.${e.column_name}`), i || await J(a, D.SqlClient.pipe(E.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table_name)} DROP COLUMN ${G(e.column_name)}`)))), u++);
|
|
1310
|
+
}
|
|
1311
|
+
for (let e of c) {
|
|
1312
|
+
let t = l(e.table_name);
|
|
1313
|
+
t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop table" : "dropping table"} ${e.table_name}`), i || await J(a, D.SqlClient.pipe(E.flatMap((t) => t.unsafe(`DROP TABLE ${G(e.table_name)}`)))), u++);
|
|
1314
|
+
}
|
|
1315
|
+
return console.log(`\n${i ? `[dry-run] ${u} snapshot(s) would be dropped` : `✓ dropped ${u} snapshot(s)`} (older than ${r}).\n`), 0;
|
|
1316
|
+
}, Zt = async (e) => {
|
|
1317
|
+
let t = o(e, "--before");
|
|
1318
|
+
if (!t) return console.error("voltro db squash --before <iso-date> [--note \"<text>\"]"), 2;
|
|
1319
|
+
let n = t;
|
|
1320
|
+
if (Number.isNaN(new Date(n).getTime())) return console.error(`voltro db squash: --before '${n}' is not a valid ISO date`), 2;
|
|
1321
|
+
let r = o(e, "--note"), i = H(e), a = await Z(i);
|
|
1322
|
+
if (a === void 0) return B.warn("db squash: no schema files found", { root: i }), 1;
|
|
1323
|
+
let s = oe(A(a)), c = await q();
|
|
1324
|
+
try {
|
|
1325
|
+
let e = await J(c, he({
|
|
1326
|
+
before: n,
|
|
1327
|
+
fingerprint: s,
|
|
1328
|
+
appliedBy: process.env.USER ?? "cli",
|
|
1329
|
+
environment: $(),
|
|
1330
|
+
...r === void 0 ? {} : { note: r }
|
|
1331
|
+
}));
|
|
1332
|
+
return console.log(`\n✓ squashed ${e.squashedCount} migration plan(s)\n snapshot id: ${e.snapshotId}\n fingerprint: ${N(e.snapshotFingerprint)}\n`), 0;
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
return console.error(`squash refused: ${e.message}`), 1;
|
|
1335
|
+
}
|
|
1336
|
+
}, Qt = async (e) => {
|
|
1337
|
+
try {
|
|
1338
|
+
let t = oe(await Y(e));
|
|
1339
|
+
await J(e, D.SqlClient.pipe(E.flatMap((e) => e`
|
|
1340
|
+
UPDATE ${e("_voltro_migration_plans")}
|
|
1341
|
+
SET ${e("liveFingerprint")} = ${t}
|
|
1342
|
+
WHERE ${e("id")} = (
|
|
1343
|
+
SELECT ${e("id")} FROM (
|
|
1344
|
+
SELECT ${e("id")} FROM ${e("_voltro_migration_plans")}
|
|
1345
|
+
ORDER BY ${e("appliedAt")} DESC LIMIT 1
|
|
1346
|
+
) AS ${e("newest")}
|
|
1347
|
+
)
|
|
1348
|
+
AND ${e("liveFingerprint")} IS NULL
|
|
1349
|
+
`)));
|
|
1350
|
+
} catch {}
|
|
1351
|
+
}, $t = (e) => typeof e == "string" ? e : e == null ? "" : JSON.stringify(e), en = (e) => e instanceof Date ? e.toISOString() : String(e ?? ""), tn = (e) => e === void 0 ? {
|
|
1352
|
+
accept: !1,
|
|
1353
|
+
reason: "no-declared-schema"
|
|
1354
|
+
} : e > 0 ? {
|
|
1355
|
+
accept: !1,
|
|
1356
|
+
reason: "pending"
|
|
1357
|
+
} : {
|
|
1358
|
+
accept: !0,
|
|
1359
|
+
reason: "converged"
|
|
1360
|
+
}, nn = async (e) => {
|
|
1361
|
+
let t = e.includes("--accept"), n = await q(), r = await J(n, D.SqlClient.pipe(E.flatMap((e) => e`
|
|
1362
|
+
SELECT * FROM ${e("_voltro_migration_plans")}
|
|
1363
|
+
ORDER BY ${e("appliedAt")} DESC
|
|
1364
|
+
LIMIT 1
|
|
1365
|
+
`))), i = await Y(n), a = oe(i);
|
|
1366
|
+
if (r.length === 0) return B.info("db drift: no _voltro_migration_plans rows yet"), console.log(`live user-schema fingerprint: ${N(a)}`), console.log("No baseline to compare against. Run `voltro db apply` to lock in the current state."), 0;
|
|
1367
|
+
let o = r[0];
|
|
1368
|
+
if (o.liveFingerprint === null || o.liveFingerprint === void 0) return B.warn("db drift: no drift baseline recorded yet — cannot compare"), console.log(), console.log(` last applied: ${o.id} at ${en(o.appliedAt)}`), console.log(` live now: ${N(a)}`), console.log(), console.log("That row was written before the framework recorded a live-schema baseline,"), console.log("and the value it does carry is the DECLARED snapshot's hash — not comparable"), console.log("with a live one. Run `voltro db apply` once to establish the baseline (a no-op"), console.log("apply backfills it too); use `voltro db plan` meanwhile, which compares declared"), console.log("against live directly."), console.log(), console.log("Exit code 3 — NOT 0. \"Did not compare\" is not \"clean\", and a gate on the exit"), console.log("code has to be able to tell them apart."), 3;
|
|
1369
|
+
if (o.liveFingerprint === a) return B.info(`db drift: live matches last applied (${N(a)})`), 0;
|
|
1370
|
+
let s = await (async () => {
|
|
1371
|
+
try {
|
|
1372
|
+
let t = await Z(H(e));
|
|
1373
|
+
return t === void 0 ? void 0 : M({
|
|
1374
|
+
declared: t,
|
|
1375
|
+
live: i,
|
|
1376
|
+
dialect: W(),
|
|
1377
|
+
...j()
|
|
1378
|
+
}).operations;
|
|
1379
|
+
} catch {
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
})();
|
|
1383
|
+
if (t) {
|
|
1384
|
+
let e = tn(s?.length);
|
|
1385
|
+
if (e.reason === "no-declared-schema") return B.error("db drift --accept: no schema files found — cannot verify the live schema is current."), 1;
|
|
1386
|
+
if (!e.accept && s !== void 0) {
|
|
1387
|
+
B.error(`db drift --accept: refusing — ${s.length} operation(s) still pending against your declared schema.`), console.log();
|
|
1388
|
+
for (let e of s.slice(0, 10)) console.log(` - ${e.op.kind} ${"table" in e.op ? `[${e.op.table}]` : ""}`);
|
|
1389
|
+
return s.length > 10 && console.log(` … and ${s.length - 10} more`), console.log(), console.log("`--accept` records the CURRENT live schema as the applied baseline. It is for a"), console.log("change that already landed out of band and left the schema correct — not for one"), console.log("that has not landed. Run `voltro db apply` instead; it writes a real baseline."), 4;
|
|
1390
|
+
}
|
|
1391
|
+
return await J(n, D.SqlClient.pipe(E.flatMap((e) => e`
|
|
1392
|
+
UPDATE ${e("_voltro_migration_plans")}
|
|
1393
|
+
SET ${e("liveFingerprint")} = ${a}
|
|
1394
|
+
WHERE ${e("id")} = (
|
|
1395
|
+
SELECT ${e("id")} FROM (
|
|
1396
|
+
SELECT ${e("id")} FROM ${e("_voltro_migration_plans")}
|
|
1397
|
+
ORDER BY ${e("appliedAt")} DESC LIMIT 1
|
|
1398
|
+
) AS ${e("newest")}
|
|
1399
|
+
)
|
|
1400
|
+
`))), B.info(`db drift: accepted the live schema as the baseline (${N(a)})`), console.log(), console.log("`voltro db plan` reports 0 operations against this schema, so the live state already"), console.log("matches your declaration — only the ledger did not know. It does now."), console.log(), console.log("No history entry was invented: the latest row's baseline was updated in place,"), console.log("because no migration ran."), 0;
|
|
1401
|
+
}
|
|
1402
|
+
return B.error("db drift: live schema DIVERGED from last applied state"), console.log(), console.log(` baseline: ${N(o.liveFingerprint)} at ${en(o.appliedAt)} (${o.id})`), console.log(` live now: ${N(a)}`), console.log(), console.log("Something changed the live schema after the last apply. This command can see"), console.log("THAT it changed, not what or who — the fingerprints are hashes, not a diff."), console.log(), s === void 0 ? (console.log("No schema files found here, so the declared side could not be compared."), console.log("Run `voltro db plan` from your project root for the actual difference."), 4) : s.length === 0 ? (console.log("Your DECLARED schema is already satisfied — `voltro db plan` reports 0 operations"), console.log("against this database. So the live schema is not wrong, only unrecorded: something"), console.log("applied a change without going through the planner (a hand-run ALTER, a DBA"), console.log("window, a restored dump), or it touched a table your code does not declare."), console.log(), console.log("If that was deliberate and the schema is right, record it:"), console.log(), console.log(" voltro db drift --accept"), console.log(), console.log("It updates the latest ledger row's baseline to the live schema and invents no"), console.log("history entry. Drift then measures from here."), 4) : (console.log(`\`voltro db plan\` reports ${s.length} operation(s) still pending against your`), console.log("declared schema, so this is not merely an unrecorded change — run it for the diff,"), console.log("then `voltro db apply`, which writes a fresh baseline of its own."), 4);
|
|
1403
|
+
}, rn = async (e, t) => {
|
|
1404
|
+
let n = $();
|
|
1405
|
+
try {
|
|
1406
|
+
let r = await J(t, D.SqlClient.pipe(E.flatMap((t) => me(t, {
|
|
1407
|
+
projectRoot: e,
|
|
1408
|
+
env: n,
|
|
1409
|
+
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
|
|
1410
|
+
}))));
|
|
1411
|
+
return r.applied.length > 0 && B.info(`db apply: applied ${r.applied.length} pending file-based migration(s) first`, { applied: r.applied.map((e) => `${e.id} (${e.durationMs}ms)`) }), { applied: r.applied.length };
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
return { failed: e };
|
|
1414
|
+
}
|
|
1415
|
+
}, an = async (e) => {
|
|
1416
|
+
let t = H(e), n = await q(), r = $();
|
|
1417
|
+
await J(n, O(P, W()));
|
|
1418
|
+
let i = await J(n, D.SqlClient.pipe(E.flatMap((e) => me(e, {
|
|
1419
|
+
projectRoot: t,
|
|
1420
|
+
env: r,
|
|
1421
|
+
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
|
|
1422
|
+
}))));
|
|
1423
|
+
return i.applied.length === 0 ? (B.info(`db files: no pending file-based migrations (${i.skipped.length} already applied)`), 0) : (B.info(`db files: applied ${i.applied.length} migration(s)`, {
|
|
1424
|
+
applied: i.applied.map((e) => `${e.id} (${e.durationMs}ms)`),
|
|
1425
|
+
skipped: i.skipped.length
|
|
1426
|
+
}), 0);
|
|
1427
|
+
}, on = async (t) => {
|
|
1428
|
+
let n = H(t), r = s(t, _t);
|
|
1429
|
+
if (!r) return B.error("voltro db rollback-file <migration-id>"), 2;
|
|
1430
|
+
if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return B.error("voltro db rollback-file: refusing on prod — schema rollback runs as an explicit deploy step." + e()), 3;
|
|
1431
|
+
let i = await J(await q(), D.SqlClient.pipe(E.flatMap((e) => fe(e, {
|
|
1432
|
+
projectRoot: n,
|
|
1433
|
+
id: r
|
|
1434
|
+
}))));
|
|
1435
|
+
return B.info(`db rollback-file: rolled back ${i.id} in ${i.durationMs}ms`), 0;
|
|
1436
|
+
}, sn = () => {
|
|
1437
|
+
let e = /* @__PURE__ */ new Date();
|
|
1438
|
+
return [
|
|
1439
|
+
e.getUTCFullYear(),
|
|
1440
|
+
String(e.getUTCMonth() + 1).padStart(2, "0"),
|
|
1441
|
+
String(e.getUTCDate()).padStart(2, "0"),
|
|
1442
|
+
String(e.getUTCHours()).padStart(2, "0"),
|
|
1443
|
+
String(e.getUTCMinutes()).padStart(2, "0"),
|
|
1444
|
+
String(e.getUTCSeconds()).padStart(2, "0")
|
|
1445
|
+
].join("");
|
|
1446
|
+
}, cn = (e) => e.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration", ln = (e, t) => `import { defineMigration, SqlClient } from '@voltro/database'
|
|
1447
|
+
import { Effect } from 'effect'
|
|
1448
|
+
|
|
1449
|
+
// A hand-written migration. Reach for this when the DECLARATIVE differ can't
|
|
1450
|
+
// express the change — a data backfill, a value re-encode, a table rename that
|
|
1451
|
+
// must preserve rows. For plain schema shape changes (add a column/table/index)
|
|
1452
|
+
// you usually DON'T need this: edit the entity + run \`voltro db apply\`, which
|
|
1453
|
+
// diffs your declared schema against the live DB automatically.
|
|
1454
|
+
//
|
|
1455
|
+
// \`voltro db migrate\` applies every pending *.migration.ts in timestamp order
|
|
1456
|
+
// and records each in \`_voltro_migrations\`; \`voltro db rollback\` runs \`undo\`.
|
|
1457
|
+
export default defineMigration({
|
|
1458
|
+
id: '${e}',
|
|
1459
|
+
name: '${t}',
|
|
1460
|
+
steps: ({ step }) => [
|
|
1461
|
+
step('${t}', {
|
|
1462
|
+
// Forward migration. Runs inside a transaction; the SqlClient is provided.
|
|
1463
|
+
apply: Effect.gen(function* () {
|
|
1464
|
+
const sql = yield* SqlClient.SqlClient
|
|
1465
|
+
// TODO: your change, e.g.
|
|
1466
|
+
// yield* sql\`ALTER TABLE widgets ADD COLUMN archived boolean NOT NULL DEFAULT false\`
|
|
1467
|
+
}),
|
|
1468
|
+
// The inverse — what \`voltro db rollback\` runs to undo the step. Keep it
|
|
1469
|
+
// an exact reverse so a rollback restores the prior state.
|
|
1470
|
+
undo: Effect.gen(function* () {
|
|
1471
|
+
const sql = yield* SqlClient.SqlClient
|
|
1472
|
+
// TODO: reverse the apply above, e.g.
|
|
1473
|
+
// yield* sql\`ALTER TABLE widgets DROP COLUMN archived\`
|
|
1474
|
+
}),
|
|
1475
|
+
}),
|
|
1476
|
+
],
|
|
1477
|
+
})
|
|
1478
|
+
`, un = {
|
|
1479
|
+
plan: Rt,
|
|
1480
|
+
apply: Bt,
|
|
1481
|
+
branch: qt,
|
|
1482
|
+
plans: Jt,
|
|
1483
|
+
drift: nn,
|
|
1484
|
+
files: an,
|
|
1485
|
+
"rollback-file": on,
|
|
1486
|
+
migrate: bt,
|
|
1487
|
+
rollback: xt,
|
|
1488
|
+
status: St,
|
|
1489
|
+
seed: wt,
|
|
1490
|
+
generate: async (e) => {
|
|
1491
|
+
let { value: t, rest: n } = i(e, "--root"), { value: r, rest: a } = i(n, "--name"), o = t ? C(t) : process.cwd(), c = cn(r ?? s(a, []) ?? "migration"), l = `${sn()}_${c}`, u = x(o, "migrations"), d = x(u, `${l}.migration.ts`);
|
|
1492
|
+
return await w.mkdir(u, { recursive: !0 }), await w.writeFile(d, ln(l, c), "utf8"), B.info(`created ${S(o, d)}`), console.error("Fill in the apply/undo steps, then run `voltro db migrate` (rollback with `voltro db rollback`)."), console.error("For a plain schema shape change you may not need this — `voltro db apply` diffs declared vs live."), 0;
|
|
1493
|
+
},
|
|
1494
|
+
squash: Zt,
|
|
1495
|
+
"restore-snapshot": Yt,
|
|
1496
|
+
"gc-snapshots": Xt,
|
|
1497
|
+
adopt: async (e) => {
|
|
1498
|
+
let t = (t) => {
|
|
1499
|
+
let n = e.indexOf(`--${t}`);
|
|
1500
|
+
return n === -1 ? void 0 : e[n + 1];
|
|
1501
|
+
}, n = t("from"), i = t("into"), a = t("map");
|
|
1502
|
+
if (n === void 0 || i === void 0 || a === void 0) return console.error("voltro db adopt --from <table> --into <table> --map <file.ts> [--apply] [--keep-source]"), console.error(" Dry run unless --apply. The map file default-exports { map, leaveUnset? }."), 1;
|
|
1503
|
+
let o = (await r(C(process.cwd(), a))).default;
|
|
1504
|
+
if (o?.map === void 0) return console.error(`voltro db adopt: ${a} must default-export { map: { <targetColumn>: <source> } }`), 1;
|
|
1505
|
+
let s = {
|
|
1506
|
+
from: n,
|
|
1507
|
+
into: i,
|
|
1508
|
+
map: o.map,
|
|
1509
|
+
...o.leaveUnset ? { leaveUnset: o.leaveUnset } : {}
|
|
1510
|
+
}, c = W(), l = await q(), u = await J(l, D.SqlClient.pipe(E.flatMap(ce))), d = (e) => u.tables.find((t) => t.name === e)?.columns.map((e) => ({
|
|
1511
|
+
name: e.name,
|
|
1512
|
+
nullable: e.nullable,
|
|
1513
|
+
hasDefault: e.defaultValue !== void 0 && e.defaultValue !== null
|
|
1514
|
+
})), f = d(n), p = d(i);
|
|
1515
|
+
if (f === void 0) return console.error(`voltro db adopt: '${n}' does not exist in the database`), 1;
|
|
1516
|
+
if (p === void 0) return console.error(`voltro db adopt: '${i}' does not exist in the database — boot once so the plugin's table is created`), 1;
|
|
1517
|
+
let m = async (e) => await J(l, D.SqlClient.pipe(E.flatMap((t) => t.unsafe(e)))), h = async (e) => Number(Object.values((await m(`SELECT COUNT(*) AS n FROM "${e}"`))[0] ?? { n: 0 })[0] ?? 0), g = Ae(s, f, p, await h(i));
|
|
1518
|
+
for (let e of Me(s, g, await h(n))) console.log(e);
|
|
1519
|
+
if (g.fatal) return 1;
|
|
1520
|
+
if (!e.includes("--apply")) return console.log(""), console.log(" (dry run — nothing written. Re-run with --apply.)"), 0;
|
|
1521
|
+
let _ = await Le(s, g, c, "id", void 0, m, { keepSource: e.includes("--keep-source") });
|
|
1522
|
+
return console.log(""), console.log(` copied ${_.copied}/${_.sourceRows} row(s)`), console.log(` snapshot: ${_.snapshotTable} — restore from here, then drop it yourself`), _.verified ? (console.log(_.sourceDropped ? ` dropped ${n}` : ` ${n} left in place (--keep-source)`), 0) : (console.error(" ✗ counts DIFFER — the source was NOT dropped. Compare the two tables before doing anything else."), 1);
|
|
1523
|
+
},
|
|
1524
|
+
"scan-credentials": async (e) => {
|
|
1525
|
+
let t = W(), r = [];
|
|
1526
|
+
for (let t = 0; t < e.length; t += 1) {
|
|
1527
|
+
if (e[t] !== "--table") continue;
|
|
1528
|
+
let i = e[t + 1];
|
|
1529
|
+
if (i === void 0) throw new n("voltro db scan-credentials: --table needs a value");
|
|
1530
|
+
let [a, o = "subject"] = i.split(":");
|
|
1531
|
+
r.push({
|
|
1532
|
+
table: a,
|
|
1533
|
+
column: o
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
let i = [...Re, ...r], a = await J(await q(), D.SqlClient.pipe(E.flatMap((e) => E.promise(() => Xe(e, t, i, G)))));
|
|
1537
|
+
return console.log(Ze(a, t)), Qe(a);
|
|
1538
|
+
},
|
|
1539
|
+
"encrypt-column": async (e) => {
|
|
1540
|
+
let t = W(), r = e.includes("--dry-run"), i = e.includes("--yes"), o = (t) => {
|
|
1541
|
+
let n = e.indexOf(t);
|
|
1542
|
+
return n >= 0 ? e[n + 1] : void 0;
|
|
1543
|
+
}, s = Math.max(1, Number(o("--batch") ?? 500)), c = o("--key-env") ?? "FIELD_ENCRYPTION_KEY", l = process.env[c];
|
|
1544
|
+
if (!l) throw new n(`voltro db encrypt-column: ${c} is not set. It must be the SAME key your app runs with (\`governancePlugin({ fieldEncryption: { key } })\`) — encrypting with a different one produces rows the app cannot read. Use --key-env to name a different variable.`);
|
|
1545
|
+
let u = De(l), d = a(e, _t).filter((e) => e.includes(".")).map(ot);
|
|
1546
|
+
if (d.length === 0) throw new n("voltro db encrypt-column: name at least one <table>.<column>");
|
|
1547
|
+
let f = await q(), p = [];
|
|
1548
|
+
for (let e of d) {
|
|
1549
|
+
let a = G(e.table), o = G(e.column), l = await J(f, D.SqlClient.pipe(E.flatMap((r) => E.promise(async () => {
|
|
1550
|
+
let i = await E.runPromise(r.unsafe(`SELECT character_maximum_length AS n FROM information_schema.columns\n WHERE ${K(t)} AND table_name = '${e.table}' AND column_name = '${e.column}'`));
|
|
1551
|
+
if (i.length === 0) throw new n(`voltro db encrypt-column: ${e.table}.${e.column} does not exist on this database`);
|
|
1552
|
+
let a = Object.values(i[0])[0];
|
|
1553
|
+
return a == null ? void 0 : Number(a);
|
|
1554
|
+
})))), d = await J(f, D.SqlClient.pipe(E.flatMap((e) => E.promise(async () => await E.runPromise(e.unsafe(`SELECT ${G("id")} AS id, ${o} AS v FROM ${a}`)))))), m = 0;
|
|
1555
|
+
for (let e of d) typeof e.v != "string" || F(e.v) || (m = Math.max(m, ft(e.v)));
|
|
1556
|
+
let h = ct(m, l);
|
|
1557
|
+
if (!h.fits) return console.error(` ✗ ${lt(e, h)}`), 1;
|
|
1558
|
+
if (pt(d.map((e) => e.v).find((e) => typeof e == "string" && F(e)), u) === !1) return console.error(` ✗ ${mt(e, c)}`), 1;
|
|
1559
|
+
let g = d.filter((t) => dt(t.v, u, e) !== void 0).length;
|
|
1560
|
+
if (!r && !i) return console.error(` ✗ ${e.table}.${e.column}: ${g} row(s) would be rewritten in place. Re-run with --yes, or --dry-run to see the counts without writing.`), 1;
|
|
1561
|
+
let _ = 0, v = 0, y = 0, b = [];
|
|
1562
|
+
for (let t of d) {
|
|
1563
|
+
if (t.v === null || t.v === void 0 || t.v === "") {
|
|
1564
|
+
v += 1;
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
if (typeof t.v == "string" && F(t.v)) {
|
|
1568
|
+
_ += 1;
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
let n = dt(t.v, u, e);
|
|
1572
|
+
if (n === void 0) {
|
|
1573
|
+
v += 1;
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
b.push({
|
|
1577
|
+
id: t.id,
|
|
1578
|
+
value: n
|
|
1579
|
+
}), y += 1;
|
|
1580
|
+
}
|
|
1581
|
+
if (!r && b.length > 0) for (let t = 0; t < b.length; t += s) {
|
|
1582
|
+
let n = b.slice(t, t + s);
|
|
1583
|
+
await J(f, D.SqlClient.pipe(E.flatMap((t) => E.promise(async () => {
|
|
1584
|
+
for (let r of n) await E.runPromise(t`UPDATE ${t(e.table)} SET ${t(e.column)} = ${r.value} WHERE ${t("id")} = ${r.id}`);
|
|
1585
|
+
})))), B.info(`encrypt-column ${e.table}.${e.column}: ${Math.min(t + n.length, b.length)}/${b.length}`);
|
|
1586
|
+
}
|
|
1587
|
+
p.push({
|
|
1588
|
+
table: e.table,
|
|
1589
|
+
column: e.column,
|
|
1590
|
+
scanned: d.length,
|
|
1591
|
+
alreadyEncrypted: _,
|
|
1592
|
+
empty: v,
|
|
1593
|
+
encrypted: y
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
console.log(`field encryption · dialect ${t}${r ? " · DRY RUN, nothing written" : ""}`), console.log("");
|
|
1597
|
+
for (let e of p) console.log(ut(e, r));
|
|
1598
|
+
return r || console.log(ht(d)), 0;
|
|
1599
|
+
}
|
|
1600
|
+
}, dn = async (e) => {
|
|
1601
|
+
let [t, ...n] = e;
|
|
1602
|
+
if (!t) {
|
|
1603
|
+
console.error("voltro db <subcommand> [args]"), console.error("subcommands:");
|
|
1604
|
+
for (let e of Object.keys(un)) console.error(` ${e}`);
|
|
1605
|
+
return 1;
|
|
1606
|
+
}
|
|
1607
|
+
let r = un[t];
|
|
1608
|
+
if (!r) return console.error(`voltro db: unknown subcommand '${t}'`), 2;
|
|
1609
|
+
(process.env.DB_DIRECT_URL !== void 0 || process.env.DB_MIGRATE_URL !== void 0) && B.info("using a DIRECT (non-pooler) connection for the db-command path (DB_DIRECT_URL/DB_MIGRATE_URL set)");
|
|
1610
|
+
let i = (e) => {
|
|
1611
|
+
vt(e) ? B.error(yt) : console.error(e), process.exit(1);
|
|
1612
|
+
};
|
|
1613
|
+
process.on("uncaughtException", i);
|
|
1614
|
+
try {
|
|
1615
|
+
return await r(n);
|
|
1616
|
+
} finally {
|
|
1617
|
+
process.off("uncaughtException", i);
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
//#endregion
|
|
1621
|
+
export { Dt as a, tn as i, U as n, vt as o, Et as r, dn as s, yt as t };
|