@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,11 @@
|
|
|
1
|
+
// Core table — the multi-tenancy boundary. Every row in a `tenant()`-marked
|
|
2
|
+
// table carries `tenantId` pointing here. Unlike `actors`, you DECLARE this
|
|
3
|
+
// one (it's app-shaped — add the fields your org/tenant needs).
|
|
4
|
+
|
|
5
|
+
import { id, table, text, timestamp } from '@voltro/database'
|
|
6
|
+
|
|
7
|
+
export const tenants = table('tenants', {
|
|
8
|
+
id: id(),
|
|
9
|
+
name: text(),
|
|
10
|
+
createdAt: timestamp().default('now'),
|
|
11
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"test": "voltro test",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@effect/platform": "^0.96.1",
|
|
13
|
+
"@effect/rpc": "^0.75.1",
|
|
14
|
+
"@voltro/cli": "0.1.0",
|
|
15
|
+
"@voltro/database": "0.1.0",
|
|
16
|
+
"@voltro/env": "0.1.0",
|
|
17
|
+
"@voltro/plugin-governance": "0.1.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.1.0",
|
|
19
|
+
"@voltro/protocol": "0.1.0",
|
|
20
|
+
"@voltro/runtime": "0.1.0",
|
|
21
|
+
"effect": "^3.21.2"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@voltro/testing": "0.1.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"vitest": "^3.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// EXECUTOR (server-only): returns a query builder with an eager `.with(...)`
|
|
2
|
+
// spec. `.with({ books: true })` attaches each author's books as an array on
|
|
3
|
+
// the row — compiled into ONE SQL roundtrip via the dialect's JSON-aggregation
|
|
4
|
+
// idiom (postgres jsonb_agg, sqlite json_group_array, mysql JSON_ARRAYAGG,
|
|
5
|
+
// mssql FOR JSON PATH). The per-branch `limit`/`orderBy` apply PER author.
|
|
6
|
+
//
|
|
7
|
+
// `authors` carries `tenant()`, so the runtime scopes the root read to the
|
|
8
|
+
// caller's tenant automatically.
|
|
9
|
+
|
|
10
|
+
import type { AppContext } from '@voltro/runtime'
|
|
11
|
+
import { database } from '../database/index'
|
|
12
|
+
|
|
13
|
+
const execute = (_input: Record<string, never>, _ctx: AppContext) =>
|
|
14
|
+
database.authors.with({
|
|
15
|
+
books: { orderBy: [{ column: 'title', direction: 'asc' }], limit: 50 },
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export default execute
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Authors with their books eager-loaded — demonstrates `.with({ books: true })`.
|
|
2
|
+
//
|
|
3
|
+
// DESCRIPTOR (browser-safe). The `output` shape includes the nested `books`
|
|
4
|
+
// array the eager spec attaches to each author row (the relation registered
|
|
5
|
+
// in authors.relations.ts).
|
|
6
|
+
//
|
|
7
|
+
// `source: ['authors', 'books']` makes the reactive subscription re-run when
|
|
8
|
+
// EITHER table changes — a new book under an author pushes a fresh snapshot.
|
|
9
|
+
// No `cache` here: this is a join-shaped result; we let the live dispatcher
|
|
10
|
+
// keep it fresh rather than cache a snapshot.
|
|
11
|
+
|
|
12
|
+
import { defineQuery } from '@voltro/protocol'
|
|
13
|
+
import { Schema } from 'effect'
|
|
14
|
+
|
|
15
|
+
const Book = Schema.Struct({
|
|
16
|
+
id: Schema.String,
|
|
17
|
+
authorId: Schema.String,
|
|
18
|
+
title: Schema.String,
|
|
19
|
+
summary: Schema.String,
|
|
20
|
+
genre: Schema.String,
|
|
21
|
+
tags: Schema.Array(Schema.String),
|
|
22
|
+
slug: Schema.String,
|
|
23
|
+
tenantId: Schema.String,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const authorsWithBooks = defineQuery({
|
|
27
|
+
name: 'authors.withBooks',
|
|
28
|
+
source: ['authors', 'books'],
|
|
29
|
+
input: Schema.Struct({}),
|
|
30
|
+
output: Schema.Struct({
|
|
31
|
+
id: Schema.String,
|
|
32
|
+
name: Schema.String,
|
|
33
|
+
// `bio` is the `.encrypted()` column — handlers (and the wire) see
|
|
34
|
+
// plaintext; it's ciphertext only on disk. Nullable in the seed/data.
|
|
35
|
+
bio: Schema.NullOr(Schema.String),
|
|
36
|
+
tenantId: Schema.String,
|
|
37
|
+
// Eager-loaded relation: each author carries an array of their books.
|
|
38
|
+
books: Schema.Array(Book),
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// EXECUTOR (server-only): returns a query builder. `.matching('bookSearch',
|
|
2
|
+
// input.q)` compiles to the dialect's FTS predicate (postgres tsvector,
|
|
3
|
+
// mysql/mariadb MATCH…AGAINST, sqlite FTS5, mssql ranked-LIKE) — resolving
|
|
4
|
+
// the index's covered columns + config from the `bookSearch` declaration on
|
|
5
|
+
// the `books` table.
|
|
6
|
+
//
|
|
7
|
+
// We do NOT add a tenant filter by hand — `books` carries the `tenant()`
|
|
8
|
+
// mixin, so the runtime AND-merges `eq('tenantId', subject.tenantId)` into
|
|
9
|
+
// the predicate automatically.
|
|
10
|
+
|
|
11
|
+
import type { AppContext } from '@voltro/runtime'
|
|
12
|
+
import { database } from '../database/index'
|
|
13
|
+
|
|
14
|
+
const execute = (input: { q: string }, _ctx: AppContext) =>
|
|
15
|
+
database.books.matching('bookSearch', input.q).limit(50)
|
|
16
|
+
|
|
17
|
+
export default execute
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Full-text book search — demonstrates `.matching(...)` FTS + declarative
|
|
2
|
+
// query result caching.
|
|
3
|
+
//
|
|
4
|
+
// DESCRIPTOR (browser-safe): NO node/database/server imports. The codegen
|
|
5
|
+
// pulls this into rpcGroup.generated.ts, which the web client loads
|
|
6
|
+
// value-level — so it must stay free of server-only modules.
|
|
7
|
+
//
|
|
8
|
+
// `cache` opts into server-side snapshot caching with auto-invalidation: the
|
|
9
|
+
// framework caches the snapshot and drops it whenever a mutation writes any
|
|
10
|
+
// table the query depends on (here, `books`). `scope` is REQUIRED and is a
|
|
11
|
+
// security decision — this query is tenant-filtered (the `tenant()` mixin on
|
|
12
|
+
// `books` AND-merges the caller's tenantId), so it depends on the caller →
|
|
13
|
+
// `scope: 'subject'` (the cache key includes the subject id, no cross-tenant
|
|
14
|
+
// leak). NEVER `global` on a subject-filtered query.
|
|
15
|
+
//
|
|
16
|
+
// The `cache` field is backend-agnostic: this snapshot lives in the
|
|
17
|
+
// in-process `memory` cache by default and in Redis (cross-instance) the
|
|
18
|
+
// moment you flip `CACHE_BACKEND=redis` / app.config `cache: 'redis'` — the
|
|
19
|
+
// query code below does not change. `voltro add redis` wires that for you.
|
|
20
|
+
|
|
21
|
+
import { defineQuery } from '@voltro/protocol'
|
|
22
|
+
import { Schema } from 'effect'
|
|
23
|
+
|
|
24
|
+
export const searchBooks = defineQuery({
|
|
25
|
+
name: 'books.search',
|
|
26
|
+
source: 'books',
|
|
27
|
+
input: Schema.Struct({ q: Schema.String }),
|
|
28
|
+
output: Schema.Struct({
|
|
29
|
+
id: Schema.String,
|
|
30
|
+
authorId: Schema.String,
|
|
31
|
+
title: Schema.String,
|
|
32
|
+
summary: Schema.String,
|
|
33
|
+
genre: Schema.String,
|
|
34
|
+
tags: Schema.Array(Schema.String),
|
|
35
|
+
slug: Schema.String,
|
|
36
|
+
tenantId: Schema.String,
|
|
37
|
+
}),
|
|
38
|
+
cache: { ttl: '30s', swr: '5m', scope: 'subject' },
|
|
39
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Boot-lifecycle seed — a couple of authors + their books so the catalog has
|
|
2
|
+
// data the moment `voltro dev` boots. Idempotent via `upsertByUnique`, so
|
|
3
|
+
// re-running on every boot is safe (the fingerprint ledger also dedupes).
|
|
4
|
+
//
|
|
5
|
+
// NOTE: the seed store is the RAW store — it has no authenticated subject, so
|
|
6
|
+
// the `tenant()` auto-fill doesn't fire. We pass `tenantId` explicitly here
|
|
7
|
+
// (and seed the demo tenant row first). The dev AuthMiddleware default resolves
|
|
8
|
+
// the `acme` tenant from the `x-tenant` header, so seeding under `acme` lines
|
|
9
|
+
// up with what queries see out of the box.
|
|
10
|
+
//
|
|
11
|
+
// `bio` is the `.encrypted()` column — we pass plaintext; the store middleware
|
|
12
|
+
// encrypts it on write (requires governancePlugin({ fieldEncryption: true })).
|
|
13
|
+
// `tags` is the `array(text())` column — pass a plain JS array. `slug` is the
|
|
14
|
+
// DB-computed `.generatedAs(...)` column — DON'T pass it; the engine fills it.
|
|
15
|
+
|
|
16
|
+
import { defineSeed } from '@voltro/database'
|
|
17
|
+
|
|
18
|
+
const TENANT_ID = 'acme'
|
|
19
|
+
|
|
20
|
+
export default defineSeed({
|
|
21
|
+
id: 'catalog',
|
|
22
|
+
name: 'Library catalog demo data',
|
|
23
|
+
lifecycle: 'boot',
|
|
24
|
+
steps: ({ step }) => [
|
|
25
|
+
step('tenant', async ({ upsertByUnique }) => {
|
|
26
|
+
await upsertByUnique(
|
|
27
|
+
'tenants',
|
|
28
|
+
{ id: TENANT_ID },
|
|
29
|
+
{ id: TENANT_ID, name: 'Acme Library', createdAt: new Date() },
|
|
30
|
+
)
|
|
31
|
+
return { rowsTouched: 1 }
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
step('authors-and-books', async ({ upsertByUnique }) => {
|
|
35
|
+
let touched = 0
|
|
36
|
+
|
|
37
|
+
const ursula = await upsertByUnique(
|
|
38
|
+
'authors',
|
|
39
|
+
{ id: 'author_leguin' },
|
|
40
|
+
{
|
|
41
|
+
id: 'author_leguin',
|
|
42
|
+
name: 'Ursula K. Le Guin',
|
|
43
|
+
bio: 'American author best known for works of speculative fiction.',
|
|
44
|
+
tenantId: TENANT_ID,
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
touched += 1
|
|
48
|
+
|
|
49
|
+
const carl = await upsertByUnique(
|
|
50
|
+
'authors',
|
|
51
|
+
{ id: 'author_sagan' },
|
|
52
|
+
{
|
|
53
|
+
id: 'author_sagan',
|
|
54
|
+
name: 'Carl Sagan',
|
|
55
|
+
bio: 'Astronomer and science communicator.',
|
|
56
|
+
tenantId: TENANT_ID,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
touched += 1
|
|
60
|
+
|
|
61
|
+
const books = [
|
|
62
|
+
{
|
|
63
|
+
id: 'book_leftHand',
|
|
64
|
+
authorId: ursula.row['id'] as string,
|
|
65
|
+
title: 'The Left Hand of Darkness',
|
|
66
|
+
summary: 'A lone envoy on a frozen world where inhabitants have no fixed sex.',
|
|
67
|
+
genre: 'sciFi',
|
|
68
|
+
tags: ['classic', 'gender', 'winter'],
|
|
69
|
+
tenantId: TENANT_ID,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'book_earthsea',
|
|
73
|
+
authorId: ursula.row['id'] as string,
|
|
74
|
+
title: 'A Wizard of Earthsea',
|
|
75
|
+
summary: 'A young mage learns the true cost of power and names.',
|
|
76
|
+
genre: 'fantasy',
|
|
77
|
+
tags: ['magic', 'coming-of-age'],
|
|
78
|
+
tenantId: TENANT_ID,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'book_cosmos',
|
|
82
|
+
authorId: carl.row['id'] as string,
|
|
83
|
+
title: 'Cosmos',
|
|
84
|
+
summary: 'A sweeping tour of the universe and our place in it.',
|
|
85
|
+
genre: 'nonfiction',
|
|
86
|
+
tags: ['astronomy', 'science'],
|
|
87
|
+
tenantId: TENANT_ID,
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for (const book of books) {
|
|
92
|
+
// `slug` is omitted — the `.generatedAs(...)` STORED column is filled
|
|
93
|
+
// by the DB engine from `title`.
|
|
94
|
+
await upsertByUnique('books', { id: book.id }, book)
|
|
95
|
+
touched += 1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { rowsTouched: touched }
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-data-advanced",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Advanced schema modeling: *.entity.ts/*.relations.ts split with relations + eager loading, full-text search, dbEnum, array + generated + encrypted columns, and declarative query caching. A small library/catalog (authors + books).",
|
|
5
|
+
"tags": ["api", "backend", "schema", "relations", "fts", "enum", "encryption", "cache", "advanced"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Unit tests with `@voltro/testing` — no database, no running server.
|
|
2
|
+
//
|
|
3
|
+
// 1. `authors.withBooks` — the runnable primitive. The executor returns a
|
|
4
|
+
// `database.authors.with({ books: … })` query BUILDER; we run its
|
|
5
|
+
// `.descriptor` through `ctx.store.query`, which (a) resolves the eager
|
|
6
|
+
// `books` relation against the in-memory store and (b) AND-merges the
|
|
7
|
+
// caller's tenant scope — the exact isolation the runtime applies. So we
|
|
8
|
+
// assert both eager-loading AND tenant isolation.
|
|
9
|
+
// 2. `books.search` — descriptor-pinned. Its executor calls `.matching(...)`
|
|
10
|
+
// (dialect FTS: postgres tsvector / mysql MATCH…AGAINST / sqlite FTS5).
|
|
11
|
+
// The in-memory test store does NOT evaluate `descriptor.fullTextSearch`,
|
|
12
|
+
// so a run there would silently return UNFILTERED rows — a misleading
|
|
13
|
+
// assertion. FTS behavior is proven by an integration test against a real
|
|
14
|
+
// dialect; here we pin the wire contract (name / source / input+output
|
|
15
|
+
// decode) instead of faking a search.
|
|
16
|
+
//
|
|
17
|
+
// Run with `voltro test` (vitest).
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
20
|
+
import { Schema } from 'effect'
|
|
21
|
+
import { registerCoreTables, registerRelations } from '@voltro/database'
|
|
22
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
23
|
+
import { database, actors, tenants } from '../database/index' // registers authors / books (tables)
|
|
24
|
+
import { authorsRelations } from '../database/authors.relations'
|
|
25
|
+
import { booksRelations } from '../database/books.relations'
|
|
26
|
+
import authorsWithBooksExecute from '../queries/authors.withBooks.query.server'
|
|
27
|
+
import { searchBooks } from '../queries/books.search.query'
|
|
28
|
+
|
|
29
|
+
// Two boot steps `voltro dev` runs that the unit harness (`voltro test`) does
|
|
30
|
+
// NOT, both needed for the eager-load walker:
|
|
31
|
+
// 1. registerCoreTables — the audit()/tenant() mixins add createdBy/updatedBy/
|
|
32
|
+
// tenantId reference() columns whose thunks resolve `actors`/`tenants` via
|
|
33
|
+
// this registry. `inferForeignKey` walks EVERY reference() column on `books`
|
|
34
|
+
// to find the FK back to `authors`, so it dereferences those mixin columns —
|
|
35
|
+
// which throw unless the core tables are registered.
|
|
36
|
+
// 2. registerRelations — `*.relations.ts` files only DEFINE specs; runtime
|
|
37
|
+
// module discovery (registerDiscoveredRelations) is what REGISTERS them.
|
|
38
|
+
// We do both here — the exact wiring the dev/serve dispatcher does at boot — so
|
|
39
|
+
// `.with({ books })` resolves against the in-memory store just as at runtime.
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
registerCoreTables({ actors: () => actors, tenants: () => tenants })
|
|
42
|
+
registerRelations(authorsRelations)
|
|
43
|
+
registerRelations(booksRelations)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// `bio` is an `.encrypted()` column. We seed it `null` (the descriptor allows
|
|
47
|
+
// NullOr) — a plaintext/null value passes the read cipher through untouched, so
|
|
48
|
+
// no governance cipher needs wiring for these reads.
|
|
49
|
+
const book = (over: Partial<Record<string, unknown>> & { id: string; authorId: string; tenantId: string }) => ({
|
|
50
|
+
title: 'Untitled',
|
|
51
|
+
summary: '',
|
|
52
|
+
genre: 'fiction',
|
|
53
|
+
tags: [] as ReadonlyArray<string>,
|
|
54
|
+
slug: 'untitled',
|
|
55
|
+
...over,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('authors.withBooks', () => {
|
|
59
|
+
it('returns the tenant’s authors with their books eager-loaded', async () => {
|
|
60
|
+
const ctx = makeTestContext({
|
|
61
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
62
|
+
store: mockStore({
|
|
63
|
+
authors: [{ id: 'author_1', name: 'Ada', bio: null, tenantId: 'acme' }],
|
|
64
|
+
books: [
|
|
65
|
+
book({ id: 'book_1', authorId: 'author_1', tenantId: 'acme', title: 'Analytical Engine' }),
|
|
66
|
+
book({ id: 'book_2', authorId: 'author_1', tenantId: 'acme', title: 'Notes' }),
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const rows = await ctx.store.query(authorsWithBooksExecute({}, ctx).descriptor)
|
|
72
|
+
|
|
73
|
+
expect(rows).toHaveLength(1)
|
|
74
|
+
const ada = rows[0] as { id: string; name: string; books: ReadonlyArray<{ title: string }> }
|
|
75
|
+
expect(ada.name).toBe('Ada')
|
|
76
|
+
expect(ada.books.map((b) => b.title).sort()).toEqual(['Analytical Engine', 'Notes'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('isolates tenants — t2 never sees t1 authors (REAL tenant scoping)', async () => {
|
|
80
|
+
const ctx = makeTestContext({
|
|
81
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
82
|
+
store: mockStore({
|
|
83
|
+
authors: [{ id: 'author_t1', name: 'Secret', bio: null, tenantId: 't1' }],
|
|
84
|
+
books: [book({ id: 'book_t1', authorId: 'author_t1', tenantId: 't1' })],
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Same store, re-scoped to t2 — the runtime AND-merges eq('tenantId','t2'),
|
|
89
|
+
// so t1's author is invisible.
|
|
90
|
+
const seenByT2 = await ctx.withTenant('t2', (c) =>
|
|
91
|
+
c.store.query(authorsWithBooksExecute({}, c).descriptor),
|
|
92
|
+
)
|
|
93
|
+
expect(seenByT2).toHaveLength(0)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('books.search (query descriptor)', () => {
|
|
98
|
+
it('declares the expected wire name + source table', () => {
|
|
99
|
+
expect(searchBooks.name).toBe('books.search')
|
|
100
|
+
expect(searchBooks.source).toBe('books')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('accepts a valid { q } input and rejects a malformed one', () => {
|
|
104
|
+
const decode = Schema.decodeUnknownSync(searchBooks.input)
|
|
105
|
+
expect(decode({ q: 'effect' })).toEqual({ q: 'effect' })
|
|
106
|
+
expect(() => decode({})).toThrow() // missing q
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('decodes a well-formed book row against the output schema', () => {
|
|
110
|
+
const decode = Schema.decodeUnknownSync(searchBooks.output)
|
|
111
|
+
const row = decode({
|
|
112
|
+
id: 'book_1',
|
|
113
|
+
authorId: 'author_1',
|
|
114
|
+
title: 'Effect in Action',
|
|
115
|
+
summary: 'A book.',
|
|
116
|
+
genre: 'nonfiction',
|
|
117
|
+
tags: ['effect', 'ts'],
|
|
118
|
+
slug: 'effect-in-action',
|
|
119
|
+
tenantId: 'acme',
|
|
120
|
+
})
|
|
121
|
+
expect(row.tags).toEqual(['effect', 'ts'])
|
|
122
|
+
expect(() => decode({ id: 'book_1' })).toThrow() // missing required fields
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro backend scaffold (template: **api-durable**).
|
|
4
|
+
|
|
5
|
+
A cohesive **order-fulfillment** domain that exercises the framework's
|
|
6
|
+
entire **durable-execution surface** in one app — durable workflows with
|
|
7
|
+
a human-approval gate, event triggers, a cron schedule, a table
|
|
8
|
+
subscriber, a materialized aggregate, and a run-once startup hook. It
|
|
9
|
+
boots with **zero infrastructure** (`store: 'memory'`).
|
|
10
|
+
|
|
11
|
+
## Boot
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm install # at the repo root
|
|
15
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
16
|
+
# → http://localhost:4000
|
|
17
|
+
# → ws://localhost:4000/ws
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No env or services required. `store: 'memory'` resets on every
|
|
21
|
+
supervised restart — switch `app.config.ts` to `store: 'postgres'`
|
|
22
|
+
(and `docker compose up` from the repo root) when you want durable
|
|
23
|
+
state + multi-instance cluster workflow runners.
|
|
24
|
+
|
|
25
|
+
## What it demonstrates
|
|
26
|
+
|
|
27
|
+
| File | Primitive | Role |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `app.config.ts` | app config | `store: 'memory'`, zero-infra boot. |
|
|
30
|
+
| `database/schema.ts` | schema | core `actors`/`tenants` + an `orders` table (`tenant()` + `.reactive()`). |
|
|
31
|
+
| `mutations/orders.place.mutation.*` | mutation | insert an order, then **emit `order.placed`** post-commit via `ctx.events.emit`. Tenant write-guard. |
|
|
32
|
+
| `triggers/order.placed.trigger.tsx` | event trigger | maps event `order.placed` → **start** the `orders.fulfill` workflow. |
|
|
33
|
+
| `workflows/order.fulfill.workflow.*` | durable workflow | `step('reserve-stock')` (with **`withCompensation`** saga rollback) → `sleep` → **`awaitSignal('approval')`** human gate → `step('ship')` / mark rejected. |
|
|
34
|
+
| `mutations/orders.approve.mutation.*` | mutation | inject the `approval` signal into the parked run via `ctx.workflows.signal(...)`. |
|
|
35
|
+
| `schedules/nightlyReport.cron.tsx` | schedule | `defineSchedule` cron `0 2 * * *` (UTC), tallies open orders. |
|
|
36
|
+
| `subscribers/orderChanges.subscribe.ts` | subscriber | `defineSubscriber({ table: 'orders', on: 'any' })` post-commit reaction. |
|
|
37
|
+
| `aggregates/orderStats.aggregate.ts` | aggregate | `defineAggregate` materialized per-tenant/per-status order roll-up, refreshed every minute. |
|
|
38
|
+
| `startup/warm.startup.tsx` | startup hook | run-once boot hook holding a resource + `onShutdown` teardown. |
|
|
39
|
+
|
|
40
|
+
## The end-to-end flow
|
|
41
|
+
|
|
42
|
+
1. **Place an order** → `orders.place` inserts the row and emits
|
|
43
|
+
`order.placed` *after the transaction commits* (so a rollback never
|
|
44
|
+
starts a workflow for a non-existent order).
|
|
45
|
+
2. **The trigger fires** → `order.placed.trigger.tsx` starts the durable
|
|
46
|
+
`orders.fulfill` workflow with `{ orderId, tenantId }`.
|
|
47
|
+
3. **The workflow runs durably** → reserves stock, waits a (short)
|
|
48
|
+
journaled delay, then **parks on `awaitSignal('approval')`**.
|
|
49
|
+
4. **A human approves** → `orders.approve` derives the workflow's
|
|
50
|
+
deterministic executionId (from its `idempotencyKey`) and injects the
|
|
51
|
+
`approval` signal; the parked run wakes (sub-second) and either ships
|
|
52
|
+
or marks the order rejected.
|
|
53
|
+
|
|
54
|
+
Meanwhile the **subscriber** logs every `orders` write, the **aggregate**
|
|
55
|
+
keeps a live per-status tally, the **schedule** reports nightly, and the
|
|
56
|
+
**startup hook** holds a process-lifetime resource.
|
|
57
|
+
|
|
58
|
+
## Try it
|
|
59
|
+
|
|
60
|
+
Over the rpc surface (e.g. from a `voltro dev` web client, or
|
|
61
|
+
`POST /rpc` / the inspect `invoke` endpoint):
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
// 1. place — starts the fulfillment workflow via order.placed
|
|
65
|
+
{ "tag": "orders.place", "input": { "tenantId": "acme", "customerName": "Ada", "amountCents": 4200 } }
|
|
66
|
+
// 2. approve — wakes the parked awaitSignal('approval') gate
|
|
67
|
+
{ "tag": "orders.approve", "input": { "tenantId": "acme", "orderId": "order_…", "approved": true } }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Watch the run in the dashboard's **Workflows** tab (the `awaitSignal`
|
|
71
|
+
gate also has a "Send signal…" button), and the order's status walk
|
|
72
|
+
`placed → reserved → shipped` live.
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- All primitives are **auto-discovered by file convention** — nothing is
|
|
77
|
+
registered in `app.config.ts`.
|
|
78
|
+
- `query` / `mutation` / `action` / `workflow` keep the **descriptor /
|
|
79
|
+
executor split**: the browser-safe `*.ts` / `*.tsx` descriptor never
|
|
80
|
+
imports `@voltro/database`, `@voltro/runtime`, `node:*`, or the
|
|
81
|
+
workflow executor's server graph. The `.server.*` executors do.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// `orderStats` — a materialized aggregate (`*.aggregate.ts`).
|
|
2
|
+
//
|
|
3
|
+
// The framework owns the lifecycle: it runs `build` on the `refresh`
|
|
4
|
+
// interval, stores the rows in an auto-managed backing table, and serves
|
|
5
|
+
// them via `ctx.aggregates.orderStats.read(...)`. Use an aggregate when
|
|
6
|
+
// the SOURCE query is expensive but the RESULT is small + bounded — a
|
|
7
|
+
// per-status order roll-up here.
|
|
8
|
+
//
|
|
9
|
+
// `output` MUST match each returned row's shape. `build` runs as the
|
|
10
|
+
// system (no per-request subject), so it reads across every tenant; the
|
|
11
|
+
// `tenantId` column is part of the grouping so reads stay per-tenant.
|
|
12
|
+
|
|
13
|
+
import { defineAggregate } from '@voltro/runtime'
|
|
14
|
+
import { column, count } from '@voltro/database'
|
|
15
|
+
import { Schema } from 'effect'
|
|
16
|
+
import { database } from '../database/schema'
|
|
17
|
+
|
|
18
|
+
export const OrderStat = Schema.Struct({
|
|
19
|
+
tenantId: Schema.String,
|
|
20
|
+
status: Schema.String,
|
|
21
|
+
orders: Schema.Number,
|
|
22
|
+
})
|
|
23
|
+
export type OrderStat = Schema.Schema.Type<typeof OrderStat>
|
|
24
|
+
|
|
25
|
+
export default defineAggregate({
|
|
26
|
+
name: 'orderStats',
|
|
27
|
+
refresh: '1m', // re-run every minute (interval shorthand)
|
|
28
|
+
output: OrderStat,
|
|
29
|
+
build: async (ctx) => {
|
|
30
|
+
const rows = await ctx.store.query(
|
|
31
|
+
database.orders
|
|
32
|
+
.groupBy(['tenantId', 'status'])
|
|
33
|
+
// Grouped columns must be EXPLICITLY projected via `column(...)`
|
|
34
|
+
// in the aggregate spec — they are NOT auto-included (SQL rule:
|
|
35
|
+
// a selected non-aggregate column must be in GROUP BY).
|
|
36
|
+
.aggregate({
|
|
37
|
+
tenantId: column('tenantId'),
|
|
38
|
+
status: column('status'),
|
|
39
|
+
orders: count(),
|
|
40
|
+
})
|
|
41
|
+
.descriptor,
|
|
42
|
+
)
|
|
43
|
+
return rows.map((r) => ({
|
|
44
|
+
tenantId: (r as { tenantId: string }).tenantId,
|
|
45
|
+
status: (r as { status: string }).status,
|
|
46
|
+
orders: (r as { orders: number }).orders,
|
|
47
|
+
}))
|
|
48
|
+
},
|
|
49
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Backend app for the {{projectName}} project. Read by `voltro dev`.
|
|
2
|
+
//
|
|
3
|
+
// This template demonstrates the framework's DURABLE-EXECUTION surface
|
|
4
|
+
// end-to-end on an order-fulfillment domain. Everything is wired so it
|
|
5
|
+
// boots with ZERO infra:
|
|
6
|
+
//
|
|
7
|
+
// - `store: 'memory'` — no docker, no postgres. Workflows run with
|
|
8
|
+
// the in-process durable engine (replay within one process; swap to
|
|
9
|
+
// `'postgres'` for multi-instance cluster runners). Memory resets on
|
|
10
|
+
// every supervised restart, which is fine for kicking the tyres.
|
|
11
|
+
//
|
|
12
|
+
// What's in here (all auto-discovered by file convention — nothing is
|
|
13
|
+
// registered in this config):
|
|
14
|
+
//
|
|
15
|
+
// mutations/orders.place.mutation.* — insert an order, then emit a
|
|
16
|
+
// post-commit `order.placed` event
|
|
17
|
+
// triggers/order.placed.trigger.tsx — event → start `orders.fulfill`
|
|
18
|
+
// workflows/order.fulfill.workflow.* — step → sleep → awaitSignal
|
|
19
|
+
// (human approval) → step
|
|
20
|
+
// mutations/orders.approve.mutation.* — inject the `approval` signal
|
|
21
|
+
// schedules/nightlyReport.cron.tsx — nightly summary cron
|
|
22
|
+
// subscribers/orderChanges.subscribe.ts— react to every orders write
|
|
23
|
+
// aggregates/orderStats.aggregate.ts — materialized per-status roll-up
|
|
24
|
+
// startup/warm.startup.tsx — run-once boot hook
|
|
25
|
+
//
|
|
26
|
+
// Add your own env below; it's validated at boot (fail-fast) and read on
|
|
27
|
+
// the server via `serverEnv` / `getSecret` from '@voltro/env/server'.
|
|
28
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
29
|
+
|
|
30
|
+
export const env = defineEnv({
|
|
31
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
32
|
+
// Add your own, e.g.:
|
|
33
|
+
// STRIPE_KEY: envVar.string({ access: 'secret' }),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
type: 'api' as const,
|
|
38
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
39
|
+
store: 'memory' as const,
|
|
40
|
+
env,
|
|
41
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Schema for the {{projectName}} backend.
|
|
2
|
+
//
|
|
3
|
+
// Tables are auto-discovered: every exported `table(...)` in this file
|
|
4
|
+
// (or any `*.entity.ts` / `*.schema.ts` file) is registered by
|
|
5
|
+
// `voltro dev` — no manual barrel. `actors` + `tenants` are the
|
|
6
|
+
// framework's core tables: the built-in `audit()` / `tenant()` /
|
|
7
|
+
// `softDelete()` mixins reference them, so they must be declared (the
|
|
8
|
+
// CLI wires them into the mixin registry once it finds them).
|
|
9
|
+
//
|
|
10
|
+
// `orders` carries the `tenant()` mixin (→ tenantId + audit columns,
|
|
11
|
+
// auto-stamped by the runtime) and `.reactive()` so the `orderChanges`
|
|
12
|
+
// subscriber + any live subscription wake on every write.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
databaseHandle,
|
|
16
|
+
id,
|
|
17
|
+
integer,
|
|
18
|
+
table,
|
|
19
|
+
text,
|
|
20
|
+
timestamp,
|
|
21
|
+
type InferRow,
|
|
22
|
+
} from '@voltro/database'
|
|
23
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
24
|
+
|
|
25
|
+
// ---------- Core tables (required by the audit / tenant mixins) ----------
|
|
26
|
+
|
|
27
|
+
export const actors = table('actors', {
|
|
28
|
+
id: id(),
|
|
29
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
30
|
+
displayName: text().nullable(),
|
|
31
|
+
createdAt: timestamp().default('now'),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const tenants = table('tenants', {
|
|
35
|
+
id: id(),
|
|
36
|
+
name: text(),
|
|
37
|
+
createdAt: timestamp().default('now'),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// ---------- Application table — order-fulfillment domain ----------
|
|
41
|
+
|
|
42
|
+
export const orders = table('orders', {
|
|
43
|
+
id: id({ prefix: 'order' }),
|
|
44
|
+
// The fulfillment lifecycle. `oneOf` narrows the row type to the
|
|
45
|
+
// literal union AND emits a cross-dialect CHECK constraint.
|
|
46
|
+
status: text().oneOf([
|
|
47
|
+
'placed', // created, fulfillment workflow kicked off
|
|
48
|
+
'reserved', // stock reserved (workflow step 1)
|
|
49
|
+
'approved', // human approval signal accepted
|
|
50
|
+
'rejected', // human approval signal rejected
|
|
51
|
+
'shipped', // shipped (workflow final step)
|
|
52
|
+
]).default('placed'),
|
|
53
|
+
customerName: text(),
|
|
54
|
+
amountCents: integer(),
|
|
55
|
+
})
|
|
56
|
+
// tenant() pulls audit() transitively → adds tenantId + createdAt /
|
|
57
|
+
// updatedAt / createdBy / updatedBy (auto-stamped by the runtime).
|
|
58
|
+
.with(tenant())
|
|
59
|
+
// Opt into the matcher engine: the `orderChanges` subscriber + any
|
|
60
|
+
// live subscription against `orders` fire on every mutation write.
|
|
61
|
+
.reactive()
|
|
62
|
+
|
|
63
|
+
export type Order = InferRow<typeof orders>
|
|
64
|
+
|
|
65
|
+
export const database = databaseHandle({ actors, tenants, orders })
|