@voltro/cli 0.33.0 → 0.35.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 +1968 -0
- 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-B8aoJvuw.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
- package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
- package/dist/appGraph-zuMGKVYX.js +2 -0
- 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-CD8K4XOr.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Ct9xkTrS.js +232 -0
- package/dist/checkCommand-DKpDLlqu.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-D5wsCmA_.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-C4YoQIc2.js +30 -0
- package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-BE8E7zF3.js +816 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-D7SgZGaN.js +25 -0
- package/dist/dataCommand-BhYwDgg-.js +537 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/dbCommand-DS4b97Is.js +2 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
- package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
- package/dist/dev-D2BikO7a.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-CM4Ch9C7.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
- package/dist/dormancyCommand-QewYug_s.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
- package/dist/envCommand-CCGPRQY1.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-CHsLCtDf.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
- package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
- package/dist/generateCommand-DbgcUpGw.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-DwOgK1t6.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
- package/dist/inspect-P4pxoMaV.js +2 -0
- package/dist/inspectCmd-EHFZ9yYu.js +224 -0
- package/dist/inspectFetch-EMuhTG_9.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-D36xK7Zu.js +260 -0
- package/dist/manifestBuild-COkJoyAr.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-BV7I-ZHZ.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/probeCommand-CZfaaUOZ.js +122 -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-CH3eUiMw.js +91 -0
- package/dist/scheduleCmd-DQRu6BZC.js +149 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
- package/dist/seedRunner-ZmLSqNe2.js +333 -0
- package/dist/serveCommand-CbDHU6l-.js +2 -0
- package/dist/serveCommand-iwlUBNS1.js +1766 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-BgN62boB.js +3 -0
- package/dist/start-T4VesWiM.js +1087 -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-DStmCJPi.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-BlMZhWgO.js +2 -0
- package/dist/updateCommand-x0pI_x-B.js +585 -0
- package/dist/webDev-BcykISYQ2.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
- package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
- package/package.json +209 -17
- 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 +23 -6
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +313 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +853 -50
- package/templates/agent-docs/data.md +608 -12
- package/templates/agent-docs/database/migrations.md +223 -25
- package/templates/agent-docs/database/misc.md +156 -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 +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +284 -25
- package/templates/agent-docs/introduction.md +88 -17
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +188 -42
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +247 -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 +32 -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 +162 -8
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/storage.md +11 -0
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +152 -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 +27 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- 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 +98 -136
- package/templates/agent-docs/workflows.md +231 -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-C-x9YacA.js +0 -2
- package/dist/appGraph-CvQCte0z.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
- 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-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -41,6 +41,29 @@ Verify in the boot log:
|
|
|
41
41
|
That's it. Reads now flow to the replica unless RYW pins them back.
|
|
42
42
|
Writes always go to primary.
|
|
43
43
|
|
|
44
|
+
## Replica pools take the primary's connection settings
|
|
45
|
+
|
|
46
|
+
A replica is a **connection pool in the same process**, and it is configured by
|
|
47
|
+
the same environment variables the primary reads — `DB_MAX_CONNECTIONS`,
|
|
48
|
+
`PG_SSL`, `DB_SCHEMA`, `DB_STATEMENT_TIMEOUT_MS`, `DB_ACQUIRE_TIMEOUT_MS`. The
|
|
49
|
+
replica URL decides only WHICH server it talks to.
|
|
50
|
+
|
|
51
|
+
That matters for two reasons an operator has to plan for:
|
|
52
|
+
|
|
53
|
+
- **Connection budget.** `DB_REPLICA_URLS` with two entries means this process
|
|
54
|
+
opens **three** pools of `DB_MAX_CONNECTIONS` each, not one. The boot line
|
|
55
|
+
says so:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
db pool: max=10 per replica (DB_MAX_CONNECTIONS) → max × 3 = 30 per pod × 4 replicas = up to 120 connections.
|
|
59
|
+
This process opens 3 pools of that size — 1 primary + 2 read replica(s) (DB_REPLICA_URLS), each with its own DB_MAX_CONNECTIONS.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **TLS.** `PG_SSL=require` now applies to replica connections as well. A
|
|
63
|
+
`?sslmode=require` in the replica URL still works and still wins over the
|
|
64
|
+
driver default, but an explicit `PG_SSL` outranks it — so the TLS decision is
|
|
65
|
+
made once, for every connection the process opens.
|
|
66
|
+
|
|
44
67
|
## Read-your-writes (RYW)
|
|
45
68
|
|
|
46
69
|
After a mutation commits, the subject who wrote is **pinned to
|
|
@@ -319,6 +342,43 @@ will NOT reach clients on other replicas. Add @voltro/plugin-broadcast (Redis /
|
|
|
319
342
|
|
|
320
343
|
When BOTH a native path and the broadcast plugin are wired (e.g. postgres + broadcast), both stay active — the own-origin skip dedups, so there's no double-emit. The native path is the primary; the bus is harmless redundancy.
|
|
321
344
|
|
|
345
|
+
## sqlite and the memory store behind replicas
|
|
346
|
+
|
|
347
|
+
Neither dialect has a cross-instance path, and neither can get one: a local
|
|
348
|
+
database file and an in-process store are, by construction, **this process's**.
|
|
349
|
+
Putting several replicas in front of one is not a reactivity gap — the replicas
|
|
350
|
+
do not share a database at all, so each one is also reading its own data.
|
|
351
|
+
|
|
352
|
+
On a laptop that is the correct, normal configuration, which is why the framework
|
|
353
|
+
said nothing about it for a long time. It now warns when it can see evidence of
|
|
354
|
+
an **orchestrator**:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
[voltro:serve] this app runs on 'sqlite' — a local database file — but POD_IP is set
|
|
358
|
+
(Kubernetes), which means several replicas. Neither dialect has ANY cross-instance
|
|
359
|
+
change capture: a client connected to replica A never sees a write made on replica B,
|
|
360
|
+
for every table, and each replica is also reading its OWN data…
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The evidence is one of `REPLICA_COUNT > 1`, `KUBERNETES_SERVICE_HOST`, `POD_IP`,
|
|
364
|
+
`POD_NAME`, `FLY_ALLOC_ID`, `FLY_MACHINE_ID`, `ECS_CONTAINER_METADATA_URI[_V4]`,
|
|
365
|
+
`K_REVISION`, `CONTAINER_APP_REPLICA_NAME` or `RENDER_INSTANCE_ID`. `HOSTNAME`,
|
|
366
|
+
`NODE_ENV` and `PORT` are deliberately **not** evidence — every single-instance
|
|
367
|
+
container sets those too, and a warning that fires on a laptop gets filtered out
|
|
368
|
+
before it reaches the deployment where it is true.
|
|
369
|
+
|
|
370
|
+
Two ways to silence it, and they are not equivalent:
|
|
371
|
+
|
|
372
|
+
- `REPLICA_COUNT=1` — you are telling the framework there is exactly one process.
|
|
373
|
+
This is the honest one, and it is the only positive evidence *against* that
|
|
374
|
+
exists, so it outranks every platform signal.
|
|
375
|
+
- Declaring `@voltro/plugin-broadcast` with a real provider — not because a bus
|
|
376
|
+
fixes it (the replicas still have separate databases), but because declaring
|
|
377
|
+
one means you have already thought about the question.
|
|
378
|
+
|
|
379
|
+
The real fix is postgres or mysql/mariadb: a shared database with a native
|
|
380
|
+
cross-instance change path.
|
|
381
|
+
|
|
322
382
|
## Where it lives in the codebase
|
|
323
383
|
|
|
324
384
|
- `voltro/packages/plugin-broadcast` — the plugin, the `BroadcastProvider` contract, the redis / nats / memory providers, `attachBroadcastBus`.
|
|
@@ -373,6 +373,7 @@ import { TableValidationFailed } from '@voltro/runtime'
|
|
|
373
373
|
|
|
374
374
|
export const createUser = defineMutation({
|
|
375
375
|
name: 'users.create',
|
|
376
|
+
guards: [{ scope: 'users:write' }],
|
|
376
377
|
input: Schema.Struct({ email: Schema.String }),
|
|
377
378
|
output: Schema.Struct({ id: Schema.String }),
|
|
378
379
|
target: { table: 'users', op: 'insert' },
|
|
@@ -74,21 +74,68 @@ table. If you want one tenant's rows, say so in your own predicate.
|
|
|
74
74
|
|---|---|---|
|
|
75
75
|
| `boot` | On every `voltro dev` boot, **only if the fingerprint changed** | — |
|
|
76
76
|
| `manual` | Only via `voltro db seed --id <name>` or the dashboard | — |
|
|
77
|
-
| `onTenantCreate` |
|
|
78
|
-
| `onSchemaChange` |
|
|
79
|
-
| `cron` |
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
| `onTenantCreate` | Every time a tenant namespace is provisioned, **into that namespace** | a store with namespace isolation |
|
|
78
|
+
| `onSchemaChange` | After a schema apply that touched a watched table (`voltro dev`'s boot auto-migrate, `voltro db apply`, `voltro migrate`) | `watchedTables` |
|
|
79
|
+
| `cron` | On the cron expression, through the coordinated scheduler — one firing fleet-wide | `cron` |
|
|
80
|
+
|
|
81
|
+
### `onTenantCreate`
|
|
82
|
+
|
|
83
|
+
Fires from `provisionTenantNamespace`, after the namespace DDL lands and before
|
|
84
|
+
provisioning resolves — so a caller that awaits it gets a tenant whose tables
|
|
85
|
+
**and** reference data exist, or an error. Three properties worth knowing:
|
|
86
|
+
|
|
87
|
+
- **Steps run scoped to the new namespace**, never the shared tables. A store
|
|
88
|
+
without namespace isolation (sqlite, a single-schema deployment) makes the run
|
|
89
|
+
**refuse** rather than fall back — the fallback would write one tenant's
|
|
90
|
+
fixture into every tenant's data. On a single-namespace deployment, declare
|
|
91
|
+
the seed `lifecycle: 'boot'` instead.
|
|
92
|
+
- **No fingerprint skip.** A new namespace has none of the data, whatever
|
|
93
|
+
another tenant's run recorded. The ledger row is keyed `<seedId>@<namespace>`,
|
|
94
|
+
so N tenants produce N rows in `_voltro_seeds`.
|
|
95
|
+
- **A failure fails provisioning.** Unlike a boot seed, which logs and lets the
|
|
96
|
+
server come up, a failed tenant-create seed rejects the provisioning call — a
|
|
97
|
+
tenant whose namespace exists and whose data does not, reported as success, is
|
|
98
|
+
worse than a loud error. Re-run provisioning (it is idempotent) or
|
|
99
|
+
`voltro db seed --id <name>` once the cause is fixed.
|
|
100
|
+
|
|
101
|
+
### `onSchemaChange`
|
|
102
|
+
|
|
103
|
+
Fires from the migration applier, so it covers every path that applies a schema:
|
|
104
|
+
`voltro dev`'s boot auto-migrate, `voltro db apply` (bare and `--plan`) and
|
|
105
|
+
`voltro migrate`. Every seed whose `watchedTables` intersect the tables the apply
|
|
106
|
+
actually changed runs once, after the apply.
|
|
107
|
+
|
|
108
|
+
Three properties worth knowing:
|
|
109
|
+
|
|
110
|
+
- **Strictly post-apply.** It runs after the audit row is written, so on a
|
|
111
|
+
transactional dialect the DDL has already committed and the seed talks to its
|
|
112
|
+
own `DataStore` rather than the migration's connection.
|
|
113
|
+
- **It cannot fail the migration.** The schema landed; a fixture that throws is
|
|
114
|
+
logged and recorded in `_voltro_seeds`. A successful apply is never reported as
|
|
115
|
+
failed because reference data did not load.
|
|
116
|
+
- **`voltro serve` does not run it** — for the same reason it does not run boot
|
|
117
|
+
seeds. Serve never applies a schema, so there is no schema change for it to
|
|
118
|
+
react to; the pre-deploy `voltro db apply` is where it happens.
|
|
119
|
+
|
|
120
|
+
> This seam was **declared and never called** until 0.34.0: the hook installed,
|
|
121
|
+
> the seeds were discovered, listed and ledgered, and nothing ran. If you
|
|
122
|
+
> declared an `onSchemaChange` seed before that release, expect it to fire on
|
|
123
|
+
> your next apply.
|
|
124
|
+
|
|
125
|
+
### `cron`
|
|
126
|
+
|
|
127
|
+
`cron` seeds are projected into real schedules — the same coordinated scheduler
|
|
128
|
+
`*.cron.tsx` uses, so one firing happens fleet-wide instead of one per replica.
|
|
129
|
+
Both `voltro dev` and `voltro serve` register them at boot and log the schedule
|
|
130
|
+
names they created.
|
|
131
|
+
|
|
132
|
+
The schedule is named `seed:<id>`, which is also the name its runs appear under
|
|
133
|
+
in `_voltro_schedule_runs` and in the dashboard — distinct from a `*.cron.tsx`
|
|
134
|
+
namespace, so a schedule and a seed may share an id without colliding.
|
|
135
|
+
|
|
136
|
+
A cron firing runs the seed **unconditionally**. The fingerprint skip that makes
|
|
137
|
+
a `boot` seed cheap does not apply here: the trigger is the clock, not a change
|
|
138
|
+
in the source.
|
|
92
139
|
|
|
93
140
|
```ts
|
|
94
141
|
defineSeed({
|
|
@@ -96,11 +143,24 @@ defineSeed({
|
|
|
96
143
|
name: 'Rebuild search index nightly',
|
|
97
144
|
lifecycle: 'cron',
|
|
98
145
|
cron: '0 3 * * *', // required for lifecycle: 'cron'
|
|
146
|
+
timezone: 'Europe/Berlin', // optional; defaults to an explicit 'UTC'
|
|
99
147
|
steps: ({ step }) => [ /* … */ ],
|
|
100
148
|
})
|
|
101
149
|
```
|
|
102
150
|
|
|
103
|
-
`
|
|
151
|
+
`timezone` is defaulted rather than required (unlike `defineSchedule`'s, where
|
|
152
|
+
it is mandatory) — reference data rarely cares about a local wall clock. The
|
|
153
|
+
default is an explicit `'UTC'`, never the container's clock.
|
|
154
|
+
|
|
155
|
+
`defineSeed` validates at definition time: `cron` lifecycle without a `cron` field throws, `onSchemaChange` without `watchedTables` throws, and a seed with zero steps throws. The cron EXPRESSION is validated at boot, so a typo fails the boot naming the seed rather than never firing.
|
|
156
|
+
|
|
157
|
+
> **An app with any `*.seed.ts` gets the two schedule ledger tables**
|
|
158
|
+
> (`_voltro_schedule_runs`, `_voltro_schedule_claims`), even without a cron seed.
|
|
159
|
+
> Whether a seed is a cron seed is a field INSIDE the file, and `voltro migrate`
|
|
160
|
+
> never imports your modules — so the table set is decided from the FILENAME, and
|
|
161
|
+
> it has to be decided identically by `voltro dev`, `voltro serve`, `voltro db
|
|
162
|
+
> apply` and `voltro migrate` or the schema fingerprint diverges. Two empty ledger
|
|
163
|
+
> tables is the price of that agreement.
|
|
104
164
|
|
|
105
165
|
## Steps
|
|
106
166
|
|
|
@@ -138,6 +198,7 @@ voltro db seed --store memory # explicit opt-in: in-memory smoke run
|
|
|
138
198
|
|
|
139
199
|
- **Reference data** (must exist in every environment): `lifecycle: 'boot'`, idempotent via `upsertByUnique`. Safe in production.
|
|
140
200
|
- **Demo data** (populate a fresh dashboard): `lifecycle: 'boot'` in dev, or `lifecycle: 'manual'` so it's operator-triggered and never auto-runs in prod.
|
|
201
|
+
- **Per-tenant starter data** (a new tenant's default categories, roles, settings): `lifecycle: 'onTenantCreate'`, which runs scoped to the tenant's own namespace as it is provisioned.
|
|
141
202
|
|
|
142
203
|
See [migrations](/docs/database/migrations) for schema changes — seeds populate data, migrations shape the tables.
|
|
143
204
|
|
|
@@ -237,6 +298,49 @@ It applies to the **runtime query path only**. Migrations (`voltro db apply`) ru
|
|
|
237
298
|
|
|
238
299
|
**Wired for postgres today** (the default dialect), where it maps to the server-side `statement_timeout` — a real, server-enforced cancel (SQLSTATE `57014`), not a client-side disconnect that leaves the query running. Other dialects accept the variable but currently ignore it, and the reasons are honest rather than incidental: `@effect/sql-mssql` exposes only a connection-establishment timeout, not a per-request one; MySQL/MariaDB's `max_execution_time` bounds `SELECT`s only (writes stay unbounded), which would be a misleading half-guarantee; and SQLite is in-process with a single connection, so there is no pool to protect. Unset (or any non-postgres dialect) = no timeout.
|
|
239
300
|
|
|
301
|
+
### Acquire timeout — bound the wait for a free connection
|
|
302
|
+
|
|
303
|
+
`DB_STATEMENT_TIMEOUT_MS` above bounds a query the **server** is running. Nothing bounded a query the client had not sent yet: a request arriving when every pooled connection is busy waited — with no error, no retry and no log line — until something else finished. Those are the two halves of "a request is stuck", and only one of them was covered.
|
|
304
|
+
|
|
305
|
+
Every dialect that can bound an acquire now does, **by default**:
|
|
306
|
+
|
|
307
|
+
| Dialect | Default | What is bounded |
|
|
308
|
+
|---|---|---|
|
|
309
|
+
| postgres | 10 s | the whole acquire — waiting in the pool's queue *and* connecting |
|
|
310
|
+
| mysql / mariadb | 10 s connect · 100 queued waiters | connecting; the wait is bounded by queue **length**, see below |
|
|
311
|
+
| mssql | 10 s | connecting (and the boot probe) |
|
|
312
|
+
| sqlite / turso | — | one in-process connection, no pool to exhaust |
|
|
313
|
+
|
|
314
|
+
An exhausted pool now fails with an error that **names the pool** — `Failed to acquire connection` — at the moment the pool is the cause, instead of surfacing as unexplained latency somewhere with no connection information in it.
|
|
315
|
+
|
|
316
|
+
Move it with `DB_ACQUIRE_TIMEOUT_MS`, in milliseconds. `0` restores the driver's unbounded wait; a negative or non-numeric value falls back to the 10 s default rather than to no bound at all:
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
DB_ACQUIRE_TIMEOUT_MS=3000 # 0 = the driver's unbounded wait
|
|
320
|
+
DB_ACQUIRE_QUEUE_LIMIT=2000 # mysql / mariadb only, OPT-IN — read the hazard below
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Both are read on every command that opens a pool — `voltro dev`, `voltro serve`, `voltro migrate` and every `voltro db …` subcommand alike — so the variable means one thing across your deployment. (`DB_STATEMENT_TIMEOUT_MS` above is deliberately runtime-only: a migration runs legitimately long *statements*. An acquire bound fires when no connection is free at all, which a migration has no more reason to wait forever for than a request does.)
|
|
324
|
+
|
|
325
|
+
That "every command" is literal, and it is worth stating because it has not always been true. Every pool the CLI opens gets its configuration from **one** resolver, so a knob cannot be honoured by one command and ignored by the next. The only two things the command changes are the two named here: `DB_STATEMENT_TIMEOUT_MS` (runtime only) and `DB_DIRECT_URL` / `DB_MIGRATE_URL` (migration only). Everything else — pool size, `DB_SCHEMA`, TLS (`PG_SSL`), the acquire bounds — resolves identically everywhere, including in the two places that open a bare postgres client rather than a pool (the web process's ISR cache and its CDC listener).
|
|
326
|
+
|
|
327
|
+
The same value is `ConnectionConfig.acquireTimeoutMs` when you build a layer yourself:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { postgresDialect } from '@voltro/sql-postgres'
|
|
331
|
+
|
|
332
|
+
const layer = postgresDialect.makeSqlLayer({
|
|
333
|
+
url: process.env.DB_URL!,
|
|
334
|
+
acquireTimeoutMs: 3_000, // 0 = the driver's unbounded wait
|
|
335
|
+
})
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**mysql/mariadb cannot express a time bound on the waiting half, and the length bound that exists is opt-in.** `mysql2`'s pool queues the waiting caller with no timer at all, so there is nothing to set. The one bound the driver has is a LENGTH — `ConnectionConfig.acquireQueueLimit` / `DB_ACQUIRE_QUEUE_LIMIT`, mysql2's `queueLimit`. **The framework does not set it for you**, and the reason is worth understanding before you do:
|
|
339
|
+
|
|
340
|
+
A time bound self-throttles. The acquire fails only once the wait has elapsed, so nothing can retry it faster than the timeout. A length bound is free: past the limit mysql2 rejects the acquire *synchronously*, so any caller that retries a failed acquire without a delay retries in the same tick — forever. The event loop is never reached again, which means no timer fires and the queue whose depth caused the rejection can never drain. The symptom is a process pinned at 100% CPU with no error, no log line, and an idle database.
|
|
341
|
+
|
|
342
|
+
That caller is not hypothetical: the workflow cluster releases shards one statement per shard (300 by default) and retries a failed release immediately. So set `DB_ACQUIRE_QUEUE_LIMIT` only if you know nothing in your process retries an acquire without backoff, and set it well above the peak concurrency of anything that might. Unset — the default — you get mysql2's unbounded queue: callers wait rather than fail. mssql's pool exposes neither knob, so only establishment is bounded there — stated rather than papered over, for the same reason `DB_STATEMENT_TIMEOUT_MS` is left unwired on the dialects that cannot enforce it honestly.
|
|
343
|
+
|
|
240
344
|
## Local development — bring up all five
|
|
241
345
|
|
|
242
346
|
The framework ships a docker-compose at `voltro/test/docker-compose.yml` that brings up postgres + mysql + mariadb + mssql on distinct ports so per-dialect tests can run side-by-side and the dev fixture never clashes with your starter postgres on `:5432`:
|
|
@@ -378,6 +482,8 @@ The framework wraps `pg` (`node-postgres`) via Effect's typed connection layer.
|
|
|
378
482
|
| `PG_MAX_CONNECTIONS` | `10` | pool ceiling. Tune up for high concurrency; CPU-bound workloads rarely benefit past ~2× cores. |
|
|
379
483
|
| `PG_SSL` | unset | `'require'` (TLS without certificate verification — what production deployments behind RDS / Cloud SQL / Supabase want) / `'disable'` (force plaintext). Any other value — including libpq's `'prefer'`, which node-postgres cannot express (the driver has no TLS-then-plaintext fallback) — fails at boot instead of silently downgrading to plaintext. A `?sslmode=require\|disable` query on `DB_URL` works too; an explicit `PG_SSL` wins when both are set. |
|
|
380
484
|
|
|
485
|
+
`PG_SSL` is read by **every** command that opens a connection — `voltro dev`, `voltro serve`, `voltro start`, `voltro migrate` and every `voltro db …` subcommand — so TLS is not something one command negotiates and the next one skips.
|
|
486
|
+
|
|
381
487
|
## CDC — LISTEN/NOTIFY
|
|
382
488
|
|
|
383
489
|
Every table gets an AFTER trigger that emits a `framework_changes` notification on each insert / update / delete. The dispatcher LISTENs on that channel once per process and fans out to subscribers in-memory.
|
|
@@ -409,10 +515,63 @@ Trade-offs of the LISTEN/NOTIFY path:
|
|
|
409
515
|
- ✅ Sub-frame latency (60fps UIs feel real-time).
|
|
410
516
|
- ✅ Zero polling load even on idle tables.
|
|
411
517
|
- ❌ Requires a long-lived connection per process. Connection-pooled deploys must use a sidecar listener or pgbouncer in session mode.
|
|
412
|
-
- ❌ NOTIFY payloads are capped at
|
|
518
|
+
- ❌ NOTIFY payloads are capped at 8000 bytes. The trigger sends the full row images (`row_to_json(OLD)`/`row_to_json(NEW)`) so subscribers get the pre/post values directly; a wide row that would exceed the cap falls back to a key-only notification. See the delivery guarantee below — the fallback is not free, and it is not the same for a subscription as it is for a tap.
|
|
413
519
|
|
|
414
520
|
Set `CDC=0` to disable + force the inline-emit path (single-process only, no cross-process fan-out). Useful for tests + single-binary deploys.
|
|
415
521
|
|
|
522
|
+
### Oversized rows — what is guaranteed
|
|
523
|
+
|
|
524
|
+
A row whose JSON image exceeds ~8000 bytes (a document, a large `json()` column,
|
|
525
|
+
an embedded array) cannot travel in a NOTIFY payload. The trigger keeps the
|
|
526
|
+
change and the **primary key**, drops the images, and the CDC consumer re-reads
|
|
527
|
+
the row from the database before the event reaches anything. What each consumer
|
|
528
|
+
gets:
|
|
529
|
+
|
|
530
|
+
| Change | What is delivered | `event.oversized` |
|
|
531
|
+
|---|---|---|
|
|
532
|
+
| insert / update | the row **re-read from the database** | `'rehydrated'` |
|
|
533
|
+
| delete | the **primary key only** — a tombstone | `'tombstone'` |
|
|
534
|
+
| key missing, re-read failed, or the row is already gone | both images null | `'unrecovered'` |
|
|
535
|
+
|
|
536
|
+
Read the marker before you treat an image as a snapshot. Three limits are real
|
|
537
|
+
and cannot be engineered away:
|
|
538
|
+
|
|
539
|
+
- **A re-read returns the row as it is NOW.** If a second write lands between
|
|
540
|
+
the change and the re-read, this event carries the newer state — and the
|
|
541
|
+
second change delivers it again. The stream is convergent, not
|
|
542
|
+
point-in-time. Postgres keeps no copy of an image the transport dropped.
|
|
543
|
+
- **`old` is null on an oversized update, and pk-only on an oversized delete.**
|
|
544
|
+
There is nowhere to read a pre-image from. A tombstone is enough to REMOVE the
|
|
545
|
+
row from a search index, an analytics mirror or a CDC stream; it is not a
|
|
546
|
+
record of what the row contained, and `@voltro/plugin-versioning` writes
|
|
547
|
+
`data: null` for one rather than a fabricated empty snapshot.
|
|
548
|
+
- **`'unrecovered'` means the content is gone.** No retry can bring it back —
|
|
549
|
+
it was never delivered. Subscriptions are unaffected (they re-query); taps
|
|
550
|
+
miss that row until the next write to it or a re-seed.
|
|
551
|
+
|
|
552
|
+
Every fallback is counted as `voltro_cdc_oversized_total{outcome=…}` (scrapeable
|
|
553
|
+
via `@voltro/plugin-prometheus` at `/metrics`, or `GET
|
|
554
|
+
/_voltro/inspect/metrics`), the first one per table is logged at `warn`, and
|
|
555
|
+
every `unrecovered` one is logged at `error`. **Alert on
|
|
556
|
+
`outcome="unrecovered"`** — a non-zero rate means the deployment is losing
|
|
557
|
+
changes for its taps.
|
|
558
|
+
|
|
559
|
+
Tunables (options on the postgres store, or environment):
|
|
560
|
+
|
|
561
|
+
| Env | Default | Meaning |
|
|
562
|
+
|---|---|---|
|
|
563
|
+
| `VOLTRO_CDC_REHYDRATE_TIMEOUT_MS` | `5000` | total budget for recovering one oversized change. The LISTEN consumer is serial, so this also bounds how long one oversized row can hold up the change stream. |
|
|
564
|
+
| `VOLTRO_CDC_REHYDRATE_RETRIES` | `2` | re-reads after the first attempt, inside that budget. |
|
|
565
|
+
|
|
566
|
+
The re-read is issued against the schema the write landed in, so it is correct
|
|
567
|
+
under namespace (schema-per-tenant) isolation.
|
|
568
|
+
|
|
569
|
+
**This lives in the database, so it has to be applied.** The trigger function is
|
|
570
|
+
DDL: a database created before this shipped still carries the old body, which
|
|
571
|
+
drops the key and makes every oversized change `'unrecovered'`. `voltro db
|
|
572
|
+
apply` replaces it (`voltro dev` reports it at boot as trigger drift, and the
|
|
573
|
+
`error` log line names the same remedy).
|
|
574
|
+
|
|
416
575
|
## Workflow cluster
|
|
417
576
|
|
|
418
577
|
`@effect/cluster`'s `SqlRunnerStorage` uses `pg_advisory_lock(key)` to claim shard ownership. The framework wires this transparently — set `DB_DIALECT=postgres` + provide a SqlClient layer and `workflowEngineLayer({ runnerStorage: 'sql' })` does the rest.
|
|
@@ -504,9 +663,25 @@ DB_HOST=… DB_PORT=… DB_USER=… DB_PASSWORD=… DB_DATABASE=…
|
|
|
504
663
|
# at migration time.
|
|
505
664
|
```
|
|
506
665
|
|
|
666
|
+
## TLS — honoured, or refused
|
|
667
|
+
|
|
668
|
+
Ask for TLS in the connection URL:
|
|
669
|
+
|
|
670
|
+
```sh
|
|
671
|
+
DB_URL=mysql://app:app@db.internal:3306/app?sslmode=require # TLS, certificate not verified
|
|
672
|
+
DB_URL=mysql://app:app@db.internal:3306/app?ssl=true # the provider-style alias — same thing
|
|
673
|
+
DB_URL=mysql://app:app@db.internal:3306/app?sslmode=disable # explicit plaintext
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
`require` maps to mysql2's `{ rejectUnauthorized: false }` — the connection is **encrypted, the server certificate is not authenticated**. That is the mode managed providers expect, and it is the same meaning `?sslmode=require` has on the postgres dialect.
|
|
677
|
+
|
|
678
|
+
**Anything else fails at boot**, on purpose: `prefer`, `allow`, `verify-ca`, `verify-full`, a CA-profile name, `?ssl=yes`. mysql2 either sends an SSLRequest or it does not, so `prefer` is not expressible; and a verification mode cannot round-trip through the cross-dialect boolean `ssl` field, so honouring the word would mean quietly giving you something weaker than it names. A boot failure is the correct answer to a TLS request that cannot be satisfied.
|
|
679
|
+
|
|
680
|
+
> **This changed.** Before this release the mysql/mariadb dialect had **no TLS path at all** — `?ssl=true` was parsed off the URL and discarded, and the connection went out in plaintext with no warning. If your URL carries `?ssl=` or `?sslmode=`, that request is now real: verify your server accepts TLS before deploying.
|
|
681
|
+
|
|
507
682
|
## Driver: `@effect/sql-mysql2`
|
|
508
683
|
|
|
509
|
-
Wraps the `mysql2` driver (Node.js mysql client). The framework's `MysqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10).
|
|
684
|
+
Wraps the `mysql2` driver (Node.js mysql client). The framework's `MysqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10), with a 10 s bound on **establishing** a connection. Waiting for a busy one is **not** bounded: mysql2 has no timer for that phase, and the length ceiling it does offer (`ConnectionConfig.acquireQueueLimit`) is opt-in because mysql2 rejects past the limit synchronously — which turns any zero-delay acquire retry into an event-loop-starving spin. See the [acquire-timeout table](../dialects) for the full reasoning before you set it.
|
|
510
685
|
|
|
511
686
|
The driver returns BOOLEAN as `0|1` (TINYINT(1) is MySQL's underlying type). The framework's `decodeRowsFromSchema` post-processor converts back to `true|false` for any column the schema registry declares as `boolean()`. Without this, every reactive subscription opening on a BOOLEAN column would see `1` instead of `true` and fail framework Schema validators.
|
|
512
687
|
|
|
@@ -709,6 +884,8 @@ makeMysqlStore({ sqlLayer, variant: 'mariadb', changeStrategy: 'cdc', cdcConfig
|
|
|
709
884
|
|
|
710
885
|
The variant flows through to per-operation getters (`supportsInsertReturning`, `supportsDeleteReturning`, `supportsUpdateReturning`) and to the JSON-agg compiler's dialect branch.
|
|
711
886
|
|
|
887
|
+
TLS and the pool bounds are shared with MySQL, driver-for-driver: `?sslmode=require` / `?ssl=true` encrypts (certificate not verified), an unsupported mode fails at boot rather than falling back to plaintext, and the acquire wait is bounded by queue length because mysql2 has no time-based bound for it. See [MySQL → TLS](./mysql) and the [acquire-timeout table](../dialects).
|
|
888
|
+
|
|
712
889
|
## `UPDATE … RETURNING` does NOT exist — anywhere
|
|
713
890
|
|
|
714
891
|
Despite MariaDB's broad RETURNING support — `INSERT … RETURNING *` since 10.5, `DELETE … RETURNING *` since 10.0 — there is **no UPDATE … RETURNING in any MariaDB version**. The framework's first attempt at supportsReturning treated the whole RETURNING family as one flag and emitted `UPDATE … RETURNING *` on mariadb, which fails with a parse error.
|
|
@@ -914,9 +1091,21 @@ DB_URL=mssql://sa:<password>@localhost:11433/voltro_test
|
|
|
914
1091
|
|
|
915
1092
|
The framework's docker-compose at `voltro/test/docker-compose.yml` brings up MSSQL on `:11433` with an `mssql-init` sidecar that runs `CREATE DATABASE voltro_test` once the server is healthy. mssql doesn't have a `docker-entrypoint-initdb.d` equivalent, so the framework synthesizes one.
|
|
916
1093
|
|
|
1094
|
+
## TLS — honoured, or refused
|
|
1095
|
+
|
|
1096
|
+
```sh
|
|
1097
|
+
DB_URL=mssql://sa:<password>@sql.internal:1433/app?sslmode=require # encrypt, don't verify the certificate
|
|
1098
|
+
DB_URL=mssql://sa:<password>@sql.internal:1433/app?encrypt=1 # tedious' own spelling — same thing
|
|
1099
|
+
DB_URL=mssql://sa:<password>@sql.internal:1433/app?sslmode=disable # explicit plaintext
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
`require` maps to tedious `encrypt: true` + `trustServerCertificate: true` — encrypted, server certificate not authenticated, which is what lets a Developer-edition instance's self-signed certificate work. Unsupported modes (`prefer`, `verify-ca`, `verify-full`, `?encrypt=yes`) **fail at boot** rather than connecting with something weaker than they name.
|
|
1103
|
+
|
|
1104
|
+
> **This changed.** `@effect/sql-mssql` defaults `encrypt` to **false** (it overrides tedious' own `true`), and the framework never read the `ssl` config or the URL query — so every mssql session was plaintext, while a hard-coded `trustServerCertificate: true` made the configuration look TLS-aware. Unset still means plaintext (unchanged); a TLS request in the URL is now real.
|
|
1105
|
+
|
|
917
1106
|
## Driver: `@effect/sql-mssql`
|
|
918
1107
|
|
|
919
|
-
Wraps `tedious` (the Node.js TDS driver). The framework's `MssqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10).
|
|
1108
|
+
Wraps `tedious` (the Node.js TDS driver). The framework's `MssqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10), with a 10 s bound on establishing a connection (`ConnectionConfig.acquireTimeoutMs`). Waiting for a BUSY pooled connection is not bounded here — the client's pool exposes no such knob; see the [acquire-timeout table](../dialects) for what each dialect can enforce.
|
|
920
1109
|
|
|
921
1110
|
Tedious returns BIT as proper boolean (✓) and NVARCHAR as string. JSON columns are NVARCHAR(MAX) under the hood — driver returns the raw string. The framework's `decodeRowsFromSchema` JSON.parses any column declared as `json()` in the schema registry; without this, every reactive subscription reading a JSON column would see strings and fail framework Schema validators.
|
|
922
1111
|
|
|
@@ -47,6 +47,43 @@ await ctx.store.transactional(async (tx) => {
|
|
|
47
47
|
|
|
48
48
|
`transactional()` **must not be nested** — a nested invocation throws to surface a design mistake. Use a single top-level `transactional()` per unit of work.
|
|
49
49
|
|
|
50
|
+
## Typed errors survive a transaction
|
|
51
|
+
|
|
52
|
+
A `Data.TaggedError` thrown inside a transaction reaches the caller — and the browser client — **as itself**: `_tag`, payload and prototype intact, so a mutation's declared `error:` union still matches.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { Data } from 'effect'
|
|
56
|
+
|
|
57
|
+
class NoteNotFound extends Data.TaggedError('NoteNotFound')<{ noteId: string }> {}
|
|
58
|
+
|
|
59
|
+
// apps/api/mutations/notes.rename.mutation.ts — auto-transactional
|
|
60
|
+
export default async (input, ctx) => {
|
|
61
|
+
const note = await ctx.store.query({ table: 'notes', /* … */ })
|
|
62
|
+
if (!note) throw new NoteNotFound({ noteId: input.id }) // arrives typed on the client
|
|
63
|
+
// …
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
On the client:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const res = await client.notes.rename({ id, title })
|
|
71
|
+
if (res.error?._tag === 'NoteNotFound') { /* this branch fires */ }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This holds on **every** dialect and on **both** tenancy topologies — shared-schema and namespace isolation — because all four dialect stores and both postgres entry points run through one shared transaction bracket. Never match on `error.message` to identify a transaction failure; the `_tag` is the contract.
|
|
75
|
+
|
|
76
|
+
## Automatic retry on transient conflicts
|
|
77
|
+
|
|
78
|
+
A transaction that fails with a transient contention error is retried automatically: exponential backoff from 10 ms, up to 3 retries (4 attempts total). What counts as transient is per-dialect — postgres `serialization_failure` (40001) and `deadlock_detected` (40P01), mysql/mariadb deadlock and lock-wait timeout, mssql deadlock victim (1205), sqlite `SQLITE_BUSY`/`SQLITE_LOCKED`, and Turso's MVCC write-write conflict.
|
|
79
|
+
|
|
80
|
+
Two properties worth knowing:
|
|
81
|
+
|
|
82
|
+
- **A conflict raised by the COMMIT itself is retried too.** Under `SERIALIZABLE`, the engine can only detect some conflicts at commit time — those are caught and replayed like any other.
|
|
83
|
+
- **Each attempt gets a fresh transaction and a fresh event buffer.** A retried attempt's queued ChangeEvents are discarded with it, so subscribers see exactly one event set: the winning attempt's.
|
|
84
|
+
|
|
85
|
+
Because the body can run more than once, keep `transactional()` bodies **idempotent** — no counters incremented in JS, no external calls (see the anti-patterns below).
|
|
86
|
+
|
|
50
87
|
## Optimistic concurrency
|
|
51
88
|
|
|
52
89
|
The fluent update builder carries an `.expectVersion(n)` guard: the update only matches rows whose `version` equals `n`, and throws `OptimisticLockError` when none match (the row was concurrently changed). Add a `version` column to the table to use it.
|
|
@@ -134,6 +171,37 @@ array is a no-op that returns `[]`.
|
|
|
134
171
|
see each new row's delta — identical to N single inserts. Inside a
|
|
135
172
|
`transactional()` the per-row events queue until commit and drop on a throw.
|
|
136
173
|
|
|
174
|
+
### Large arrays are chunked for you — and stay all-or-nothing
|
|
175
|
+
|
|
176
|
+
Every engine caps what ONE statement may carry, and the caps are far apart:
|
|
177
|
+
|
|
178
|
+
| Dialect | Bind parameters per statement | Rows per `VALUES` |
|
|
179
|
+
| --- | --- | --- |
|
|
180
|
+
| postgres | 65 535 | — |
|
|
181
|
+
| mysql / mariadb | 65 535 | — |
|
|
182
|
+
| **mssql** | **2 098** | **1 000** |
|
|
183
|
+
| sqlite / turso | 32 766 | — |
|
|
184
|
+
|
|
185
|
+
`INSERT … VALUES` binds one parameter per **column per row**, so the row limit
|
|
186
|
+
is `floor(cap / columns)` — a 12-column table caps at 5 461 rows on postgres and
|
|
187
|
+
at **174** on mssql. Past that the driver refuses with its own error about a
|
|
188
|
+
limit you never chose.
|
|
189
|
+
|
|
190
|
+
`insertMany` splits the array for you at that boundary. Two properties are
|
|
191
|
+
guaranteed:
|
|
192
|
+
|
|
193
|
+
- **A fitting array is still ONE statement.** Nothing changes for the normal
|
|
194
|
+
case — no extra round-trips, no behaviour difference.
|
|
195
|
+
- **A chunked insert is still all-or-nothing.** When the call is not already
|
|
196
|
+
inside a `transactional()`, the chunks run in one transaction the framework
|
|
197
|
+
opens, so a failure in the last chunk rolls back the earlier ones. Without
|
|
198
|
+
that, chunking would quietly add partial-success-on-failure to a call that
|
|
199
|
+
never had it.
|
|
200
|
+
|
|
201
|
+
There is nothing to configure. If a SINGLE row is wider than the cap (a
|
|
202
|
+
3 000-column table on mssql) the engine's own error is what you get — a row
|
|
203
|
+
cannot be split.
|
|
204
|
+
|
|
137
205
|
## `upsert` — insert or update on conflict
|
|
138
206
|
|
|
139
207
|
The 90% case: "make this row exist with these values; if it already
|