@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,33 @@
|
|
|
1
|
+
// Content-moderation backend for the {{projectName}} project.
|
|
2
|
+
//
|
|
3
|
+
// `moderationPlugin` runs on the rpc interceptors BEFORE the handler: it checks
|
|
4
|
+
// the named input fields against a provider and, per rule, either BLOCKS the
|
|
5
|
+
// call (typed `ContentRejected` — the write never commits) or FLAGS it for
|
|
6
|
+
// review (the write commits, the item lands in the review queue). `keywordProvider`
|
|
7
|
+
// is a zero-dep deny-list; `aiProvider()` is an optional LLM classifier.
|
|
8
|
+
// Config-only, zero infra.
|
|
9
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
10
|
+
import { moderationPlugin, keywordProvider } from '@voltro/plugin-moderation'
|
|
11
|
+
|
|
12
|
+
export const env = defineEnv({
|
|
13
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
type: 'api' as const,
|
|
18
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
19
|
+
store: 'memory' as const,
|
|
20
|
+
plugins: [
|
|
21
|
+
moderationPlugin({
|
|
22
|
+
provider: keywordProvider(['spam', 'scam', 'phishing', 'malware']),
|
|
23
|
+
rules: [
|
|
24
|
+
// BLOCK: a post whose title/body trips the deny-list is rejected
|
|
25
|
+
// (ContentRejected) before it's ever written.
|
|
26
|
+
{ match: 'posts.create', fields: ['title', 'body'], action: 'block' },
|
|
27
|
+
// FLAG: a comment is still written, but flagged for review.
|
|
28
|
+
{ match: 'comments.create', fields: ['body'], action: 'flag' },
|
|
29
|
+
],
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
env,
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Schema for the {{projectName}} moderation backend — `posts` (block rule) and
|
|
2
|
+
// `comments` (flag rule). `actors` + `tenants` are the framework core tables.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
databaseHandle,
|
|
6
|
+
id,
|
|
7
|
+
table,
|
|
8
|
+
text,
|
|
9
|
+
timestamp,
|
|
10
|
+
type InferRow,
|
|
11
|
+
} from '@voltro/database'
|
|
12
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
13
|
+
|
|
14
|
+
export const actors = table('actors', {
|
|
15
|
+
id: id(),
|
|
16
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
17
|
+
displayName: text().nullable(),
|
|
18
|
+
createdAt: timestamp().default('now'),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const tenants = table('tenants', {
|
|
22
|
+
id: id(),
|
|
23
|
+
name: text(),
|
|
24
|
+
createdAt: timestamp().default('now'),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const posts = table('posts', {
|
|
28
|
+
id: id({ prefix: 'post' }),
|
|
29
|
+
title: text(),
|
|
30
|
+
body: text(),
|
|
31
|
+
})
|
|
32
|
+
.with(tenant())
|
|
33
|
+
.reactive()
|
|
34
|
+
|
|
35
|
+
export const comments = table('comments', {
|
|
36
|
+
id: id({ prefix: 'cmt' }),
|
|
37
|
+
body: text(),
|
|
38
|
+
})
|
|
39
|
+
.with(tenant())
|
|
40
|
+
.reactive()
|
|
41
|
+
|
|
42
|
+
export type Post = InferRow<typeof posts>
|
|
43
|
+
export type Comment = InferRow<typeof comments>
|
|
44
|
+
|
|
45
|
+
export const database = databaseHandle({ actors, tenants, posts, comments })
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
|
|
3
|
+
// Plain insert — a `flag` rule lets the write through (and records the flag);
|
|
4
|
+
// only a `block` rule would have stopped us. tenant() auto-fills tenantId.
|
|
5
|
+
const execute = async (input: { body: string }, ctx: AppContext) =>
|
|
6
|
+
ctx.store.insert('comments', { body: input.body })
|
|
7
|
+
|
|
8
|
+
export default execute
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Creates a comment. The `flag`-action moderation rule (app.config.ts) checks
|
|
2
|
+
// the body, but unlike `block` it does NOT reject — the comment is written and
|
|
3
|
+
// flagged for review (the dashboard's Moderation panel surfaces the queue).
|
|
4
|
+
// So there's no moderation error on this procedure.
|
|
5
|
+
|
|
6
|
+
import { defineMutation } from '@voltro/protocol'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const createComment = defineMutation({
|
|
10
|
+
name: 'comments.create',
|
|
11
|
+
target: { table: 'comments', op: 'insert' },
|
|
12
|
+
input: Schema.Struct({
|
|
13
|
+
body: Schema.String,
|
|
14
|
+
}),
|
|
15
|
+
output: Schema.Struct({
|
|
16
|
+
id: Schema.String,
|
|
17
|
+
body: Schema.String,
|
|
18
|
+
tenantId: Schema.String,
|
|
19
|
+
}),
|
|
20
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
|
|
3
|
+
// Plain insert — the `block` moderation rule already rejected banned content
|
|
4
|
+
// before we got here, so this only runs on clean input. tenant() auto-fills
|
|
5
|
+
// tenantId from the subject.
|
|
6
|
+
const execute = async (input: { title: string; body: string }, ctx: AppContext) =>
|
|
7
|
+
ctx.store.insert('posts', { title: input.title, body: input.body })
|
|
8
|
+
|
|
9
|
+
export default execute
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Creates a post. The `block`-action moderation rule (app.config.ts) checks
|
|
2
|
+
// title + body BEFORE this handler runs; banned content fails typed
|
|
3
|
+
// `ContentRejected` and the write never commits. There's no moderation code
|
|
4
|
+
// in the handler. Declaring `error: ContentRejected` surfaces it to the client
|
|
5
|
+
// typed. `ContentRejected` comes from the browser-safe `/errors` subpath.
|
|
6
|
+
|
|
7
|
+
import { defineMutation } from '@voltro/protocol'
|
|
8
|
+
import { ContentRejected } from '@voltro/plugin-moderation/errors'
|
|
9
|
+
import { Schema } from 'effect'
|
|
10
|
+
|
|
11
|
+
export const createPost = defineMutation({
|
|
12
|
+
name: 'posts.create',
|
|
13
|
+
target: { table: 'posts', op: 'insert' },
|
|
14
|
+
input: Schema.Struct({
|
|
15
|
+
title: Schema.NonEmptyString,
|
|
16
|
+
body: Schema.String,
|
|
17
|
+
}),
|
|
18
|
+
output: Schema.Struct({
|
|
19
|
+
id: Schema.String,
|
|
20
|
+
title: Schema.String,
|
|
21
|
+
body: Schema.String,
|
|
22
|
+
tenantId: Schema.String,
|
|
23
|
+
}),
|
|
24
|
+
error: ContentRejected,
|
|
25
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
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-moderation": "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
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@voltro/testing": "0.1.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-moderation",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Pre-commit content moderation — @voltro/plugin-moderation checks named input fields BEFORE the handler: a block rule rejects banned content with typed ContentRejected (write never commits), a flag rule writes it but queues it for review. keywordProvider (zero-dep deny-list) or aiProvider. Memory store, zero infra.",
|
|
5
|
+
"tags": ["api", "moderation", "content", "trust-safety", "plugin"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Unit test for `posts.create` — driven through `@voltro/testing`.
|
|
2
|
+
//
|
|
3
|
+
// Content moderation is enforced by the plugin's rpc INTERCEPTOR (a block rule
|
|
4
|
+
// wired in app.config.ts), which runs BEFORE this executor and rejects banned
|
|
5
|
+
// content with a typed `ContentRejected` — a dispatch-time middleware, NOT the
|
|
6
|
+
// executor. `invoke` reproduces only the input-Schema decode, not middleware
|
|
7
|
+
// (see its header), so we DON'T fake the interceptor: we test the executor's own
|
|
8
|
+
// observable behavior (the tenant-stamped insert) + pin the descriptor's
|
|
9
|
+
// declared `ContentRejected` error shape. Run with `voltro test` (vitest).
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { Schema } from 'effect'
|
|
13
|
+
import { makeTestContext, mockStore, invoke } from '@voltro/testing'
|
|
14
|
+
import { database } from '../database/schema' // registers actors / tenants / posts / comments
|
|
15
|
+
import { createPost } from '../mutations/posts.create.mutation'
|
|
16
|
+
import createPostHandler from '../mutations/posts.create.mutation.server'
|
|
17
|
+
import { ContentRejected } from '@voltro/plugin-moderation/errors'
|
|
18
|
+
|
|
19
|
+
const subject = { type: 'user', id: 'u1', tenantId: 'acme' } as const
|
|
20
|
+
|
|
21
|
+
describe('posts.create', () => {
|
|
22
|
+
it('inserts a post stamped with the caller tenant + a post_ TypeID', async () => {
|
|
23
|
+
const ctx = makeTestContext({ subject, store: mockStore({ posts: [] }) })
|
|
24
|
+
|
|
25
|
+
const row = (await invoke(
|
|
26
|
+
createPost,
|
|
27
|
+
createPostHandler,
|
|
28
|
+
{ title: 'Hello', body: 'World' },
|
|
29
|
+
ctx,
|
|
30
|
+
)) as { id: string; title: string; body: string; tenantId: string }
|
|
31
|
+
|
|
32
|
+
expect(row.title).toBe('Hello')
|
|
33
|
+
expect(row.body).toBe('World')
|
|
34
|
+
expect(row.tenantId).toBe('acme') // tenant() stamped it from the subject
|
|
35
|
+
expect(row.id).toMatch(/^post_/)
|
|
36
|
+
|
|
37
|
+
// The row is really in the store — the live tenant-scoped query sees it.
|
|
38
|
+
const rows = await ctx.store.query(database.posts.descriptor)
|
|
39
|
+
expect(rows).toHaveLength(1)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('rejects an empty title at the input-schema decode (NonEmptyString)', async () => {
|
|
43
|
+
const ctx = makeTestContext({ subject, store: mockStore({ posts: [] }) })
|
|
44
|
+
|
|
45
|
+
await expect(
|
|
46
|
+
invoke(createPost, createPostHandler, { title: '', body: '' }, ctx),
|
|
47
|
+
).rejects.toThrow()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('isolates tenants — a second tenant never sees the first tenant post', async () => {
|
|
51
|
+
const ctx = makeTestContext({
|
|
52
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
53
|
+
store: mockStore({ posts: [] }),
|
|
54
|
+
})
|
|
55
|
+
await invoke(createPost, createPostHandler, { title: 'secret', body: '' }, ctx)
|
|
56
|
+
|
|
57
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.posts.descriptor))
|
|
58
|
+
expect(seenByT2).toHaveLength(0)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('pins the descriptor: name, input decode + the declared ContentRejected error', async () => {
|
|
62
|
+
// The moderation error is a dispatch-time interceptor concern, not reachable
|
|
63
|
+
// from the executor here — so we pin the CONTRACT the client relies on: the
|
|
64
|
+
// rpc tag, that the input Schema decodes a valid post, and that the typed
|
|
65
|
+
// ContentRejected error the interceptor raises is declared on the procedure.
|
|
66
|
+
expect(createPost.name).toBe('posts.create')
|
|
67
|
+
|
|
68
|
+
const decoded = Schema.decodeUnknownSync(createPost.input)({ title: 'ok', body: 'b' })
|
|
69
|
+
expect(decoded).toEqual({ title: 'ok', body: 'b' })
|
|
70
|
+
|
|
71
|
+
const rejected = new ContentRejected({
|
|
72
|
+
tag: 'posts.create',
|
|
73
|
+
categories: ['profanity'],
|
|
74
|
+
reason: 'banned term',
|
|
75
|
+
})
|
|
76
|
+
expect(rejected).toBeInstanceOf(ContentRejected)
|
|
77
|
+
expect(Schema.is(ContentRejected)(rejected)).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — observability + testing
|
|
2
|
+
|
|
3
|
+
The production-readiness slice in one backend: **Prometheus metrics**, **Sentry
|
|
4
|
+
error tracking**, **distributed tracing**, and a **`@voltro/testing` unit test**
|
|
5
|
+
— so a real service is observable AND covered from day one.
|
|
6
|
+
|
|
7
|
+
## What it demonstrates
|
|
8
|
+
|
|
9
|
+
- **Prometheus** ([`@voltro/plugin-prometheus`](https://voltro.cloud/docs/observability/metrics)) —
|
|
10
|
+
`GET /metrics` in the text exposition format. The framework already records
|
|
11
|
+
`voltro_rpc_*` / `voltro_http_*` / subscription / cache metrics; `notes.create`
|
|
12
|
+
adds a **custom counter** (`app_notes_created_total`) that shows up automatically.
|
|
13
|
+
- **Sentry** ([`@voltro/plugin-sentry`](https://voltro.cloud/docs/observability/errors)) —
|
|
14
|
+
rpc / REST / workflow / render errors reported correlated to the active
|
|
15
|
+
`traceId`. **Inert without `SENTRY_DSN`**, so it ships wired — set the env to turn it on.
|
|
16
|
+
- **Tracing** — on by default in `voltro dev` (DevTools Traces panel + `traceId`
|
|
17
|
+
on every log line). Set `OTEL_EXPORTER_OTLP_ENDPOINT` to ship to Jaeger / Tempo / Honeycomb.
|
|
18
|
+
- **Testing** ([`@voltro/testing`](https://voltro.cloud/docs/testing)) — a unit
|
|
19
|
+
test that runs the `notes.create` handler with `makeTestContext` + `mockStore`
|
|
20
|
+
(no DB, no server) and asserts the **real** tenant scoping.
|
|
21
|
+
|
|
22
|
+
## Run it
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
voltro dev .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Metrics
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Create a note (custom counter increments)
|
|
32
|
+
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
|
|
33
|
+
-H 'content-type: application/json' \
|
|
34
|
+
-d '{"tag":"notes.create","input":{"title":"Hello","body":"World"}}'
|
|
35
|
+
|
|
36
|
+
# Scrape Prometheus — your custom counter + the framework's rpc metrics
|
|
37
|
+
curl -s http://localhost:4000/metrics | grep -E 'app_notes_created_total|voltro_rpc_requests_total'
|
|
38
|
+
# app_notes_created_total 1
|
|
39
|
+
# voltro_rpc_requests_total{tag="notes.create",status="ok"} 1
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Point Prometheus / Grafana Agent at `/metrics`; gate it with
|
|
43
|
+
`prometheusPlugin({ token })` / `PROMETHEUS_TOKEN` for a public deploy.
|
|
44
|
+
|
|
45
|
+
### Errors → Sentry
|
|
46
|
+
|
|
47
|
+
Set `SENTRY_DSN` (api) — then any handler throw, REST 5xx, workflow failure, or
|
|
48
|
+
React render/loader error is captured, tagged with the request's `traceId`. For
|
|
49
|
+
the full browser→backend waterfall, add the `sentry` field to a paired web app's
|
|
50
|
+
`app.config.ts` (see the docs).
|
|
51
|
+
|
|
52
|
+
### A custom metric
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// mutations/notes.create.mutation.server.ts
|
|
56
|
+
import { Effect, Metric } from 'effect'
|
|
57
|
+
import { counter } from '@voltro/runtime'
|
|
58
|
+
|
|
59
|
+
const notesCreated = counter('app_notes_created_total', 'Notes created.')
|
|
60
|
+
Effect.runSync(Metric.increment(notesCreated)) // shows up in /metrics + the dashboard
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Test it
|
|
64
|
+
|
|
65
|
+
The template ships a unit test (`tests/notes.create.test.ts`) and the dev deps
|
|
66
|
+
(`@voltro/testing`, `vitest`). After `pnpm install`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
voltro test # vitest against this app
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const ctx = makeTestContext({
|
|
74
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
75
|
+
store: mockStore({ notes: [] }),
|
|
76
|
+
})
|
|
77
|
+
const row = await createNote({ title: 'Hello', body: 'World' }, ctx)
|
|
78
|
+
expect(row.tenantId).toBe('acme') // tenant() stamped it — the REAL store, no DB
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`ctx.store` is the same mixin-wrapped store production uses, so tenant
|
|
82
|
+
auto-scoping, soft-delete filtering, and audit auto-fill all behave identically —
|
|
83
|
+
in milliseconds, with no docker. `ctx.withTenant('t2', …)` re-scopes the same
|
|
84
|
+
data to prove cross-tenant isolation. See `@voltro/testing` for `MockClock` /
|
|
85
|
+
`MockEmail` / `mockAi` / `makeWorkflowRunner` / `runDialectParity`.
|
|
86
|
+
|
|
87
|
+
## Going to production
|
|
88
|
+
|
|
89
|
+
| Want… | Do |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Metrics survive a public deploy | `prometheusPlugin({ token })` + scrape over TLS |
|
|
92
|
+
| Errors in Sentry | set `SENTRY_DSN`; add `sentryPlugin({ traces: true })` for performance traces |
|
|
93
|
+
| Spans in your tracer | `OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318` |
|
|
94
|
+
| Vendor-native (Datadog) | add `@voltro/plugin-datadog` — agentless metrics + correlated logs/traces |
|
|
95
|
+
| CI gate | run `voltro test` in CI; add `runDialectParity` for hand-written SQL |
|
|
96
|
+
|
|
97
|
+
## Anti-patterns
|
|
98
|
+
|
|
99
|
+
- **Re-creating the counter per call.** Declare `counter(...)` at MODULE level —
|
|
100
|
+
a per-call `counter()` resets the series. (The runtime dedupes by name, but
|
|
101
|
+
module-level is the clear intent.)
|
|
102
|
+
- **Assuming Sentry needs code to wire each primitive.** The plugin subscribes
|
|
103
|
+
to the framework's server-error bus once — rpc, REST, workflows, schedules,
|
|
104
|
+
subscribers, webhooks, and startups are all covered. You just set the DSN.
|
|
105
|
+
- **Reaching for a real DB in unit tests.** `makeTestContext` + `mockStore` give
|
|
106
|
+
the real store behaviour in-memory. Save a live database for `voltro e2e`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Api app config — the observability + testing showcase. Read by `voltro dev`.
|
|
2
|
+
//
|
|
3
|
+
// Two observability plugins, both turnkey:
|
|
4
|
+
// • prometheusPlugin() — exposes GET /metrics in the text exposition format.
|
|
5
|
+
// The framework already records rpc / http / subscription / cache metrics
|
|
6
|
+
// into Effect's global MetricRegistry; the plugin just exports them. Your
|
|
7
|
+
// OWN counters (see mutations/notes.create) show up there too, automatically.
|
|
8
|
+
// • sentryPlugin() — reports rpc-handler + REST + workflow + render errors,
|
|
9
|
+
// correlated to the active trace. INERT without SENTRY_DSN, so it's safe to
|
|
10
|
+
// ship wired: set the env to turn it on, no code change.
|
|
11
|
+
//
|
|
12
|
+
// Tracing is on by default in `voltro dev` (a buffer-only tracer powers the
|
|
13
|
+
// DevTools Traces panel + traceId-in-logs); set OTEL_EXPORTER_OTLP_ENDPOINT to
|
|
14
|
+
// ship spans to Jaeger / Tempo / Honeycomb.
|
|
15
|
+
//
|
|
16
|
+
// `store: 'memory'` keeps first-run zero-infra.
|
|
17
|
+
|
|
18
|
+
import { prometheusPlugin } from '@voltro/plugin-prometheus'
|
|
19
|
+
import { sentryPlugin } from '@voltro/plugin-sentry'
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
type: 'api' as const,
|
|
23
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
24
|
+
store: 'memory' as const,
|
|
25
|
+
plugins: [
|
|
26
|
+
// GET /metrics → Prometheus text. Optional bearer gate: prometheusPlugin({ token })
|
|
27
|
+
// / PROMETHEUS_TOKEN. processMetrics defaults on (RSS + CPU).
|
|
28
|
+
prometheusPlugin(),
|
|
29
|
+
// Inert until SENTRY_DSN is set; then errors flow correlated to the traceId.
|
|
30
|
+
sentryPlugin(),
|
|
31
|
+
],
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Schema for the {{projectName}} observability backend — a minimal tenant-scoped
|
|
2
|
+
// `notes` domain. The point of this template is what surrounds it: Prometheus
|
|
3
|
+
// metrics, Sentry error tracking, tracing, and a unit test (see tests/).
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
boolean,
|
|
7
|
+
databaseHandle,
|
|
8
|
+
id,
|
|
9
|
+
table,
|
|
10
|
+
text,
|
|
11
|
+
type InferRow,
|
|
12
|
+
} from '@voltro/database'
|
|
13
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
14
|
+
|
|
15
|
+
// ---------- Core tables ----------
|
|
16
|
+
|
|
17
|
+
export const actors = table('actors', {
|
|
18
|
+
id: id(),
|
|
19
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
20
|
+
displayName: text().nullable(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const tenants = table('tenants', { id: id(), name: text() })
|
|
24
|
+
|
|
25
|
+
// ---------- Application table ----------
|
|
26
|
+
|
|
27
|
+
export const notes = table('notes', {
|
|
28
|
+
id: id({ prefix: 'note' }),
|
|
29
|
+
title: text(),
|
|
30
|
+
body: text(),
|
|
31
|
+
done: boolean().default(false),
|
|
32
|
+
})
|
|
33
|
+
.with(tenant()) // tenantId + audit columns, auto-stamped
|
|
34
|
+
.reactive()
|
|
35
|
+
|
|
36
|
+
export type Note = InferRow<typeof notes>
|
|
37
|
+
|
|
38
|
+
export const database = databaseHandle({ actors, tenants, notes })
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Create a note + record a CUSTOM Prometheus metric.
|
|
2
|
+
//
|
|
3
|
+
// `counter(name, desc)` registers a metric in Effect's global MetricRegistry —
|
|
4
|
+
// the same registry GET /metrics (prometheusPlugin) and the DevTools Metrics
|
|
5
|
+
// panel both read, so your counter shows up in both with no extra wiring.
|
|
6
|
+
// We keep the handler ASYNC (so the test can `await execute(input, ctx)`) and
|
|
7
|
+
// bump the counter with a tiny `Effect.runSync` — Metric.increment is a
|
|
8
|
+
// synchronous effect.
|
|
9
|
+
|
|
10
|
+
import { Effect, Metric } from 'effect'
|
|
11
|
+
import type { AppContext } from '@voltro/runtime'
|
|
12
|
+
import { counter } from '@voltro/runtime'
|
|
13
|
+
|
|
14
|
+
// Module-level so it's the SAME metric across every call (not re-created).
|
|
15
|
+
const notesCreated = counter('app_notes_created_total', 'Notes created via notes.create.')
|
|
16
|
+
|
|
17
|
+
const execute = async (input: { title: string; body: string }, ctx: AppContext) => {
|
|
18
|
+
Effect.runSync(Metric.increment(notesCreated)) // → app_notes_created_total in /metrics
|
|
19
|
+
// tenant() auto-stamps tenantId (+ audit columns) from ctx.request.subject.
|
|
20
|
+
// `done: false` is set explicitly — a column DEFAULT only fills in on a SQL
|
|
21
|
+
// store; the memory store leaves an omitted column null, which would fail the
|
|
22
|
+
// `done: Schema.Boolean` output encode over the rpc wire.
|
|
23
|
+
const row = await ctx.store.insert('notes', { title: input.title, body: input.body, done: false })
|
|
24
|
+
return {
|
|
25
|
+
id: row['id'] as string,
|
|
26
|
+
title: row['title'] as string,
|
|
27
|
+
body: row['body'] as string,
|
|
28
|
+
done: row['done'] as boolean,
|
|
29
|
+
tenantId: row['tenantId'] as string,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default execute
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineMutation } from '@voltro/protocol'
|
|
2
|
+
import { Schema } from 'effect'
|
|
3
|
+
|
|
4
|
+
export const createNote = defineMutation({
|
|
5
|
+
name: 'notes.create',
|
|
6
|
+
target: { table: 'notes', op: 'insert' },
|
|
7
|
+
input: Schema.Struct({ title: Schema.NonEmptyString, body: Schema.String }),
|
|
8
|
+
output: Schema.Struct({
|
|
9
|
+
id: Schema.String,
|
|
10
|
+
title: Schema.String,
|
|
11
|
+
body: Schema.String,
|
|
12
|
+
done: Schema.Boolean,
|
|
13
|
+
tenantId: Schema.String,
|
|
14
|
+
}),
|
|
15
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
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-multitenancy": "0.1.0",
|
|
18
|
+
"@voltro/plugin-prometheus": "0.1.0",
|
|
19
|
+
"@voltro/plugin-sentry": "0.1.0",
|
|
20
|
+
"@voltro/protocol": "0.1.0",
|
|
21
|
+
"@voltro/runtime": "0.1.0",
|
|
22
|
+
"effect": "^3.21.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@voltro/testing": "0.1.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineQuery } from '@voltro/protocol'
|
|
2
|
+
import { Schema } from 'effect'
|
|
3
|
+
|
|
4
|
+
export const listNotes = defineQuery({
|
|
5
|
+
name: 'notes.list',
|
|
6
|
+
source: 'notes',
|
|
7
|
+
input: Schema.Struct({}),
|
|
8
|
+
output: Schema.Array(Schema.Struct({
|
|
9
|
+
id: Schema.String,
|
|
10
|
+
title: Schema.String,
|
|
11
|
+
body: Schema.String,
|
|
12
|
+
done: Schema.Boolean,
|
|
13
|
+
})),
|
|
14
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-observability",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Production-readiness in one backend: Prometheus metrics (GET /metrics + a custom counter), Sentry error tracking (inert without a DSN), distributed tracing, AND a @voltro/testing example (makeTestContext + mockStore, run with voltro test).",
|
|
5
|
+
"tags": ["api", "observability", "prometheus", "metrics", "sentry", "testing", "tracing"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Unit test with `@voltro/testing` — no database, no running server, no real
|
|
2
|
+
// model calls. `makeTestContext` hands the handler the SAME mixin-wrapped
|
|
3
|
+
// `ctx.store` it gets in production, so tenant auto-scoping + audit auto-fill
|
|
4
|
+
// behave exactly as at runtime. Run with `voltro test` (vitest).
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
8
|
+
import { database } from '../database/schema' // registers notes / actors / tenants
|
|
9
|
+
import createNote from '../mutations/notes.create.mutation.server'
|
|
10
|
+
|
|
11
|
+
describe('notes.create', () => {
|
|
12
|
+
it('inserts a note, auto-stamped to the caller tenant', async () => {
|
|
13
|
+
const ctx = makeTestContext({
|
|
14
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
15
|
+
store: mockStore({ notes: [] }),
|
|
16
|
+
})
|
|
17
|
+
const row = await createNote({ title: 'Hello', body: 'World' }, ctx)
|
|
18
|
+
|
|
19
|
+
expect(row.title).toBe('Hello')
|
|
20
|
+
expect(row.tenantId).toBe('acme') // tenant() stamped it from the subject
|
|
21
|
+
expect(row.id).toMatch(/^note_/) // TypeID auto-injected
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('isolates tenants — t2 never sees t1 rows (REAL tenant scoping)', async () => {
|
|
25
|
+
const ctx = makeTestContext({
|
|
26
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
27
|
+
store: mockStore({ notes: [] }),
|
|
28
|
+
})
|
|
29
|
+
await createNote({ title: 'secret', body: '' }, ctx)
|
|
30
|
+
|
|
31
|
+
// Re-scope the SAME store to a different tenant — `withTenant` hands the
|
|
32
|
+
// callback a re-scoped `c`; query THAT (the real WHERE filter hides t1's row).
|
|
33
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.notes.descriptor))
|
|
34
|
+
expect(seenByT2).toHaveLength(0)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro API scaffold (template: **api-ratelimit**) — per-endpoint /
|
|
4
|
+
per-subject / per-tenant rate limiting with `@voltro/plugin-ratelimit`.
|
|
5
|
+
|
|
6
|
+
## Boot
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm install
|
|
10
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
11
|
+
# → http://localhost:4000 (store: memory — zero infra)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What this shows
|
|
15
|
+
|
|
16
|
+
- **Limits as config** — `rateLimitPlugin` runs on the rpc interceptors (the
|
|
17
|
+
only surface that sees every call AND the resolved subject). A `default`
|
|
18
|
+
fallback applies everywhere; `rules` override per endpoint:
|
|
19
|
+
- `match` — an exact tag or a `/regex/`; `kind` narrows to mutation/query/action.
|
|
20
|
+
- `algorithm` — `sliding-window` (default), `fixed-window`, or
|
|
21
|
+
`token-bucket` (with `burst`).
|
|
22
|
+
- `by` — the bucket key: `subject` (default), `tenant`, `apiKey`, `global`,
|
|
23
|
+
or a composite array.
|
|
24
|
+
- **Typed `RateLimited`** — over-limit calls fail a `RateLimited` error
|
|
25
|
+
(`{ tag, limit, retryAfterMs, resetAtMs }`). The plugin merges it into every
|
|
26
|
+
procedure's wire error union, so the client decodes it typed and can show a
|
|
27
|
+
"try again in N seconds".
|
|
28
|
+
|
|
29
|
+
This template caps `notes.create` at **3/min per tenant** (token-bucket,
|
|
30
|
+
burst 3): the first three calls land, the fourth is rejected.
|
|
31
|
+
|
|
32
|
+
## Try it
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Fire it four times for tenant 'acme' — the 4th trips the limit:
|
|
36
|
+
for i in 1 2 3 4; do
|
|
37
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
38
|
+
-d "{\"tag\":\"notes.create\",\"input\":{\"tenantId\":\"acme\",\"title\":\"n$i\",\"body\":\"x\"}}" \
|
|
39
|
+
| python3 -c 'import sys,json;d=json.load(sys.stdin);print(i, d.get("ok"), (d.get("error") or {}).get("name",""))' i=$i 2>/dev/null || true
|
|
40
|
+
done
|
|
41
|
+
# → 1 True ; 2 True ; 3 True ; 4 False RateLimited
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Multi-node
|
|
45
|
+
|
|
46
|
+
`store: 'memory'` is single-process. For a cluster, switch to `'postgres'`
|
|
47
|
+
(state under a row lock, reuses `DB_*`/`PG_*`) or `'redis'` (fastest — one
|
|
48
|
+
atomic Lua script; reads `CACHE_REDIS_URL` / `REDIS_URL`). All shared stores
|
|
49
|
+
fail OPEN on a backend error.
|