@voltro/cli 0.27.0 → 0.29.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 +513 -0
- package/THIRD-PARTY-NOTICES.md +88 -2
- package/dist/{apiBuild-D22_EpoR.js → apiBuild-BESZbTjA.js} +4 -4
- package/dist/apiBuild-C8d74T6g.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-BPAtyWs8.js +2420 -0
- package/dist/checkCommand-DBvZvCh1.js +2 -0
- package/dist/cliRuntime-Oh517vCV.js +96 -0
- package/dist/{commands-jBX8no1I.js → commands-82BDEktg.js} +6476 -7672
- package/dist/dbCommand-CaIVTp-a.js +2 -0
- package/dist/{dbCommand-uuNCrFAb.js → dbCommand-bCBbk6tz.js} +3 -3
- package/dist/{dev-DcbIJrWg.js → dev-Cg4BKLmi.js} +3034 -2619
- package/dist/dev-DeEJj5T4.js +3 -0
- package/dist/fileConventions-Cof68_BL.js +33 -0
- package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-4Db40V23.js} +2 -2
- package/dist/frameworkTableAssembly-BTNirAAk.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspect-CUCCzw2I.js → inspect-BMvQpwqy.js} +211 -190
- package/dist/inspect-DHeaMZak.js +2 -0
- package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-DHYygE4s.js} +999 -961
- package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-C4R1EdHn.js} +1 -1
- package/dist/manifestBuild-Ck_90gZy.js +2 -0
- package/dist/sdkgen-CW5NbYF0.js +582 -0
- package/dist/{seedRunner-D6eu-u5U.js → seedRunner-Bqxgp7HZ.js} +60 -59
- package/dist/serveCommand-BXl8mhZD.js +1489 -0
- package/dist/serveEntry.js +3 -3
- package/dist/{start-BGXIf6zT.js → start-CsCIaW4-.js} +282 -269
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.md +2 -1
- package/templates/agent-docs/_index.md +2 -1
- package/templates/agent-docs/_manifest.json +10 -1
- package/templates/agent-docs/ai.md +135 -0
- package/templates/agent-docs/cli.md +124 -6
- package/templates/agent-docs/configuration.md +32 -0
- package/templates/agent-docs/data.md +282 -0
- package/templates/agent-docs/database/migrations.md +47 -0
- package/templates/agent-docs/database/misc.md +57 -0
- package/templates/agent-docs/database/schema.md +3 -1
- package/templates/agent-docs/deployment.md +56 -0
- package/templates/agent-docs/internationalization.md +161 -1
- package/templates/agent-docs/local-first-mobile.md +414 -0
- package/templates/agent-docs/routing.md +93 -2
- package/templates/agent-docs/schema-driven-ui.md +12 -0
- package/templates/agent-docs/templates/apibackends.md +1 -1
- package/templates/agent-docs/testing.md +42 -0
- package/templates/agent-docs/whats-new.md +165 -131
- package/templates/agent-docs/workflows.md +11 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/.env.example +19 -0
- package/templates/apps/api-backend-sqlite/README.md +38 -0
- package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
- package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-sqlite/package.json +30 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-sqlite/template.json +6 -0
- package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/README.md +104 -0
- package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
- package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
- package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
- package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
- package/templates/apps/api-cms/actions/me.action.ts +16 -0
- package/templates/apps/api-cms/app.config.ts +61 -0
- package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
- package/templates/apps/api-cms/content/index.ts +18 -0
- package/templates/apps/api-cms/content/page.contentType.ts +24 -0
- package/templates/apps/api-cms/database/schema.ts +64 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
- package/templates/apps/api-cms/package.json +32 -0
- package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
- package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
- package/templates/apps/api-cms/template.json +6 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
- package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
- package/templates/apps/api-cms/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/README.md +103 -0
- package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
- package/templates/apps/api-saas-starter/app.config.ts +87 -0
- package/templates/apps/api-saas-starter/database/schema.ts +57 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/package.json +32 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas-starter/template.json +6 -0
- package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
- package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
- package/templates/apps/api-saas-starter/tsconfig.json +5 -0
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/README.md +88 -0
- package/templates/apps/api-status/app.config.ts +36 -0
- package/templates/apps/api-status/authz.ts +33 -0
- package/templates/apps/api-status/database/schema.ts +70 -0
- package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
- package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
- package/templates/apps/api-status/package.json +30 -0
- package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/components.list.query.ts +13 -0
- package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
- package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
- package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
- package/templates/apps/api-status/template.json +6 -0
- package/templates/apps/api-status/tests/status.test.ts +70 -0
- package/templates/apps/api-status/tsconfig.json +5 -0
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/README.md +78 -0
- package/templates/apps/frontend-auth/app.config.ts +34 -0
- package/templates/apps/frontend-auth/package.json +32 -0
- package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
- package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
- package/templates/apps/frontend-auth/src/config.ts +11 -0
- package/templates/apps/frontend-auth/src/globals.css +105 -0
- package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
- package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
- package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
- package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
- package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
- package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
- package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
- package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
- package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
- package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
- package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
- package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
- package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
- package/templates/apps/frontend-auth/template.json +6 -0
- package/templates/apps/frontend-auth/tsconfig.json +5 -0
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/README.md +47 -0
- package/templates/apps/frontend-cms/app.config.ts +37 -0
- package/templates/apps/frontend-cms/package.json +33 -0
- package/templates/apps/frontend-cms/src/config.ts +8 -0
- package/templates/apps/frontend-cms/src/globals.css +105 -0
- package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
- package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
- package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
- package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
- package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
- package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
- package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
- package/templates/apps/frontend-cms/template.json +6 -0
- package/templates/apps/frontend-cms/tsconfig.json +5 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/README.md +71 -0
- package/templates/apps/frontend-portal/app.config.ts +37 -0
- package/templates/apps/frontend-portal/package.json +32 -0
- package/templates/apps/frontend-portal/src/config.ts +8 -0
- package/templates/apps/frontend-portal/src/globals.css +93 -0
- package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
- package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
- package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
- package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
- package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
- package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
- package/templates/apps/frontend-portal/template.json +6 -0
- package/templates/apps/frontend-portal/tsconfig.json +5 -0
- package/templates/apps/frontend-saas/README.md +73 -0
- package/templates/apps/frontend-saas/app.config.ts +46 -0
- package/templates/apps/frontend-saas/package.json +32 -0
- package/templates/apps/frontend-saas/src/config.ts +8 -0
- package/templates/apps/frontend-saas/src/globals.css +85 -0
- package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
- package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
- package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
- package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
- package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
- package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-saas/template.json +6 -0
- package/templates/apps/frontend-saas/tsconfig.json +5 -0
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/README.md +51 -0
- package/templates/apps/frontend-status/app.config.ts +39 -0
- package/templates/apps/frontend-status/package.json +32 -0
- package/templates/apps/frontend-status/src/config.ts +7 -0
- package/templates/apps/frontend-status/src/globals.css +70 -0
- package/templates/apps/frontend-status/src/globals.d.ts +6 -0
- package/templates/apps/frontend-status/src/lib/status.ts +81 -0
- package/templates/apps/frontend-status/src/locales/de.ts +41 -0
- package/templates/apps/frontend-status/src/locales/en.ts +45 -0
- package/templates/apps/frontend-status/src/locales/index.ts +13 -0
- package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
- package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
- package/templates/apps/frontend-status/template.json +6 -0
- package/templates/apps/frontend-status/tsconfig.json +11 -0
- package/templates/baselines/compose/docker-compose.prod.yml +15 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
- package/dist/apiBuild-BrjrVJJh.js +0 -2
- package/dist/dbCommand-DrzXimKf.js +0 -2
- package/dist/dev-DNkso403.js +0 -3
- package/dist/fileConventions-3bffWssN.js +0 -30
- package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
- package/dist/inspect-gt8bq-Tz.js +0 -2
- package/dist/manifestBuild-ifczArzr.js +0 -2
- package/dist/serveCommand-DfkisVWP.js +0 -1310
|
@@ -680,9 +680,20 @@ The three recovery tools, and when each applies:
|
|
|
680
680
|
| `retry` | The input was wrong, or the job is short + idempotent | Fresh execution, empty journal — every step runs again |
|
|
681
681
|
| `resume` | The run is `suspended` (you set `suspendOnFailure: true`, or it awaits a signal) | Continues from the failure/suspend point — completed steps replay |
|
|
682
682
|
| `redrive` | The run is `failed` (dead-letter) and re-running earlier steps is expensive/unsafe | Continues from the failure point — completed steps replay, failed steps re-run |
|
|
683
|
+
| `resume-from-step` | The run is `failed`, but the failure point is **not** the right recovery point — an earlier step ran on stale/wrong state | Rewinds to a chosen step — steps before it replay, that step + everything after re-run |
|
|
683
684
|
|
|
684
685
|
`redrive` refuses a run that isn't a not-yet-discarded failure (use `resume` for a suspended run, `retry` for a fresh execution), and it needs a durable journal — on the memory store there is nothing to re-drive, so it declines cleanly (`redriven: false` with a reason) rather than pretending. It works under `voltro serve` as well as `voltro dev`, because dead-letter recovery happens in production. A re-drive records a `run-redriven` event on the run's timeline.
|
|
685
686
|
|
|
687
|
+
### Rewind further back — `resume-from-step`
|
|
688
|
+
|
|
689
|
+
`redrive` continues from the step that **failed**. Sometimes that isn't the right place to restart: step 5 failed, but the real problem is that step 3 completed against stale external state, and re-running from the failure point would carry that bad result forward. `resume-from-step` rewinds to a step **you** choose:
|
|
690
|
+
|
|
691
|
+
```sh
|
|
692
|
+
voltro workflows resume-from-step wf_01H... charge-card # rewind to 'charge-card' and re-run from there
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
It resets the chosen step **and every step after it** — including steps that succeeded — so they re-execute, while the steps *before* your target replay from the journal unchanged. `redrive` is the special case where your chosen step is exactly the one that failed. Like `redrive`, it works under both `voltro serve` and `voltro dev`, refuses a run that isn't a not-yet-discarded failure, refuses an unknown step name, and declines cleanly on a run with no durable journal.
|
|
696
|
+
|
|
686
697
|
## Compensation
|
|
687
698
|
|
|
688
699
|
For saga-style workflows, model compensation explicitly with `Effect.catchAll` around the step that can fail after an earlier side effect:
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/ai": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/ai": "0.29.0",
|
|
16
|
+
"@voltro/cli": "0.29.0",
|
|
17
|
+
"@voltro/database": "0.29.0",
|
|
18
|
+
"@voltro/env": "0.29.0",
|
|
19
|
+
"@voltro/protocol": "0.29.0",
|
|
20
|
+
"@voltro/runtime": "0.29.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.29.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
}
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-auth": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.29.0",
|
|
17
|
+
"@voltro/database": "0.29.0",
|
|
18
|
+
"@voltro/env": "0.29.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.29.0",
|
|
20
|
+
"@voltro/protocol": "0.29.0",
|
|
21
|
+
"@voltro/runtime": "0.29.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.29.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.29.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"vitest": "^4.1.10"
|
|
29
29
|
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.29.0",
|
|
17
|
+
"@voltro/database": "0.29.0",
|
|
18
|
+
"@voltro/env": "0.29.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.29.0",
|
|
20
|
+
"@voltro/protocol": "0.29.0",
|
|
21
|
+
"@voltro/runtime": "0.29.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.29.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
28
28
|
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-deactivation": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.29.0",
|
|
17
|
+
"@voltro/database": "0.29.0",
|
|
18
|
+
"@voltro/env": "0.29.0",
|
|
19
|
+
"@voltro/plugin-deactivation": "0.29.0",
|
|
20
|
+
"@voltro/protocol": "0.29.0",
|
|
21
|
+
"@voltro/runtime": "0.29.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.29.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
28
28
|
}
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@react-email/components": "^1.0.12",
|
|
15
15
|
"@react-email/render": "^1.4.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-mail": "0.
|
|
20
|
-
"@voltro/plugin-multitenancy": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.29.0",
|
|
17
|
+
"@voltro/database": "0.29.0",
|
|
18
|
+
"@voltro/env": "0.29.0",
|
|
19
|
+
"@voltro/plugin-mail": "0.29.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.29.0",
|
|
21
|
+
"@voltro/protocol": "0.29.0",
|
|
22
|
+
"@voltro/runtime": "0.29.0",
|
|
23
23
|
"effect": "^3.22.0",
|
|
24
24
|
"react": "^19.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.29.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
30
30
|
}
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-storage": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
23
|
-
"@voltro/sql-mysql": "0.
|
|
16
|
+
"@voltro/cli": "0.29.0",
|
|
17
|
+
"@voltro/database": "0.29.0",
|
|
18
|
+
"@voltro/env": "0.29.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.29.0",
|
|
20
|
+
"@voltro/plugin-storage": "0.29.0",
|
|
21
|
+
"@voltro/protocol": "0.29.0",
|
|
22
|
+
"@voltro/runtime": "0.29.0",
|
|
23
|
+
"@voltro/sql-mysql": "0.29.0",
|
|
24
24
|
"effect": "^3.22.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.29.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
30
30
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# {{capProjectName}} — SQLite backend env. Copy to `.env` and adjust.
|
|
2
|
+
|
|
3
|
+
# ── Database (embedded SQLite) ─────────────────────────────────────────
|
|
4
|
+
DB_DIALECT=sqlite
|
|
5
|
+
# A file path keeps data across restarts (the `.data/` dir is gitignored):
|
|
6
|
+
DB_URL=file:./.data/{{projectNameSnake}}.sqlite
|
|
7
|
+
# Or an ephemeral, in-process DB that vanishes on exit (great for tests):
|
|
8
|
+
# DB_URL=:memory:
|
|
9
|
+
|
|
10
|
+
# Session signing secret — NO VALUE SHIPS HERE ON PURPOSE.
|
|
11
|
+
#
|
|
12
|
+
# `voltro dev` mints a unique one for this project into a gitignored
|
|
13
|
+
# .env.local on first boot, so local development needs nothing from you.
|
|
14
|
+
# A shipped placeholder would be a signing key published to everyone who
|
|
15
|
+
# downloads this template, making every session in your deployment forgeable.
|
|
16
|
+
#
|
|
17
|
+
# Your DEPLOYMENT needs its own — `voltro serve` refuses to start without it:
|
|
18
|
+
# voltro secret generate session
|
|
19
|
+
VOLTRO_SESSION_SECRET=
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro backend scaffold (template: **api-backend-sqlite**) — an embedded,
|
|
4
|
+
single-process **SQLite** store. No database server to run; one native
|
|
5
|
+
dependency (`better-sqlite3`, via `@voltro/sql-sqlite`).
|
|
6
|
+
|
|
7
|
+
## Boot
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install # at the repo root
|
|
11
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
12
|
+
# → http://localhost:4000
|
|
13
|
+
# → ws://localhost:4000/ws
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The database file is created on first boot at the `DB_URL` path
|
|
17
|
+
(`.env.example` defaults to `file:./.data/{{projectNameSnake}}.sqlite`, which is
|
|
18
|
+
gitignored). Use `DB_URL=:memory:` for an ephemeral, in-process database.
|
|
19
|
+
|
|
20
|
+
## What's in here
|
|
21
|
+
|
|
22
|
+
| File | Role |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `app.config.ts` | App declaration (`store: 'sqlite'`); `voltro dev` reads this first. |
|
|
25
|
+
| `.env.example` | `DB_DIALECT` + `DB_URL` — copy to `.env`. |
|
|
26
|
+
| `database/schema.ts` | One example table (`notes`) with the `tenant()` mixin. |
|
|
27
|
+
| `queries/notes.query.ts` | Streaming subscription example. |
|
|
28
|
+
| `mutations/notes.create.mutation.ts` | Tenant-guarded mutation example. |
|
|
29
|
+
|
|
30
|
+
Drop more `*.query.ts`, `*.mutation.ts`, `*.action.ts`, or `*.workflow.tsx`
|
|
31
|
+
files anywhere in this tree — discovery is by file convention.
|
|
32
|
+
|
|
33
|
+
## When to reach for something else
|
|
34
|
+
|
|
35
|
+
SQLite is single-writer and single-process, so real-time subscriptions do not
|
|
36
|
+
fan out across replicas (there is no CDC bus — within the one process they work
|
|
37
|
+
as normal). Scale to `store: 'postgres'` / `'mysql'` / `'mariadb'` when you run
|
|
38
|
+
more than one replica.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Backend app for the {{projectName}} project, backed by SQLite. Read by
|
|
2
|
+
// `voltro dev` (local) and `voltro serve` (production).
|
|
3
|
+
//
|
|
4
|
+
// `store: 'sqlite'` is the simplest DURABLE store: an embedded, single-file
|
|
5
|
+
// (or `:memory:`) database with no server to run and one native dependency
|
|
6
|
+
// (`better-sqlite3`, pulled by `@voltro/sql-sqlite`). Great for a single
|
|
7
|
+
// process — a CLI tool, an edge box, a small self-hosted app, or a durable
|
|
8
|
+
// local dev store that survives restarts (unlike `store: 'memory'`).
|
|
9
|
+
//
|
|
10
|
+
// The database file + dialect come from env — see `.env.example`:
|
|
11
|
+
// DB_DIALECT=sqlite
|
|
12
|
+
// DB_URL=file:./.data/{{projectNameSnake}}.sqlite (or ':memory:')
|
|
13
|
+
//
|
|
14
|
+
// SQLite is single-writer and single-process by design, so it does NOT
|
|
15
|
+
// fan real-time subscriptions across replicas (there is no CDC bus). Within
|
|
16
|
+
// the one process, reactive queries work exactly as elsewhere. Reach for
|
|
17
|
+
// postgres/mysql/mariadb when you scale to more than one replica.
|
|
18
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
19
|
+
|
|
20
|
+
// Typed environment — declare your env once; it's validated at boot (fail-fast)
|
|
21
|
+
// and read on the server via `serverEnv` / `getSecret` from '@voltro/env/server'.
|
|
22
|
+
// `access` is required: 'public' (browser-safe) or 'secret' (server-only, never
|
|
23
|
+
// bundled). Run `voltro env` to see the manifest. See the Environment docs.
|
|
24
|
+
export const env = defineEnv({
|
|
25
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
26
|
+
// Add your own, e.g.:
|
|
27
|
+
// STRIPE_KEY: envVar.string({ access: 'secret' }),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
type: 'api' as const,
|
|
32
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
33
|
+
store: 'sqlite' as const,
|
|
34
|
+
env,
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Schema for the {{projectName}} backend.
|
|
2
|
+
//
|
|
3
|
+
// Tables are auto-discovered: every exported `table(...)` in this file
|
|
4
|
+
// (or any `*.entity.ts` / `*.schema.ts` file) is registered by
|
|
5
|
+
// `voltro dev` — no manual barrel. `actors` + `tenants` are the
|
|
6
|
+
// framework's core tables: the built-in `audit()` / `tenant()` /
|
|
7
|
+
// `softDelete()` mixins reference them, so they must be declared (the
|
|
8
|
+
// CLI wires them into the mixin registry once it finds them).
|
|
9
|
+
//
|
|
10
|
+
// Add the `tenant()` mixin to a table to make it tenant-scoped — the
|
|
11
|
+
// runtime AND-merges `eq('tenantId', subject.tenantId)` into every
|
|
12
|
+
// subscription predicate, so cross-tenant reads can't leak.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
boolean,
|
|
16
|
+
databaseHandle,
|
|
17
|
+
id,
|
|
18
|
+
table,
|
|
19
|
+
text,
|
|
20
|
+
timestamp,
|
|
21
|
+
type InferRow,
|
|
22
|
+
} from '@voltro/database'
|
|
23
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
24
|
+
|
|
25
|
+
// ---------- Core tables (required by the audit / tenant mixins) ----------
|
|
26
|
+
|
|
27
|
+
export const actors = table('actors', {
|
|
28
|
+
id: id(),
|
|
29
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
30
|
+
displayName: text().nullable(),
|
|
31
|
+
createdAt: timestamp().default('now'),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const tenants = table('tenants', {
|
|
35
|
+
id: id(),
|
|
36
|
+
name: text(),
|
|
37
|
+
createdAt: timestamp().default('now'),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// ---------- Application tables — rename / replace with your own ----------
|
|
41
|
+
|
|
42
|
+
export const notes = table('notes', {
|
|
43
|
+
id: id({ prefix: 'note' }),
|
|
44
|
+
title: text(),
|
|
45
|
+
body: text(),
|
|
46
|
+
done: boolean().default(false),
|
|
47
|
+
})
|
|
48
|
+
// tenant() pulls audit() transitively → adds tenantId + createdAt /
|
|
49
|
+
// updatedAt / createdBy / updatedBy (auto-stamped by the runtime).
|
|
50
|
+
.with(tenant())
|
|
51
|
+
|
|
52
|
+
export type Note = InferRow<typeof notes>
|
|
53
|
+
|
|
54
|
+
export const database = databaseHandle({ actors, tenants, notes })
|
|
@@ -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 },
|
|
6
|
+
ctx: AppContext,
|
|
7
|
+
) => {
|
|
8
|
+
assertOwnTenant(input.tenantId, ctx.request.subject)
|
|
9
|
+
// Framework auto-injects a `note_…` id from the table's id() decl.
|
|
10
|
+
return ctx.store.insert('notes', {
|
|
11
|
+
title: input.title,
|
|
12
|
+
body: input.body,
|
|
13
|
+
done: false,
|
|
14
|
+
tenantId: input.tenantId,
|
|
15
|
+
createdAt: new Date(),
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default execute
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Creates a note. The handler's `assertOwnTenant` guard rejects
|
|
2
|
+
// cross-tenant spoofing — input.tenantId MUST match the resolved
|
|
3
|
+
// subject's tenantId, else the rpc layer surfaces a typed
|
|
4
|
+
// `TenantMismatch` error.
|
|
5
|
+
|
|
6
|
+
import { defineMutation } from '@voltro/protocol'
|
|
7
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
8
|
+
import { Schema } from 'effect'
|
|
9
|
+
|
|
10
|
+
export const createNote = defineMutation({
|
|
11
|
+
name: 'notes.create',
|
|
12
|
+
target: {
|
|
13
|
+
table: 'notes',
|
|
14
|
+
op: 'insert',
|
|
15
|
+
shape: (input: { tenantId: string; title: string; body: string }) => ({
|
|
16
|
+
title: input.title,
|
|
17
|
+
body: input.body,
|
|
18
|
+
done: false,
|
|
19
|
+
tenantId: input.tenantId,
|
|
20
|
+
createdAt: new Date(),
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
input: Schema.Struct({
|
|
24
|
+
tenantId: Schema.String,
|
|
25
|
+
title: Schema.NonEmptyString,
|
|
26
|
+
body: Schema.String,
|
|
27
|
+
}),
|
|
28
|
+
output: Schema.Struct({
|
|
29
|
+
id: Schema.String,
|
|
30
|
+
title: Schema.String,
|
|
31
|
+
body: Schema.String,
|
|
32
|
+
done: Schema.Boolean,
|
|
33
|
+
tenantId: Schema.String,
|
|
34
|
+
createdAt: Schema.Date,
|
|
35
|
+
}),
|
|
36
|
+
error: TenantMismatch,
|
|
37
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
9
|
+
"migrate": "voltro migrate",
|
|
10
|
+
"test": "voltro test",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@effect/platform": "^0.97.0",
|
|
15
|
+
"@effect/rpc": "^0.76.0",
|
|
16
|
+
"@voltro/cli": "0.29.0",
|
|
17
|
+
"@voltro/database": "0.29.0",
|
|
18
|
+
"@voltro/env": "0.29.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.29.0",
|
|
20
|
+
"@voltro/protocol": "0.29.0",
|
|
21
|
+
"@voltro/runtime": "0.29.0",
|
|
22
|
+
"@voltro/sql-sqlite": "0.29.0",
|
|
23
|
+
"effect": "^3.22.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@voltro/testing": "0.29.0",
|
|
27
|
+
"typescript": "^6.0.3",
|
|
28
|
+
"vitest": "^4.1.10"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -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: 'notes' 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,20 @@
|
|
|
1
|
+
// Streaming subscription: every change to `notes` for the caller's
|
|
2
|
+
// tenant lands as a delta. The runtime AND-merges tenant scope into
|
|
3
|
+
// the predicate via the `tenant()` mixin on the table — no manual
|
|
4
|
+
// `eq('tenantId', ...)` needed in the handler.
|
|
5
|
+
|
|
6
|
+
import { defineQuery } from '@voltro/protocol'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const listNotes = defineQuery({
|
|
10
|
+
name: 'notes.list',
|
|
11
|
+
input: Schema.Struct({}),
|
|
12
|
+
output: Schema.Struct({
|
|
13
|
+
id: Schema.String,
|
|
14
|
+
title: Schema.String,
|
|
15
|
+
body: Schema.String,
|
|
16
|
+
done: Schema.Boolean,
|
|
17
|
+
tenantId: Schema.String,
|
|
18
|
+
createdAt: Schema.Date,
|
|
19
|
+
}),
|
|
20
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-backend-sqlite",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "SQLite-backed Voltro backend: a single-process, embedded, zero-server database (file or :memory:). The simplest durable-store shape — no docker, one dependency.",
|
|
5
|
+
"tags": ["api", "backend", "sqlite", "embedded", "single-process", "durable"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Unit test with `@voltro/testing` — no database, no running server. Importing
|
|
2
|
+
// `../database/schema` registers the `notes` / `actors` / `tenants` tables
|
|
3
|
+
// globally, so `makeTestContext` auto-wires them into the in-memory store.
|
|
4
|
+
// `ctx.store` is the SAME mixin-wrapped store the handler gets in production,
|
|
5
|
+
// so tenant auto-scoping + audit auto-fill behave exactly as at runtime.
|
|
6
|
+
// Run with `voltro test` (vitest).
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
10
|
+
import { database } from '../database/schema' // registers notes / actors / tenants
|
|
11
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
12
|
+
import createNote from '../mutations/notes.create.mutation.server'
|
|
13
|
+
|
|
14
|
+
describe('notes.create', () => {
|
|
15
|
+
it('inserts a note stamped to the caller tenant', async () => {
|
|
16
|
+
const ctx = makeTestContext({
|
|
17
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
18
|
+
store: mockStore({ notes: [] }),
|
|
19
|
+
})
|
|
20
|
+
const row = await createNote({ tenantId: 'acme', title: 'Hello', body: 'World' }, ctx)
|
|
21
|
+
|
|
22
|
+
expect(row['title']).toBe('Hello')
|
|
23
|
+
expect(row['tenantId']).toBe('acme') // assertOwnTenant let it through + stamped
|
|
24
|
+
expect(row['done']).toBe(false)
|
|
25
|
+
expect(String(row['id'])).toMatch(/^note_/) // TypeID auto-injected from id()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects a cross-tenant spoof — input.tenantId must match the subject', async () => {
|
|
29
|
+
const ctx = makeTestContext({
|
|
30
|
+
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
|
|
31
|
+
store: mockStore({ notes: [] }),
|
|
32
|
+
})
|
|
33
|
+
await expect(
|
|
34
|
+
createNote({ tenantId: 'other-tenant', title: 'spoof', body: '' }, ctx),
|
|
35
|
+
).rejects.toBeInstanceOf(TenantMismatch)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('isolates tenants — t2 never sees t1 rows (REAL tenant scoping)', async () => {
|
|
39
|
+
const ctx = makeTestContext({
|
|
40
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
41
|
+
store: mockStore({ notes: [] }),
|
|
42
|
+
})
|
|
43
|
+
await createNote({ tenantId: 't1', title: 'secret', body: '' }, ctx)
|
|
44
|
+
|
|
45
|
+
// Re-scope the SAME store to a different tenant — the real WHERE filter
|
|
46
|
+
// (eq tenantId) hides t1's row from t2.
|
|
47
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.notes.descriptor))
|
|
48
|
+
expect(seenByT2).toHaveLength(0)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
"test": "voltro test"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.
|
|
18
|
-
"@voltro/plugin-storage": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/cli": "0.29.0",
|
|
15
|
+
"@voltro/database": "0.29.0",
|
|
16
|
+
"@voltro/env": "0.29.0",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.29.0",
|
|
18
|
+
"@voltro/plugin-storage": "0.29.0",
|
|
19
|
+
"@voltro/protocol": "0.29.0",
|
|
20
|
+
"@voltro/runtime": "0.29.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.29.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
}
|