@voltro/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +20236 -0
- package/bin/voltro.mjs +43 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/commands-CfPH2Wf4.js +18061 -0
- package/dist/frameworkInspectState-CX2250XB.js +86 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +5 -0
- package/dist/inspectState.d.ts +116 -0
- package/dist/inspectState.js +2 -0
- package/dist/startup.d.ts +26 -0
- package/dist/startup.js +2 -0
- package/dist/startupRunner-CRhuUl91.js +71 -0
- package/package.json +88 -0
- package/templates/AGENTS.core.md +258 -0
- package/templates/AGENTS.md +351 -0
- package/templates/agent-docs/_index.md +93 -0
- package/templates/agent-docs/_manifest.json +655 -0
- package/templates/agent-docs/ai.md +1845 -0
- package/templates/agent-docs/authentication.md +1788 -0
- package/templates/agent-docs/caching.md +624 -0
- package/templates/agent-docs/cli.md +1650 -0
- package/templates/agent-docs/configuration.md +295 -0
- package/templates/agent-docs/data.md +2172 -0
- package/templates/agent-docs/database/advancedqueries.md +1583 -0
- package/templates/agent-docs/database/columntypes.md +1200 -0
- package/templates/agent-docs/database/hosting.md +881 -0
- package/templates/agent-docs/database/migrations.md +2938 -0
- package/templates/agent-docs/database/misc.md +270 -0
- package/templates/agent-docs/database/overview.md +108 -0
- package/templates/agent-docs/database/querying.md +1622 -0
- package/templates/agent-docs/database/scaling.md +331 -0
- package/templates/agent-docs/database/schema.md +1458 -0
- package/templates/agent-docs/database/seedsdialects.md +1235 -0
- package/templates/agent-docs/database/transactions.md +285 -0
- package/templates/agent-docs/deployment.md +999 -0
- package/templates/agent-docs/internationalization.md +359 -0
- package/templates/agent-docs/introduction.md +438 -0
- package/templates/agent-docs/multi-tenancy.md +610 -0
- package/templates/agent-docs/observability.md +350 -0
- package/templates/agent-docs/plugins.md +1273 -0
- package/templates/agent-docs/reference.md +978 -0
- package/templates/agent-docs/routing.md +1553 -0
- package/templates/agent-docs/scheduling.md +666 -0
- package/templates/agent-docs/schema-driven-ui.md +607 -0
- package/templates/agent-docs/security.md +42 -0
- package/templates/agent-docs/templates/apibackends.md +3214 -0
- package/templates/agent-docs/templates/appshells.md +2062 -0
- package/templates/agent-docs/templates/custom.md +128 -0
- package/templates/agent-docs/templates/overview.md +122 -0
- package/templates/agent-docs/templates/serverless.md +315 -0
- package/templates/agent-docs/testing.md +376 -0
- package/templates/agent-docs/workflows.md +1351 -0
- package/templates/apps/api-ai/README.md +105 -0
- package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
- package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
- package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
- package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
- package/templates/apps/api-ai/app.config.ts +57 -0
- package/templates/apps/api-ai/database/schema.ts +67 -0
- package/templates/apps/api-ai/package.json +27 -0
- package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
- package/templates/apps/api-ai/template.json +6 -0
- package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
- package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
- package/templates/apps/api-ai/tsconfig.json +5 -0
- package/templates/apps/api-auth/.env +17 -0
- package/templates/apps/api-auth/README.md +109 -0
- package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
- package/templates/apps/api-auth/actions/me.action.ts +19 -0
- package/templates/apps/api-auth/app.config.ts +59 -0
- package/templates/apps/api-auth/database/schema.ts +26 -0
- package/templates/apps/api-auth/package.json +28 -0
- package/templates/apps/api-auth/template.json +6 -0
- package/templates/apps/api-auth/tests/me.test.ts +37 -0
- package/templates/apps/api-auth/tsconfig.json +5 -0
- package/templates/apps/api-backend/README.md +25 -0
- package/templates/apps/api-backend/app.config.ts +39 -0
- package/templates/apps/api-backend/database/schema.ts +57 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend/package.json +28 -0
- package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend/template.json +6 -0
- package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend/tsconfig.json +5 -0
- package/templates/apps/api-backend-deactivation/README.md +49 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
- package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
- package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
- package/templates/apps/api-backend-deactivation/package.json +28 -0
- package/templates/apps/api-backend-deactivation/template.json +6 -0
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
- package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
- package/templates/apps/api-backend-mail/README.md +39 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
- package/templates/apps/api-backend-mail/app.config.ts +34 -0
- package/templates/apps/api-backend-mail/database/schema.ts +57 -0
- package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mail/package.json +30 -0
- package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mail/template.json +6 -0
- package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mail/tsconfig.json +5 -0
- package/templates/apps/api-backend-mariadb/.env.example +35 -0
- package/templates/apps/api-backend-mariadb/README.md +31 -0
- package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
- package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mariadb/package.json +30 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mariadb/template.json +6 -0
- package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/README.md +86 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
- package/templates/apps/api-backend-storage/app.config.ts +38 -0
- package/templates/apps/api-backend-storage/database/schema.ts +57 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-storage/package.json +27 -0
- package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-storage/template.json +6 -0
- package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-storage/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/.env +17 -0
- package/templates/apps/api-data-advanced/README.md +88 -0
- package/templates/apps/api-data-advanced/app.config.ts +56 -0
- package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
- package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
- package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
- package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
- package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
- package/templates/apps/api-data-advanced/database/index.ts +23 -0
- package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
- package/templates/apps/api-data-advanced/package.json +28 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
- package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
- package/templates/apps/api-data-advanced/template.json +6 -0
- package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
- package/templates/apps/api-data-advanced/tsconfig.json +5 -0
- package/templates/apps/api-durable/README.md +81 -0
- package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
- package/templates/apps/api-durable/app.config.ts +41 -0
- package/templates/apps/api-durable/database/schema.ts +65 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
- package/templates/apps/api-durable/package.json +28 -0
- package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
- package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
- package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
- package/templates/apps/api-durable/template.json +6 -0
- package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
- package/templates/apps/api-durable/tsconfig.json +5 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
- package/templates/apps/api-feature-flags/README.md +63 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
- package/templates/apps/api-feature-flags/app.config.ts +41 -0
- package/templates/apps/api-feature-flags/database/schema.ts +37 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
- package/templates/apps/api-feature-flags/package.json +29 -0
- package/templates/apps/api-feature-flags/template.json +6 -0
- package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
- package/templates/apps/api-feature-flags/tsconfig.json +5 -0
- package/templates/apps/api-governance/.env +4 -0
- package/templates/apps/api-governance/README.md +61 -0
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
- package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
- package/templates/apps/api-governance/app.config.ts +35 -0
- package/templates/apps/api-governance/database/schema.ts +41 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
- package/templates/apps/api-governance/package.json +29 -0
- package/templates/apps/api-governance/template.json +6 -0
- package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
- package/templates/apps/api-governance/tsconfig.json +5 -0
- package/templates/apps/api-kv/README.md +100 -0
- package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
- package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
- package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
- package/templates/apps/api-kv/app.config.ts +48 -0
- package/templates/apps/api-kv/database/schema.ts +62 -0
- package/templates/apps/api-kv/package.json +28 -0
- package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
- package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
- package/templates/apps/api-kv/template.json +6 -0
- package/templates/apps/api-kv/tests/sync.test.ts +103 -0
- package/templates/apps/api-kv/tsconfig.json +5 -0
- package/templates/apps/api-moderation/README.md +46 -0
- package/templates/apps/api-moderation/app.config.ts +33 -0
- package/templates/apps/api-moderation/database/schema.ts +45 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
- package/templates/apps/api-moderation/package.json +29 -0
- package/templates/apps/api-moderation/template.json +6 -0
- package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
- package/templates/apps/api-moderation/tsconfig.json +5 -0
- package/templates/apps/api-observability/README.md +106 -0
- package/templates/apps/api-observability/app.config.ts +32 -0
- package/templates/apps/api-observability/database/schema.ts +38 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
- package/templates/apps/api-observability/package.json +29 -0
- package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
- package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
- package/templates/apps/api-observability/template.json +6 -0
- package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
- package/templates/apps/api-observability/tsconfig.json +5 -0
- package/templates/apps/api-ratelimit/README.md +49 -0
- package/templates/apps/api-ratelimit/app.config.ts +42 -0
- package/templates/apps/api-ratelimit/database/schema.ts +37 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
- package/templates/apps/api-ratelimit/package.json +29 -0
- package/templates/apps/api-ratelimit/template.json +6 -0
- package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
- package/templates/apps/api-ratelimit/tsconfig.json +5 -0
- package/templates/apps/api-rbac/README.md +59 -0
- package/templates/apps/api-rbac/app.config.ts +48 -0
- package/templates/apps/api-rbac/database/schema.ts +37 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
- package/templates/apps/api-rbac/package.json +29 -0
- package/templates/apps/api-rbac/template.json +6 -0
- package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
- package/templates/apps/api-rbac/tsconfig.json +5 -0
- package/templates/apps/api-rest/README.md +85 -0
- package/templates/apps/api-rest/app.config.ts +70 -0
- package/templates/apps/api-rest/database/schema.ts +52 -0
- package/templates/apps/api-rest/lib/product.ts +30 -0
- package/templates/apps/api-rest/package.json +27 -0
- package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
- package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
- package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
- package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
- package/templates/apps/api-rest/template.json +6 -0
- package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
- package/templates/apps/api-rest/tsconfig.json +5 -0
- package/templates/apps/api-saas/README.md +106 -0
- package/templates/apps/api-saas/app.config.ts +51 -0
- package/templates/apps/api-saas/database/schema.ts +50 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas/package.json +31 -0
- package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
- package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas/template.json +6 -0
- package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
- package/templates/apps/api-saas/tsconfig.json +5 -0
- package/templates/apps/api-search/README.md +68 -0
- package/templates/apps/api-search/app.config.ts +31 -0
- package/templates/apps/api-search/database/schema.ts +47 -0
- package/templates/apps/api-search/lib/search.ts +27 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
- package/templates/apps/api-search/package.json +29 -0
- package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
- package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
- package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
- package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
- package/templates/apps/api-search/template.json +6 -0
- package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
- package/templates/apps/api-search/tsconfig.json +5 -0
- package/templates/apps/api-versioning/README.md +51 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
- package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
- package/templates/apps/api-versioning/app.config.ts +24 -0
- package/templates/apps/api-versioning/database/schema.ts +38 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
- package/templates/apps/api-versioning/package.json +29 -0
- package/templates/apps/api-versioning/template.json +6 -0
- package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
- package/templates/apps/api-versioning/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/.env +6 -0
- package/templates/apps/api-webhooks/README.md +106 -0
- package/templates/apps/api-webhooks/app.config.ts +16 -0
- package/templates/apps/api-webhooks/database/schema.ts +49 -0
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
- package/templates/apps/api-webhooks/package.json +28 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
- package/templates/apps/api-webhooks/template.json +6 -0
- package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
- package/templates/apps/api-webhooks/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
- package/templates/apps/changelog/README.md +77 -0
- package/templates/apps/changelog/app.config.ts +27 -0
- package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
- package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
- package/templates/apps/changelog/package.json +30 -0
- package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
- package/templates/apps/changelog/src/globals.css +66 -0
- package/templates/apps/changelog/src/globals.d.ts +16 -0
- package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
- package/templates/apps/changelog/src/lib/locale.ts +55 -0
- package/templates/apps/changelog/src/lib/releases.ts +21 -0
- package/templates/apps/changelog/src/locales/de.ts +22 -0
- package/templates/apps/changelog/src/locales/en.ts +30 -0
- package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
- package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
- package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
- package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
- package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
- package/templates/apps/changelog/src/pages/index.tsx +55 -0
- package/templates/apps/changelog/src/pages/layout.tsx +84 -0
- package/templates/apps/changelog/template.json +6 -0
- package/templates/apps/changelog/tsconfig.json +11 -0
- package/templates/apps/edge-functions/README.md +55 -0
- package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
- package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
- package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
- package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
- package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
- package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
- package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
- package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
- package/templates/apps/edge-functions/package.json +21 -0
- package/templates/apps/edge-functions/template.json +6 -0
- package/templates/apps/edge-functions/tsconfig.json +10 -0
- package/templates/apps/frontend-admin/README.md +40 -0
- package/templates/apps/frontend-admin/app.config.ts +37 -0
- package/templates/apps/frontend-admin/package.json +30 -0
- package/templates/apps/frontend-admin/src/config.ts +8 -0
- package/templates/apps/frontend-admin/src/globals.css +76 -0
- package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
- package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
- package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
- package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
- package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
- package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
- package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
- package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
- package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
- package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-admin/template.json +6 -0
- package/templates/apps/frontend-admin/tsconfig.json +11 -0
- package/templates/apps/frontend-app/README.md +98 -0
- package/templates/apps/frontend-app/app.config.ts +43 -0
- package/templates/apps/frontend-app/package.json +30 -0
- package/templates/apps/frontend-app/src/locales/de.ts +36 -0
- package/templates/apps/frontend-app/src/locales/en.ts +43 -0
- package/templates/apps/frontend-app/src/locales/index.ts +15 -0
- package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
- package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
- package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
- package/templates/apps/frontend-app/template.json +6 -0
- package/templates/apps/frontend-app/tsconfig.json +11 -0
- package/templates/apps/frontend-blank/README.md +18 -0
- package/templates/apps/frontend-blank/app.config.ts +29 -0
- package/templates/apps/frontend-blank/package.json +29 -0
- package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
- package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
- package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
- package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
- package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
- package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
- package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
- package/templates/apps/frontend-blank/template.json +6 -0
- package/templates/apps/frontend-blank/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/README.md +65 -0
- package/templates/apps/frontend-contact/app.config.ts +25 -0
- package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
- package/templates/apps/frontend-contact/package.json +33 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
- package/templates/apps/frontend-contact/src/config.ts +12 -0
- package/templates/apps/frontend-contact/src/globals.css +84 -0
- package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
- package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
- package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
- package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
- package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
- package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
- package/templates/apps/frontend-contact/template.json +6 -0
- package/templates/apps/frontend-contact/tsconfig.json +11 -0
- package/templates/apps/frontend-dashboard/README.md +54 -0
- package/templates/apps/frontend-dashboard/app.config.ts +39 -0
- package/templates/apps/frontend-dashboard/package.json +29 -0
- package/templates/apps/frontend-dashboard/src/config.ts +8 -0
- package/templates/apps/frontend-dashboard/src/globals.css +72 -0
- package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
- package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
- package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
- package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
- package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-dashboard/template.json +6 -0
- package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
- package/templates/apps/frontend-docs/README.md +19 -0
- package/templates/apps/frontend-docs/app.config.ts +25 -0
- package/templates/apps/frontend-docs/package.json +29 -0
- package/templates/apps/frontend-docs/src/globals.css +32 -0
- package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
- package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
- package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
- package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
- package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-docs/template.json +6 -0
- package/templates/apps/frontend-docs/tsconfig.json +11 -0
- package/templates/apps/frontend-i18n/README.md +61 -0
- package/templates/apps/frontend-i18n/app.config.ts +33 -0
- package/templates/apps/frontend-i18n/package.json +28 -0
- package/templates/apps/frontend-i18n/src/globals.css +46 -0
- package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
- package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
- package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
- package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
- package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
- package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
- package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
- package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
- package/templates/apps/frontend-i18n/template.json +6 -0
- package/templates/apps/frontend-i18n/tsconfig.json +11 -0
- package/templates/apps/frontend-landing/README.md +17 -0
- package/templates/apps/frontend-landing/app.config.ts +25 -0
- package/templates/apps/frontend-landing/package.json +29 -0
- package/templates/apps/frontend-landing/src/globals.css +23 -0
- package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
- package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
- package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
- package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
- package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-landing/template.json +6 -0
- package/templates/apps/frontend-landing/tsconfig.json +11 -0
- package/templates/apps/frontend-spa/README.md +45 -0
- package/templates/apps/frontend-spa/app.config.ts +27 -0
- package/templates/apps/frontend-spa/package.json +29 -0
- package/templates/apps/frontend-spa/src/globals.css +84 -0
- package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
- package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
- package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
- package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
- package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
- package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-spa/template.json +6 -0
- package/templates/apps/frontend-spa/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr/README.md +68 -0
- package/templates/apps/frontend-ssr/app.config.ts +32 -0
- package/templates/apps/frontend-ssr/package.json +29 -0
- package/templates/apps/frontend-ssr/src/globals.css +67 -0
- package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
- package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
- package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
- package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
- package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
- package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
- package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
- package/templates/apps/frontend-ssr/template.json +6 -0
- package/templates/apps/frontend-ssr/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr-api/README.md +50 -0
- package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
- package/templates/apps/frontend-ssr-api/package.json +30 -0
- package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
- package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
- package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
- package/templates/apps/frontend-ssr-api/template.json +6 -0
- package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
- package/templates/apps/frontend-static-blog/README.md +49 -0
- package/templates/apps/frontend-static-blog/app.config.ts +34 -0
- package/templates/apps/frontend-static-blog/package.json +28 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
- package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
- package/templates/apps/frontend-static-blog/src/globals.css +75 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
- package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
- package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
- package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
- package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
- package/templates/apps/frontend-static-blog/template.json +6 -0
- package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
- package/templates/baselines/bare/.env.example +37 -0
- package/templates/baselines/bare/README.md +47 -0
- package/templates/baselines/bare/baseline.json +31 -0
- package/templates/baselines/bare/deploy/README.md +43 -0
- package/templates/baselines/bare/deploy/voltro.service.example +36 -0
- package/templates/baselines/compose/.env.example +46 -0
- package/templates/baselines/compose/README.md +69 -0
- package/templates/baselines/compose/baseline.json +51 -0
- package/templates/baselines/compose/docker/.dockerignore +38 -0
- package/templates/baselines/compose/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose/docker-compose.dev.yml +89 -0
- package/templates/baselines/compose/docker-compose.prod.yml +87 -0
- package/templates/baselines/compose/docker-compose.yml +41 -0
- package/templates/baselines/compose-mariadb/.env.example +57 -0
- package/templates/baselines/compose-mariadb/README.md +78 -0
- package/templates/baselines/compose-mariadb/baseline.json +51 -0
- package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
- package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
- package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
- package/templates/baselines/helm/.env.example +39 -0
- package/templates/baselines/helm/README.md +98 -0
- package/templates/baselines/helm/baseline.json +53 -0
- package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
- package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
- package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
- package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
- package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
- package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
- package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
- package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
- package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
- package/templates/baselines/helm/deploy/README.md +94 -0
- package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
|
@@ -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
|
+
"migrate": "voltro migrate",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"test": "voltro test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@react-email/components": "^1.0.12",
|
|
14
|
+
"@react-email/render": "^1.4.0",
|
|
15
|
+
"@voltro/cli": "0.1.0",
|
|
16
|
+
"@voltro/database": "0.1.0",
|
|
17
|
+
"@voltro/env": "0.1.0",
|
|
18
|
+
"@voltro/plugin-mail": "0.1.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.1.0",
|
|
20
|
+
"@voltro/protocol": "0.1.0",
|
|
21
|
+
"@voltro/runtime": "0.1.0",
|
|
22
|
+
"effect": "^3.21.2",
|
|
23
|
+
"react": "^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@voltro/testing": "0.1.0",
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"vitest": "^3.0.0"
|
|
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-mail",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Voltro backend with transactional email wired: @voltro/plugin-mail + a React-Email welcome template you preview/send from the dashboard. Same minimal base as api-backend.",
|
|
5
|
+
"tags": ["api", "backend", "mail", "email", "starter"]
|
|
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
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# {{capProjectName}} — MariaDB backend env. Copy to `.env` and adjust.
|
|
2
|
+
|
|
3
|
+
# ── Database (MariaDB + binlog CDC) ────────────────────────────────────
|
|
4
|
+
DB_DIALECT=mariadb
|
|
5
|
+
DB_URL=mysql://app:app@localhost:3307/{{projectNameSnake}}
|
|
6
|
+
# Push-based real-time across replicas via the binlog reader. Requires the
|
|
7
|
+
# DB to run with binlog_format=ROW + binlog_row_image=FULL (the compose
|
|
8
|
+
# baseline sets these) and a user with REPLICATION SLAVE, REPLICATION
|
|
9
|
+
# CLIENT. Set CDC=0 to fall back to single-process inline emit.
|
|
10
|
+
CDC=1
|
|
11
|
+
|
|
12
|
+
# ── File storage (A2) — MinIO locally, S3 in prod ──────────────────────
|
|
13
|
+
STORAGE_PROVIDER=minio
|
|
14
|
+
S3_ENDPOINT=http://localhost:9000
|
|
15
|
+
S3_BUCKET={{projectNameSnake}}
|
|
16
|
+
S3_ACCESS_KEY_ID=minioadmin
|
|
17
|
+
S3_SECRET_ACCESS_KEY=minioadmin
|
|
18
|
+
S3_FORCE_PATH_STYLE=1
|
|
19
|
+
|
|
20
|
+
# ── AI runtime (A3) — set a real provider + key to enable agents ───────
|
|
21
|
+
AI_PROVIDER=mock
|
|
22
|
+
# AI_PROVIDER=anthropic
|
|
23
|
+
# AI_MODEL=claude-opus-4-7
|
|
24
|
+
# ANTHROPIC_API_KEY=
|
|
25
|
+
|
|
26
|
+
# ── Auth ───────────────────────────────────────────────────────────────
|
|
27
|
+
# HMAC key for signed sessions — REQUIRED in production (refuses to boot
|
|
28
|
+
# without it). Generate: openssl rand -hex 32
|
|
29
|
+
VOLTRO_SESSION_SECRET=dev-only-change-me
|
|
30
|
+
|
|
31
|
+
# ── Cluster (K8s, >1 replica) ──────────────────────────────────────────
|
|
32
|
+
# The workflow runner must advertise a routable host so a workflow can
|
|
33
|
+
# resume on another pod. Inject POD_IP via the downward API in prod
|
|
34
|
+
# (fieldRef: status.podIP); leave unset for single-node local dev.
|
|
35
|
+
# VOLTRO_WORKFLOW_RUNNER_HOST=
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro backend scaffold (template: **api-backend-mariadb**) — MariaDB-backed
|
|
4
|
+
with binlog CDC real-time + file storage.
|
|
5
|
+
|
|
6
|
+
## Boot
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm install # at the repo root
|
|
10
|
+
cp .env.example .env # then adjust DB_URL / secrets
|
|
11
|
+
# Start MariaDB (binlog ROW) + MinIO — see the compose baseline.
|
|
12
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
13
|
+
# → http://localhost:4000
|
|
14
|
+
# → ws://localhost:4000/ws
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What's in here
|
|
18
|
+
|
|
19
|
+
| File | Role |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `app.config.ts` | `store: 'mariadb'` (binlog CDC) + the storage plugin. |
|
|
22
|
+
| `.env.example` | DB / storage / AI / auth / cluster env. |
|
|
23
|
+
| `database/schema.ts` | One example table (`notes`) with the `tenant()` mixin. |
|
|
24
|
+
| `queries/notes.*` | Streaming subscription example. |
|
|
25
|
+
| `mutations/notes.create.*` | Tenant-guarded mutation example. |
|
|
26
|
+
|
|
27
|
+
`store: 'mariadb'` fans subscriptions out across replicas via the binary
|
|
28
|
+
log — the DB must run with `binlog_format=ROW` + `binlog_row_image=FULL`
|
|
29
|
+
and a replication user (the compose baseline sets this up). Drop more
|
|
30
|
+
`*.query.ts` + `*.query.server.ts`, `*.mutation.ts` + `*.mutation.server.ts`, `*.action.ts` + `*.action.server.ts`, or `*.stream.ts` + `*.stream.server.ts`, `*.workflow.tsx`, or `*.cron.tsx` files
|
|
31
|
+
anywhere in this tree — discovery is by file convention.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Backend app for the {{projectName}} project, backed by MariaDB. Read by
|
|
2
|
+
// `voltro dev` (local) and `voltro serve` (production).
|
|
3
|
+
//
|
|
4
|
+
// `store: 'mariadb'` turns on the binlog CDC reader: real-time
|
|
5
|
+
// subscriptions fan out across every replica via the database's binary
|
|
6
|
+
// log (no Redis/NATS — the binlog IS the message bus). The DB must run
|
|
7
|
+
// with `binlog_format=ROW` + `binlog_row_image=FULL` and a user holding
|
|
8
|
+
// `REPLICATION SLAVE, REPLICATION CLIENT` — the compose baseline sets all
|
|
9
|
+
// of this up; in prod the Helm baseline wires the per-pod `server_id`.
|
|
10
|
+
//
|
|
11
|
+
// `plugins` composes cross-cutting behaviour. File storage is on by
|
|
12
|
+
// default (MinIO locally, S3 in prod).
|
|
13
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
14
|
+
import { storagePlugin } from '@voltro/plugin-storage'
|
|
15
|
+
// Auth strategies (uncomment + configure to enable — see `auth` below):
|
|
16
|
+
// import { jwtBearerStrategy } from '@voltro/protocol'
|
|
17
|
+
// import { apiKeyStrategy } from '@voltro/protocol/apikey'
|
|
18
|
+
|
|
19
|
+
// Typed environment — declare your env once; it's validated at boot (fail-fast)
|
|
20
|
+
// and read on the server via `serverEnv` / `getSecret` from '@voltro/env/server'.
|
|
21
|
+
// `access` is required: 'public' (browser-safe) or 'secret' (server-only, never
|
|
22
|
+
// bundled). Run `voltro env` to see the manifest. See the Environment docs.
|
|
23
|
+
export const env = defineEnv({
|
|
24
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
25
|
+
// Add your own, e.g.:
|
|
26
|
+
// STRIPE_KEY: envVar.string({ access: 'secret' }),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
type: 'api' as const,
|
|
31
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
32
|
+
store: 'mariadb' as const,
|
|
33
|
+
env,
|
|
34
|
+
plugins: [
|
|
35
|
+
// Blobs in object storage; metadata in `_voltro_storage_refs`.
|
|
36
|
+
// Provider resolved from `STORAGE_PROVIDER` (minio | s3 | filesystem).
|
|
37
|
+
storagePlugin(),
|
|
38
|
+
],
|
|
39
|
+
|
|
40
|
+
// Auth: the built-in signed-cookie password strategy ALWAYS runs first;
|
|
41
|
+
// these run after it, in order, until one matches or rejects. The same
|
|
42
|
+
// chain serves dev and prod. Uncomment + configure for your IdP / keys.
|
|
43
|
+
//
|
|
44
|
+
// auth: {
|
|
45
|
+
// strategies: [
|
|
46
|
+
// // JWKS cookie sessions — verifies a JWT from your IdP against its
|
|
47
|
+
// // JWKS endpoint and maps a claim to the active org (tenantId).
|
|
48
|
+
// jwtBearerStrategy({
|
|
49
|
+
// id: 'idp',
|
|
50
|
+
// jwksUrl: process.env.JWKS_URL!,
|
|
51
|
+
// issuer: process.env.JWT_ISSUER,
|
|
52
|
+
// cookieName: '{{projectName}}-session',
|
|
53
|
+
// claimsToTenantId: (claims) => (claims.org as string) ?? null,
|
|
54
|
+
// }),
|
|
55
|
+
// // `{{projectName}}_`-prefixed API keys: the strategy strips the
|
|
56
|
+
// // prefix, sha256-hashes the token, and hands you the hash —
|
|
57
|
+
// // look it up in your key table and return id/tenantId/scopes.
|
|
58
|
+
// apiKeyStrategy({
|
|
59
|
+
// prefix: '{{projectName}}_',
|
|
60
|
+
// resolveKey: async (hash) => {
|
|
61
|
+
// // const row = await lookupApiKeyByHash(hash)
|
|
62
|
+
// // return row ? { id: row.id, tenantId: row.orgId, scopes: row.scopes } : null
|
|
63
|
+
// return null
|
|
64
|
+
// },
|
|
65
|
+
// }),
|
|
66
|
+
// ],
|
|
67
|
+
// },
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
// Opt into the matcher engine: subscriptions against `notes` get a
|
|
52
|
+
// fresh snapshot/delta whenever a mutation writes the table.
|
|
53
|
+
.reactive()
|
|
54
|
+
|
|
55
|
+
export type Note = InferRow<typeof notes>
|
|
56
|
+
|
|
57
|
+
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
|
+
"migrate": "voltro migrate",
|
|
9
|
+
"test": "voltro test",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@effect/platform": "^0.96.1",
|
|
14
|
+
"@effect/rpc": "^0.75.1",
|
|
15
|
+
"@voltro/cli": "0.1.0",
|
|
16
|
+
"@voltro/database": "0.1.0",
|
|
17
|
+
"@voltro/env": "0.1.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.1.0",
|
|
19
|
+
"@voltro/plugin-storage": "0.1.0",
|
|
20
|
+
"@voltro/protocol": "0.1.0",
|
|
21
|
+
"@voltro/runtime": "0.1.0",
|
|
22
|
+
"@voltro/sql-mysql": "0.1.0",
|
|
23
|
+
"effect": "^3.21.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@voltro/testing": "0.1.0",
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"vitest": "^3.0.0"
|
|
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-mariadb",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "MariaDB-backed Voltro backend: binlog CDC real-time, file storage (MinIO/S3), tenant-aware schema. The dogfooded shape for K8s multi-replica apps.",
|
|
5
|
+
"tags": ["api", "backend", "mariadb", "cdc", "storage", "k8s"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Unit test with `@voltro/testing` — no database, no running server. The
|
|
2
|
+
// MariaDB store dialect is IRRELEVANT here: `makeTestContext` runs the handler
|
|
3
|
+
// against the in-memory harness, so tenant auto-scoping + audit auto-fill
|
|
4
|
+
// behave exactly as at runtime regardless of the production store. Importing
|
|
5
|
+
// `../database/schema` registers the `notes` / `actors` / `tenants` tables
|
|
6
|
+
// globally. 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
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro backend scaffold (template: **api-backend-storage**).
|
|
4
|
+
|
|
5
|
+
## Boot
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install # at the repo root
|
|
9
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
10
|
+
# → http://localhost:4000
|
|
11
|
+
# → ws://localhost:4000/ws
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What's in here
|
|
15
|
+
|
|
16
|
+
| File | Role |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `app.config.ts` | App declaration + `storagePlugin` wired (`memory` provider). |
|
|
19
|
+
| `database/schema.ts` | One example table (`notes`) with the `tenant()` mixin. |
|
|
20
|
+
| `queries/` + `mutations/` | Streaming subscription + tenant-guarded mutation examples. |
|
|
21
|
+
| `actions/uploadAvatar.{rpc,handler}.ts` | `storage.uploadAvatar` — stores a PUBLIC avatar, returns its URL. |
|
|
22
|
+
| `actions/uploadDocument.{rpc,handler}.ts` | `storage.uploadDocument` — stores a PRIVATE doc (owner + admin + tenant policy). |
|
|
23
|
+
|
|
24
|
+
## Storage
|
|
25
|
+
|
|
26
|
+
File storage is wired out of the box via `@voltro/plugin-storage`:
|
|
27
|
+
|
|
28
|
+
- **Public objects** (`visibility: 'public'`) are served direct from the
|
|
29
|
+
bucket/CDN — set `cdnBaseUrl` on the plugin for production. The app's
|
|
30
|
+
`/_voltro/storage/:id` route is only a fallback for the `memory` /
|
|
31
|
+
`filesystem` providers in dev.
|
|
32
|
+
- **Private objects** (default) are gated by an **access policy** — any-of
|
|
33
|
+
rules over `owner` / `roles` / `groups` / `scopes` / `tenant` / `apiKey`
|
|
34
|
+
/ `password` / a custom `guard` — PLUS **per-object grants** (share a
|
|
35
|
+
file with a specific user / group / api-key, optionally expiring).
|
|
36
|
+
- **Browse + share** stored objects from the dashboard's **Storage** tab
|
|
37
|
+
(`/_voltro/inspect/plugins/storage/*`).
|
|
38
|
+
|
|
39
|
+
### Delivering a private file
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// In an action/handler — access-checked, returns a presigned URL (s3) or
|
|
43
|
+
// an app URL carrying a short-lived signed grant token (fs/memory):
|
|
44
|
+
import { StorageService } from '@voltro/plugin-storage'
|
|
45
|
+
const storage = yield* StorageService
|
|
46
|
+
const url = yield* storage.mintUrl(fileId, ctx.request.subject)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or use the plugin's built-in typed RPC routes from the client:
|
|
50
|
+
`storage.upload`, `storage.mintUrl`, `storage.mintUploadUrl` (presigned
|
|
51
|
+
PUT), `storage.share`, `storage.revoke`, `storage.listGrants`.
|
|
52
|
+
|
|
53
|
+
### Switching providers
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
storagePlugin({ provider: 's3', bucket: 'my-bucket', region: 'us-east-1',
|
|
57
|
+
cdnBaseUrl: 'https://cdn.example.com' }) // + S3_* env
|
|
58
|
+
storagePlugin({ provider: 'minio', bucket: 'my-bucket', endpoint: 'http://localhost:9000' })
|
|
59
|
+
storagePlugin({ provider: 'azure', bucket: 'my-container', accountName: 'acct' }) // + AZURE_STORAGE_KEY
|
|
60
|
+
storagePlugin({ provider: 'filesystem', root: '.voltro-storage' })
|
|
61
|
+
storagePlugin({ provider: 'database' }) // bytes in your DB — zero extra infra
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Any S3-compatible bucket works via `endpoint` (AWS, Cloudflare R2, GCS,
|
|
65
|
+
Backblaze B2, MinIO, Wasabi). `azure` covers Azure Blob; `database` keeps
|
|
66
|
+
small blobs in the app's DB. `_voltro_storage_refs` +
|
|
67
|
+
`_voltro_storage_grants` (+ `_voltro_storage_blobs` for the `database`
|
|
68
|
+
provider) are auto-migrated on `store: 'postgres'`.
|
|
69
|
+
|
|
70
|
+
### Constraints, virus scanning & image transforms
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { storagePlugin, clamavScanner } from '@voltro/plugin-storage'
|
|
74
|
+
|
|
75
|
+
storagePlugin({
|
|
76
|
+
provider: 'memory',
|
|
77
|
+
limits: { maxBytes: 10_000_000, allowedContentTypes: ['image/*', 'application/pdf'], sniff: true },
|
|
78
|
+
scan: clamavScanner({ host: '127.0.0.1', port: 3310 }), // optional — needs a clamd
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **Constraints** reject oversized / wrong-type uploads (`sniff` magic-byte-checks
|
|
83
|
+
the bytes) with a typed `StorageRejected`.
|
|
84
|
+
- **Virus scan** runs on every `put`; a detection fails with `StorageScanRejected`.
|
|
85
|
+
- **Image transforms** are on-the-fly via the serve URL — `?w=400&format=webp&q=80`
|
|
86
|
+
(needs the optional `sharp` dependency).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import type { AppContext } from '@voltro/runtime'
|
|
3
|
+
import { StorageService } from '@voltro/plugin-storage'
|
|
4
|
+
|
|
5
|
+
const execute = (input: { bytesBase64: string; contentType: string }, ctx: AppContext) =>
|
|
6
|
+
Effect.gen(function* () {
|
|
7
|
+
const storage = yield* StorageService
|
|
8
|
+
const bytes = new Uint8Array(Buffer.from(input.bytesBase64, 'base64'))
|
|
9
|
+
const ref = yield* storage.put({
|
|
10
|
+
bytes,
|
|
11
|
+
contentType: input.contentType,
|
|
12
|
+
visibility: 'public',
|
|
13
|
+
ownerId: ctx.request.subject.id,
|
|
14
|
+
tenantId: ctx.request.subject.tenantId,
|
|
15
|
+
key: `avatars/${ctx.request.subject.id ?? 'anon'}/avatar`,
|
|
16
|
+
})
|
|
17
|
+
const url = yield* storage.getUrl(ref.id)
|
|
18
|
+
return { id: ref.id, url }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export default execute
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// `storage.uploadAvatar` — stores a PUBLIC avatar. Public objects are
|
|
2
|
+
// served direct from the bucket/CDN; the returned `url` is what you'd put
|
|
3
|
+
// in an <img src>. File upload is external I/O → an action.
|
|
4
|
+
//
|
|
5
|
+
// For large files prefer a presigned direct-to-bucket upload via the
|
|
6
|
+
// plugin's built-in `storage.mintUploadUrl` route.
|
|
7
|
+
|
|
8
|
+
import { Schema } from 'effect'
|
|
9
|
+
import { defineAction } from '@voltro/protocol'
|
|
10
|
+
|
|
11
|
+
export const uploadAvatar = defineAction({
|
|
12
|
+
name: 'storage.uploadAvatar',
|
|
13
|
+
input: Schema.Struct({
|
|
14
|
+
bytesBase64: Schema.String,
|
|
15
|
+
contentType: Schema.String,
|
|
16
|
+
}),
|
|
17
|
+
output: Schema.Struct({
|
|
18
|
+
id: Schema.String,
|
|
19
|
+
url: Schema.String,
|
|
20
|
+
}),
|
|
21
|
+
})
|