@voltro/cli 0.28.0 → 0.30.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 +655 -0
- package/THIRD-PARTY-NOTICES.md +90 -4
- package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
- package/dist/apiBuild-CxvR1Dwt.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-BMr-X48w.js +2427 -0
- package/dist/checkCommand-BOk9y264.js +2 -0
- package/dist/cliRuntime-Oh517vCV.js +96 -0
- package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
- package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
- package/dist/dbCommand-D8nhyppR.js +2 -0
- package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
- package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
- package/dist/fileConventions-Cof68_BL.js +33 -0
- package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
- package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
- package/dist/inspect-mmBuRXmy.js +2 -0
- package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
- package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
- package/dist/manifestBuild-Dj8Jjoto.js +2 -0
- package/dist/sdkgen-CqpOq_fy.js +582 -0
- package/dist/serveCommand-Ciq_wIY8.js +1577 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +50 -0
- package/templates/AGENTS.md +54 -2
- package/templates/agent-docs/_index.md +4 -2
- package/templates/agent-docs/_manifest.json +26 -5
- package/templates/agent-docs/ai.md +191 -0
- package/templates/agent-docs/cli.md +143 -6
- package/templates/agent-docs/configuration.md +32 -0
- package/templates/agent-docs/data.md +282 -0
- package/templates/agent-docs/database/migrations.md +47 -0
- package/templates/agent-docs/database/misc.md +57 -0
- package/templates/agent-docs/database/schema.md +3 -1
- package/templates/agent-docs/internationalization.md +169 -8
- package/templates/agent-docs/local-first-mobile.md +426 -0
- package/templates/agent-docs/routing.md +94 -3
- package/templates/agent-docs/schema-driven-ui.md +12 -0
- package/templates/agent-docs/security.md +17 -0
- package/templates/agent-docs/templates/apibackends.md +210 -1
- package/templates/agent-docs/templates/appshells.md +144 -1
- package/templates/agent-docs/templates/mobile.md +60 -0
- package/templates/agent-docs/templates/overview.md +17 -7
- package/templates/agent-docs/whats-new.md +238 -158
- package/templates/agent-docs/workflows.md +376 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/.env.example +19 -0
- package/templates/apps/api-backend-sqlite/README.md +38 -0
- package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
- package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-sqlite/package.json +30 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-sqlite/template.json +6 -0
- package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/README.md +104 -0
- package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
- package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
- package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
- package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
- package/templates/apps/api-cms/actions/me.action.ts +16 -0
- package/templates/apps/api-cms/app.config.ts +61 -0
- package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
- package/templates/apps/api-cms/content/index.ts +18 -0
- package/templates/apps/api-cms/content/page.contentType.ts +24 -0
- package/templates/apps/api-cms/database/schema.ts +64 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
- package/templates/apps/api-cms/package.json +32 -0
- package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
- package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
- package/templates/apps/api-cms/template.json +6 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
- package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
- package/templates/apps/api-cms/tsconfig.json +5 -0
- package/templates/apps/api-collab/README.md +70 -0
- package/templates/apps/api-collab/app.config.ts +28 -0
- package/templates/apps/api-collab/database/schema.ts +63 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
- package/templates/apps/api-collab/package.json +30 -0
- package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
- package/templates/apps/api-collab/queries/documents.query.ts +23 -0
- package/templates/apps/api-collab/template.json +6 -0
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
- package/templates/apps/api-collab/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/README.md +103 -0
- package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
- package/templates/apps/api-saas-starter/app.config.ts +87 -0
- package/templates/apps/api-saas-starter/database/schema.ts +57 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/package.json +32 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas-starter/template.json +6 -0
- package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
- package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
- package/templates/apps/api-saas-starter/tsconfig.json +5 -0
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/README.md +88 -0
- package/templates/apps/api-status/app.config.ts +36 -0
- package/templates/apps/api-status/authz.ts +33 -0
- package/templates/apps/api-status/database/schema.ts +70 -0
- package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
- package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
- package/templates/apps/api-status/package.json +30 -0
- package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/components.list.query.ts +13 -0
- package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
- package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
- package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
- package/templates/apps/api-status/template.json +6 -0
- package/templates/apps/api-status/tests/status.test.ts +70 -0
- package/templates/apps/api-status/tsconfig.json +5 -0
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/app.config.ts +1 -1
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/app.config.ts +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-auth/README.md +78 -0
- package/templates/apps/frontend-auth/app.config.ts +34 -0
- package/templates/apps/frontend-auth/package.json +32 -0
- package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
- package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
- package/templates/apps/frontend-auth/src/config.ts +11 -0
- package/templates/apps/frontend-auth/src/globals.css +105 -0
- package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
- package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
- package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
- package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
- package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
- package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
- package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
- package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
- package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
- package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
- package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
- package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
- package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
- package/templates/apps/frontend-auth/template.json +6 -0
- package/templates/apps/frontend-auth/tsconfig.json +5 -0
- package/templates/apps/frontend-blank/app.config.ts +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-cms/README.md +47 -0
- package/templates/apps/frontend-cms/app.config.ts +37 -0
- package/templates/apps/frontend-cms/package.json +33 -0
- package/templates/apps/frontend-cms/src/config.ts +8 -0
- package/templates/apps/frontend-cms/src/globals.css +105 -0
- package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
- package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
- package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
- package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
- package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
- package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
- package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
- package/templates/apps/frontend-cms/template.json +6 -0
- package/templates/apps/frontend-cms/tsconfig.json +5 -0
- package/templates/apps/frontend-collab/README.md +80 -0
- package/templates/apps/frontend-collab/app.config.ts +38 -0
- package/templates/apps/frontend-collab/package.json +33 -0
- package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
- package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
- package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
- package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
- package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
- package/templates/apps/frontend-collab/template.json +6 -0
- package/templates/apps/frontend-collab/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/app.config.ts +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/README.md +71 -0
- package/templates/apps/frontend-portal/app.config.ts +37 -0
- package/templates/apps/frontend-portal/package.json +32 -0
- package/templates/apps/frontend-portal/src/config.ts +8 -0
- package/templates/apps/frontend-portal/src/globals.css +93 -0
- package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
- package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
- package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
- package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
- package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
- package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
- package/templates/apps/frontend-portal/template.json +6 -0
- package/templates/apps/frontend-portal/tsconfig.json +5 -0
- package/templates/apps/frontend-saas/README.md +73 -0
- package/templates/apps/frontend-saas/app.config.ts +46 -0
- package/templates/apps/frontend-saas/package.json +32 -0
- package/templates/apps/frontend-saas/src/config.ts +8 -0
- package/templates/apps/frontend-saas/src/globals.css +85 -0
- package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
- package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
- package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
- package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
- package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
- package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-saas/template.json +6 -0
- package/templates/apps/frontend-saas/tsconfig.json +5 -0
- package/templates/apps/frontend-spa/app.config.ts +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr/app.config.ts +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/README.md +51 -0
- package/templates/apps/frontend-status/app.config.ts +39 -0
- package/templates/apps/frontend-status/package.json +32 -0
- package/templates/apps/frontend-status/src/config.ts +7 -0
- package/templates/apps/frontend-status/src/globals.css +70 -0
- package/templates/apps/frontend-status/src/globals.d.ts +6 -0
- package/templates/apps/frontend-status/src/lib/status.ts +81 -0
- package/templates/apps/frontend-status/src/locales/de.ts +41 -0
- package/templates/apps/frontend-status/src/locales/en.ts +45 -0
- package/templates/apps/frontend-status/src/locales/index.ts +13 -0
- package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
- package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
- package/templates/apps/frontend-status/template.json +6 -0
- package/templates/apps/frontend-status/tsconfig.json +11 -0
- package/templates/apps/mobile-app/README.md +58 -0
- package/templates/apps/mobile-app/app.config.ts +33 -0
- package/templates/apps/mobile-app/babel.config.js +5 -0
- package/templates/apps/mobile-app/metro.config.js +20 -0
- package/templates/apps/mobile-app/package.json +33 -0
- package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
- package/templates/apps/mobile-app/src/app/index.tsx +62 -0
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
- package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
- package/templates/apps/mobile-app/src/client.ts +74 -0
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
- package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
- package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
- package/templates/apps/mobile-app/src/persistence.ts +38 -0
- package/templates/apps/mobile-app/template.json +6 -0
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
- package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
- package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
- package/templates/apps/mobile-app/tsconfig.json +10 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
- package/templates/baselines/compose/docker-compose.prod.yml +15 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
- package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
- package/dist/apiBuild-DgBS9ayv.js +0 -2
- package/dist/dbCommand-DvguqlzF.js +0 -2
- package/dist/fileConventions-3bffWssN.js +0 -30
- package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
- package/dist/inspect-gt8bq-Tz.js +0 -2
- package/dist/manifestBuild-ifczArzr.js +0 -2
- package/dist/serveCommand-ZTn-dPFa.js +0 -1425
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Backend app for the {{projectName}} project, backed by SQLite. Read by
|
|
2
|
+
// `voltro dev` (local) and `voltro serve` (production).
|
|
3
|
+
//
|
|
4
|
+
// `store: 'sqlite'` is the simplest DURABLE store: an embedded, single-file
|
|
5
|
+
// (or `:memory:`) database with no server to run and one native dependency
|
|
6
|
+
// (`better-sqlite3`, pulled by `@voltro/sql-sqlite`). Great for a single
|
|
7
|
+
// process — a CLI tool, an edge box, a small self-hosted app, or a durable
|
|
8
|
+
// local dev store that survives restarts (unlike `store: 'memory'`).
|
|
9
|
+
//
|
|
10
|
+
// The database file + dialect come from env — see `.env.example`:
|
|
11
|
+
// DB_DIALECT=sqlite
|
|
12
|
+
// DB_URL=file:./.data/{{projectNameSnake}}.sqlite (or ':memory:')
|
|
13
|
+
//
|
|
14
|
+
// SQLite is single-writer and single-process by design, so it does NOT
|
|
15
|
+
// fan real-time subscriptions across replicas (there is no CDC bus). Within
|
|
16
|
+
// the one process, reactive queries work exactly as elsewhere. Reach for
|
|
17
|
+
// postgres/mysql/mariadb when you scale to more than one replica.
|
|
18
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
19
|
+
|
|
20
|
+
// Typed environment — declare your env once; it's validated at boot (fail-fast)
|
|
21
|
+
// and read on the server via `serverEnv` / `getSecret` from '@voltro/env/server'.
|
|
22
|
+
// `access` is required: 'public' (browser-safe) or 'secret' (server-only, never
|
|
23
|
+
// bundled). Run `voltro env` to see the manifest. See the Environment docs.
|
|
24
|
+
export const env = defineEnv({
|
|
25
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
26
|
+
// Add your own, e.g.:
|
|
27
|
+
// STRIPE_KEY: envVar.string({ access: 'secret' }),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
type: 'api' as const,
|
|
32
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
33
|
+
store: 'sqlite' as const,
|
|
34
|
+
env,
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Schema for the {{projectName}} backend.
|
|
2
|
+
//
|
|
3
|
+
// Tables are auto-discovered: every exported `table(...)` in this file
|
|
4
|
+
// (or any `*.entity.ts` / `*.schema.ts` file) is registered by
|
|
5
|
+
// `voltro dev` — no manual barrel. `actors` + `tenants` are the
|
|
6
|
+
// framework's core tables: the built-in `audit()` / `tenant()` /
|
|
7
|
+
// `softDelete()` mixins reference them, so they must be declared (the
|
|
8
|
+
// CLI wires them into the mixin registry once it finds them).
|
|
9
|
+
//
|
|
10
|
+
// Add the `tenant()` mixin to a table to make it tenant-scoped — the
|
|
11
|
+
// runtime AND-merges `eq('tenantId', subject.tenantId)` into every
|
|
12
|
+
// subscription predicate, so cross-tenant reads can't leak.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
boolean,
|
|
16
|
+
databaseHandle,
|
|
17
|
+
id,
|
|
18
|
+
table,
|
|
19
|
+
text,
|
|
20
|
+
timestamp,
|
|
21
|
+
type InferRow,
|
|
22
|
+
} from '@voltro/database'
|
|
23
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
24
|
+
|
|
25
|
+
// ---------- Core tables (required by the audit / tenant mixins) ----------
|
|
26
|
+
|
|
27
|
+
export const actors = table('actors', {
|
|
28
|
+
id: id(),
|
|
29
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
30
|
+
displayName: text().nullable(),
|
|
31
|
+
createdAt: timestamp().default('now'),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const tenants = table('tenants', {
|
|
35
|
+
id: id(),
|
|
36
|
+
name: text(),
|
|
37
|
+
createdAt: timestamp().default('now'),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// ---------- Application tables — rename / replace with your own ----------
|
|
41
|
+
|
|
42
|
+
export const notes = table('notes', {
|
|
43
|
+
id: id({ prefix: 'note' }),
|
|
44
|
+
title: text(),
|
|
45
|
+
body: text(),
|
|
46
|
+
done: boolean().default(false),
|
|
47
|
+
})
|
|
48
|
+
// tenant() pulls audit() transitively → adds tenantId + createdAt /
|
|
49
|
+
// updatedAt / createdBy / updatedBy (auto-stamped by the runtime).
|
|
50
|
+
.with(tenant())
|
|
51
|
+
|
|
52
|
+
export type Note = InferRow<typeof notes>
|
|
53
|
+
|
|
54
|
+
export const database = databaseHandle({ actors, tenants, notes })
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { assertOwnTenant } from '@voltro/plugin-multitenancy/guard'
|
|
2
|
+
import type { AppContext } from '@voltro/runtime'
|
|
3
|
+
|
|
4
|
+
const execute = async (
|
|
5
|
+
input: { tenantId: string; title: string; body: string },
|
|
6
|
+
ctx: AppContext,
|
|
7
|
+
) => {
|
|
8
|
+
assertOwnTenant(input.tenantId, ctx.request.subject)
|
|
9
|
+
// Framework auto-injects a `note_…` id from the table's id() decl.
|
|
10
|
+
return ctx.store.insert('notes', {
|
|
11
|
+
title: input.title,
|
|
12
|
+
body: input.body,
|
|
13
|
+
done: false,
|
|
14
|
+
tenantId: input.tenantId,
|
|
15
|
+
createdAt: new Date(),
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default execute
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Creates a note. The handler's `assertOwnTenant` guard rejects
|
|
2
|
+
// cross-tenant spoofing — input.tenantId MUST match the resolved
|
|
3
|
+
// subject's tenantId, else the rpc layer surfaces a typed
|
|
4
|
+
// `TenantMismatch` error.
|
|
5
|
+
|
|
6
|
+
import { defineMutation } from '@voltro/protocol'
|
|
7
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
8
|
+
import { Schema } from 'effect'
|
|
9
|
+
|
|
10
|
+
export const createNote = defineMutation({
|
|
11
|
+
name: 'notes.create',
|
|
12
|
+
target: {
|
|
13
|
+
table: 'notes',
|
|
14
|
+
op: 'insert',
|
|
15
|
+
shape: (input: { tenantId: string; title: string; body: string }) => ({
|
|
16
|
+
title: input.title,
|
|
17
|
+
body: input.body,
|
|
18
|
+
done: false,
|
|
19
|
+
tenantId: input.tenantId,
|
|
20
|
+
createdAt: new Date(),
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
input: Schema.Struct({
|
|
24
|
+
tenantId: Schema.String,
|
|
25
|
+
title: Schema.NonEmptyString,
|
|
26
|
+
body: Schema.String,
|
|
27
|
+
}),
|
|
28
|
+
output: Schema.Struct({
|
|
29
|
+
id: Schema.String,
|
|
30
|
+
title: Schema.String,
|
|
31
|
+
body: Schema.String,
|
|
32
|
+
done: Schema.Boolean,
|
|
33
|
+
tenantId: Schema.String,
|
|
34
|
+
createdAt: Schema.Date,
|
|
35
|
+
}),
|
|
36
|
+
error: TenantMismatch,
|
|
37
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
9
|
+
"migrate": "voltro migrate",
|
|
10
|
+
"test": "voltro test",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@effect/platform": "^0.97.0",
|
|
15
|
+
"@effect/rpc": "^0.76.0",
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/database": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.30.0",
|
|
20
|
+
"@voltro/protocol": "0.30.0",
|
|
21
|
+
"@voltro/runtime": "0.30.0",
|
|
22
|
+
"@voltro/sql-sqlite": "0.30.0",
|
|
23
|
+
"effect": "^3.22.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@voltro/testing": "0.30.0",
|
|
27
|
+
"typescript": "^6.0.3",
|
|
28
|
+
"vitest": "^4.1.10"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
|
|
3
|
+
const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
|
|
4
|
+
descriptor: {
|
|
5
|
+
table: 'notes' as const,
|
|
6
|
+
predicate: undefined,
|
|
7
|
+
order: [{ column: 'createdAt' as const, direction: 'desc' as const }],
|
|
8
|
+
take: 100,
|
|
9
|
+
skip: undefined,
|
|
10
|
+
projection: undefined,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default execute
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Streaming subscription: every change to `notes` for the caller's
|
|
2
|
+
// tenant lands as a delta. The runtime AND-merges tenant scope into
|
|
3
|
+
// the predicate via the `tenant()` mixin on the table — no manual
|
|
4
|
+
// `eq('tenantId', ...)` needed in the handler.
|
|
5
|
+
|
|
6
|
+
import { defineQuery } from '@voltro/protocol'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const listNotes = defineQuery({
|
|
10
|
+
name: 'notes.list',
|
|
11
|
+
input: Schema.Struct({}),
|
|
12
|
+
output: Schema.Struct({
|
|
13
|
+
id: Schema.String,
|
|
14
|
+
title: Schema.String,
|
|
15
|
+
body: Schema.String,
|
|
16
|
+
done: Schema.Boolean,
|
|
17
|
+
tenantId: Schema.String,
|
|
18
|
+
createdAt: Schema.Date,
|
|
19
|
+
}),
|
|
20
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-backend-sqlite",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "SQLite-backed Voltro backend: a single-process, embedded, zero-server database (file or :memory:). The simplest durable-store shape — no docker, one dependency.",
|
|
5
|
+
"tags": ["api", "backend", "sqlite", "embedded", "single-process", "durable"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Unit test with `@voltro/testing` — no database, no running server. Importing
|
|
2
|
+
// `../database/schema` registers the `notes` / `actors` / `tenants` tables
|
|
3
|
+
// globally, so `makeTestContext` auto-wires them into the in-memory store.
|
|
4
|
+
// `ctx.store` is the SAME mixin-wrapped store the handler gets in production,
|
|
5
|
+
// so tenant auto-scoping + audit auto-fill behave exactly as at runtime.
|
|
6
|
+
// Run with `voltro test` (vitest).
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
10
|
+
import { database } from '../database/schema' // registers notes / actors / tenants
|
|
11
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
12
|
+
import createNote from '../mutations/notes.create.mutation.server'
|
|
13
|
+
|
|
14
|
+
describe('notes.create', () => {
|
|
15
|
+
it('inserts a note stamped to the caller tenant', async () => {
|
|
16
|
+
const ctx = makeTestContext({
|
|
17
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
18
|
+
store: mockStore({ notes: [] }),
|
|
19
|
+
})
|
|
20
|
+
const row = await createNote({ tenantId: 'acme', title: 'Hello', body: 'World' }, ctx)
|
|
21
|
+
|
|
22
|
+
expect(row['title']).toBe('Hello')
|
|
23
|
+
expect(row['tenantId']).toBe('acme') // assertOwnTenant let it through + stamped
|
|
24
|
+
expect(row['done']).toBe(false)
|
|
25
|
+
expect(String(row['id'])).toMatch(/^note_/) // TypeID auto-injected from id()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects a cross-tenant spoof — input.tenantId must match the subject', async () => {
|
|
29
|
+
const ctx = makeTestContext({
|
|
30
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
31
|
+
store: mockStore({ notes: [] }),
|
|
32
|
+
})
|
|
33
|
+
await expect(
|
|
34
|
+
createNote({ tenantId: 'other-tenant', title: 'spoof', body: '' }, ctx),
|
|
35
|
+
).rejects.toBeInstanceOf(TenantMismatch)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('isolates tenants — t2 never sees t1 rows (REAL tenant scoping)', async () => {
|
|
39
|
+
const ctx = makeTestContext({
|
|
40
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
41
|
+
store: mockStore({ notes: [] }),
|
|
42
|
+
})
|
|
43
|
+
await createNote({ tenantId: 't1', title: 'secret', body: '' }, ctx)
|
|
44
|
+
|
|
45
|
+
// Re-scope the SAME store to a different tenant — the real WHERE filter
|
|
46
|
+
// (eq tenantId) hides t1's row from t2.
|
|
47
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.notes.descriptor))
|
|
48
|
+
expect(seenByT2).toHaveLength(0)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
"test": "voltro test"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.
|
|
18
|
-
"@voltro/plugin-storage": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/cli": "0.30.0",
|
|
15
|
+
"@voltro/database": "0.30.0",
|
|
16
|
+
"@voltro/env": "0.30.0",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.30.0",
|
|
18
|
+
"@voltro/plugin-storage": "0.30.0",
|
|
19
|
+
"@voltro/protocol": "0.30.0",
|
|
20
|
+
"@voltro/runtime": "0.30.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.30.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — headless CMS backend
|
|
2
|
+
|
|
3
|
+
A headless CMS composed from [`@voltro/cms`](https://voltro.cloud/docs): content
|
|
4
|
+
types declared as **code**, compiled to draft/published tables, driven by the
|
|
5
|
+
write pipeline, and exposed as a typed rpc surface an editor consumes. Content is
|
|
6
|
+
tenant-scoped and gated by real editor auth. Boots **zero-infra** (`store:
|
|
7
|
+
'memory'`, in-process users).
|
|
8
|
+
|
|
9
|
+
Pair it with the **frontend-cms** editor web template:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
voltro init my-cms --api=api-cms --web=frontend-cms
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Content types as code
|
|
16
|
+
|
|
17
|
+
Each `content/*.contentType.ts` file declares a type with the CMS `Schema` field
|
|
18
|
+
DSL. `@voltro/cms` is a **library, not a plugin** — there is no discovery of
|
|
19
|
+
`*.contentType.ts`; `database/schema.ts` imports the types and registers their
|
|
20
|
+
derived tables, and THAT is what makes them migrate.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// content/blogPost.contentType.ts
|
|
24
|
+
export const blogPost = defineContentType({
|
|
25
|
+
name: 'blogPost', displayName: 'Blog post', pluralName: 'Blog posts',
|
|
26
|
+
fields: {
|
|
27
|
+
title: Schema.String.pipe(Schema.maxLength(200)),
|
|
28
|
+
slug: Schema.String.pipe(Schema.pattern(/^[a-z0-9-]+$/), Schema.unique(), derivedFrom('title', slugify)),
|
|
29
|
+
body: Schema.RichText({ allowImages: true }),
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`contentTypeToEntities(blogPost)` compiles this to a `blogPost_drafts` +
|
|
35
|
+
`blogPost_published` table pair — both tenant-scoped (`tenant()`), both carrying
|
|
36
|
+
the lifecycle `status` column, both reactive so a publish wakes subscriptions.
|
|
37
|
+
|
|
38
|
+
## The rpc surface
|
|
39
|
+
|
|
40
|
+
| Procedure | Kind | Does |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `content.types` | action | the registered types, wire-safe — the editor renders forms from this |
|
|
43
|
+
| `content.list` | query (live) | one type's rows, tenant-scoped; `status` picks drafts vs published |
|
|
44
|
+
| `content.get` | action | one row by id, for the edit form |
|
|
45
|
+
| `content.saveDraft` | mutation | derive → validate → upsert `<type>_drafts`; invalid input → typed `{ ok: false, violations }` |
|
|
46
|
+
| `content.publish` | mutation | copy draft → published (atomic); wakes consumer subscriptions |
|
|
47
|
+
| `content.unpublish` | mutation | remove the published copy, keep the draft |
|
|
48
|
+
| `session.me` | action | the resolved Subject (the editor's SSR gate reads it) |
|
|
49
|
+
|
|
50
|
+
Validation and not-found are RESULT UNIONS, not thrown errors — `saveDraft`
|
|
51
|
+
returns `{ ok: false, violations }` so the editor annotates the form, and
|
|
52
|
+
`publish` returns `{ ok: false }` for a missing/foreign draft. The descriptors
|
|
53
|
+
stay pure effect/Schema (browser-safe with nothing extra pulled in).
|
|
54
|
+
|
|
55
|
+
## Run it
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
voltro dev .
|
|
59
|
+
# → http://localhost:4000 (rpc + /auth/* HTTP routes)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`voltro dev` mints a unique `VOLTRO_SESSION_SECRET` into a gitignored
|
|
63
|
+
`.env.local` on first boot — no secret ships with the template. It signs both
|
|
64
|
+
the session cookie and CMS preview tokens.
|
|
65
|
+
|
|
66
|
+
## Files
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
app.config.ts authRoutesPlugin + voltroPasswordStrategy (editor auth)
|
|
70
|
+
content/
|
|
71
|
+
blogPost.contentType.ts a content type as code (derived slug, rich text)
|
|
72
|
+
page.contentType.ts a second type (Literal layout → <select>)
|
|
73
|
+
index.ts the type registry + name lookup
|
|
74
|
+
database/schema.ts core actors/tenants + contentTypeToEntities(...) tables
|
|
75
|
+
actions/
|
|
76
|
+
content.types.* the wire-safe type descriptors the editor renders
|
|
77
|
+
content.get.* one row by id
|
|
78
|
+
me.action.* session.me
|
|
79
|
+
queries/content.list.* live, tenant-scoped, drafts-or-published
|
|
80
|
+
mutations/
|
|
81
|
+
content.saveDraft.* derive + validate + upsert draft
|
|
82
|
+
content.publish.* draft → published
|
|
83
|
+
content.unpublish.* take a published row down
|
|
84
|
+
tests/ write pipeline vs a real store + descriptor pins
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Going to production
|
|
88
|
+
|
|
89
|
+
| Want… | Do |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Durable content + editors | `store: 'postgres'` + `postgresUserStore({ sql })` |
|
|
92
|
+
| A REST read API for consumers | mount `handleCmsRest(...)` (from `@voltro/cms`) — `GET /v1/cms/<type>`, API-key scoped |
|
|
93
|
+
| Draft preview from a consumer app | `previewToken(type, id)` / `verifyPreviewToken` (HMAC over `VOLTRO_SESSION_SECRET`) |
|
|
94
|
+
| Media fields | `Schema.Media()` + `@voltro/plugin-storage`; resolve keys with `resolveMedia(...)` |
|
|
95
|
+
| A real rich-text editor | swap the `richText` widget via `<ContentForm widgets={{ richText: … }} />` in the editor |
|
|
96
|
+
|
|
97
|
+
## Anti-patterns
|
|
98
|
+
|
|
99
|
+
- **Expecting `*.contentType.ts` to be auto-discovered.** It isn't — register the
|
|
100
|
+
derived entities in `database/schema.ts` or the tables never migrate.
|
|
101
|
+
- **Importing a content-type file into the browser.** The editor fetches
|
|
102
|
+
`content.types` instead; content-type files import the server `@voltro/cms`.
|
|
103
|
+
- **Reusing a reserved field name** (`status`, `tenantId`, `publishAt`, the audit
|
|
104
|
+
columns). `defineContentType` throws — rename the field.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Executor for `content.get`. Reads one row by id from the requested lifecycle
|
|
2
|
+
// table. `ctx.store.query` runs through the tenant-scoped store spine, so the
|
|
3
|
+
// read is automatically confined to the caller's tenant — a foreign id returns
|
|
4
|
+
// nothing.
|
|
5
|
+
import { eq } from '@voltro/database'
|
|
6
|
+
import type { AppContext } from '@voltro/runtime'
|
|
7
|
+
import { contentTypeByName } from '../content'
|
|
8
|
+
|
|
9
|
+
const execute = async (
|
|
10
|
+
input: { type: string; id: string; status?: 'draft' | 'published' },
|
|
11
|
+
ctx: AppContext,
|
|
12
|
+
) => {
|
|
13
|
+
const ct = contentTypeByName.get(input.type)
|
|
14
|
+
if (ct === undefined) return { row: null }
|
|
15
|
+
const table = `${ct.name}_${input.status === 'published' ? 'published' : 'drafts'}`
|
|
16
|
+
const rows = await ctx.store.query({
|
|
17
|
+
table,
|
|
18
|
+
predicate: eq('id', input.id),
|
|
19
|
+
order: [],
|
|
20
|
+
take: 1,
|
|
21
|
+
skip: undefined,
|
|
22
|
+
projection: undefined,
|
|
23
|
+
})
|
|
24
|
+
return { row: rows[0] ?? null }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default execute
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// `content.get` — one content row by id, for the edit form. Returns the full
|
|
2
|
+
// row (arbitrary per-type fields), so the output is a loose record rather than a
|
|
3
|
+
// closed struct. `status` selects the table: 'draft' (the editor's in-progress
|
|
4
|
+
// copy, the default) or 'published' (the live copy). A row outside the caller's
|
|
5
|
+
// tenant reads as absent (null) — the store scope never returns it.
|
|
6
|
+
import { defineAction } from '@voltro/protocol'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const getContent = defineAction({
|
|
10
|
+
name: 'content.get',
|
|
11
|
+
input: Schema.Struct({
|
|
12
|
+
type: Schema.NonEmptyString,
|
|
13
|
+
id: Schema.NonEmptyString,
|
|
14
|
+
status: Schema.optional(Schema.Literal('draft', 'published')),
|
|
15
|
+
}),
|
|
16
|
+
output: Schema.Struct({
|
|
17
|
+
row: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
|
|
18
|
+
}),
|
|
19
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Executor for `content.types`. Projects the registered content types down to
|
|
2
|
+
// the wire-safe descriptor the editor renders forms from (dropping the
|
|
3
|
+
// server-only `derived.transform` function + nested element/struct depth — the
|
|
4
|
+
// two shipped types are flat, and deep widgets fall back to a placeholder).
|
|
5
|
+
import type { AppContext } from '@voltro/runtime'
|
|
6
|
+
import { contentTypes as registered } from '../content'
|
|
7
|
+
|
|
8
|
+
const execute = async (_input: Record<string, never>, _ctx: AppContext) => ({
|
|
9
|
+
types: registered.map((ct) => ({
|
|
10
|
+
name: ct.name,
|
|
11
|
+
displayName: ct.displayName,
|
|
12
|
+
pluralName: ct.pluralName,
|
|
13
|
+
columns: ct.list?.columns ?? Object.keys(ct.fields),
|
|
14
|
+
fields: Object.entries(ct.fields).map(([name, field]) => ({
|
|
15
|
+
name,
|
|
16
|
+
kind: field.kind,
|
|
17
|
+
editorHint: field.editorHint,
|
|
18
|
+
isOptional: field.isOptional,
|
|
19
|
+
...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
|
|
20
|
+
...(field.literals !== undefined ? { literals: field.literals } : {}),
|
|
21
|
+
...(field.referenceType !== undefined ? { referenceType: field.referenceType } : {}),
|
|
22
|
+
})),
|
|
23
|
+
})),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default execute
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `content.types` — the registered content-type descriptors, in a WIRE-SAFE
|
|
2
|
+
// shape. The editor calls this once and renders a `<ContentForm>` per field
|
|
3
|
+
// purely from `editorHint` + the field metadata — no content-type file is
|
|
4
|
+
// imported into the browser bundle, so adding a type on the server surfaces in
|
|
5
|
+
// the editor with zero client changes.
|
|
6
|
+
//
|
|
7
|
+
// A field's `derived.transform` is a FUNCTION (not serialisable) and the editor
|
|
8
|
+
// doesn't need it — the transform runs server-side on save — so the executor
|
|
9
|
+
// projects each field down to the data the form widgets read.
|
|
10
|
+
import { defineAction } from '@voltro/protocol'
|
|
11
|
+
import { Schema } from 'effect'
|
|
12
|
+
|
|
13
|
+
// The browser-facing view of one field: enough to pick + configure a widget.
|
|
14
|
+
const FieldWire = Schema.Struct({
|
|
15
|
+
name: Schema.String,
|
|
16
|
+
kind: Schema.String,
|
|
17
|
+
editorHint: Schema.String,
|
|
18
|
+
isOptional: Schema.Boolean,
|
|
19
|
+
maxLength: Schema.optional(Schema.Number),
|
|
20
|
+
literals: Schema.optional(Schema.Array(Schema.String)),
|
|
21
|
+
referenceType: Schema.optional(Schema.String),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const contentTypes = defineAction({
|
|
25
|
+
name: 'content.types',
|
|
26
|
+
input: Schema.Struct({}),
|
|
27
|
+
output: Schema.Struct({
|
|
28
|
+
types: Schema.Array(
|
|
29
|
+
Schema.Struct({
|
|
30
|
+
name: Schema.String,
|
|
31
|
+
displayName: Schema.String,
|
|
32
|
+
pluralName: Schema.String,
|
|
33
|
+
// An ORDERED list (not a Record) so the editor renders fields in
|
|
34
|
+
// declaration order.
|
|
35
|
+
fields: Schema.Array(FieldWire),
|
|
36
|
+
columns: Schema.Array(Schema.String),
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Executor for `session.me`. Reads the Subject the AuthMiddleware resolved (via
|
|
2
|
+
// voltroPasswordStrategy reading the session cookie) and returns it.
|
|
3
|
+
import type { AppContext } from '@voltro/runtime'
|
|
4
|
+
|
|
5
|
+
const execute = async (_input: Record<string, never>, ctx: AppContext) => {
|
|
6
|
+
const subject = ctx.request.subject as {
|
|
7
|
+
readonly type: string
|
|
8
|
+
readonly id: string | null
|
|
9
|
+
readonly tenantId: string | null
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
type: subject.type,
|
|
13
|
+
id: subject.id ?? null,
|
|
14
|
+
tenantId: subject.tenantId ?? null,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default execute
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `session.me` — the caller's resolved identity. The editor's SSR gate reads it
|
|
2
|
+
// to decide render-vs-redirect-to-login, and it proves the auth loop end to end:
|
|
3
|
+
// no cookie → anonymous Subject; sign in via POST /auth/sign-in → a `user`
|
|
4
|
+
// Subject. Browser-safe descriptor — no server imports here.
|
|
5
|
+
import { defineAction } from '@voltro/protocol'
|
|
6
|
+
import { Schema } from 'effect'
|
|
7
|
+
|
|
8
|
+
export const me = defineAction({
|
|
9
|
+
name: 'session.me',
|
|
10
|
+
input: Schema.Struct({}),
|
|
11
|
+
output: Schema.Struct({
|
|
12
|
+
type: Schema.String,
|
|
13
|
+
id: Schema.NullOr(Schema.String),
|
|
14
|
+
tenantId: Schema.NullOr(Schema.String),
|
|
15
|
+
}),
|
|
16
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The headless CMS backend for the {{projectName}} project. Read by `voltro dev`.
|
|
2
|
+
//
|
|
3
|
+
// It composes @voltro/cms — a LIBRARY, not a plugin — with real editor auth:
|
|
4
|
+
//
|
|
5
|
+
// • @voltro/cms — content types declared as code (`content/*.contentType.ts`
|
|
6
|
+
// via `defineContentType` + the `Schema` field DSL). `contentTypeToEntities`
|
|
7
|
+
// compiles each to a `<type>_drafts` + `<type>_published` table pair (see
|
|
8
|
+
// database/schema.ts), and the write pipeline (`saveDraft` → derive+validate,
|
|
9
|
+
// `publish`/`unpublish`) drives the lifecycle. There is NO framework
|
|
10
|
+
// discovery of `*.contentType.ts` — the schema file imports the types and
|
|
11
|
+
// registers their tables, which is what makes them migrate.
|
|
12
|
+
// • @voltro/plugin-auth — password sign-up/in/out over an HttpOnly session
|
|
13
|
+
// cookie so only signed-in editors reach the write surface, and
|
|
14
|
+
// `voltroPasswordStrategy` resolving that cookie into a typed `Subject`.
|
|
15
|
+
//
|
|
16
|
+
// Content is tenant-scoped: every derived content table carries `tenant()`, so
|
|
17
|
+
// the runtime AND-merges `tenantId = subject.tenantId` into reads and stamps it
|
|
18
|
+
// on writes — one tenant's drafts never leak to another. Boots ZERO-infra
|
|
19
|
+
// (`store: 'memory'`, in-process users); the README shows the production swaps.
|
|
20
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
21
|
+
import { authRoutesPlugin, memoryUserStore, voltroPasswordStrategy } from '@voltro/plugin-auth'
|
|
22
|
+
|
|
23
|
+
export const env = defineEnv({
|
|
24
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
25
|
+
// HMAC key that signs + verifies the session cookie AND the CMS preview
|
|
26
|
+
// tokens (`previewToken` reads VOLTRO_SESSION_SECRET). `generate` means this
|
|
27
|
+
// project mints its OWN key into a gitignored `.env.local` on first boot — no
|
|
28
|
+
// value ships with the template (a shipped placeholder would be a signing key
|
|
29
|
+
// published to everyone who downloads it). Production mints its own the same
|
|
30
|
+
// way; a missing secret is a hard boot failure in serve/build/start.
|
|
31
|
+
VOLTRO_SESSION_SECRET: envVar.secret({
|
|
32
|
+
generate: 'base64url',
|
|
33
|
+
description: 'HMAC key that signs session cookies and CMS preview tokens.',
|
|
34
|
+
}),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// In-process user store — zero infra. `postgresUserStore({ sql })` for durable
|
|
38
|
+
// editor accounts (it manages its own `_voltro_auth_*` tables, auto-migrated).
|
|
39
|
+
const userStore = memoryUserStore()
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
type: 'api' as const,
|
|
43
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
44
|
+
store: 'memory' as const,
|
|
45
|
+
env,
|
|
46
|
+
plugins: [
|
|
47
|
+
authRoutesPlugin({
|
|
48
|
+
store: userStore,
|
|
49
|
+
// New editor sign-ups land in this tenant; all content they author is
|
|
50
|
+
// scoped to it.
|
|
51
|
+
defaultTenantId: 'acme',
|
|
52
|
+
successRedirect: '/',
|
|
53
|
+
cookieSecure: process.env.NODE_ENV === 'production',
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
auth: {
|
|
57
|
+
// Resolves the session cookie into the request Subject the handlers read
|
|
58
|
+
// (`ctx.request.subject`) and the store spine scopes writes by.
|
|
59
|
+
strategies: [voltroPasswordStrategy()],
|
|
60
|
+
},
|
|
61
|
+
}
|