@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,100 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro backend scaffold (template: **api-kv**).
|
|
4
|
+
|
|
5
|
+
The **durable key-value** template. It demonstrates `ctx.kv` — the framework's
|
|
6
|
+
storage primitive for state you **can't recompute** — on a small
|
|
7
|
+
**external-event-sync** domain: a sync **cursor** (a watermark) and TTL-bounded
|
|
8
|
+
**idempotency markers** that dedupe redelivered events. It boots with **zero
|
|
9
|
+
infrastructure** (`store: 'memory'`).
|
|
10
|
+
|
|
11
|
+
## The one idea
|
|
12
|
+
|
|
13
|
+
Three storage primitives, three contracts — pick by what a loss costs you:
|
|
14
|
+
|
|
15
|
+
| Primitive | Holds | If you lose it | Backend default |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `ctx.cache` | recomputable derived views | free — recompute | memory (evicts for capacity) |
|
|
18
|
+
| **`ctx.kv`** | **the cursor + idempotency markers** | **re-process / double-process** | **database (durable, never evicted)** |
|
|
19
|
+
| `ctx.store` | the synced rows | re-fetchable from the source | your SQL store |
|
|
20
|
+
|
|
21
|
+
A cursor is the textbook `ctx.kv` case: it isn't derived from anything you still
|
|
22
|
+
have, so a cache (which evicts) is wrong and a hand-managed table row is
|
|
23
|
+
overkill. This template shows why.
|
|
24
|
+
|
|
25
|
+
## Boot
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm install # at the repo root
|
|
29
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
30
|
+
# → http://localhost:4000
|
|
31
|
+
# → ws://localhost:4000/ws
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No env or services required. On `store: 'memory'`, `ctx.kv` runs on its
|
|
35
|
+
`database` backend in-process — durable within the run, reset on restart.
|
|
36
|
+
Switch `app.config.ts` to `store: 'postgres'` and the cursor + markers survive
|
|
37
|
+
restarts and are shared across replicas. To put **just the KV** on Redis (a
|
|
38
|
+
persistent one), set `kv: 'redis'` / `KV_BACKEND=redis` — the handler code does
|
|
39
|
+
not change.
|
|
40
|
+
|
|
41
|
+
## What it demonstrates
|
|
42
|
+
|
|
43
|
+
| File | Primitive | `ctx.kv` surface |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `app.config.ts` | app config | zero-infra boot; the optional `kv:` / `cache:` backend knobs. |
|
|
46
|
+
| `database/schema.ts` | schema | the `synced_events` rows (store) — deliberately NOT the cursor. |
|
|
47
|
+
| `actions/sync.pull.action.*` | action | **`getOrElse`** (cursor) → **`has`** / **`set(…, { ttlMs })`** (markers) → **`set`** (advance cursor). |
|
|
48
|
+
| `actions/sync.status.action.*` | action (Effect) | **`yield* Kv`** — `getOrElse` (cursor) + **`list`** (count live markers). |
|
|
49
|
+
| `actions/sync.reset.action.*` | action | **`delete`** (cursor) + **`list`** / **`delete`** (markers). |
|
|
50
|
+
| `queries/events.list.query.*` | query | reactive stream of the synced ROWS (store, for contrast). |
|
|
51
|
+
|
|
52
|
+
## The end-to-end flow
|
|
53
|
+
|
|
54
|
+
1. **`sync.pull`** reads the durable cursor with `getOrElse` (default `0` on the
|
|
55
|
+
first-ever call), pulls the next page from the (simulated) upstream, and for
|
|
56
|
+
each event: `has(marker)` → skip if seen, else `store.insert` the row +
|
|
57
|
+
`set(marker, { ttlMs })`. Finally it `set`s the cursor to the new watermark.
|
|
58
|
+
2. **`sync.status`** reads the cursor and `list`s the live markers.
|
|
59
|
+
3. **`sync.reset`** `delete`s the cursor. A following `sync.pull` re-reads from
|
|
60
|
+
`0` but **skips everything** — the markers survive independently, so
|
|
61
|
+
idempotency holds across a cursor rewind. Pass `{ markers: true }` to also
|
|
62
|
+
drop the markers and force a full re-ingest.
|
|
63
|
+
|
|
64
|
+
## Try it
|
|
65
|
+
|
|
66
|
+
Over the rpc surface (a `voltro dev` web client, `POST /rpc`, or the inspect
|
|
67
|
+
`invoke` endpoint):
|
|
68
|
+
|
|
69
|
+
```jsonc
|
|
70
|
+
{ "tag": "sync.pull", "input": { "limit": 5 } } // → { pulled: 5, skipped: 0, cursor: 5 }
|
|
71
|
+
{ "tag": "sync.pull", "input": { "limit": 5 } } // → { pulled: 5, skipped: 0, cursor: 10 }
|
|
72
|
+
{ "tag": "sync.status", "input": {} } // → { cursor: 10, activeMarkers: 10 }
|
|
73
|
+
{ "tag": "sync.reset", "input": {} } // → { cursorCleared: true, markersCleared: 0 }
|
|
74
|
+
{ "tag": "sync.pull", "input": { "limit": 5 } } // → { pulled: 0, skipped: 5, cursor: 5 } ← markers survived
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Watch the synced rows stream in live via the `events.list` subscription (the
|
|
78
|
+
dashboard's data tab, or any web client).
|
|
79
|
+
|
|
80
|
+
## TTL & tenant namespacing
|
|
81
|
+
|
|
82
|
+
- **TTL** — markers are written with `{ ttlMs }` (a 24h idempotency window).
|
|
83
|
+
Expiry is lazy: an expired marker reads as a miss and is dropped on the next
|
|
84
|
+
`has`/`get`. The cursor has **no** TTL — it's permanent until deleted.
|
|
85
|
+
- **Tenant namespacing** — `ctx.kv` keys are app-global; the facade never
|
|
86
|
+
namespaces for you. Every key here folds in `ctx.request.subject.tenantId`
|
|
87
|
+
(`sync:${tenantId}:…`) so two tenants' cursors/markers never collide.
|
|
88
|
+
|
|
89
|
+
## Notes
|
|
90
|
+
|
|
91
|
+
- All primitives are **auto-discovered by file convention** — nothing is
|
|
92
|
+
registered in `app.config.ts`.
|
|
93
|
+
- `action` / `query` keep the **descriptor / executor split**: the browser-safe
|
|
94
|
+
`*.ts` descriptor never imports `@voltro/database`, `@voltro/runtime`, or
|
|
95
|
+
`node:*`; the `.server.ts` executor does the work.
|
|
96
|
+
- Durable KV has **two handler surfaces**, both wired in every runtime
|
|
97
|
+
(`voltro dev` AND `voltro serve`): the async **`ctx.kv`** facade (used by
|
|
98
|
+
`sync.pull` / `sync.reset`) and the Effect-native **`Kv`** service via
|
|
99
|
+
`yield* Kv` (used by `sync.status`). They share one resolved store, so pick by
|
|
100
|
+
whether your handler is async or an `Effect` — the state is identical.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// `sync.pull` — EXECUTOR (server-only, default export).
|
|
2
|
+
//
|
|
3
|
+
// The heart of the template: durable `ctx.kv` used for the two kinds of state
|
|
4
|
+
// you CAN'T recompute in an external sync.
|
|
5
|
+
//
|
|
6
|
+
// 1. the CURSOR — a watermark past everything already pulled. Lose it and
|
|
7
|
+
// you re-process (or skip) events. Read with getOrElse, advanced with set.
|
|
8
|
+
// 2. the MARKERS — one per external id, so a redelivered event isn't ingested
|
|
9
|
+
// twice. Written with a TTL (a bounded idempotency window). has() checks
|
|
10
|
+
// one without deserializing.
|
|
11
|
+
//
|
|
12
|
+
// The event ROWS themselves go to `ctx.store` — they're re-fetchable from the
|
|
13
|
+
// source, so a relational table is their home (query/join/report on them).
|
|
14
|
+
|
|
15
|
+
import type { AppContext } from '@voltro/runtime'
|
|
16
|
+
|
|
17
|
+
// Stand-in for a real upstream (a webhook backlog, a 3rd-party API page).
|
|
18
|
+
// Deterministic + pure so the template boots with zero infra and no network —
|
|
19
|
+
// event N has externalId `ext-N`. A real handler would `fetch()` here instead.
|
|
20
|
+
const fetchSince = (sequence: number, limit: number) =>
|
|
21
|
+
Array.from({ length: limit }, (_unused, i) => {
|
|
22
|
+
const seq = sequence + i + 1
|
|
23
|
+
return {
|
|
24
|
+
externalId: `ext-${seq}`,
|
|
25
|
+
kind: seq % 3 === 0 ? 'updated' : 'created',
|
|
26
|
+
payload: JSON.stringify({ seq }),
|
|
27
|
+
sequence: seq,
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Idempotency window: how long a "seen" marker lives. Long enough to dedupe
|
|
32
|
+
// redelivery, not forever — a bounded lifetime on an otherwise-durable store.
|
|
33
|
+
const MARKER_TTL_MS = 24 * 60 * 60_000 // 24h
|
|
34
|
+
|
|
35
|
+
const execute = async (input: { limit?: number }, ctx: AppContext) => {
|
|
36
|
+
// KV keys are app-global — fold the tenant in so two tenants never collide.
|
|
37
|
+
const tenantId = ctx.request.subject.tenantId ?? 'anon'
|
|
38
|
+
const cursorKey = `sync:${tenantId}:cursor`
|
|
39
|
+
const limit = Math.min(input.limit ?? 5, 50)
|
|
40
|
+
|
|
41
|
+
// Durable cursor — getOrElse writes the default (0) ONLY on a genuine
|
|
42
|
+
// first-run miss; on every later call it returns the stored watermark.
|
|
43
|
+
const cursor = await ctx.kv.getOrElse<number>(cursorKey, () => 0)
|
|
44
|
+
|
|
45
|
+
let pulled = 0
|
|
46
|
+
let skipped = 0
|
|
47
|
+
let highest = cursor
|
|
48
|
+
|
|
49
|
+
for (const evt of fetchSince(cursor, limit)) {
|
|
50
|
+
const markerKey = `sync:${tenantId}:seen:${evt.externalId}`
|
|
51
|
+
if (await ctx.kv.has(markerKey)) {
|
|
52
|
+
// Already ingested within the TTL window — the source redelivered it.
|
|
53
|
+
skipped++
|
|
54
|
+
} else {
|
|
55
|
+
await ctx.store.insert('synced_events', {
|
|
56
|
+
externalId: evt.externalId,
|
|
57
|
+
kind: evt.kind,
|
|
58
|
+
payload: evt.payload,
|
|
59
|
+
sequence: evt.sequence,
|
|
60
|
+
tenantId,
|
|
61
|
+
})
|
|
62
|
+
// Mark seen, with a TTL. Value carries the sequence for traceability.
|
|
63
|
+
await ctx.kv.set(markerKey, evt.sequence, { ttlMs: MARKER_TTL_MS })
|
|
64
|
+
pulled++
|
|
65
|
+
}
|
|
66
|
+
highest = Math.max(highest, evt.sequence)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Advance the durable watermark past everything we just saw.
|
|
70
|
+
await ctx.kv.set(cursorKey, highest)
|
|
71
|
+
|
|
72
|
+
return { pulled, skipped, cursor: highest }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default execute
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// `sync.pull` — DESCRIPTOR (browser-safe: schema + name only; NO
|
|
2
|
+
// node/database/server imports — the codegen pulls this into the client
|
|
3
|
+
// rpcGroup bundle).
|
|
4
|
+
//
|
|
5
|
+
// Pull the next page of events from the (simulated) upstream. This is an
|
|
6
|
+
// ACTION, not a mutation/query: it does arbitrary imperative work and returns
|
|
7
|
+
// a computed summary rather than a single optimistic row or a reactive table
|
|
8
|
+
// scan. The durable `ctx.kv` cursor + idempotency markers live in the
|
|
9
|
+
// executor (`.server.ts`).
|
|
10
|
+
|
|
11
|
+
import { defineAction } from '@voltro/protocol'
|
|
12
|
+
import { Schema } from 'effect'
|
|
13
|
+
|
|
14
|
+
export const syncPull = defineAction({
|
|
15
|
+
name: 'sync.pull',
|
|
16
|
+
input: Schema.Struct({
|
|
17
|
+
// How many upstream events to pull this call (default 5, capped in the executor).
|
|
18
|
+
limit: Schema.optional(Schema.Int.pipe(Schema.greaterThan(0))),
|
|
19
|
+
}),
|
|
20
|
+
output: Schema.Struct({
|
|
21
|
+
pulled: Schema.Int, // newly ingested (marker written)
|
|
22
|
+
skipped: Schema.Int, // already seen within the idempotency window
|
|
23
|
+
cursor: Schema.Int, // the durable watermark after this call
|
|
24
|
+
}),
|
|
25
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// `sync.reset` — EXECUTOR (server-only, default export).
|
|
2
|
+
//
|
|
3
|
+
// Shows the delete side of the KV surface: delete(key) → boolean (did it
|
|
4
|
+
// exist?), and list(prefix) + delete() to sweep a set of keys. `ctx.kv.clear()`
|
|
5
|
+
// would drop this app's ENTIRE KV namespace in one call — too broad here, since
|
|
6
|
+
// we only want this tenant's sync keys.
|
|
7
|
+
|
|
8
|
+
import type { AppContext } from '@voltro/runtime'
|
|
9
|
+
|
|
10
|
+
const execute = async (input: { markers?: boolean }, ctx: AppContext) => {
|
|
11
|
+
const tenantId = ctx.request.subject.tenantId ?? 'anon'
|
|
12
|
+
|
|
13
|
+
const cursorCleared = await ctx.kv.delete(`sync:${tenantId}:cursor`)
|
|
14
|
+
|
|
15
|
+
let markersCleared = 0
|
|
16
|
+
if (input.markers) {
|
|
17
|
+
for (const key of await ctx.kv.list(`sync:${tenantId}:seen:`)) {
|
|
18
|
+
if (await ctx.kv.delete(key)) markersCleared++
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { cursorCleared, markersCleared }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default execute
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// `sync.reset` — DESCRIPTOR (browser-safe).
|
|
2
|
+
//
|
|
3
|
+
// Rewind the sync. By default it clears ONLY the cursor — a following
|
|
4
|
+
// `sync.pull` then re-reads from the start but SKIPS everything, because the
|
|
5
|
+
// idempotency markers survive independently (proof the two KV concerns don't
|
|
6
|
+
// depend on each other). Pass `markers: true` to also drop the markers, which
|
|
7
|
+
// makes the next `sync.pull` re-ingest from scratch.
|
|
8
|
+
|
|
9
|
+
import { defineAction } from '@voltro/protocol'
|
|
10
|
+
import { Schema } from 'effect'
|
|
11
|
+
|
|
12
|
+
export const syncReset = defineAction({
|
|
13
|
+
name: 'sync.reset',
|
|
14
|
+
input: Schema.Struct({
|
|
15
|
+
// Also delete the idempotency markers (default: keep them).
|
|
16
|
+
markers: Schema.optional(Schema.Boolean),
|
|
17
|
+
}),
|
|
18
|
+
output: Schema.Struct({
|
|
19
|
+
cursorCleared: Schema.Boolean, // did a cursor exist?
|
|
20
|
+
markersCleared: Schema.Int, // how many markers were dropped
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `sync.status` — EXECUTOR (server-only, default export), written in EFFECT mode.
|
|
2
|
+
//
|
|
3
|
+
// The same durable KV, reached the Effect-native way: `yield* Kv` from
|
|
4
|
+
// `@voltro/kv` instead of the async `ctx.kv` facade. The framework provides the
|
|
5
|
+
// `Kv` service to every handler runtime in BOTH boot paths (`voltro dev` and
|
|
6
|
+
// `voltro serve`), so this resolves identically in dev and prod. The methods
|
|
7
|
+
// mirror the facade but return `Effect`s (get yields an `Option`; the rest yield
|
|
8
|
+
// plain values). Pick whichever style fits — an async handler would use
|
|
9
|
+
// `ctx.kv` exactly as `sync.pull` does.
|
|
10
|
+
|
|
11
|
+
import { Effect } from 'effect'
|
|
12
|
+
import { Kv } from '@voltro/kv'
|
|
13
|
+
import type { AppContext } from '@voltro/runtime'
|
|
14
|
+
|
|
15
|
+
const execute = (_input: Record<string, never>, ctx: AppContext) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const kv = yield* Kv
|
|
18
|
+
const tenantId = ctx.request.subject.tenantId ?? 'anon'
|
|
19
|
+
|
|
20
|
+
const cursor = yield* kv.getOrElse(`sync:${tenantId}:cursor`, () => 0)
|
|
21
|
+
// list(prefix) → every live (non-expired) key under the prefix — here, the
|
|
22
|
+
// outstanding idempotency markers for this tenant.
|
|
23
|
+
const markers = yield* kv.list(`sync:${tenantId}:seen:`)
|
|
24
|
+
|
|
25
|
+
return { cursor, activeMarkers: markers.length }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export default execute
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// `sync.status` — DESCRIPTOR (browser-safe).
|
|
2
|
+
//
|
|
3
|
+
// Read-only view of the durable sync state: where the cursor sits and how many
|
|
4
|
+
// idempotency markers are still live. The executor is written in Effect mode
|
|
5
|
+
// (`yield* Kv`) to show the Effect-native surface alongside the async `ctx.kv`.
|
|
6
|
+
|
|
7
|
+
import { defineAction } from '@voltro/protocol'
|
|
8
|
+
import { Schema } from 'effect'
|
|
9
|
+
|
|
10
|
+
export const syncStatus = defineAction({
|
|
11
|
+
name: 'sync.status',
|
|
12
|
+
input: Schema.Struct({}),
|
|
13
|
+
output: Schema.Struct({
|
|
14
|
+
cursor: Schema.Int, // the durable watermark
|
|
15
|
+
activeMarkers: Schema.Int, // idempotency markers not yet expired
|
|
16
|
+
}),
|
|
17
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Backend app for the {{projectName}} project. Read by `voltro dev`.
|
|
2
|
+
//
|
|
3
|
+
// This is the DURABLE KEY-VALUE template. It demonstrates `ctx.kv` — the
|
|
4
|
+
// framework's second storage primitive, for state you CAN'T recompute — on a
|
|
5
|
+
// small external-event-sync domain. `ctx.kv` is the opposite contract to
|
|
6
|
+
// `ctx.cache`: entries are NEVER evicted for capacity; they live until you
|
|
7
|
+
// delete them or their (optional) TTL lapses.
|
|
8
|
+
//
|
|
9
|
+
// The three storage primitives, side by side (the whole point of this template):
|
|
10
|
+
// • ctx.store — the relational rows you sync in (queryable, joinable). LOSS = re-fetchable.
|
|
11
|
+
// • ctx.kv — the sync CURSOR + idempotency MARKERS. LOSS = re-process / double-process. Durable.
|
|
12
|
+
// • ctx.cache — recomputable derived views. LOSS = free (recompute). Not used here; see the api-data-advanced template.
|
|
13
|
+
//
|
|
14
|
+
// What's in here (all auto-discovered by file convention — nothing is
|
|
15
|
+
// registered in this config):
|
|
16
|
+
//
|
|
17
|
+
// database/schema.ts — `syncedEvents` table (the rows we ingest)
|
|
18
|
+
// actions/sync.pull.action.* — THE STAR: getOrElse(cursor) → dedupe via
|
|
19
|
+
// has()/set(marker,{ttlMs}) → insert row →
|
|
20
|
+
// set(cursor). The full write surface.
|
|
21
|
+
// actions/sync.status.action.* — Effect-mode handler (`yield* Kv`): read
|
|
22
|
+
// the cursor + count markers via list().
|
|
23
|
+
// actions/sync.reset.action.* — delete(cursor) + list()/delete() the markers.
|
|
24
|
+
// queries/events.list.query.* — reactive list of the synced rows (store, not kv).
|
|
25
|
+
//
|
|
26
|
+
// `store: 'memory'` keeps boot zero-infra: `ctx.kv` runs on the `database`
|
|
27
|
+
// backend (the framework's `_voltro_kv` table) which, on the memory store, is
|
|
28
|
+
// in-process — durable within the run, reset on restart. Switch to `'postgres'`
|
|
29
|
+
// and the cursor/markers survive restarts and are shared across replicas.
|
|
30
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
31
|
+
|
|
32
|
+
// Typed environment — declared once, validated at boot (fail-fast).
|
|
33
|
+
export const env = defineEnv({
|
|
34
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
type: 'api' as const,
|
|
39
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
40
|
+
store: 'memory' as const,
|
|
41
|
+
// OPTIONAL infra knobs (both default to a zero-infra backend):
|
|
42
|
+
// kv: 'redis' — durable ctx.kv on a PERSISTENT redis (default 'database').
|
|
43
|
+
// cache: 'redis' — distributed query/result cache (this template uses no cache).
|
|
44
|
+
// Env `KV_BACKEND` / `CACHE_BACKEND` override. See docs → Caching → Key-value backends.
|
|
45
|
+
// kv: 'redis' as const,
|
|
46
|
+
// cache: 'redis' as const,
|
|
47
|
+
env,
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Schema for the {{projectName}} backend.
|
|
2
|
+
//
|
|
3
|
+
// Tables are auto-discovered: every exported `table(...)` here (or in any
|
|
4
|
+
// `*.entity.ts` / `*.schema.ts` file) is registered by `voltro dev`.
|
|
5
|
+
// `actors` + `tenants` are the framework's core tables the `tenant()` /
|
|
6
|
+
// `audit()` mixins reference, so they must be declared.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT contrast (the whole reason this template exists): the ROWS below
|
|
9
|
+
// live in `ctx.store` — they're re-fetchable from the external source, so a
|
|
10
|
+
// relational table is the right home. The sync CURSOR and the idempotency
|
|
11
|
+
// MARKERS do NOT live here — they're state you can't recompute, so they live
|
|
12
|
+
// in `ctx.kv` (durable, never evicted). Don't model a cursor as a table row
|
|
13
|
+
// you hand-manage; that's exactly what `ctx.kv` is for.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
databaseHandle,
|
|
17
|
+
id,
|
|
18
|
+
integer,
|
|
19
|
+
table,
|
|
20
|
+
text,
|
|
21
|
+
timestamp,
|
|
22
|
+
type InferRow,
|
|
23
|
+
} from '@voltro/database'
|
|
24
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
25
|
+
|
|
26
|
+
// ---------- Core tables (required by the tenant / audit mixins) ----------
|
|
27
|
+
|
|
28
|
+
export const actors = table('actors', {
|
|
29
|
+
id: id(),
|
|
30
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
31
|
+
displayName: text().nullable(),
|
|
32
|
+
createdAt: timestamp().default('now'),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const tenants = table('tenants', {
|
|
36
|
+
id: id(),
|
|
37
|
+
name: text(),
|
|
38
|
+
createdAt: timestamp().default('now'),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// ---------- Application table — the events we ingest from the source ----------
|
|
42
|
+
|
|
43
|
+
export const syncedEvents = table('synced_events', {
|
|
44
|
+
id: id({ prefix: 'evt' }),
|
|
45
|
+
// The external id we deduped on — carried through so you can trace a row
|
|
46
|
+
// back to its source record.
|
|
47
|
+
externalId: text(),
|
|
48
|
+
kind: text(),
|
|
49
|
+
payload: text(),
|
|
50
|
+
// The source sequence number this row came from (the cursor advances past it).
|
|
51
|
+
sequence: integer(),
|
|
52
|
+
})
|
|
53
|
+
// tenant() pulls audit() transitively → tenantId + created/updated columns,
|
|
54
|
+
// auto-stamped by the runtime from the caller's subject.
|
|
55
|
+
.with(tenant())
|
|
56
|
+
// Opt into the matcher engine so the `events.list` subscription wakes on
|
|
57
|
+
// every insert.
|
|
58
|
+
.reactive()
|
|
59
|
+
|
|
60
|
+
export type SyncedEvent = InferRow<typeof syncedEvents>
|
|
61
|
+
|
|
62
|
+
export const database = databaseHandle({ actors, tenants, syncedEvents })
|
|
@@ -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/kv": "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,14 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
|
|
3
|
+
const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
|
|
4
|
+
descriptor: {
|
|
5
|
+
table: 'synced_events' as const,
|
|
6
|
+
predicate: undefined,
|
|
7
|
+
order: [{ column: 'sequence' as const, direction: 'desc' as const }],
|
|
8
|
+
take: 100,
|
|
9
|
+
skip: undefined,
|
|
10
|
+
projection: undefined,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default execute
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// `events.list` — DESCRIPTOR (browser-safe).
|
|
2
|
+
//
|
|
3
|
+
// A reactive subscription over the synced ROWS (which live in `ctx.store`, not
|
|
4
|
+
// `ctx.kv`). This is the contrast the template is built around: the rows are
|
|
5
|
+
// relational data you query/stream; the cursor + markers that produced them are
|
|
6
|
+
// durable KV. The runtime AND-merges the caller's tenant scope via the
|
|
7
|
+
// `tenant()` mixin — no manual `eq('tenantId', …)` needed.
|
|
8
|
+
|
|
9
|
+
import { defineQuery } from '@voltro/protocol'
|
|
10
|
+
import { Schema } from 'effect'
|
|
11
|
+
|
|
12
|
+
export const listEvents = defineQuery({
|
|
13
|
+
name: 'events.list',
|
|
14
|
+
input: Schema.Struct({}),
|
|
15
|
+
output: Schema.Struct({
|
|
16
|
+
id: Schema.String,
|
|
17
|
+
externalId: Schema.String,
|
|
18
|
+
kind: Schema.String,
|
|
19
|
+
sequence: Schema.Number,
|
|
20
|
+
tenantId: Schema.String,
|
|
21
|
+
createdAt: Schema.Date,
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-kv",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Durable key-value state (`ctx.kv`) on an external-event-sync domain: a sync cursor you can't recompute, TTL-bounded idempotency markers to dedupe redelivered events, and the store-vs-kv-vs-cache contrast. Exercises the full ctx.kv surface (getOrElse / has / set+TTL / set / list / delete) in async AND Effect handlers.",
|
|
5
|
+
"tags": ["api", "kv", "key-value", "durable", "state", "idempotency", "cursor", "sync"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Unit tests with `@voltro/testing` — the headline of this template is the
|
|
2
|
+
// durable KV behavior, so we exercise it thoroughly against the REAL in-memory
|
|
3
|
+
// `ctx.kv` that `makeTestContext` provides (same contract as the production KV:
|
|
4
|
+
// durable, TTL-honouring, never capacity-evicted).
|
|
5
|
+
//
|
|
6
|
+
// - `sync.pull` reads/advances a durable cursor and writes per-event
|
|
7
|
+
// idempotency markers → asserted across TWO calls (the cursor must carry
|
|
8
|
+
// over so the second call continues past it, not re-pull from 0).
|
|
9
|
+
// - `sync.reset` clears the cursor (and optionally the markers).
|
|
10
|
+
//
|
|
11
|
+
// `sync.status` is written in Effect mode (`yield* Kv` from `@voltro/kv`), which
|
|
12
|
+
// needs the `Kv` service layer provided — an integration concern, not a plain
|
|
13
|
+
// unit run — so it is intentionally not driven here.
|
|
14
|
+
//
|
|
15
|
+
// The simulated upstream is deterministic: event N has externalId `ext-N` and
|
|
16
|
+
// sequence N, so pulling `limit` fresh events advances the cursor by exactly
|
|
17
|
+
// `limit`. Run with `voltro test` (vitest).
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest'
|
|
20
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
21
|
+
import { database } from '../database/schema' // registers synced_events / actors / tenants
|
|
22
|
+
import syncPull from '../actions/sync.pull.action.server'
|
|
23
|
+
import syncReset from '../actions/sync.reset.action.server'
|
|
24
|
+
|
|
25
|
+
const subject = { type: 'user', id: 'user_1', tenantId: 'acme' } as const
|
|
26
|
+
|
|
27
|
+
describe('sync.pull (durable cursor + idempotency markers)', () => {
|
|
28
|
+
it('first pull ingests N events, inserts the rows, and advances the cursor to N', async () => {
|
|
29
|
+
const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) })
|
|
30
|
+
|
|
31
|
+
const result = await syncPull({ limit: 3 }, ctx)
|
|
32
|
+
|
|
33
|
+
expect(result.pulled).toBe(3)
|
|
34
|
+
expect(result.skipped).toBe(0)
|
|
35
|
+
expect(result.cursor).toBe(3)
|
|
36
|
+
|
|
37
|
+
// The event rows landed in the relational store, tenant-stamped.
|
|
38
|
+
const rows = await ctx.store.query(database.syncedEvents.descriptor)
|
|
39
|
+
expect(rows).toHaveLength(3)
|
|
40
|
+
expect(rows.every((r) => r['tenantId'] === 'acme')).toBe(true)
|
|
41
|
+
expect((rows.map((r) => r['externalId']) as ReadonlyArray<string>).sort()).toEqual([
|
|
42
|
+
'ext-1',
|
|
43
|
+
'ext-2',
|
|
44
|
+
'ext-3',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
// The durable cursor persisted in ctx.kv.
|
|
48
|
+
expect(await ctx.kv.get<number>('sync:acme:cursor')).toBe(3)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('a second pull continues PAST the cursor rather than re-pulling from 0', async () => {
|
|
52
|
+
const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) })
|
|
53
|
+
|
|
54
|
+
const first = await syncPull({ limit: 2 }, ctx)
|
|
55
|
+
expect(first.cursor).toBe(2)
|
|
56
|
+
|
|
57
|
+
const second = await syncPull({ limit: 2 }, ctx)
|
|
58
|
+
|
|
59
|
+
// Continues at 3..4 — all fresh (distinct externalIds), none skipped.
|
|
60
|
+
expect(second.pulled).toBe(2)
|
|
61
|
+
expect(second.skipped).toBe(0)
|
|
62
|
+
expect(second.cursor).toBe(4)
|
|
63
|
+
|
|
64
|
+
const rows = await ctx.store.query(database.syncedEvents.descriptor)
|
|
65
|
+
expect(rows).toHaveLength(4) // 2 + 2, no duplicates
|
|
66
|
+
expect((rows.map((r) => r['sequence']) as ReadonlyArray<number>).sort((a, b) => a - b)).toEqual([
|
|
67
|
+
1, 2, 3, 4,
|
|
68
|
+
])
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('sync.reset', () => {
|
|
73
|
+
it('clears the durable cursor so the next pull restarts from 0', async () => {
|
|
74
|
+
const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) })
|
|
75
|
+
|
|
76
|
+
await syncPull({ limit: 2 }, ctx)
|
|
77
|
+
expect(await ctx.kv.get<number>('sync:acme:cursor')).toBe(2)
|
|
78
|
+
|
|
79
|
+
const reset = await syncReset({}, ctx)
|
|
80
|
+
expect(reset.cursorCleared).toBe(true) // the key existed and was removed
|
|
81
|
+
expect(await ctx.kv.has('sync:acme:cursor')).toBe(false)
|
|
82
|
+
|
|
83
|
+
// Cursor gone → the next pull re-reads from the getOrElse default (0).
|
|
84
|
+
const afterReset = await syncPull({ limit: 1 }, ctx)
|
|
85
|
+
expect(afterReset.cursor).toBe(1)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('reports cursorCleared=false when there was no cursor to clear', async () => {
|
|
89
|
+
const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) })
|
|
90
|
+
const reset = await syncReset({}, ctx)
|
|
91
|
+
expect(reset.cursorCleared).toBe(false)
|
|
92
|
+
expect(reset.markersCleared).toBe(0)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('sweeps the idempotency markers when { markers: true }', async () => {
|
|
96
|
+
const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) })
|
|
97
|
+
await syncPull({ limit: 3 }, ctx) // writes 3 seen-markers
|
|
98
|
+
|
|
99
|
+
const reset = await syncReset({ markers: true }, ctx)
|
|
100
|
+
expect(reset.markersCleared).toBe(3)
|
|
101
|
+
expect(await ctx.kv.list('sync:acme:seen:')).toHaveLength(0)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro API scaffold (template: **api-moderation**) — pre-commit content
|
|
4
|
+
moderation with `@voltro/plugin-moderation`.
|
|
5
|
+
|
|
6
|
+
## Boot
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm install
|
|
10
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
11
|
+
# → http://localhost:4000 (store: memory — zero infra)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What this shows
|
|
15
|
+
|
|
16
|
+
- **Two actions, two outcomes** — `moderationPlugin` checks named input fields
|
|
17
|
+
on the rpc interceptors, BEFORE the handler:
|
|
18
|
+
- **block** (`posts.create`, fields `title`/`body`): banned content fails
|
|
19
|
+
typed `ContentRejected` and the write **never commits**.
|
|
20
|
+
- **flag** (`comments.create`, field `body`): the comment IS written, but
|
|
21
|
+
flagged for review (the dashboard's **Moderation** panel surfaces the queue).
|
|
22
|
+
- **Pluggable provider** — `keywordProvider([...])` is a zero-dep deny-list;
|
|
23
|
+
`aiProvider()` is an optional LLM classifier (fails open). Swap one for the
|
|
24
|
+
other in `app.config.ts` — the rules don't change.
|
|
25
|
+
- **In-handler redaction** — when you need to rewrite rather than reject, the
|
|
26
|
+
`moderate(text)` helper returns a verdict you can act on inside a handler
|
|
27
|
+
(the interceptor can't rewrite input).
|
|
28
|
+
|
|
29
|
+
## Try it
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# banned word in a post → blocked, nothing written:
|
|
33
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
34
|
+
-d '{"tag":"posts.create","input":{"title":"hi","body":"buy cheap spam now"}}'
|
|
35
|
+
# → { ok:false, error:{ _tag:"ContentRejected", reason:"matched denied term(s): spam" } }
|
|
36
|
+
|
|
37
|
+
# clean post → written:
|
|
38
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
39
|
+
-d '{"tag":"posts.create","input":{"title":"hi","body":"a normal post"}}'
|
|
40
|
+
# → { ok:true, result:{ id, … } }
|
|
41
|
+
|
|
42
|
+
# banned word in a comment → WRITTEN but flagged (flag rule doesn't block):
|
|
43
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
44
|
+
-d '{"tag":"comments.create","input":{"body":"this is spam"}}'
|
|
45
|
+
# → { ok:true, result:{ id, … } } (and queued for review)
|
|
46
|
+
```
|