@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,70 @@
|
|
|
1
|
+
// Api app config — a PUBLIC REST API. Read by `voltro dev`.
|
|
2
|
+
//
|
|
3
|
+
// What's wired here:
|
|
4
|
+
// • restRoutes — the four `defineRestRoute` descriptors, registered
|
|
5
|
+
// EXPLICITLY (REST routes are the one primitive that is NOT auto-
|
|
6
|
+
// discovered — they're opt-in public HTTP surface).
|
|
7
|
+
// • openapiPlugin — generates an OpenAPI 3.1 spec from those same
|
|
8
|
+
// descriptors and serves it at GET /openapi.json + a Swagger UI at /docs.
|
|
9
|
+
// Zero hand-maintained API docs: the descriptors ARE the spec.
|
|
10
|
+
// • auth.strategies — an apiKeyStrategy so `Authorization: Bearer <key>`
|
|
11
|
+
// resolves to a scoped Subject. The guarded routes gate on those scopes.
|
|
12
|
+
// • idempotency — POST/PUT/PATCH/DELETE carrying `Idempotency-Key` are
|
|
13
|
+
// deduplicated (replay the first response; 409 while in-flight).
|
|
14
|
+
//
|
|
15
|
+
// `store: 'memory'` keeps first-run zero-infra (no docker). Data resets on
|
|
16
|
+
// every restart — switch to `store: 'postgres'` (+ a local postgres) for
|
|
17
|
+
// anything real.
|
|
18
|
+
|
|
19
|
+
import { apiKeyStrategy } from '@voltro/protocol/apikey'
|
|
20
|
+
import { openapiPlugin } from '@voltro/plugin-openapi'
|
|
21
|
+
import listProducts from './routes/v1/products.list.route'
|
|
22
|
+
import getProduct from './routes/v1/products.get.route'
|
|
23
|
+
import createProduct from './routes/v1/products.create.route'
|
|
24
|
+
import deleteProduct from './routes/v1/products.delete.route'
|
|
25
|
+
|
|
26
|
+
// The descriptors, in one array — handed to BOTH `restRoutes` (to mount them)
|
|
27
|
+
// and `openapiPlugin` (to document them).
|
|
28
|
+
const routes = [listProducts, getProduct, createProduct, deleteProduct]
|
|
29
|
+
|
|
30
|
+
// ─── DEV ONLY ────────────────────────────────────────────────────────────
|
|
31
|
+
// A single hardcoded API key so the guarded routes work the moment you boot:
|
|
32
|
+
//
|
|
33
|
+
// curl -H 'Authorization: Bearer restdemo_devkey' ...
|
|
34
|
+
//
|
|
35
|
+
// The strategy SHA-256-hashes the full bearer token and hands the hash to
|
|
36
|
+
// `resolveKey`; we match it to a fixed scoped Subject. In PRODUCTION, delete
|
|
37
|
+
// the constant below and look the hash up against your own key store (apps
|
|
38
|
+
// store key HASHES, never raw tokens) — or flip on the framework's first-
|
|
39
|
+
// class `apiKeys: true` for admin-gated issue/list/revoke against
|
|
40
|
+
// `_voltro_api_keys`. This demo key is the moral equivalent of a sample
|
|
41
|
+
// password in a README: fine for a starter, never for a real deployment.
|
|
42
|
+
const DEMO_KEY_SHA256 = 'c8917896a6fbdd429ca2410e6e1958953d7421393f649d44fe8fc88c267441cd' // sha256('restdemo_devkey')
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
type: 'api' as const,
|
|
46
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
47
|
+
store: 'memory' as const,
|
|
48
|
+
auth: {
|
|
49
|
+
strategies: [
|
|
50
|
+
apiKeyStrategy({
|
|
51
|
+
prefix: 'restdemo_',
|
|
52
|
+
resolveKey: (hash) =>
|
|
53
|
+
hash === DEMO_KEY_SHA256
|
|
54
|
+
? { id: 'svc_demo', tenantId: 'acme', scopes: ['products:read', 'products:write'] }
|
|
55
|
+
: null,
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
plugins: [
|
|
60
|
+
openapiPlugin({
|
|
61
|
+
routes,
|
|
62
|
+
info: { title: '{{capProjectName}} API', version: '1.0.0' },
|
|
63
|
+
// specPath: '/openapi.json' (default) · docsPath: '/docs' (default)
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
// Public REST endpoints — registered explicitly, NOT auto-discovered.
|
|
67
|
+
restRoutes: routes,
|
|
68
|
+
// Idempotency-Key dedup for the mutating routes (records in _voltro_idempotency).
|
|
69
|
+
idempotency: true,
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Schema for the {{projectName}} public-REST backend.
|
|
2
|
+
//
|
|
3
|
+
// `products` is a plain catalog table the REST routes read + write. It
|
|
4
|
+
// intentionally carries NO mixins — a public REST surface is usually a
|
|
5
|
+
// separate, explicitly-scoped API (the routes own their auth + filtering),
|
|
6
|
+
// so we keep the row shape flat and write `id` / `createdAt` by hand in the
|
|
7
|
+
// handler. (For the tenant()-scoped, reactive-over-WebSocket shape, see the
|
|
8
|
+
// `api-backend` template.)
|
|
9
|
+
//
|
|
10
|
+
// `actors` + `tenants` are the framework's core tables — declared for
|
|
11
|
+
// convention parity even though no mixin references them here.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
databaseHandle,
|
|
15
|
+
id,
|
|
16
|
+
integer,
|
|
17
|
+
table,
|
|
18
|
+
text,
|
|
19
|
+
timestamp,
|
|
20
|
+
type InferRow,
|
|
21
|
+
} from '@voltro/database'
|
|
22
|
+
|
|
23
|
+
// ---------- Core tables ----------
|
|
24
|
+
|
|
25
|
+
export const actors = table('actors', {
|
|
26
|
+
id: id(),
|
|
27
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
28
|
+
displayName: text().nullable(),
|
|
29
|
+
createdAt: timestamp().default('now'),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const tenants = table('tenants', {
|
|
33
|
+
id: id(),
|
|
34
|
+
name: text(),
|
|
35
|
+
createdAt: timestamp().default('now'),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// ---------- Application table ----------
|
|
39
|
+
|
|
40
|
+
export const products = table('products', {
|
|
41
|
+
id: id({ prefix: 'prod' }),
|
|
42
|
+
name: text(),
|
|
43
|
+
priceCents: integer(),
|
|
44
|
+
createdAt: timestamp().default('now'),
|
|
45
|
+
})
|
|
46
|
+
// Opt into the matcher engine so a future reactive WS query over the
|
|
47
|
+
// same table stays live. The REST routes don't need it, but it's free.
|
|
48
|
+
.reactive()
|
|
49
|
+
|
|
50
|
+
export type Product = InferRow<typeof products>
|
|
51
|
+
|
|
52
|
+
export const database = databaseHandle({ actors, tenants, products })
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Shared wire-shape for a product + a row→wire mapper.
|
|
2
|
+
//
|
|
3
|
+
// Imported by every `*.route.tsx` AND by `openapiPlugin({ routes })` (both
|
|
4
|
+
// server-side — REST route files are NOT pulled into the browser rpcGroup,
|
|
5
|
+
// so they're free to import server modules). One source of truth means the
|
|
6
|
+
// generated OpenAPI schema and the runtime encode can never disagree.
|
|
7
|
+
|
|
8
|
+
import { Schema } from 'effect'
|
|
9
|
+
|
|
10
|
+
export const Product = Schema.Struct({
|
|
11
|
+
id: Schema.String,
|
|
12
|
+
name: Schema.String,
|
|
13
|
+
priceCents: Schema.Number,
|
|
14
|
+
// ISO-8601 string on the wire (we stringify the Date in the mapper below).
|
|
15
|
+
createdAt: Schema.String,
|
|
16
|
+
})
|
|
17
|
+
export type Product = Schema.Schema.Type<typeof Product>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Coerce a raw store row into the wire shape. Two store-path quirks this
|
|
21
|
+
* absorbs: a `timestamp` column can come back as a `Date` OR an ISO string,
|
|
22
|
+
* and an `integer` column can come back as a STRING on some dialects — so we
|
|
23
|
+
* normalise both before the strict output Schema encodes them.
|
|
24
|
+
*/
|
|
25
|
+
export const toProduct = (r: Record<string, unknown>): Product => ({
|
|
26
|
+
id: r['id'] as string,
|
|
27
|
+
name: r['name'] as string,
|
|
28
|
+
priceCents: Number(r['priceCents']),
|
|
29
|
+
createdAt: r['createdAt'] instanceof Date ? r['createdAt'].toISOString() : String(r['createdAt']),
|
|
30
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
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/plugin-openapi": "0.1.0",
|
|
18
|
+
"@voltro/protocol": "0.1.0",
|
|
19
|
+
"@voltro/runtime": "0.1.0",
|
|
20
|
+
"effect": "^3.21.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@voltro/testing": "0.1.0",
|
|
24
|
+
"typescript": "^5.7.0",
|
|
25
|
+
"vitest": "^3.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// POST /v1/products — create a product. GUARDED + IDEMPOTENT.
|
|
2
|
+
//
|
|
3
|
+
// - `guards: [requireScope('products:write')]` → 403 unless the resolved
|
|
4
|
+
// Subject carries that scope. The app's `apiKeyStrategy` (see app.config)
|
|
5
|
+
// maps the demo bearer token to a subject WITH the scope; an anonymous
|
|
6
|
+
// caller is rejected.
|
|
7
|
+
// - `idempotency: true` in app.config makes a POST carrying an
|
|
8
|
+
// `Idempotency-Key` header replay-safe: a retry with the same key replays
|
|
9
|
+
// the first response instead of inserting twice.
|
|
10
|
+
//
|
|
11
|
+
// The `body` is parsed from the JSON request body and decoded against the
|
|
12
|
+
// `body` sub-schema (a bad shape → 400 before the handler runs).
|
|
13
|
+
|
|
14
|
+
import { defineRestRoute, requireScope } from '@voltro/protocol/rest'
|
|
15
|
+
import { Schema } from 'effect'
|
|
16
|
+
import { randomUUID } from 'node:crypto'
|
|
17
|
+
import type { DataStore } from '@voltro/runtime'
|
|
18
|
+
import { Product, toProduct } from '../../lib/product'
|
|
19
|
+
|
|
20
|
+
export default defineRestRoute({
|
|
21
|
+
method: 'POST',
|
|
22
|
+
path: '/v1/products',
|
|
23
|
+
input: Schema.Struct({
|
|
24
|
+
body: Schema.Struct({
|
|
25
|
+
name: Schema.NonEmptyString,
|
|
26
|
+
priceCents: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(0)),
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
output: Product,
|
|
30
|
+
summary: 'Create a product',
|
|
31
|
+
guards: [requireScope('products:write')],
|
|
32
|
+
handler: async ({ body }, ctx) => {
|
|
33
|
+
const store = ctx.store as DataStore
|
|
34
|
+
// Explicit id + createdAt — the REST store path doesn't run the mutation
|
|
35
|
+
// middleware that auto-injects them, so we own them here.
|
|
36
|
+
const row = {
|
|
37
|
+
id: `prod_${randomUUID().replace(/-/g, '')}`,
|
|
38
|
+
name: body.name,
|
|
39
|
+
priceCents: body.priceCents,
|
|
40
|
+
createdAt: new Date(),
|
|
41
|
+
}
|
|
42
|
+
await store.insert('products', row as never)
|
|
43
|
+
return toProduct(row)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// DELETE /v1/products/:id — GUARDED. Deletes one product or 404.
|
|
2
|
+
//
|
|
3
|
+
// Same scope guard as create. `DELETE` is one of the idempotent methods the
|
|
4
|
+
// `Idempotency-Key` dedup covers (app.config `idempotency: true`).
|
|
5
|
+
|
|
6
|
+
import { defineRestRoute, requireScope } from '@voltro/protocol/rest'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
import type { DataStore } from '@voltro/runtime'
|
|
9
|
+
|
|
10
|
+
export default defineRestRoute({
|
|
11
|
+
method: 'DELETE',
|
|
12
|
+
path: '/v1/products/:id',
|
|
13
|
+
input: Schema.Struct({
|
|
14
|
+
params: Schema.Struct({ id: Schema.NonEmptyString }),
|
|
15
|
+
}),
|
|
16
|
+
output: Schema.Struct({ id: Schema.String, deleted: Schema.Boolean }),
|
|
17
|
+
summary: 'Delete a product by id',
|
|
18
|
+
guards: [requireScope('products:write')],
|
|
19
|
+
handler: async ({ params }, ctx) => {
|
|
20
|
+
const store = ctx.store as DataStore
|
|
21
|
+
const ok = await store.delete('products', params.id)
|
|
22
|
+
if (!ok) throw { status: 404, message: `product '${params.id}' not found` }
|
|
23
|
+
return { id: params.id, deleted: true }
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// GET /v1/products/:id — public, one product or 404.
|
|
2
|
+
//
|
|
3
|
+
// `:id` is a PATH param — the desugar re-derives it from the matched pattern
|
|
4
|
+
// into `params`. A handler `throw { status, message }` maps to that HTTP code;
|
|
5
|
+
// anything else → 500.
|
|
6
|
+
|
|
7
|
+
import { defineRestRoute } from '@voltro/protocol/rest'
|
|
8
|
+
import { Schema } from 'effect'
|
|
9
|
+
import { eq } from '@voltro/database'
|
|
10
|
+
import type { DataStore } from '@voltro/runtime'
|
|
11
|
+
import { database } from '../../database/schema'
|
|
12
|
+
import { Product, toProduct } from '../../lib/product'
|
|
13
|
+
|
|
14
|
+
export default defineRestRoute({
|
|
15
|
+
method: 'GET',
|
|
16
|
+
path: '/v1/products/:id',
|
|
17
|
+
input: Schema.Struct({
|
|
18
|
+
params: Schema.Struct({ id: Schema.NonEmptyString }),
|
|
19
|
+
}),
|
|
20
|
+
output: Product,
|
|
21
|
+
summary: 'Get a product by id',
|
|
22
|
+
handler: async ({ params }, ctx) => {
|
|
23
|
+
const store = ctx.store as DataStore
|
|
24
|
+
const rows = await store.query(database.products.where(eq('id', params.id)).limit(1).descriptor)
|
|
25
|
+
const row = rows[0]
|
|
26
|
+
if (row === undefined) throw { status: 404, message: `product '${params.id}' not found` }
|
|
27
|
+
return toProduct(row as Record<string, unknown>)
|
|
28
|
+
},
|
|
29
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// GET /v1/products?limit=&cursor= — public, cursor-paginated list.
|
|
2
|
+
//
|
|
3
|
+
// The desugar parses the query string into the `query` shape and decodes it.
|
|
4
|
+
// `Schema.NumberFromString` turns the string query param "20" into a number;
|
|
5
|
+
// we clamp it in the handler. Cursor pagination keys off the sortable `prod_…`
|
|
6
|
+
// TypeID (no OFFSET scan): pass the last row's id back as `cursor`.
|
|
7
|
+
|
|
8
|
+
import { defineRestRoute } from '@voltro/protocol/rest'
|
|
9
|
+
import { Schema } from 'effect'
|
|
10
|
+
import { gt } from '@voltro/database'
|
|
11
|
+
import type { DataStore } from '@voltro/runtime'
|
|
12
|
+
import { database } from '../../database/schema'
|
|
13
|
+
import { Product, toProduct } from '../../lib/product'
|
|
14
|
+
|
|
15
|
+
export default defineRestRoute({
|
|
16
|
+
method: 'GET',
|
|
17
|
+
path: '/v1/products',
|
|
18
|
+
input: Schema.Struct({
|
|
19
|
+
query: Schema.Struct({
|
|
20
|
+
limit: Schema.optional(Schema.NumberFromString),
|
|
21
|
+
cursor: Schema.optional(Schema.String),
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
output: Schema.Struct({
|
|
25
|
+
data: Schema.Array(Product),
|
|
26
|
+
nextCursor: Schema.NullOr(Schema.String),
|
|
27
|
+
}),
|
|
28
|
+
summary: 'List products',
|
|
29
|
+
description: 'Cursor-paginated. Pass the response `nextCursor` back as `cursor` for the next page.',
|
|
30
|
+
handler: async ({ query }, ctx) => {
|
|
31
|
+
const store = ctx.store as DataStore
|
|
32
|
+
const limit = Math.min(Math.max(query.limit ?? 20, 1), 100)
|
|
33
|
+
|
|
34
|
+
const base = query.cursor
|
|
35
|
+
? database.products.where(gt('id', query.cursor))
|
|
36
|
+
: database.products
|
|
37
|
+
const rows = await store.query(base.orderBy('id', 'asc').limit(limit).descriptor)
|
|
38
|
+
|
|
39
|
+
const data = rows.map((r) => toProduct(r as Record<string, unknown>))
|
|
40
|
+
const nextCursor = data.length === limit ? data[data.length - 1]!.id : null
|
|
41
|
+
return { data, nextCursor }
|
|
42
|
+
},
|
|
43
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-rest",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Public REST API: defineRestRoute endpoints (GET/POST/DELETE, query+path+body) + scope guards + Idempotency-Key + @voltro/plugin-openapi (Swagger UI at /docs).",
|
|
5
|
+
"tags": ["api", "rest", "openapi", "swagger", "public-api"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Unit test with `@voltro/testing` — no database, no running HTTP server. A
|
|
2
|
+
// `defineRestRoute` carries its logic in a plain `handler(input, ctx)`, so the
|
|
3
|
+
// test calls that handler with a `RestRouteContext` built over the SAME
|
|
4
|
+
// mixin-wrapped `ctx.store` a route gets at runtime, and asserts the wire shape
|
|
5
|
+
// it returns. The REST routing + guard + idempotency layers are dispatch-time
|
|
6
|
+
// concerns (integration), not exercised here. Run with `voltro test` (vitest).
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
10
|
+
import { anonymousSubject } from '@voltro/protocol'
|
|
11
|
+
import type { RestRouteContext } from '@voltro/protocol/rest'
|
|
12
|
+
import { database } from '../database/schema' // registers products / actors / tenants
|
|
13
|
+
import createProduct from '../routes/v1/products.create.route'
|
|
14
|
+
|
|
15
|
+
// A REST handler receives `{ subject, headers, store }` (not the full rpc
|
|
16
|
+
// AppContext). Build that shape over the real test store so `store.insert`
|
|
17
|
+
// runs the same mixin/validation path it does in production. `products` carries
|
|
18
|
+
// no tenant() mixin, so the subject identity is irrelevant to the insert.
|
|
19
|
+
const restCtx = (store: unknown): RestRouteContext => ({
|
|
20
|
+
subject: anonymousSubject(null),
|
|
21
|
+
headers: {},
|
|
22
|
+
store,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('POST /v1/products', () => {
|
|
26
|
+
it('inserts a product and returns the wire shape', async () => {
|
|
27
|
+
const ctx = makeTestContext({ store: mockStore({ products: [] }) })
|
|
28
|
+
const product = await createProduct.handler(
|
|
29
|
+
{ body: { name: 'Widget', priceCents: 1999 } },
|
|
30
|
+
restCtx(ctx.store),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
expect(product.name).toBe('Widget')
|
|
34
|
+
expect(product.priceCents).toBe(1999)
|
|
35
|
+
expect(product.id).toMatch(/^prod_/) // handler-minted id
|
|
36
|
+
// `createdAt` is normalised to an ISO-8601 string by the row→wire mapper.
|
|
37
|
+
expect(typeof product.createdAt).toBe('string')
|
|
38
|
+
expect(Number.isNaN(Date.parse(product.createdAt))).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('persists the row to the store (readable back through the descriptor)', async () => {
|
|
42
|
+
const ctx = makeTestContext({ store: mockStore({ products: [] }) })
|
|
43
|
+
const product = await createProduct.handler(
|
|
44
|
+
{ body: { name: 'Gadget', priceCents: 500 } },
|
|
45
|
+
restCtx(ctx.store),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const rows = await ctx.store.query(database.products.descriptor)
|
|
49
|
+
expect(rows).toHaveLength(1)
|
|
50
|
+
expect(rows[0]!['id']).toBe(product.id)
|
|
51
|
+
expect(rows[0]!['name']).toBe('Gadget')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — the SaaS plugin bundle
|
|
2
|
+
|
|
3
|
+
Four cross-cutting **SaaS plugins** wired turnkey, in one cohesive domain — the
|
|
4
|
+
billing / notifications / analytics / presence slice of the framework end to
|
|
5
|
+
end. A single `projects.create` mutation pulls three of them together.
|
|
6
|
+
|
|
7
|
+
## What it demonstrates
|
|
8
|
+
|
|
9
|
+
- **Billing** ([`@voltro/plugin-billing`](https://voltro.cloud/docs/plugins/overview)) —
|
|
10
|
+
subscriptions + entitlements over the in-memory `mock` provider. The `free`
|
|
11
|
+
plan caps `projects` at 3; `requireEntitlement(ctx, 'projects', 1)` gates the
|
|
12
|
+
create and fails typed **`EntitlementExceeded`** on the 4th.
|
|
13
|
+
- **Notifications** ([`@voltro/plugin-notifications`](https://voltro.cloud/docs/plugins/overview)) —
|
|
14
|
+
one `NotificationService.send(...)` across channels + a durable in-app inbox.
|
|
15
|
+
- **Analytics** ([`@voltro/plugin-analytics-postgres`](https://voltro.cloud/docs/plugins/overview)) —
|
|
16
|
+
`useAnalytics().track('project_created', …)`. Noop-safe out of the box; one
|
|
17
|
+
config line + a SQL store makes it durable (see below).
|
|
18
|
+
- **Presence** ([`@voltro/plugin-presence`](https://voltro.cloud/docs/plugins/overview)) —
|
|
19
|
+
ephemeral "who's online": `presence.heartbeat` / `list` / `leave` + the
|
|
20
|
+
`usePresence(channel)` hook.
|
|
21
|
+
|
|
22
|
+
Each plugin contributes its OWN routes + tables automatically — you wire them in
|
|
23
|
+
`app.config.ts` and consume their services in handlers. Boots **zero-infra**
|
|
24
|
+
(`store: 'memory'`).
|
|
25
|
+
|
|
26
|
+
## The bundle, in one handler
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// mutations/projects.create.mutation.server.ts (Effect-mode)
|
|
30
|
+
yield* requireEntitlement(ctx, 'projects', 1) // billing quota gate
|
|
31
|
+
const row = yield* (yield* EffectStore).insert('projects', { name: input.name })
|
|
32
|
+
yield* (yield* useAnalytics()).track({ name: 'project_created', subjectId: ctx.request.subject.id })
|
|
33
|
+
yield* Effect.promise(() => (yield* NotificationService).send({
|
|
34
|
+
to: ctx.request.subject.id ?? 'system', category: 'project', title: 'Project created', body: `"${input.name}" is live.`,
|
|
35
|
+
}))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Run it
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
voltro dev .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`projects.create` / `projects.list` and the plugin routes (`billing.*`,
|
|
45
|
+
`presence.*`, `notifications.*`) are rpc procedures — call them from a web app
|
|
46
|
+
with `useMutation` / `useSubscription`, or over HTTP via the dev inspect endpoint:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Create projects — the free plan allows 3
|
|
50
|
+
for i in 1 2 3; do
|
|
51
|
+
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
|
|
52
|
+
-H 'content-type: application/json' \
|
|
53
|
+
-d "{\"tag\":\"projects.create\",\"input\":{\"name\":\"Project $i\"}}"
|
|
54
|
+
done
|
|
55
|
+
# → { "ok": true, "result": { "id": "proj_…", … } }
|
|
56
|
+
|
|
57
|
+
# The 4th trips the billing entitlement
|
|
58
|
+
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
|
|
59
|
+
-H 'content-type: application/json' \
|
|
60
|
+
-d '{"tag":"projects.create","input":{"name":"Project 4"}}'
|
|
61
|
+
# → { "ok": false, "error": { "_tag": "EntitlementExceeded", "entitlement": "projects", "limit": 3, … } }
|
|
62
|
+
|
|
63
|
+
# Presence — announce yourself, then read the roster
|
|
64
|
+
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
|
|
65
|
+
-H 'content-type: application/json' \
|
|
66
|
+
-d '{"tag":"presence.heartbeat","input":{"channel":"lobby"}}'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Enable durable analytics
|
|
70
|
+
|
|
71
|
+
`useAnalytics().track(...)` is noop-safe without a sink. To STORE + query events
|
|
72
|
+
(`aggregate` / `timeseries` / `topN`), add a sink AND a SQL store — the
|
|
73
|
+
postgres-lite sink needs a `SqlClient`:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// app.config.ts
|
|
77
|
+
import { postgresAnalytics } from '@voltro/plugin-analytics-postgres'
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
type: 'api' as const, name: 'AcmeApi',
|
|
81
|
+
store: 'postgres' as const, // or 'sqlite' (file:./.voltro/db.sqlite)
|
|
82
|
+
analytics: postgresAnalytics(), // events → _voltro_events
|
|
83
|
+
plugins: [ /* … */ ],
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Going to production
|
|
88
|
+
|
|
89
|
+
| Want… | Do |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Real subscriptions / checkout | `billingPlugin({ provider: 'stripe', apiKey, webhookSecret, plans })` with real `priceId`s |
|
|
92
|
+
| Email / Slack / SMS notifications | add `emailChannel()` / `webhookChannel()` / `smsChannel()` to `channels` |
|
|
93
|
+
| Durable analytics + warehouse | `postgresAnalytics()` on postgres, or swap to `@voltro/plugin-clickhouse` / `@voltro/plugin-duckdb` at scale |
|
|
94
|
+
| Live presence roster in the UI | subscribe a reactive query over `_voltro_presence`, or use `usePresence(channel)` |
|
|
95
|
+
|
|
96
|
+
## Anti-patterns
|
|
97
|
+
|
|
98
|
+
- **Authoring your own `notifications.*` / `billing.*` / `presence.*` procedures.**
|
|
99
|
+
The plugins own those tag namespaces — boot fails on a collision. Use the
|
|
100
|
+
plugin routes + services, don't hand-roll them.
|
|
101
|
+
- **Expecting analytics to persist on `memory`.** The postgres-lite sink needs
|
|
102
|
+
a SqlClient. `track()` is reachable + safe on memory, but events only land in
|
|
103
|
+
`_voltro_events` once you wire a sink + a SQL store.
|
|
104
|
+
- **Skipping the entitlement gate on a paid action.** A scope says "may you call
|
|
105
|
+
this"; an entitlement says "do you have quota left". A create-project action
|
|
106
|
+
needs the quota check — `requireEntitlement` is one line.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Api app config — the SaaS plugin bundle. Read by `voltro dev`.
|
|
2
|
+
//
|
|
3
|
+
// Four cross-cutting SaaS plugins, wired turnkey. Each contributes its own
|
|
4
|
+
// routes + tables; the `projects.create` mutation pulls three of them together
|
|
5
|
+
// (billing entitlement gate → analytics event → notification).
|
|
6
|
+
//
|
|
7
|
+
// `store: 'memory'` keeps first-run zero-infra. NOTE the analytics caveat below.
|
|
8
|
+
|
|
9
|
+
import { billingPlugin } from '@voltro/plugin-billing'
|
|
10
|
+
import { presencePlugin } from '@voltro/plugin-presence'
|
|
11
|
+
import { notificationsPlugin, consoleChannel } from '@voltro/plugin-notifications'
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
type: 'api' as const,
|
|
15
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
16
|
+
store: 'memory' as const,
|
|
17
|
+
plugins: [
|
|
18
|
+
// ── Billing ──────────────────────────────────────────────────────────
|
|
19
|
+
// Subscriptions + entitlements over a pluggable provider. 'mock' is the
|
|
20
|
+
// in-memory provider (no Stripe key) — great for dev. `plans` is the single
|
|
21
|
+
// source of tier→quota truth; the `free` default caps `projects` at 3, so
|
|
22
|
+
// the 4th `projects.create` fails typed `EntitlementExceeded`. Swap
|
|
23
|
+
// provider:'stripe' (+ STRIPE_SECRET_KEY) + real priceIds for production.
|
|
24
|
+
billingPlugin({
|
|
25
|
+
provider: 'mock',
|
|
26
|
+
plans: {
|
|
27
|
+
free: { entitlements: { projects: 3 } },
|
|
28
|
+
pro: { priceId: 'price_demo_pro', entitlements: { projects: 'unlimited' } },
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
// ── Notifications ────────────────────────────────────────────────────
|
|
32
|
+
// One `send` across channels + a durable in-app inbox (added automatically).
|
|
33
|
+
// consoleChannel() prints to stdout for dev; add email/webhook/sms/push for
|
|
34
|
+
// real delivery. Reach the inbox from the browser with useInbox()/useUnreadCount().
|
|
35
|
+
notificationsPlugin({ channels: [consoleChannel()] }),
|
|
36
|
+
// ── Presence ─────────────────────────────────────────────────────────
|
|
37
|
+
// Ephemeral "who's online" — presence.heartbeat / list / leave routes +
|
|
38
|
+
// the usePresence(channel) hook. Swept `_voltro_presence` table; cross-instance.
|
|
39
|
+
presencePlugin(),
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
// ── Analytics ──────────────────────────────────────────────────────────
|
|
43
|
+
// `useAnalytics().track(...)` is called in projects.create and is NOOP-SAFE
|
|
44
|
+
// with no sink configured (it never crashes) — so the event API is reachable
|
|
45
|
+
// on the zero-infra memory store. To actually STORE + query events, add a
|
|
46
|
+
// sink AND a SQL store (the postgres-lite sink needs a SqlClient):
|
|
47
|
+
//
|
|
48
|
+
// import { postgresAnalytics } from '@voltro/plugin-analytics-postgres'
|
|
49
|
+
// store: 'postgres' as const, // or 'sqlite' (file:./.voltro/db.sqlite)
|
|
50
|
+
// analytics: postgresAnalytics(), // events → _voltro_events; aggregate/timeseries/topN
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Schema for the {{projectName}} SaaS backend.
|
|
2
|
+
//
|
|
3
|
+
// One tenant-scoped `projects` table — the SaaS plugins (billing entitlements,
|
|
4
|
+
// notifications, analytics, presence) layer on top via `app.config.ts`; they
|
|
5
|
+
// contribute their OWN tables (`_voltro_billing_*`, `notification_*`,
|
|
6
|
+
// `_voltro_events`, `_voltro_presence`) automatically, so you don't declare them.
|
|
7
|
+
//
|
|
8
|
+
// `actors` + `tenants` are the framework's core tables (the audit / tenant
|
|
9
|
+
// mixins reference them).
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
databaseHandle,
|
|
13
|
+
id,
|
|
14
|
+
table,
|
|
15
|
+
text,
|
|
16
|
+
timestamp,
|
|
17
|
+
type InferRow,
|
|
18
|
+
} from '@voltro/database'
|
|
19
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
20
|
+
|
|
21
|
+
// ---------- Core tables ----------
|
|
22
|
+
|
|
23
|
+
export const actors = table('actors', {
|
|
24
|
+
id: id(),
|
|
25
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
26
|
+
displayName: text().nullable(),
|
|
27
|
+
createdAt: timestamp().default('now'),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const tenants = table('tenants', {
|
|
31
|
+
id: id(),
|
|
32
|
+
name: text(),
|
|
33
|
+
createdAt: timestamp().default('now'),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// ---------- Application table ----------
|
|
37
|
+
|
|
38
|
+
export const projects = table('projects', {
|
|
39
|
+
id: id({ prefix: 'proj' }),
|
|
40
|
+
name: text(),
|
|
41
|
+
})
|
|
42
|
+
// tenant() pulls audit() transitively → tenantId + createdAt / updatedAt /
|
|
43
|
+
// createdBy / updatedBy (auto-stamped). Creating a project is gated by the
|
|
44
|
+
// billing `projects` entitlement (see mutations/projects.create).
|
|
45
|
+
.with(tenant())
|
|
46
|
+
.reactive()
|
|
47
|
+
|
|
48
|
+
export type Project = InferRow<typeof projects>
|
|
49
|
+
|
|
50
|
+
export const database = databaseHandle({ actors, tenants, projects })
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// One mutation, three plugins. Effect-mode so it can `yield*` the plugin
|
|
2
|
+
// services the framework provides in the per-request stack:
|
|
3
|
+
//
|
|
4
|
+
// 1. BILLING — requireEntitlement check-and-decrements the tenant's
|
|
5
|
+
// `projects` quota (free = 3). Over quota → typed
|
|
6
|
+
// EntitlementExceeded, the row is never written.
|
|
7
|
+
// 2. ANALYTICS — useAnalytics().track(...) records a `project_created`
|
|
8
|
+
// event. Noop-safe with no sink configured (see app.config).
|
|
9
|
+
// 3. NOTIFICATIONS — NotificationService.send(...) drops a note in the
|
|
10
|
+
// owner's in-app inbox + the console channel.
|
|
11
|
+
|
|
12
|
+
import { Effect } from 'effect'
|
|
13
|
+
import type { AppContext } from '@voltro/runtime'
|
|
14
|
+
import { EffectStore, useAnalytics } from '@voltro/runtime'
|
|
15
|
+
import { requireEntitlement } from '@voltro/plugin-billing'
|
|
16
|
+
import { NotificationService } from '@voltro/plugin-notifications'
|
|
17
|
+
|
|
18
|
+
const execute = (input: { name: string }, ctx: AppContext) =>
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
// 1. Billing gate — fails closed (typed EntitlementExceeded) when the
|
|
21
|
+
// tenant has no `projects` quota left. Runs BEFORE the write.
|
|
22
|
+
yield* requireEntitlement(ctx, 'projects', 1)
|
|
23
|
+
|
|
24
|
+
// 2. Write the row. `tenant()` auto-stamps tenantId + audit columns.
|
|
25
|
+
const store = yield* EffectStore
|
|
26
|
+
const row = yield* store.insert('projects', { name: input.name })
|
|
27
|
+
|
|
28
|
+
// 3. Analytics — fire-and-forget event.
|
|
29
|
+
const analytics = yield* useAnalytics()
|
|
30
|
+
yield* analytics.track({
|
|
31
|
+
name: 'project_created',
|
|
32
|
+
subjectId: ctx.request.subject.id,
|
|
33
|
+
properties: { name: input.name },
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// 4. Notify the owner (in-app inbox + console).
|
|
37
|
+
const notify = yield* NotificationService
|
|
38
|
+
yield* Effect.promise(() => notify.send({
|
|
39
|
+
to: ctx.request.subject.id ?? 'system',
|
|
40
|
+
category: 'project',
|
|
41
|
+
title: 'Project created',
|
|
42
|
+
body: `"${input.name}" is live.`,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: row['id'] as string,
|
|
47
|
+
name: row['name'] as string,
|
|
48
|
+
tenantId: row['tenantId'] as string,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
export default execute
|