@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,18 @@
|
|
|
1
|
+
// Create a project — the SaaS bundle in ONE handler. The billing plugin
|
|
2
|
+
// auto-merges its typed `EntitlementExceeded` (+ `BillingError`) into this
|
|
3
|
+
// procedure's wire-error union, so the client decodes the over-quota failure
|
|
4
|
+
// typed WITHOUT a manual `error:` declaration here.
|
|
5
|
+
|
|
6
|
+
import { defineMutation } from '@voltro/protocol'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const createProject = defineMutation({
|
|
10
|
+
name: 'projects.create',
|
|
11
|
+
target: { table: 'projects', op: 'insert' },
|
|
12
|
+
input: Schema.Struct({ name: Schema.NonEmptyString }),
|
|
13
|
+
output: Schema.Struct({
|
|
14
|
+
id: Schema.String,
|
|
15
|
+
name: Schema.String,
|
|
16
|
+
tenantId: Schema.String,
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"migrate": "voltro migrate",
|
|
9
|
+
"test": "voltro test",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@effect/platform": "^0.96.1",
|
|
14
|
+
"@effect/rpc": "^0.75.1",
|
|
15
|
+
"@voltro/cli": "0.1.0",
|
|
16
|
+
"@voltro/database": "0.1.0",
|
|
17
|
+
"@voltro/plugin-analytics-postgres": "0.1.0",
|
|
18
|
+
"@voltro/plugin-billing": "0.1.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.1.0",
|
|
20
|
+
"@voltro/plugin-notifications": "0.1.0",
|
|
21
|
+
"@voltro/plugin-presence": "0.1.0",
|
|
22
|
+
"@voltro/protocol": "0.1.0",
|
|
23
|
+
"@voltro/runtime": "0.1.0",
|
|
24
|
+
"effect": "^3.21.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@voltro/testing": "0.1.0",
|
|
28
|
+
"typescript": "^5.7.0",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Reactive list of the tenant's projects. The runtime AND-merges
|
|
2
|
+
// `eq('tenantId', subject.tenantId)` (the table carries `tenant()`), so this
|
|
3
|
+
// never leaks across tenants — no manual filter needed.
|
|
4
|
+
|
|
5
|
+
import { defineQuery } from '@voltro/protocol'
|
|
6
|
+
import { Schema } from 'effect'
|
|
7
|
+
|
|
8
|
+
export const listProjects = defineQuery({
|
|
9
|
+
name: 'projects.list',
|
|
10
|
+
source: 'projects',
|
|
11
|
+
input: Schema.Struct({}),
|
|
12
|
+
output: Schema.Array(Schema.Struct({
|
|
13
|
+
id: Schema.String,
|
|
14
|
+
name: Schema.String,
|
|
15
|
+
tenantId: Schema.String,
|
|
16
|
+
createdAt: Schema.Date,
|
|
17
|
+
})),
|
|
18
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-saas",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "The SaaS plugin bundle in one domain: billing entitlements (quota gate), in-app + console notifications, analytics events, and live presence — wired turnkey. One projects.create mutation exercises billing + analytics + notifications together.",
|
|
5
|
+
"tags": ["api", "saas", "billing", "notifications", "analytics", "presence", "plugins"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Unit test with `@voltro/testing` — no database, no running server, no billing
|
|
2
|
+
// backend. The `projects.create` executor runs in Effect mode and `yield*`s the
|
|
3
|
+
// billing (requireEntitlement), analytics, and notifications plugin services —
|
|
4
|
+
// those live in the per-request plugin layer, which the unit harness does NOT
|
|
5
|
+
// wire, so we do not fake them. Instead we PIN the descriptor's wire contract
|
|
6
|
+
// (the shape the client codegen depends on) and prove the tenant() scoping that
|
|
7
|
+
// keeps one SaaS tenant's projects invisible to another — the headline feature.
|
|
8
|
+
// Run with `voltro test` (vitest).
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest'
|
|
11
|
+
import { Schema } from 'effect'
|
|
12
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
13
|
+
import { database } from '../database/schema' // registers projects / actors / tenants
|
|
14
|
+
import { createProject } from '../mutations/projects.create.mutation'
|
|
15
|
+
|
|
16
|
+
describe('projects.create descriptor', () => {
|
|
17
|
+
it('decodes a valid input and rejects a blank name', () => {
|
|
18
|
+
const decode = Schema.decodeUnknownSync(createProject.input)
|
|
19
|
+
expect(decode({ name: 'Acme App' })).toEqual({ name: 'Acme App' })
|
|
20
|
+
// `name: Schema.NonEmptyString` — an empty string fails the wire decode.
|
|
21
|
+
expect(() => decode({ name: '' })).toThrow()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('encodes the output shape the client expects', () => {
|
|
25
|
+
const encode = Schema.encodeUnknownSync(createProject.output)
|
|
26
|
+
const wire = encode({ id: 'proj_1', name: 'Acme App', tenantId: 'acme' })
|
|
27
|
+
expect(wire).toEqual({ id: 'proj_1', name: 'Acme App', tenantId: 'acme' })
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('projects tenant isolation', () => {
|
|
32
|
+
it('t2 never sees t1 projects (REAL tenant scoping)', async () => {
|
|
33
|
+
const ctx = makeTestContext({
|
|
34
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
35
|
+
store: mockStore({ projects: [] }),
|
|
36
|
+
})
|
|
37
|
+
// Raw insert through the mixin store — tenant() auto-stamps tenantId=t1.
|
|
38
|
+
const row = await ctx.store.insert('projects', { name: 'secret' })
|
|
39
|
+
expect(row['tenantId']).toBe('t1')
|
|
40
|
+
|
|
41
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.projects.descriptor))
|
|
42
|
+
expect(seenByT2).toHaveLength(0)
|
|
43
|
+
// …and t1 still sees its own row.
|
|
44
|
+
const seenByT1 = await ctx.store.query(database.projects.descriptor)
|
|
45
|
+
expect(seenByT1).toHaveLength(1)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro API scaffold (template: **api-search**) — full-text search that stays
|
|
4
|
+
in sync with your tables, powered by `@voltro/plugin-search`.
|
|
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
|
+
The boot log shows `search: backfilled 4 article(s) into the index` — the
|
|
15
|
+
demo seed's rows, indexed and ready to search.
|
|
16
|
+
|
|
17
|
+
## What this shows
|
|
18
|
+
|
|
19
|
+
- **Index stays in sync, automatically** — `searchPlugin` taps the post-commit
|
|
20
|
+
ChangeEvent stream: every `articles` insert/update upserts its doc into the
|
|
21
|
+
index, every delete removes it. No app code — create an article and it's
|
|
22
|
+
searchable on the next request.
|
|
23
|
+
- **Tenant-scoped results** — the index spec's `tenantField: 'tenantId'` makes
|
|
24
|
+
the synthesized `search.query` rpc auto-filter to the caller's tenant. One
|
|
25
|
+
tenant can never see another's hits.
|
|
26
|
+
- **Backfill existing rows** — `startup/searchBackfill.startup.tsx` calls
|
|
27
|
+
`backfillIndex(...)` on boot to seed the index from rows already in the table
|
|
28
|
+
(the seed data, or every row after a restart / engine switch).
|
|
29
|
+
- **One config line to go to production** — `lib/search.ts` uses
|
|
30
|
+
`memoryBackend()` (in-process, zero infra). Swap it for `{ engine:
|
|
31
|
+
'typesense' | 'meilisearch' | 'algolia', url, apiKey }` and nothing else
|
|
32
|
+
changes — the indexing + query code is identical.
|
|
33
|
+
|
|
34
|
+
## Try it
|
|
35
|
+
|
|
36
|
+
The plugin synthesizes a `search.query` rpc. Hit it over the inspect surface:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Search the seeded articles (tenant 'acme' via the x-tenant default):
|
|
40
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
41
|
+
-d '{"tag":"search.query","input":{"index":"articles","q":"tenant"}}'
|
|
42
|
+
|
|
43
|
+
# Create an article — it's indexed on commit, then searchable:
|
|
44
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
45
|
+
-d '{"tag":"articles.create","input":{"tenantId":"acme","title":"Reactive queries","body":"Subscriptions push deltas live.","tag":"guide"}}'
|
|
46
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
47
|
+
-d '{"tag":"search.query","input":{"index":"articles","q":"reactive"}}'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## On the web side
|
|
51
|
+
|
|
52
|
+
A browser hook ships at `@voltro/plugin-search/web`:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { useSearch } from '@voltro/plugin-search/web'
|
|
56
|
+
const { results, search, pending } = useSearch('articles')
|
|
57
|
+
// search('reactive') → results auto-scoped to the signed-in tenant
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Files
|
|
61
|
+
|
|
62
|
+
- `lib/search.ts` — the shared backend instance + the `articles` index spec.
|
|
63
|
+
- `app.config.ts` — wires `searchPlugin({ backend, indexes })`.
|
|
64
|
+
- `database/schema.ts` — the `articles` table (tenant-scoped, reactive).
|
|
65
|
+
- `mutations/articles.create.*` — writes that auto-index via the tap.
|
|
66
|
+
- `queries/articles.list.*` — a live list to sit next to the search box.
|
|
67
|
+
- `seeds/articles.seed.ts` — demo data.
|
|
68
|
+
- `startup/searchBackfill.startup.tsx` — backfills the index on boot.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Search backend for the {{projectName}} project — full-text search over a
|
|
2
|
+
// table, kept in sync automatically.
|
|
3
|
+
//
|
|
4
|
+
// `searchPlugin` taps the post-commit ChangeEvent stream: every insert/update
|
|
5
|
+
// to a source table upserts its doc into the index, every delete removes it —
|
|
6
|
+
// no app code. The synthesized `search.query` rpc returns hits, auto-filtered
|
|
7
|
+
// to the caller's tenant. `store: 'memory'` keeps this zero-infra for dev.
|
|
8
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
9
|
+
import { searchPlugin } from '@voltro/plugin-search'
|
|
10
|
+
import { searchBackend, articlesIndex } from './lib/search'
|
|
11
|
+
|
|
12
|
+
export const env = defineEnv({
|
|
13
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
type: 'api' as const,
|
|
18
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
19
|
+
store: 'memory' as const,
|
|
20
|
+
plugins: [
|
|
21
|
+
// `backend: searchBackend` is the SAME instance lib/search.ts shares with
|
|
22
|
+
// the boot startup's backfill. Swap it for a vendor engine in lib/search.ts
|
|
23
|
+
// ({ engine: 'typesense' | 'meilisearch' | 'algolia', url, apiKey }) for
|
|
24
|
+
// prod — the indexing + query code stays identical.
|
|
25
|
+
searchPlugin({
|
|
26
|
+
backend: searchBackend,
|
|
27
|
+
indexes: { articles: articlesIndex },
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
env,
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Schema for the {{projectName}} search backend.
|
|
2
|
+
//
|
|
3
|
+
// `articles` is the searchable domain. The search plugin mirrors every
|
|
4
|
+
// write to it into a search index (see app.config.ts + lib/search.ts); the
|
|
5
|
+
// table itself is an ordinary `tenant()`-scoped reactive table.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
databaseHandle,
|
|
9
|
+
id,
|
|
10
|
+
table,
|
|
11
|
+
text,
|
|
12
|
+
timestamp,
|
|
13
|
+
type InferRow,
|
|
14
|
+
} from '@voltro/database'
|
|
15
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
16
|
+
|
|
17
|
+
// ---------- Core tables (required by the audit / tenant mixins) ----------
|
|
18
|
+
|
|
19
|
+
export const actors = table('actors', {
|
|
20
|
+
id: id(),
|
|
21
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
22
|
+
displayName: text().nullable(),
|
|
23
|
+
createdAt: timestamp().default('now'),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const tenants = table('tenants', {
|
|
27
|
+
id: id(),
|
|
28
|
+
name: text(),
|
|
29
|
+
createdAt: timestamp().default('now'),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ---------- Application table ----------
|
|
33
|
+
|
|
34
|
+
export const articles = table('articles', {
|
|
35
|
+
id: id({ prefix: 'art' }),
|
|
36
|
+
title: text(),
|
|
37
|
+
body: text(),
|
|
38
|
+
tag: text(), // a coarse category — also a search filter
|
|
39
|
+
})
|
|
40
|
+
// tenant() adds tenantId + audit columns (auto-stamped). The search
|
|
41
|
+
// index's `tenantField: 'tenantId'` scopes every query to the caller.
|
|
42
|
+
.with(tenant())
|
|
43
|
+
.reactive()
|
|
44
|
+
|
|
45
|
+
export type Article = InferRow<typeof articles>
|
|
46
|
+
|
|
47
|
+
export const database = databaseHandle({ actors, tenants, articles })
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The search wiring, shared so app.config (the plugin) and the boot startup
|
|
2
|
+
// (the backfill) use the SAME backend instance + index spec.
|
|
3
|
+
//
|
|
4
|
+
// `memoryBackend()` is an in-process index — zero infra, perfect for dev. It's
|
|
5
|
+
// a closure over a Map, so two `memoryBackend()` calls would be two SEPARATE
|
|
6
|
+
// indexes; sharing ONE instance here is what lets the startup's `backfillIndex`
|
|
7
|
+
// seed the very index the plugin's `search.query` reads from. Swap it for
|
|
8
|
+
// `{ engine: 'typesense', url, apiKey }` (or meilisearch / algolia) for prod —
|
|
9
|
+
// nothing else changes.
|
|
10
|
+
import { memoryBackend, type IndexSpec, type SearchDoc } from '@voltro/plugin-search'
|
|
11
|
+
|
|
12
|
+
export const searchBackend = memoryBackend()
|
|
13
|
+
|
|
14
|
+
// One index over the `articles` table. `map` flattens a row → a search doc
|
|
15
|
+
// (must include `id`); `tenantField` makes `search.query` auto-filter to the
|
|
16
|
+
// caller's tenant, so one tenant can never see another's hits.
|
|
17
|
+
export const articlesIndex: IndexSpec = {
|
|
18
|
+
index: 'articles',
|
|
19
|
+
map: (row): SearchDoc => ({
|
|
20
|
+
id: row['id'] as string,
|
|
21
|
+
title: row['title'] as string,
|
|
22
|
+
body: row['body'] as string,
|
|
23
|
+
tag: row['tag'] as string,
|
|
24
|
+
tenantId: row['tenantId'] as string,
|
|
25
|
+
}),
|
|
26
|
+
tenantField: 'tenantId',
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { assertOwnTenant } from '@voltro/plugin-multitenancy/guard'
|
|
2
|
+
import type { AppContext } from '@voltro/runtime'
|
|
3
|
+
|
|
4
|
+
const execute = async (
|
|
5
|
+
input: { tenantId: string; title: string; body: string; tag: string },
|
|
6
|
+
ctx: AppContext,
|
|
7
|
+
) => {
|
|
8
|
+
assertOwnTenant(input.tenantId, ctx.request.subject)
|
|
9
|
+
// The framework auto-injects an `art_…` id. The insert commits, then the
|
|
10
|
+
// search plugin's tap indexes it — nothing to do here for search.
|
|
11
|
+
return ctx.store.insert('articles', {
|
|
12
|
+
title: input.title,
|
|
13
|
+
body: input.body,
|
|
14
|
+
tag: input.tag,
|
|
15
|
+
tenantId: input.tenantId,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default execute
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Creates an article. On commit, the search plugin's ChangeEvent tap mirrors
|
|
2
|
+
// the new row into the index — so it's searchable immediately, no extra call.
|
|
3
|
+
// `assertOwnTenant` rejects cross-tenant spoofing (typed `TenantMismatch`).
|
|
4
|
+
|
|
5
|
+
import { defineMutation } from '@voltro/protocol'
|
|
6
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const createArticle = defineMutation({
|
|
10
|
+
name: 'articles.create',
|
|
11
|
+
target: {
|
|
12
|
+
table: 'articles',
|
|
13
|
+
op: 'insert',
|
|
14
|
+
shape: (input: { tenantId: string; title: string; body: string; tag: string }) => ({
|
|
15
|
+
title: input.title,
|
|
16
|
+
body: input.body,
|
|
17
|
+
tag: input.tag,
|
|
18
|
+
tenantId: input.tenantId,
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
input: Schema.Struct({
|
|
22
|
+
tenantId: Schema.String,
|
|
23
|
+
title: Schema.NonEmptyString,
|
|
24
|
+
body: Schema.String,
|
|
25
|
+
tag: Schema.String,
|
|
26
|
+
}),
|
|
27
|
+
output: Schema.Struct({
|
|
28
|
+
id: Schema.String,
|
|
29
|
+
title: Schema.String,
|
|
30
|
+
body: Schema.String,
|
|
31
|
+
tag: Schema.String,
|
|
32
|
+
tenantId: Schema.String,
|
|
33
|
+
}),
|
|
34
|
+
error: TenantMismatch,
|
|
35
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"migrate": "voltro migrate",
|
|
9
|
+
"test": "voltro test",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@effect/platform": "^0.96.1",
|
|
14
|
+
"@effect/rpc": "^0.75.1",
|
|
15
|
+
"@voltro/cli": "0.1.0",
|
|
16
|
+
"@voltro/database": "0.1.0",
|
|
17
|
+
"@voltro/env": "0.1.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.1.0",
|
|
19
|
+
"@voltro/plugin-search": "0.1.0",
|
|
20
|
+
"@voltro/protocol": "0.1.0",
|
|
21
|
+
"@voltro/runtime": "0.1.0",
|
|
22
|
+
"effect": "^3.21.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@voltro/testing": "0.1.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
|
|
3
|
+
const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
|
|
4
|
+
descriptor: {
|
|
5
|
+
table: 'articles' as const,
|
|
6
|
+
predicate: undefined,
|
|
7
|
+
order: [{ column: 'createdAt' 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,19 @@
|
|
|
1
|
+
// A live list of the tenant's articles. Useful next to the search box to show
|
|
2
|
+
// the full set; the search.query rpc (synthesized by searchPlugin) is what you
|
|
3
|
+
// hit for a query string. tenant() scopes both to the caller automatically.
|
|
4
|
+
|
|
5
|
+
import { defineQuery } from '@voltro/protocol'
|
|
6
|
+
import { Schema } from 'effect'
|
|
7
|
+
|
|
8
|
+
export const listArticles = defineQuery({
|
|
9
|
+
name: 'articles.list',
|
|
10
|
+
input: Schema.Struct({}),
|
|
11
|
+
output: Schema.Struct({
|
|
12
|
+
id: Schema.String,
|
|
13
|
+
title: Schema.String,
|
|
14
|
+
body: Schema.String,
|
|
15
|
+
tag: Schema.String,
|
|
16
|
+
tenantId: Schema.String,
|
|
17
|
+
createdAt: Schema.Date,
|
|
18
|
+
}),
|
|
19
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Boot-lifecycle seed — a few articles so search returns results the moment
|
|
2
|
+
// `voltro dev` boots. Idempotent via `upsertByUnique` (+ the fingerprint
|
|
3
|
+
// ledger), so re-running on every boot is safe.
|
|
4
|
+
//
|
|
5
|
+
// The seed store is the RAW store (no authenticated subject), so the `tenant()`
|
|
6
|
+
// auto-fill doesn't fire — we pass `tenantId` explicitly and seed the demo
|
|
7
|
+
// tenant row first, under `acme` (the dev AuthMiddleware's default tenant).
|
|
8
|
+
|
|
9
|
+
import { defineSeed } from '@voltro/database'
|
|
10
|
+
|
|
11
|
+
const TENANT_ID = 'acme'
|
|
12
|
+
|
|
13
|
+
export default defineSeed({
|
|
14
|
+
id: 'articles',
|
|
15
|
+
name: 'Demo articles for search',
|
|
16
|
+
lifecycle: 'boot',
|
|
17
|
+
steps: ({ step }) => [
|
|
18
|
+
step('tenant', async ({ upsertByUnique }) => {
|
|
19
|
+
await upsertByUnique(
|
|
20
|
+
'tenants',
|
|
21
|
+
{ id: TENANT_ID },
|
|
22
|
+
{ id: TENANT_ID, name: 'Acme', createdAt: new Date() },
|
|
23
|
+
)
|
|
24
|
+
return { rowsTouched: 1 }
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
step('articles', async ({ upsertByUnique }) => {
|
|
28
|
+
const now = new Date()
|
|
29
|
+
const rows = [
|
|
30
|
+
{ id: 'art_voltro', title: 'Getting started with Voltro', body: 'Reactive end-to-end, multi-tenant by design — scaffold an app and ship.', tag: 'guide' },
|
|
31
|
+
{ id: 'art_search', title: 'Full-text search that stays in sync', body: 'The search plugin mirrors every write into the index via the post-commit change tap.', tag: 'guide' },
|
|
32
|
+
{ id: 'art_tenant', title: 'Tenant isolation', body: 'Queries auto-filter to the caller tenant — search results included, no leakage.', tag: 'concept' },
|
|
33
|
+
{ id: 'art_effect', title: 'Effect under the hood', body: 'Handlers compose with Effect for typed errors, retries, and timeouts.', tag: 'concept' },
|
|
34
|
+
]
|
|
35
|
+
for (const r of rows) {
|
|
36
|
+
await upsertByUnique('articles', { id: r.id }, { ...r, tenantId: TENANT_ID, createdAt: now })
|
|
37
|
+
}
|
|
38
|
+
return { rowsTouched: rows.length }
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Boot startup — seed the search index from rows already in the table.
|
|
2
|
+
//
|
|
3
|
+
// The live ChangeEvent tap (wired by searchPlugin) indexes every write AFTER
|
|
4
|
+
// boot. THIS catches what's already there: the demo seed's rows, or — with a
|
|
5
|
+
// persistent store or a freshly-switched vendor engine — every existing row.
|
|
6
|
+
// It uses the SAME `searchBackend` instance the plugin queries (shared via
|
|
7
|
+
// lib/search.ts), so the docs it writes are exactly what `search.query` reads.
|
|
8
|
+
|
|
9
|
+
import type { StartupContext } from '@voltro/runtime'
|
|
10
|
+
import { backfillIndex } from '@voltro/plugin-search'
|
|
11
|
+
import { database } from '../database/schema'
|
|
12
|
+
import { searchBackend, articlesIndex } from '../lib/search'
|
|
13
|
+
|
|
14
|
+
export default async ({ store, log }: StartupContext): Promise<void> => {
|
|
15
|
+
// `database.articles.descriptor` is the builder's "select all" descriptor —
|
|
16
|
+
// a properly-formed query (don't hand-roll a raw `{ table, order: undefined }`,
|
|
17
|
+
// which trips the store's order/limit handling).
|
|
18
|
+
const rows = (await store.query(
|
|
19
|
+
database.articles.descriptor,
|
|
20
|
+
)) as ReadonlyArray<Record<string, unknown>>
|
|
21
|
+
|
|
22
|
+
const count = await backfillIndex(searchBackend, articlesIndex, rows)
|
|
23
|
+
log.info(`search: backfilled ${count} article(s) into the index`)
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-search",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Full-text search that stays in sync — @voltro/plugin-search mirrors every table write into an index via the post-commit change tap; the synthesized search.query rpc returns tenant-scoped hits. Memory backend (zero infra); swap for Typesense/Meilisearch/Algolia with one config line.",
|
|
5
|
+
"tags": ["api", "search", "full-text", "typesense", "meilisearch", "algolia", "plugin"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Unit test with `@voltro/testing` — no database, no running server, no search
|
|
2
|
+
// backend. The `articles.create` executor is a tenant-guarded insert: on commit
|
|
3
|
+
// the search plugin's ChangeEvent tap mirrors the row into the index, but that
|
|
4
|
+
// tap is dispatch-time infrastructure (Typesense/Meili/Algolia in prod, the
|
|
5
|
+
// in-memory backend in dev) and is NOT wired into the unit store — so we do not
|
|
6
|
+
// fake it. We cover the DB write + the cross-tenant write guard that gates it,
|
|
7
|
+
// and pin the descriptor's typed error. Run with `voltro test` (vitest).
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest'
|
|
10
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
11
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
12
|
+
import { database } from '../database/schema' // registers articles / actors / tenants
|
|
13
|
+
import { createArticle } from '../mutations/articles.create.mutation'
|
|
14
|
+
import createArticleHandler from '../mutations/articles.create.mutation.server'
|
|
15
|
+
|
|
16
|
+
describe('articles.create', () => {
|
|
17
|
+
it('inserts an article for the caller\'s own tenant', async () => {
|
|
18
|
+
const ctx = makeTestContext({
|
|
19
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
20
|
+
store: mockStore({ articles: [] }),
|
|
21
|
+
})
|
|
22
|
+
const row = await createArticleHandler(
|
|
23
|
+
{ tenantId: 'acme', title: 'Hello', body: 'World', tag: 'news' },
|
|
24
|
+
ctx,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
expect(row['title']).toBe('Hello')
|
|
28
|
+
expect(row['tag']).toBe('news')
|
|
29
|
+
expect(row['tenantId']).toBe('acme')
|
|
30
|
+
expect(row['id'] as string).toMatch(/^art_/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('rejects a cross-tenant write with a typed TenantMismatch (the spoof guard)', async () => {
|
|
34
|
+
const ctx = makeTestContext({
|
|
35
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
36
|
+
store: mockStore({ articles: [] }),
|
|
37
|
+
})
|
|
38
|
+
// Input claims a DIFFERENT tenant than the subject → assertOwnTenant throws.
|
|
39
|
+
// The executor is async, so the throw surfaces as a rejected promise.
|
|
40
|
+
await expect(
|
|
41
|
+
createArticleHandler(
|
|
42
|
+
{ tenantId: 'evil', title: 'x', body: '', tag: '' },
|
|
43
|
+
ctx,
|
|
44
|
+
),
|
|
45
|
+
).rejects.toThrow(TenantMismatch)
|
|
46
|
+
|
|
47
|
+
// Nothing was written.
|
|
48
|
+
const rows = await ctx.store.query(database.articles.descriptor)
|
|
49
|
+
expect(rows).toHaveLength(0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('isolates tenants — t2 never sees t1 articles', async () => {
|
|
53
|
+
const ctx = makeTestContext({
|
|
54
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
55
|
+
store: mockStore({ articles: [] }),
|
|
56
|
+
})
|
|
57
|
+
await createArticleHandler({ tenantId: 't1', title: 'secret', body: '', tag: '' }, ctx)
|
|
58
|
+
|
|
59
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.articles.descriptor))
|
|
60
|
+
expect(seenByT2).toHaveLength(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('pins the descriptor error contract — articles.create declares TenantMismatch', () => {
|
|
64
|
+
// The descriptor is value-imported into the browser rpcGroup, so the client
|
|
65
|
+
// decodes this typed failure without a manual error union.
|
|
66
|
+
expect(createArticle.name).toBe('articles.create')
|
|
67
|
+
expect(createArticle.error).toBe(TenantMismatch)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro API scaffold (template: **api-versioning**) — full row history +
|
|
4
|
+
time-travel with `@voltro/plugin-versioning`.
|
|
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
|
+
- **Every change snapshotted** — `versioningPlugin({ tables: ['documents'] })`
|
|
17
|
+
rides the post-commit ChangeEvent tap and records a FULL snapshot of every
|
|
18
|
+
insert/update/delete. No code in your handlers.
|
|
19
|
+
- **Read it back through handlers** —
|
|
20
|
+
- `documents.history` → `rowHistory(ctx.store, 'documents', id)` — every
|
|
21
|
+
version, oldest to newest.
|
|
22
|
+
- `documents.asOf` → `rowAsOf(ctx.store, 'documents', id, at)` — the value as
|
|
23
|
+
of a past instant (epoch ms), or `null` if it didn't exist then.
|
|
24
|
+
- **Distinct from audit** — `audit()` records WHO/WHEN a row changed; this
|
|
25
|
+
records WHAT it changed to, so you can diff or restore a prior version.
|
|
26
|
+
|
|
27
|
+
## Try it
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# create (v1), then edit (v2):
|
|
31
|
+
ID=$(curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
32
|
+
-d '{"tag":"documents.create","input":{"title":"Spec","content":"draft v1"}}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')
|
|
33
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
34
|
+
-d "{\"tag\":\"documents.update\",\"input\":{\"id\":\"$ID\",\"content\":\"final v2\"}}" >/dev/null
|
|
35
|
+
|
|
36
|
+
# every version:
|
|
37
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
38
|
+
-d "{\"tag\":\"documents.history\",\"input\":{\"id\":\"$ID\"}}"
|
|
39
|
+
# → [ { version:1, op:"insert", content:"draft v1", changedAt }, { version:2, op:"update", content:"final v2", … } ]
|
|
40
|
+
|
|
41
|
+
# the value as of v1's timestamp → "draft v1" (time-travel):
|
|
42
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
43
|
+
-d "{\"tag\":\"documents.asOf\",\"input\":{\"id\":\"$ID\",\"at\":<v1.changedAt>}}"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Production
|
|
47
|
+
|
|
48
|
+
The memory history store is single-process. With a SQL store, history persists
|
|
49
|
+
in `_voltro_row_history`. List the tables you want versioned — high-churn
|
|
50
|
+
tables grow history fast, so version what you need to diff/restore, not
|
|
51
|
+
everything.
|