@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,43 @@
|
|
|
1
|
+
// `orders.approve` — EXECUTOR (server-only).
|
|
2
|
+
//
|
|
3
|
+
// Injects the `approval` signal that wakes the parked `orders.fulfill`
|
|
4
|
+
// run for this order. The workflow's `idempotencyKey` (`fulfill:<id>`)
|
|
5
|
+
// makes its executionId a deterministic function of the payload, so we
|
|
6
|
+
// re-derive it from `{ orderId, tenantId }` and address the signal by
|
|
7
|
+
// executionId — no need to track the run handle from the place mutation.
|
|
8
|
+
//
|
|
9
|
+
// `ctx.workflows.signal(target, name, payload)` writes a `signal-sent`
|
|
10
|
+
// row to the run's events log; the workflow's `awaitSignal('approval')`
|
|
11
|
+
// poll picks it up (sub-second) and parses the payload against its
|
|
12
|
+
// `{ approved: boolean }` schema.
|
|
13
|
+
|
|
14
|
+
import { assertOwnTenant } from '@voltro/plugin-multitenancy/guard'
|
|
15
|
+
import type { AppContext } from '@voltro/runtime'
|
|
16
|
+
|
|
17
|
+
const execute = async (
|
|
18
|
+
input: { orderId: string; tenantId: string; approved: boolean },
|
|
19
|
+
ctx: AppContext,
|
|
20
|
+
) => {
|
|
21
|
+
assertOwnTenant(input.tenantId, ctx.request.subject)
|
|
22
|
+
|
|
23
|
+
if (!ctx.workflows) {
|
|
24
|
+
throw new Error('orders.approve: workflow runtime is not available in this context')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Deterministic executionId for the running fulfillment workflow —
|
|
28
|
+
// same payload the `order.placed` trigger started it with.
|
|
29
|
+
const executionId = await ctx.workflows.executionId('orders.fulfill', {
|
|
30
|
+
orderId: input.orderId,
|
|
31
|
+
tenantId: input.tenantId,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const { eventId } = await ctx.workflows.signal(
|
|
35
|
+
{ executionId },
|
|
36
|
+
'approval',
|
|
37
|
+
{ approved: input.approved },
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return { orderId: input.orderId, eventId }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default execute
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// `orders.approve` — DESCRIPTOR (browser-safe).
|
|
2
|
+
//
|
|
3
|
+
// The human-approval surface: a client calls this with the order id +
|
|
4
|
+
// the decision; the executor injects the matching `approval` signal into
|
|
5
|
+
// the parked `orders.fulfill` run. This is a non-row-writing mutation
|
|
6
|
+
// (it pokes a workflow, not the orders table), so it declares no
|
|
7
|
+
// optimistic `target` and no `op` against `orders`.
|
|
8
|
+
|
|
9
|
+
import { defineMutation } from '@voltro/protocol'
|
|
10
|
+
import { Schema } from 'effect'
|
|
11
|
+
|
|
12
|
+
export const approveOrder = defineMutation({
|
|
13
|
+
name: 'orders.approve',
|
|
14
|
+
input: Schema.Struct({
|
|
15
|
+
orderId: Schema.String,
|
|
16
|
+
tenantId: Schema.String,
|
|
17
|
+
approved: Schema.Boolean,
|
|
18
|
+
}),
|
|
19
|
+
output: Schema.Struct({
|
|
20
|
+
orderId: Schema.String,
|
|
21
|
+
eventId: Schema.String,
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// `orders.place` — EXECUTOR (server-only, default export).
|
|
2
|
+
//
|
|
3
|
+
// Inserts the order in the mutation transaction, then emits a domain
|
|
4
|
+
// event AFTER commit. `ctx.events.emit(...)` is post-commit safe: the
|
|
5
|
+
// event is recorded + the `order.placed` trigger fans out only once the
|
|
6
|
+
// row is durably written, so a rollback never starts a fulfillment
|
|
7
|
+
// workflow for an order that doesn't exist.
|
|
8
|
+
|
|
9
|
+
import { assertOwnTenant } from '@voltro/plugin-multitenancy/guard'
|
|
10
|
+
import type { AppContext } from '@voltro/runtime'
|
|
11
|
+
|
|
12
|
+
const execute = async (
|
|
13
|
+
input: { tenantId: string; customerName: string; amountCents: number },
|
|
14
|
+
ctx: AppContext,
|
|
15
|
+
) => {
|
|
16
|
+
// Cross-tenant write guard — reject a caller spoofing input.tenantId.
|
|
17
|
+
assertOwnTenant(input.tenantId, ctx.request.subject)
|
|
18
|
+
|
|
19
|
+
// Framework auto-injects an `order_…` id + the tenant/audit columns.
|
|
20
|
+
const row = await ctx.store.insert('orders', {
|
|
21
|
+
status: 'placed',
|
|
22
|
+
customerName: input.customerName,
|
|
23
|
+
amountCents: input.amountCents,
|
|
24
|
+
tenantId: input.tenantId,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Post-commit fan-out → the `order.placed` trigger starts the
|
|
28
|
+
// `orders.fulfill` workflow. `ctx.events` is only present when the
|
|
29
|
+
// runtime wired the events facade (it always is under `voltro dev`);
|
|
30
|
+
// guard so a unit-test ctx without it doesn't throw.
|
|
31
|
+
await ctx.events?.emit('order.placed', {
|
|
32
|
+
orderId: row['id'] as string,
|
|
33
|
+
tenantId: input.tenantId,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
id: row['id'] as string,
|
|
38
|
+
status: row['status'] as string,
|
|
39
|
+
customerName: row['customerName'] as string,
|
|
40
|
+
amountCents: row['amountCents'] as number,
|
|
41
|
+
tenantId: row['tenantId'] as string,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default execute
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `orders.place` — DESCRIPTOR (browser-safe: schema + target + typed
|
|
2
|
+
// error only; NO node/database/server imports).
|
|
3
|
+
//
|
|
4
|
+
// `TenantMismatch` is imported from the `/guard` subpath, NOT the
|
|
5
|
+
// package root — the root re-exports the schema mixin, which would drag
|
|
6
|
+
// `@voltro/database` into the client rpcGroup bundle. `/guard` is the
|
|
7
|
+
// browser-safe surface for the typed error you declare here.
|
|
8
|
+
|
|
9
|
+
import { defineMutation } from '@voltro/protocol'
|
|
10
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
11
|
+
import { Schema } from 'effect'
|
|
12
|
+
|
|
13
|
+
export const placeOrder = defineMutation({
|
|
14
|
+
name: 'orders.place',
|
|
15
|
+
// Auto-optimistic: every query whose `source: 'orders'` matches gets a
|
|
16
|
+
// placeholder row prepended; the server delta replaces it on commit.
|
|
17
|
+
target: {
|
|
18
|
+
table: 'orders',
|
|
19
|
+
op: 'insert',
|
|
20
|
+
shape: (input: { tenantId: string; customerName: string; amountCents: number }) => ({
|
|
21
|
+
status: 'placed',
|
|
22
|
+
customerName: input.customerName,
|
|
23
|
+
amountCents: input.amountCents,
|
|
24
|
+
tenantId: input.tenantId,
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
input: Schema.Struct({
|
|
28
|
+
tenantId: Schema.String,
|
|
29
|
+
customerName: Schema.NonEmptyString,
|
|
30
|
+
amountCents: Schema.Int.pipe(Schema.greaterThan(0)),
|
|
31
|
+
}),
|
|
32
|
+
output: Schema.Struct({
|
|
33
|
+
id: Schema.String,
|
|
34
|
+
status: Schema.String,
|
|
35
|
+
customerName: Schema.String,
|
|
36
|
+
amountCents: Schema.Number,
|
|
37
|
+
tenantId: Schema.String,
|
|
38
|
+
}),
|
|
39
|
+
error: TenantMismatch,
|
|
40
|
+
})
|
|
@@ -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-multitenancy": "0.1.0",
|
|
18
|
+
"@voltro/protocol": "0.1.0",
|
|
19
|
+
"@voltro/runtime": "0.1.0",
|
|
20
|
+
"@voltro/workflow": "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,37 @@
|
|
|
1
|
+
// `nightlyReport` — a cron schedule (single-file `*.cron.tsx`).
|
|
2
|
+
//
|
|
3
|
+
// Runs at 02:00 UTC every day. `timezone` is MANDATORY — the cron is
|
|
4
|
+
// interpreted in this IANA zone, never the container's local TZ. The
|
|
5
|
+
// expression is validated at boot (a typo throws in the discovery log,
|
|
6
|
+
// it doesn't silently never-fire).
|
|
7
|
+
//
|
|
8
|
+
// The handler is "a mutation the clock invokes": `ctx.app` is the same
|
|
9
|
+
// AppContext shape a mutation gets (store + interceptors). For durable
|
|
10
|
+
// multi-step work, prefer the `workflow: { name, payload }` form
|
|
11
|
+
// instead of a handler.
|
|
12
|
+
//
|
|
13
|
+
// Zero-config coordination: on `store: 'memory'` this runs single-
|
|
14
|
+
// process; on a multi-instance SQL store the framework advisory-lock
|
|
15
|
+
// coordinates so exactly one replica fires each tick.
|
|
16
|
+
|
|
17
|
+
import { defineSchedule } from '@voltro/runtime'
|
|
18
|
+
import { count, eq } from '@voltro/database'
|
|
19
|
+
import { database } from '../database/schema'
|
|
20
|
+
|
|
21
|
+
export default defineSchedule({
|
|
22
|
+
name: 'nightlyReport',
|
|
23
|
+
cron: '0 2 * * *', // 02:00 every day
|
|
24
|
+
timezone: 'UTC',
|
|
25
|
+
description: 'Logs a per-status order tally each night.',
|
|
26
|
+
handler: async (ctx) => {
|
|
27
|
+
// Tally placed-but-not-yet-shipped orders as a simple health signal.
|
|
28
|
+
const open = await ctx.app.store.query(
|
|
29
|
+
database.orders.where(eq('status', 'placed')).aggregate({ total: count() }).descriptor,
|
|
30
|
+
)
|
|
31
|
+
const total = (open[0] as { total?: number } | undefined)?.total ?? 0
|
|
32
|
+
console.info(`[nightlyReport] fired for ${ctx.scheduledAt.toISOString()} — ${total} order(s) still 'placed'`)
|
|
33
|
+
},
|
|
34
|
+
// optional, all default-safe:
|
|
35
|
+
onOverlap: 'skip',
|
|
36
|
+
maxRuntimeMs: 5 * 60_000,
|
|
37
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// `warm` — a run-once startup hook (`*.startup.tsx`).
|
|
2
|
+
//
|
|
3
|
+
// Default-exports a function (no `define*` wrapper). Runs ONCE after
|
|
4
|
+
// migrations + seeds, when the rpc server is listening. Use it for
|
|
5
|
+
// long-lived boot work: warm a cache, open a broker consumer / upstream
|
|
6
|
+
// websocket, start a background interval, register a process singleton —
|
|
7
|
+
// and register a teardown via `onShutdown`.
|
|
8
|
+
//
|
|
9
|
+
// A throw here does NOT block boot (it's logged; the rpc surface stays
|
|
10
|
+
// up). Distinct from `*.seed.ts` (runs once and RETURNS) and `*.cron.tsx`
|
|
11
|
+
// (periodic) — a startup HOLDS a resource for the process lifetime.
|
|
12
|
+
//
|
|
13
|
+
// NOTE: `StartupContext` lives on the `@voltro/cli/startup` subpath (the
|
|
14
|
+
// runtime package stays free of the node:fs discovery code).
|
|
15
|
+
|
|
16
|
+
import type { StartupContext } from '@voltro/cli/startup'
|
|
17
|
+
|
|
18
|
+
export default async ({ store, log, onShutdown, id }: StartupContext) => {
|
|
19
|
+
log.info(`startup '${id}': warming order-fulfillment caches`)
|
|
20
|
+
|
|
21
|
+
// Example long-lived resource: a heartbeat interval. Replace with a
|
|
22
|
+
// real warm-up (preload a dashboard cache, open a consumer, …).
|
|
23
|
+
const timer = setInterval(() => {
|
|
24
|
+
void store // `store` is the already-migrated DataStore, ready to use.
|
|
25
|
+
}, 60_000)
|
|
26
|
+
|
|
27
|
+
// Teardown runs on SIGTERM / SIGINT, LIFO across all startups, each
|
|
28
|
+
// awaited with a hard 5s timeout. Always release what you acquire.
|
|
29
|
+
onShutdown(() => {
|
|
30
|
+
clearInterval(timer)
|
|
31
|
+
log.info(`startup '${id}': torn down`)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// `orderChanges` — a post-commit subscriber on the `orders` table.
|
|
2
|
+
//
|
|
3
|
+
// Fires AFTER commit (the row IS written), best-effort + fire-and-forget
|
|
4
|
+
// (a throw logs and the next event still arrives; it can't back-pressure
|
|
5
|
+
// the change stream). Use it for reactions that aren't part of the
|
|
6
|
+
// write transaction — notify a fulfillment desk, mirror to a search
|
|
7
|
+
// index, bump a metric. For crash-safe multi-step reactions, have the
|
|
8
|
+
// subscriber start a workflow instead.
|
|
9
|
+
//
|
|
10
|
+
// `on: 'any'` matches insert / update / delete. `event.new` is present
|
|
11
|
+
// on insert + update (null on delete); `event.old` on update + delete
|
|
12
|
+
// (null on insert).
|
|
13
|
+
|
|
14
|
+
import { defineSubscriber } from '@voltro/runtime'
|
|
15
|
+
|
|
16
|
+
export default defineSubscriber({
|
|
17
|
+
table: 'orders',
|
|
18
|
+
on: 'any',
|
|
19
|
+
handler: async (event, ctx) => {
|
|
20
|
+
const row = (event.new ?? event.old) as { id?: string; status?: string } | null
|
|
21
|
+
ctx.log.info('order changed', {
|
|
22
|
+
op: event.op,
|
|
23
|
+
id: row?.id,
|
|
24
|
+
status: row?.status,
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-durable",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "A cohesive order-fulfillment domain demonstrating the whole durable surface: a durable workflow (step / sleep / awaitSignal human-approval gate), an event trigger that starts it, a signal-injecting mutation, a nightly cron schedule, a table subscriber, a materialized aggregate, and a run-once startup hook.",
|
|
5
|
+
"tags": ["api", "durable", "workflow", "schedule", "subscriber", "aggregate", "startup", "events"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Unit tests with `@voltro/testing` — no database, no running server. Two
|
|
2
|
+
// things get covered:
|
|
3
|
+
//
|
|
4
|
+
// 1. `orders.place` — the runnable primitive. `makeTestContext` hands the
|
|
5
|
+
// handler the SAME mixin-wrapped `ctx.store` it gets in production, so
|
|
6
|
+
// tenant auto-scoping + the `order_…` id injection behave exactly as at
|
|
7
|
+
// runtime.
|
|
8
|
+
// 2. `orders.fulfill` — the workflow is descriptor-pinned. A real run PARKS
|
|
9
|
+
// on `awaitSignal('approval')` (a human-approval gate), so a plain
|
|
10
|
+
// `start()` never resolves in a unit test; a full run needs a signal
|
|
11
|
+
// (integration). Here we assert the wire contract (name / payload /
|
|
12
|
+
// success decode) and that the executor factory is wired.
|
|
13
|
+
//
|
|
14
|
+
// Run with `voltro test` (vitest).
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest'
|
|
17
|
+
import { Schema } from 'effect'
|
|
18
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
19
|
+
import { database } from '../database/schema' // registers orders / actors / tenants
|
|
20
|
+
import placeOrder from '../mutations/orders.place.mutation.server'
|
|
21
|
+
import { FulfillOrder } from '../workflows/order.fulfill.workflow'
|
|
22
|
+
import buildExecute from '../workflows/order.fulfill.workflow.server'
|
|
23
|
+
|
|
24
|
+
describe('orders.place', () => {
|
|
25
|
+
it('inserts an order, auto-stamped to the caller tenant', async () => {
|
|
26
|
+
const ctx = makeTestContext({
|
|
27
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
28
|
+
store: mockStore({ orders: [] }),
|
|
29
|
+
})
|
|
30
|
+
const row = await placeOrder(
|
|
31
|
+
{ tenantId: 'acme', customerName: 'Ada Lovelace', amountCents: 4200 },
|
|
32
|
+
ctx,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(row.status).toBe('placed')
|
|
36
|
+
expect(row.customerName).toBe('Ada Lovelace')
|
|
37
|
+
expect(row.amountCents).toBe(4200)
|
|
38
|
+
expect(row.tenantId).toBe('acme') // tenant() stamped it from the subject
|
|
39
|
+
expect(row.id).toMatch(/^order_/) // TypeID auto-injected
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('isolates tenants — t2 never sees t1 rows (REAL tenant scoping)', async () => {
|
|
43
|
+
const ctx = makeTestContext({
|
|
44
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
45
|
+
store: mockStore({ orders: [] }),
|
|
46
|
+
})
|
|
47
|
+
await placeOrder({ tenantId: 't1', customerName: 'Grace', amountCents: 100 }, ctx)
|
|
48
|
+
|
|
49
|
+
// Re-scope the SAME store to a different tenant — the real WHERE filter
|
|
50
|
+
// hides t1's row from t2.
|
|
51
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.orders.descriptor))
|
|
52
|
+
expect(seenByT2).toHaveLength(0)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('orders.fulfill (workflow descriptor)', () => {
|
|
57
|
+
it('declares the expected wire name', () => {
|
|
58
|
+
expect(FulfillOrder.name).toBe('orders.fulfill')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('accepts a valid { orderId, tenantId } payload and rejects a malformed one', () => {
|
|
62
|
+
const decode = Schema.decodeUnknownSync(FulfillOrder.payloadSchema)
|
|
63
|
+
expect(decode({ orderId: 'order_1', tenantId: 'acme' })).toMatchObject({
|
|
64
|
+
orderId: 'order_1',
|
|
65
|
+
tenantId: 'acme',
|
|
66
|
+
})
|
|
67
|
+
expect(() => decode({ orderId: 'order_1' })).toThrow() // missing tenantId
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('declares a { orderId, outcome } success schema with a literal outcome', () => {
|
|
71
|
+
const decode = Schema.decodeUnknownSync(FulfillOrder.successSchema)
|
|
72
|
+
expect(decode({ orderId: 'order_1', outcome: 'shipped' })).toEqual({
|
|
73
|
+
orderId: 'order_1',
|
|
74
|
+
outcome: 'shipped',
|
|
75
|
+
})
|
|
76
|
+
expect(decode({ orderId: 'order_1', outcome: 'rejected' })).toEqual({
|
|
77
|
+
orderId: 'order_1',
|
|
78
|
+
outcome: 'rejected',
|
|
79
|
+
})
|
|
80
|
+
expect(() => decode({ orderId: 'order_1', outcome: 'delivered' })).toThrow() // not in the literal union
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('exposes an executor factory (ctx) => run — a real run needs an approval signal (integration)', () => {
|
|
84
|
+
const ctx = makeTestContext({
|
|
85
|
+
subject: { type: 'user', id: 'user_1', tenantId: 't1' },
|
|
86
|
+
store: mockStore({ orders: [] }),
|
|
87
|
+
})
|
|
88
|
+
expect(typeof buildExecute).toBe('function')
|
|
89
|
+
expect(typeof buildExecute(ctx)).toBe('function')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Event → workflow trigger. Auto-discovered (`*.trigger.tsx`); no
|
|
2
|
+
// app.config wiring needed.
|
|
3
|
+
//
|
|
4
|
+
// When any handler emits `order.placed` (the `orders.place` mutation
|
|
5
|
+
// does, post-commit), this trigger starts the `orders.fulfill`
|
|
6
|
+
// workflow. The event payload IS the workflow payload here (both are
|
|
7
|
+
// `{ orderId, tenantId }`), so no `payload` mapper is needed — pass one
|
|
8
|
+
// when the shapes differ.
|
|
9
|
+
//
|
|
10
|
+
// `idempotencyKey` dedups deliveries: if the same event is re-delivered
|
|
11
|
+
// (crash + replay), the matching key short-circuits the second start.
|
|
12
|
+
|
|
13
|
+
import { defineEventTrigger } from '@voltro/runtime'
|
|
14
|
+
|
|
15
|
+
export default defineEventTrigger<
|
|
16
|
+
{ orderId: string; tenantId: string }
|
|
17
|
+
>({
|
|
18
|
+
event: 'order.placed',
|
|
19
|
+
workflow: 'orders.fulfill',
|
|
20
|
+
idempotencyKey: (event) => `order.placed:${event.data.orderId}`,
|
|
21
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// `orders.fulfill` — EXECUTOR (server-only).
|
|
2
|
+
//
|
|
3
|
+
// Durable order-fulfillment pipeline. The default export is a factory
|
|
4
|
+
// `(ctx) => execute`; the CLI calls `workflow.toLayer(execute)` at boot
|
|
5
|
+
// so the executor closes over the live `AppContext` (store, events, …).
|
|
6
|
+
//
|
|
7
|
+
// The flow, every step durable (replayed from its journaled result on a
|
|
8
|
+
// crash/resume rather than re-run):
|
|
9
|
+
//
|
|
10
|
+
// 1. step 'reserve-stock' — flip the order to `reserved`.
|
|
11
|
+
// 2. sleep — a (short) durable delay; the wake time is
|
|
12
|
+
// journaled in the engine. Bump to hours/days
|
|
13
|
+
// for a real "hold before charging" window.
|
|
14
|
+
// 3. awaitSignal 'approval' — PARK until a human sends the approval
|
|
15
|
+
// signal (via the `orders.approve` mutation
|
|
16
|
+
// or the dashboard "Send signal" button).
|
|
17
|
+
// 4. step 'ship' / mark rejected — branch on the decision.
|
|
18
|
+
//
|
|
19
|
+
// `withCompensation` wraps the reserve step with saga-style rollback:
|
|
20
|
+
// the finalizer runs only if the WHOLE workflow later fails, releasing
|
|
21
|
+
// the reservation so a downstream failure doesn't strand held stock.
|
|
22
|
+
|
|
23
|
+
import { step, sleep, awaitSignal, withCompensation } from '@voltro/workflow'
|
|
24
|
+
import { Effect, Schema } from 'effect'
|
|
25
|
+
import type { AppContext } from '@voltro/runtime'
|
|
26
|
+
|
|
27
|
+
const Decision = Schema.Struct({ approved: Schema.Boolean })
|
|
28
|
+
|
|
29
|
+
const buildExecute = (ctx: AppContext) =>
|
|
30
|
+
(payload: { orderId: string; tenantId: string }, _executionId: string) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const { orderId } = payload
|
|
33
|
+
|
|
34
|
+
// 1. Reserve stock. `withCompensation` registers an undo that runs
|
|
35
|
+
// only if the workflow as a whole fails after this point.
|
|
36
|
+
yield* withCompensation(
|
|
37
|
+
step({
|
|
38
|
+
name: 'reserve-stock',
|
|
39
|
+
input: { orderId },
|
|
40
|
+
success: Schema.Struct({ orderId: Schema.String }),
|
|
41
|
+
execute: Effect.tryPromise({
|
|
42
|
+
try: async () => {
|
|
43
|
+
await ctx.store.update('orders', orderId, { status: 'reserved' })
|
|
44
|
+
return { orderId }
|
|
45
|
+
},
|
|
46
|
+
catch: (error) => error as never,
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
// Saga rollback: release the reservation on whole-workflow failure.
|
|
50
|
+
() =>
|
|
51
|
+
Effect.promise(() =>
|
|
52
|
+
ctx.store
|
|
53
|
+
.update('orders', orderId, { status: 'placed' })
|
|
54
|
+
.then(() => undefined),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// 2. Durable delay. The wake is journaled — survives a restart.
|
|
59
|
+
yield* sleep({ name: 'settle-window', duration: '5 seconds' })
|
|
60
|
+
|
|
61
|
+
// 3. Human-approval gate. The run parks here until an external
|
|
62
|
+
// caller sends a matching `approval` signal; the parsed payload
|
|
63
|
+
// is journaled, so a post-signal replay returns it instantly.
|
|
64
|
+
const decision = yield* awaitSignal(ctx, {
|
|
65
|
+
name: 'approval',
|
|
66
|
+
schema: Decision,
|
|
67
|
+
// pollIntervalMs defaults to 200 (backoff → 5s); timeoutMs 24h.
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (!decision.approved) {
|
|
71
|
+
yield* step({
|
|
72
|
+
name: 'mark-rejected',
|
|
73
|
+
input: { orderId },
|
|
74
|
+
success: Schema.Struct({ orderId: Schema.String }),
|
|
75
|
+
execute: Effect.tryPromise({
|
|
76
|
+
try: async () => {
|
|
77
|
+
await ctx.store.update('orders', orderId, { status: 'rejected' })
|
|
78
|
+
return { orderId }
|
|
79
|
+
},
|
|
80
|
+
catch: (error) => error as never,
|
|
81
|
+
}),
|
|
82
|
+
})
|
|
83
|
+
return { orderId, outcome: 'rejected' as const }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Approved → ship.
|
|
87
|
+
yield* step({
|
|
88
|
+
name: 'ship',
|
|
89
|
+
input: { orderId },
|
|
90
|
+
success: Schema.Struct({ orderId: Schema.String }),
|
|
91
|
+
execute: Effect.tryPromise({
|
|
92
|
+
try: async () => {
|
|
93
|
+
await ctx.store.update('orders', orderId, { status: 'shipped' })
|
|
94
|
+
return { orderId }
|
|
95
|
+
},
|
|
96
|
+
catch: (error) => error as never,
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return { orderId, outcome: 'shipped' as const }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
export default buildExecute
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// `orders.fulfill` — DESCRIPTOR (browser-safe).
|
|
2
|
+
//
|
|
3
|
+
// Imports `workflow` from the `@voltro/workflow/define` subpath — the
|
|
4
|
+
// browser-safe contract surface. The codegen value-imports this so the
|
|
5
|
+
// web client can start the workflow over rpc; importing the executor's
|
|
6
|
+
// server graph here would drag database/cluster into the browser bundle.
|
|
7
|
+
|
|
8
|
+
import { workflow } from '@voltro/workflow/define'
|
|
9
|
+
import { Schema } from 'effect'
|
|
10
|
+
|
|
11
|
+
export const FulfillOrder = workflow({
|
|
12
|
+
name: 'orders.fulfill',
|
|
13
|
+
payload: {
|
|
14
|
+
orderId: Schema.String,
|
|
15
|
+
tenantId: Schema.String,
|
|
16
|
+
},
|
|
17
|
+
success: Schema.Struct({
|
|
18
|
+
orderId: Schema.String,
|
|
19
|
+
outcome: Schema.Literal('shipped', 'rejected'),
|
|
20
|
+
}),
|
|
21
|
+
// Dedup concurrent starts for the same order — the `order.placed`
|
|
22
|
+
// trigger could (after a crash + replay) fan out twice; the same key
|
|
23
|
+
// collapses them to one run.
|
|
24
|
+
idempotencyKey: ({ orderId }) => `fulfill:${orderId}`,
|
|
25
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro API scaffold (template: **api-feature-flags**) — feature flags with
|
|
4
|
+
`@voltro/plugin-flags`: kill-switches, % rollout, targeting, and two ways to
|
|
5
|
+
gate rpc calls.
|
|
6
|
+
|
|
7
|
+
## Boot
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
12
|
+
# → http://localhost:4000 (store: memory — zero infra)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What this shows
|
|
16
|
+
|
|
17
|
+
- **Flags as code** — declared in `app.config.ts`: a bare boolean is a
|
|
18
|
+
kill-switch (`newEditor: true`, `betaExport: false`); a rich definition adds
|
|
19
|
+
a deterministic `% rollout` (`gradualRollout: { rollout: 25 }`) and
|
|
20
|
+
OR-of-rules `targeting` (`proDashboard: { targeting: [{ metadata: { plan:
|
|
21
|
+
'pro' } }] }`).
|
|
22
|
+
- **Two ways to gate** —
|
|
23
|
+
- **Declarative** (`gatedBy`): `notes.export` is mapped to `betaExport`. The
|
|
24
|
+
framework fails typed `FlagDisabled` BEFORE the handler runs when the flag
|
|
25
|
+
is off — no code in the handler.
|
|
26
|
+
- **In-handler** (`requireFlag`): `notes.create` calls
|
|
27
|
+
`requireFlag(ctx, 'newEditor')`, the Effect-native guard (same shape as
|
|
28
|
+
`requireScope` / `requireEntitlement`).
|
|
29
|
+
- **Typed on the client** — both paths fail `FlagDisabled`, declared on the
|
|
30
|
+
procedure's `error:`, so the browser decodes it typed.
|
|
31
|
+
|
|
32
|
+
## Try it
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# newEditor is ON → requireFlag passes → note created:
|
|
36
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
37
|
+
-d '{"tag":"notes.create","input":{"tenantId":"acme","title":"Flagged note","body":"hi"}}'
|
|
38
|
+
|
|
39
|
+
# betaExport is OFF → gatedBy blocks before the handler → typed FlagDisabled:
|
|
40
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
41
|
+
-d '{"tag":"notes.export","input":{}}'
|
|
42
|
+
# → { ok:false, error:{ _tag:"FlagDisabled", flag:"betaExport" } }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Flip `betaExport: true` in `app.config.ts` (or toggle it at runtime with
|
|
46
|
+
`store: 'postgres'`) and `notes.export` starts succeeding — no code change.
|
|
47
|
+
|
|
48
|
+
## On the web side
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { useFlag } from '@voltro/plugin-flags/web'
|
|
52
|
+
const showBeta = useFlag('betaExport') // evaluated for the signed-in subject
|
|
53
|
+
{showBeta && <BetaExportButton />}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`useFlag` reads the synthesized `flags.evaluate` query, so client UI gating
|
|
57
|
+
uses the SAME evaluation (targeting + rollout) as the server guards.
|
|
58
|
+
|
|
59
|
+
## Runtime toggles
|
|
60
|
+
|
|
61
|
+
Set `store: 'postgres'` to overlay runtime-toggleable flags (in
|
|
62
|
+
`_voltro_feature_flags`) on the code baseline — flip a kill-switch without a
|
|
63
|
+
deploy. The dashboard ships a **Flags** panel for exactly this.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
import { database } from '../database/schema'
|
|
3
|
+
|
|
4
|
+
// Runs ONLY when the `betaExport` gate passes (see app.config.ts gatedBy).
|
|
5
|
+
// The body itself is flag-agnostic — just counts the tenant's notes.
|
|
6
|
+
const execute = async (_input: Record<string, never>, ctx: AppContext) => {
|
|
7
|
+
const rows = await ctx.store.query(database.notes.descriptor)
|
|
8
|
+
return { exported: rows.length }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default execute
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// "Export all notes" — gated DECLARATIVELY. There's no flag check in the
|
|
2
|
+
// handler: the `gatedBy: { 'notes.export': 'betaExport' }` map in app.config.ts
|
|
3
|
+
// makes the framework fail `FlagDisabled` BEFORE this handler runs whenever
|
|
4
|
+
// `betaExport` is off. Flip the flag (config, or a postgres-store toggle) and
|
|
5
|
+
// the same call starts succeeding — no code change.
|
|
6
|
+
//
|
|
7
|
+
// Declaring `error: FlagDisabled` surfaces the gate's rejection typed.
|
|
8
|
+
|
|
9
|
+
import { defineAction } from '@voltro/protocol'
|
|
10
|
+
import { FlagDisabled } from '@voltro/plugin-flags/errors'
|
|
11
|
+
import { Schema } from 'effect'
|
|
12
|
+
|
|
13
|
+
export const exportNotes = defineAction({
|
|
14
|
+
name: 'notes.export',
|
|
15
|
+
input: Schema.Struct({}),
|
|
16
|
+
output: Schema.Struct({ exported: Schema.Number }),
|
|
17
|
+
error: FlagDisabled,
|
|
18
|
+
})
|