@voltro/cli 0.33.0 → 0.34.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 +1801 -0
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-BICVGnEC.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CE7Ca9gm.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Dv8r9tGO.js +231 -0
- package/dist/checkCommand-YZDmkAGd.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-CNXKlcvD.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-DC6w2tNZ.js +30 -0
- package/dist/codemodRunner-DRRqXR74.js +5243 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-gutsz-Ac.js +808 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-3YG8p-UA.js +25 -0
- package/dist/dataCommand-qL0r7fPO.js +535 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
- package/dist/dbCommand-CpdKLeQq.js +2 -0
- package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
- package/dist/dev-CoG-ZPx8.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-BvqGBwNG.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
- package/dist/dormancyCommand-Dszo57d6.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
- package/dist/envCommand-DPgdV1Bq.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-DHpkgjgH.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
- package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
- package/dist/generateCommand-oibemh97.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-BJw9nLUR.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
- package/dist/inspect-C_T_WGvl.js +2 -0
- package/dist/inspectCmd-Bppy-GGw.js +224 -0
- package/dist/inspectFetch-Cm8_wVvp.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-CCca3KRZ.js +260 -0
- package/dist/manifestBuild-ChsKAhmn.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-CBwOt_iV.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CRxalXTs.js +91 -0
- package/dist/scheduleCmd--jksTrf6.js +69 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
- package/dist/seedRunner-TFHHiToI.js +329 -0
- package/dist/serveCommand-B_isw7q4.js +1647 -0
- package/dist/serveCommand-DOvbgRnQ.js +2 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-9LiUOfES.js +1087 -0
- package/dist/start-B-9Nsp-S.js +3 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-CkEZQrtt.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-Bkptutss.js +585 -0
- package/dist/updateCommand-us1_hdIC.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
- package/dist/webDev-Cg-fFiyd2.js +2 -0
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-CID96Rga.js +267 -0
- package/dist/workflowsCmd-D1VTmLMY.js +608 -0
- package/package.json +179 -17
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +22 -5
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +265 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +794 -50
- package/templates/agent-docs/data.md +550 -11
- package/templates/agent-docs/database/migrations.md +174 -25
- package/templates/agent-docs/database/misc.md +156 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +69 -3
- package/templates/agent-docs/introduction.md +87 -16
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +95 -20
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +161 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +1 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +157 -6
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +150 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +10 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +1700 -104
- package/templates/agent-docs/workflows.md +130 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-C-x9YacA.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -69,8 +69,9 @@ export default function AdminLayout({ children }: { readonly children: ReactNode
|
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
|
-
// PermissionProvider feeds the current subject's scopes to
|
|
73
|
-
//
|
|
72
|
+
// PermissionProvider feeds the current subject's scopes to useAccessDecision
|
|
73
|
+
// across the admin, which judges them against each procedure's OWN declared
|
|
74
|
+
// guards. DEMO scopes here — swap for your session's (see lib/admin.ts).
|
|
74
75
|
<PermissionProvider scopes={DEMO_SCOPES}>
|
|
75
76
|
<div className="admin">
|
|
76
77
|
<aside>
|
|
@@ -57,11 +57,16 @@ describe('admin overview — config', () => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
describe('admin overview — render', () => {
|
|
60
|
+
const spec = (over: Record<string, unknown>): Record<string, unknown> => ({
|
|
61
|
+
columns: [], serverOnlyColumns: [], sensitiveColumns: [], reactive: false, editable: true,
|
|
62
|
+
list: {}, create: {}, update: {}, delete: {}, ...over,
|
|
63
|
+
})
|
|
64
|
+
|
|
60
65
|
test('renders a linked card per entity with its column count and ops', () => {
|
|
61
66
|
capabilityManifest.mockReturnValue({ manifest: {}, loading: false })
|
|
62
67
|
entityAdmins.mockReturnValue([
|
|
63
|
-
{ table: 'todos', columns: ['id', 'title'], reactive: true,
|
|
64
|
-
{ table: 'notes', columns: ['id'],
|
|
68
|
+
spec({ table: 'todos', columns: ['id', 'title'], reactive: true, list: { tag: 'todos.list' }, create: { tag: 'todos.create' } }),
|
|
69
|
+
spec({ table: 'notes', columns: ['id'], list: { tag: 'notes.list' } }),
|
|
65
70
|
])
|
|
66
71
|
|
|
67
72
|
render(createElement(AdminOverview))
|
|
@@ -72,6 +77,18 @@ describe('admin overview — render', () => {
|
|
|
72
77
|
expect(container.textContent).toContain('list · create')
|
|
73
78
|
})
|
|
74
79
|
|
|
80
|
+
test('says how many columns are withheld rather than just showing fewer', () => {
|
|
81
|
+
// `columns` already excludes `.serverOnly()`, so a card that only printed the
|
|
82
|
+
// count would silently under-report the table. "2 columns" and "2 columns +
|
|
83
|
+
// 1 hidden" are different facts about the same entity.
|
|
84
|
+
capabilityManifest.mockReturnValue({ manifest: {}, loading: false })
|
|
85
|
+
entityAdmins.mockReturnValue([
|
|
86
|
+
spec({ table: 'users', columns: ['id', 'email'], serverOnlyColumns: ['pwHash'], list: { tag: 'users.list' } }),
|
|
87
|
+
])
|
|
88
|
+
render(createElement(AdminOverview))
|
|
89
|
+
expect(container.textContent).toContain('1 server-only column, hidden')
|
|
90
|
+
})
|
|
91
|
+
|
|
75
92
|
test('shows the error banner when the capability map is unreachable', () => {
|
|
76
93
|
capabilityManifest.mockReturnValue({ manifest: undefined, loading: false, error: 'boom' })
|
|
77
94
|
entityAdmins.mockReturnValue([])
|
|
@@ -39,19 +39,24 @@ export default function AdminOverview(): ReactNode {
|
|
|
39
39
|
<div className="cards" style={{ marginTop: '1.25rem' }}>
|
|
40
40
|
{entities.map((e) => {
|
|
41
41
|
const ops = [
|
|
42
|
-
e.
|
|
43
|
-
e.
|
|
44
|
-
e.
|
|
45
|
-
e.
|
|
42
|
+
e.list.tag && 'list',
|
|
43
|
+
e.create.tag && 'create',
|
|
44
|
+
e.update.tag && 'edit',
|
|
45
|
+
e.delete.tag && 'delete',
|
|
46
46
|
].filter(Boolean)
|
|
47
47
|
return (
|
|
48
48
|
<a className="card" key={e.table} href={`/admin/${e.table}`}>
|
|
49
49
|
<div className="value">{e.table}</div>
|
|
50
50
|
<div className="label">
|
|
51
|
+
{/* `columns` already excludes `.serverOnly()` — the count is
|
|
52
|
+
what an admin can actually show, not what the table has. */}
|
|
51
53
|
<T id="overview.columns" values={{ count: e.columns.length }} />
|
|
52
54
|
{e.reactive ? <T id="overview.reactiveSuffix" /> : null}
|
|
53
55
|
</div>
|
|
54
56
|
<div className="meta">{ops.length > 0 ? ops.join(' · ') : <T id="overview.readonly" />}</div>
|
|
57
|
+
{e.serverOnlyColumns.length > 0 ? (
|
|
58
|
+
<div className="meta"><T id="overview.serverOnly" values={{ count: e.serverOnlyColumns.length }} /></div>
|
|
59
|
+
) : null}
|
|
55
60
|
</a>
|
|
56
61
|
)
|
|
57
62
|
})}
|
|
@@ -31,9 +31,10 @@ export default {
|
|
|
31
31
|
// more languages by adding a code here + a `src/locales/<code>.ts`.
|
|
32
32
|
locales: ['en', 'de'] as const,
|
|
33
33
|
defaultLocale: 'en' as const,
|
|
34
|
-
// The api this frontend consumes. The NAME ('app') is the lookup key
|
|
35
|
-
//
|
|
36
|
-
// 'notes.
|
|
34
|
+
// The api this frontend consumes. The NAME ('app') is the lookup key — spelled
|
|
35
|
+
// ONCE, in `src/lib/api.ts`'s `createHooks<AppProcedures>('app')`, after which
|
|
36
|
+
// pages just call `useSubscription('notes.list')` and
|
|
37
|
+
// `useMutation('notes.create')`. The `package` resolves the sibling api
|
|
37
38
|
// (scaffold it with `--api=api-backend`); its port is auto-discovered from
|
|
38
39
|
// its own app.config.ts, and codegen pulls its typed rpc surface so the
|
|
39
40
|
// hooks are end-to-end typed. Add more entries to consume more apis.
|
|
@@ -12,21 +12,24 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.34.0",
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/client": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/i18n": "0.34.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
21
|
+
"@voltro/web": "0.34.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.34.0",
|
|
27
|
+
"@voltro/testing": "0.34.0",
|
|
27
28
|
"@types/react": "^19.0.0",
|
|
28
29
|
"@types/react-dom": "^19.0.0",
|
|
30
|
+
"jsdom": "^30.0.0",
|
|
29
31
|
"typescript": "^6.0.3",
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
30
33
|
"vitest": "^4.1.10"
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The typed hook surface for the `app` api — bound ONCE, here, and imported by
|
|
2
|
+
// every page instead of `@voltro/client`.
|
|
3
|
+
//
|
|
4
|
+
// `AppProcedures` is emitted by codegen into the api's `rpcGroup.generated.ts`:
|
|
5
|
+
// a type-level map of every rpc tag to the descriptor behind it. Feeding it to
|
|
6
|
+
// `createHooks` makes the tag a literal union and the input/output types INFER:
|
|
7
|
+
//
|
|
8
|
+
// const { data } = useSubscription('notes.list') // rows, typed
|
|
9
|
+
// const create = useMutation('notes.create') // input + output typed
|
|
10
|
+
//
|
|
11
|
+
// So no page declares a row `interface` mirroring the api any more. The old
|
|
12
|
+
// `useSubscription<ReadonlyArray<Note>>('app', 'notes.list')` spelled the api
|
|
13
|
+
// name at every call site, left the tag an unchecked string (a typo was a
|
|
14
|
+
// runtime error), and the `<T>` was an ASSERTION nothing compared against the
|
|
15
|
+
// server. All three are compile-checked now.
|
|
16
|
+
//
|
|
17
|
+
// `'app'` is the key this web app gave the api in `app.config.ts` -> `apis`.
|
|
18
|
+
// `import type` is erased, so the binding costs the browser bundle nothing.
|
|
19
|
+
import { createHooks } from '@voltro/client'
|
|
20
|
+
import type { AppProcedures } from '@{{projectName}}/api/rpcGroup'
|
|
21
|
+
|
|
22
|
+
// Destructured on purpose - `react-hooks/rules-of-hooks` only treats a member
|
|
23
|
+
// call as a hook when the object is PascalCase, so top-level `useX` identifiers
|
|
24
|
+
// are what keep the React lint rules working at every call site.
|
|
25
|
+
export const { useSubscription, useMutation, useAction } = createHooks<AppProcedures>('app')
|
|
@@ -1,50 +1,70 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
//
|
|
3
|
-
// `/` — the reactive notes page
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// `/` — the reactive notes page, driven through `makeVoltroTestClient`
|
|
4
|
+
// (`@voltro/testing/client`) rather than a `vi.mock` of anything.
|
|
5
|
+
//
|
|
6
|
+
// WHY THE HARNESS HERE. The old suite faked `src/lib/api` — the app's own typed
|
|
7
|
+
// binding — with the argument that mocking `@voltro/client` underneath would be
|
|
8
|
+
// the wrong seam. That argument was right about the seam and reached the wrong
|
|
9
|
+
// conclusion, because the harness does not mock `@voltro/client` at all: it
|
|
10
|
+
// PROVIDES the runtime context the real hooks read. So `createHooks` runs, the
|
|
11
|
+
// real `useSubscription` / `useMutation` run, and the fake stops at the
|
|
12
|
+
// transport — one layer below the binding rather than on top of it.
|
|
13
|
+
//
|
|
14
|
+
// What that buys, concretely: the page is the framework's flagship example of
|
|
15
|
+
// the reactive loop, and every "different data" case here used to be a fresh
|
|
16
|
+
// mount with a different mock return. Nothing pushed a delta into a MOUNTED
|
|
17
|
+
// tree, which is the one behaviour the page exists to demonstrate. The last two
|
|
18
|
+
// cases do, via `setSubscription`.
|
|
8
19
|
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, test
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
10
21
|
import { act, createElement, type ReactNode } from 'react'
|
|
11
22
|
import { createRoot, type Root } from 'react-dom/client'
|
|
12
23
|
import { I18nProvider } from '@voltro/i18n'
|
|
24
|
+
import { makeVoltroTestClient, type VoltroTestClient } from '@voltro/testing/client'
|
|
13
25
|
import enCatalog from '../locales/en'
|
|
14
|
-
|
|
15
|
-
interface SubState {
|
|
16
|
-
data: unknown
|
|
17
|
-
revision?: number
|
|
18
|
-
emittedAt?: number
|
|
19
|
-
error?: unknown
|
|
20
|
-
pendingPatches?: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const useSubscription = vi.fn<() => SubState>()
|
|
24
|
-
const mutate = vi.fn<(input: unknown) => Promise<unknown>>()
|
|
25
|
-
const useMutation = vi.fn(() => ({ mutate, pending: false, error: undefined, data: undefined }))
|
|
26
|
-
|
|
27
|
-
vi.mock('@voltro/client', () => ({
|
|
28
|
-
useSubscription: (...a: unknown[]) => useSubscription(...(a as [])),
|
|
29
|
-
useMutation: (...a: unknown[]) => useMutation(...(a as [])),
|
|
30
|
-
}))
|
|
31
|
-
|
|
32
|
-
const { default: IndexPage } = await import('./page')
|
|
26
|
+
import IndexPage from './page'
|
|
33
27
|
|
|
34
28
|
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
35
29
|
|
|
30
|
+
const LIST = 'notes.list'
|
|
31
|
+
|
|
36
32
|
let container: HTMLDivElement
|
|
37
33
|
let root: Root
|
|
34
|
+
let client: VoltroTestClient
|
|
35
|
+
|
|
36
|
+
const note = (over: Partial<{ id: string; title: string; body: string; optimistic: boolean }> = {}) => ({
|
|
37
|
+
id: over.id ?? 'n_1',
|
|
38
|
+
title: over.title ?? 'first',
|
|
39
|
+
body: over.body ?? '',
|
|
40
|
+
done: false,
|
|
41
|
+
tenantId: 'acme',
|
|
42
|
+
createdAt: new Date(0),
|
|
43
|
+
...(over.optimistic === true ? { optimistic: true } : {}),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/** `notes.create` resolves by default; a case that needs the failure path
|
|
47
|
+
* overrides it. The harness records every call on `client.calls`. */
|
|
48
|
+
const harness = (
|
|
49
|
+
subscriptions: Readonly<Record<string, unknown>> = {},
|
|
50
|
+
create: (input: unknown) => unknown = () => undefined,
|
|
51
|
+
): VoltroTestClient =>
|
|
52
|
+
makeVoltroTestClient({ subscriptions, mutations: { 'notes.create': create } })
|
|
38
53
|
|
|
39
54
|
// The page uses <T>/useTFn, so it needs an <I18nProvider>. Wrap with the real
|
|
40
55
|
// English catalog (the framework auto-wires this provider in the running app).
|
|
41
|
-
const render = (
|
|
56
|
+
const render = (): void => {
|
|
42
57
|
container = document.createElement('div')
|
|
43
58
|
document.body.appendChild(container)
|
|
44
59
|
act(() => {
|
|
45
60
|
root = createRoot(container)
|
|
46
61
|
root.render(
|
|
47
|
-
createElement(I18nProvider, {
|
|
62
|
+
createElement(I18nProvider, {
|
|
63
|
+
locale: 'en',
|
|
64
|
+
messages: enCatalog,
|
|
65
|
+
defaultLocale: 'en',
|
|
66
|
+
children: createElement(client.Provider, null, createElement(IndexPage) as ReactNode),
|
|
67
|
+
}),
|
|
48
68
|
)
|
|
49
69
|
})
|
|
50
70
|
}
|
|
@@ -61,13 +81,14 @@ const setInput = (el: HTMLInputElement | HTMLTextAreaElement, value: string): vo
|
|
|
61
81
|
})
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
const submit = async (): Promise<void> => {
|
|
85
|
+
const form = container.querySelector('form') as HTMLFormElement
|
|
86
|
+
await act(async () => {
|
|
87
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
beforeEach(() => { client = harness() })
|
|
71
92
|
|
|
72
93
|
afterEach(() => {
|
|
73
94
|
if (root) act(() => root.unmount())
|
|
@@ -76,53 +97,43 @@ afterEach(() => {
|
|
|
76
97
|
})
|
|
77
98
|
|
|
78
99
|
describe('index — subscription states', () => {
|
|
79
|
-
test('shows the loading hint before the first snapshot
|
|
80
|
-
|
|
81
|
-
render(
|
|
100
|
+
test('shows the loading hint before the first snapshot', () => {
|
|
101
|
+
// A tag with no fixture IS the cold state — no stub had to invent one.
|
|
102
|
+
render()
|
|
82
103
|
expect(container.textContent).toContain('loading…')
|
|
83
104
|
expect(container.querySelectorAll('li')).toHaveLength(0)
|
|
84
105
|
})
|
|
85
106
|
|
|
86
107
|
test('shows the empty state once a snapshot arrives with no rows', () => {
|
|
87
|
-
|
|
88
|
-
render(
|
|
108
|
+
client = harness({ [LIST]: [] })
|
|
109
|
+
render()
|
|
89
110
|
expect(container.textContent).toContain('No notes yet')
|
|
90
111
|
})
|
|
91
112
|
|
|
92
113
|
test('renders each note with its title and body', () => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{ id: 'n_2', title: 'second', body: '', done: false, tenantId: 'acme', createdAt: new Date(0) },
|
|
97
|
-
],
|
|
98
|
-
revision: 3,
|
|
99
|
-
})
|
|
100
|
-
render(createElement(IndexPage))
|
|
101
|
-
const items = Array.from(container.querySelectorAll('li'))
|
|
102
|
-
expect(items).toHaveLength(2)
|
|
114
|
+
client = harness({ [LIST]: [note({ id: 'n_1', title: 'first', body: 'body one' }), note({ id: 'n_2', title: 'second' })] })
|
|
115
|
+
render()
|
|
116
|
+
expect(container.querySelectorAll('li')).toHaveLength(2)
|
|
103
117
|
expect(container.textContent).toContain('first')
|
|
104
118
|
expect(container.textContent).toContain('body one')
|
|
105
119
|
expect(container.textContent).toContain('second')
|
|
106
120
|
})
|
|
107
121
|
|
|
108
|
-
test('renders optimistic rows faintly + italicised', () => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
],
|
|
113
|
-
revision: 1,
|
|
114
|
-
pendingPatches: 1,
|
|
122
|
+
test('renders optimistic rows faintly + italicised, with the pending count', () => {
|
|
123
|
+
client = harness({ [LIST]: [] })
|
|
124
|
+
render()
|
|
125
|
+
act(() => {
|
|
126
|
+
client.setSubscription(LIST, [note({ id: 'n_opt', title: 'pending', optimistic: true })], { pendingPatches: 1 })
|
|
115
127
|
})
|
|
116
|
-
render(createElement(IndexPage))
|
|
117
128
|
const li = container.querySelector('li')!
|
|
118
129
|
expect(li.style.opacity).toBe('0.5')
|
|
119
130
|
expect(li.style.fontStyle).toBe('italic')
|
|
120
131
|
expect(container.textContent).toContain('1 pending')
|
|
121
132
|
})
|
|
122
133
|
|
|
123
|
-
test('renders the subscription error banner when
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
test('renders the subscription error banner when the stream fails', () => {
|
|
135
|
+
render()
|
|
136
|
+
act(() => { client.failSubscription(LIST, 'boom') })
|
|
126
137
|
expect(container.textContent).toContain('subscription error')
|
|
127
138
|
expect(container.textContent).toContain('boom')
|
|
128
139
|
})
|
|
@@ -130,38 +141,75 @@ describe('index — subscription states', () => {
|
|
|
130
141
|
|
|
131
142
|
describe('index — create form', () => {
|
|
132
143
|
test('the Add button is disabled until a non-empty title is typed', () => {
|
|
133
|
-
|
|
134
|
-
render(
|
|
144
|
+
client = harness({ [LIST]: [] })
|
|
145
|
+
render()
|
|
135
146
|
const button = container.querySelector('button[type="submit"]') as HTMLButtonElement
|
|
136
147
|
expect(button.disabled).toBe(true)
|
|
137
148
|
setInput(container.querySelector('input') as HTMLInputElement, 'a note')
|
|
138
149
|
expect(button.disabled).toBe(false)
|
|
139
150
|
})
|
|
140
151
|
|
|
141
|
-
test('submitting calls create
|
|
142
|
-
|
|
143
|
-
render(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
expect(
|
|
153
|
-
expect(input.value).toBe('')
|
|
154
|
-
expect(textarea.value).toBe('')
|
|
152
|
+
test('submitting calls notes.create with the trimmed title + body + tenant, then clears', async () => {
|
|
153
|
+
client = harness({ [LIST]: [] })
|
|
154
|
+
render()
|
|
155
|
+
setInput(container.querySelector('input') as HTMLInputElement, ' my title ')
|
|
156
|
+
setInput(container.querySelector('textarea') as HTMLTextAreaElement, ' my body ')
|
|
157
|
+
await submit()
|
|
158
|
+
|
|
159
|
+
expect(client.calls).toEqual([
|
|
160
|
+
{ kind: 'mutation', tag: 'notes.create', input: { tenantId: 'acme', title: 'my title', body: 'my body' } },
|
|
161
|
+
])
|
|
162
|
+
expect((container.querySelector('input') as HTMLInputElement).value).toBe('')
|
|
163
|
+
expect((container.querySelector('textarea') as HTMLTextAreaElement).value).toBe('')
|
|
155
164
|
})
|
|
156
165
|
|
|
157
166
|
test('a whitespace-only title does not fire a mutation', async () => {
|
|
158
|
-
|
|
159
|
-
render(
|
|
167
|
+
client = harness({ [LIST]: [] })
|
|
168
|
+
render()
|
|
160
169
|
setInput(container.querySelector('input') as HTMLInputElement, ' ')
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
await submit()
|
|
171
|
+
expect(client.calls).toEqual([])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('a REJECTED create leaves the form cleared and the list untouched', async () => {
|
|
175
|
+
client = harness({ [LIST]: [] }, () => { throw new Error('TenantMismatch') })
|
|
176
|
+
render()
|
|
177
|
+
setInput(container.querySelector('input') as HTMLInputElement, 'nope')
|
|
178
|
+
await submit()
|
|
179
|
+
expect(client.calls).toHaveLength(1)
|
|
180
|
+
expect(container.textContent).toContain('No notes yet')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// The reactive loop itself — a server delta into a MOUNTED tree. This is what
|
|
185
|
+
// the page is a demonstration of, and what a per-mount mock cannot express.
|
|
186
|
+
describe('index — the reactive loop', () => {
|
|
187
|
+
test('a delta pushed by the server re-renders the list in place', () => {
|
|
188
|
+
client = harness({ [LIST]: [note({ id: 'n_1', title: 'first' })] })
|
|
189
|
+
render()
|
|
190
|
+
expect(container.querySelectorAll('li')).toHaveLength(1)
|
|
191
|
+
|
|
192
|
+
act(() => {
|
|
193
|
+
client.setSubscription(LIST, [note({ id: 'n_1', title: 'first' }), note({ id: 'n_2', title: 'second' })])
|
|
164
194
|
})
|
|
165
|
-
|
|
195
|
+
|
|
196
|
+
expect(container.querySelectorAll('li')).toHaveLength(2)
|
|
197
|
+
expect(container.textContent).toContain('second')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('the confirming delta clears the optimistic styling AND the pending count', () => {
|
|
201
|
+
client = harness({ [LIST]: [] })
|
|
202
|
+
render()
|
|
203
|
+
act(() => {
|
|
204
|
+
client.setSubscription(LIST, [note({ id: 'n_tmp', title: 'mine', optimistic: true })], { pendingPatches: 1 })
|
|
205
|
+
})
|
|
206
|
+
expect(container.querySelector('li')!.style.opacity).toBe('0.5')
|
|
207
|
+
expect(container.textContent).toContain('1 pending')
|
|
208
|
+
|
|
209
|
+
// The server confirms: the real row replaces the optimistic one.
|
|
210
|
+
act(() => { client.setSubscription(LIST, [note({ id: 'n_1', title: 'mine' })]) })
|
|
211
|
+
|
|
212
|
+
expect(container.querySelector('li')!.style.opacity).toBe('1')
|
|
213
|
+
expect(container.textContent).not.toContain('1 pending')
|
|
166
214
|
})
|
|
167
215
|
})
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// `/` — the notes page. Auto-mapped from src/pages/index.tsx by the
|
|
2
2
|
// framework's file-convention page discovery.
|
|
3
3
|
//
|
|
4
|
-
// THIS is the reactive end-to-end loop. `useSubscription('
|
|
5
|
-
//
|
|
6
|
-
// `
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// THIS is the reactive end-to-end loop. `useSubscription('notes.list')`
|
|
5
|
+
// opens a live WebSocket subscription to the api's `notes.list` query;
|
|
6
|
+
// `useMutation('notes.create')` invokes the `notes.create` mutation. When the
|
|
7
|
+
// mutation commits, the server pushes a delta and this list re-renders — no
|
|
8
|
+
// refetch, no polling.
|
|
9
|
+
//
|
|
10
|
+
// Both come from `src/lib/api.ts`, which binds them ONCE to this api's
|
|
11
|
+
// generated procedure map — so the tag autocompletes, a typo is a compile
|
|
12
|
+
// error, and the row type is inferred rather than declared here.
|
|
9
13
|
//
|
|
10
14
|
// Auto-optimistic: `notes.create` declares `target: { table: 'notes', op:
|
|
11
15
|
// 'insert' }` server-side, so the framework prepends an optimistic row to
|
|
@@ -13,11 +17,11 @@
|
|
|
13
17
|
// row (or reverts it) when the rpc resolves. There is no `.withOptimistic`,
|
|
14
18
|
// no `useOptimistic`, no `startTransition` — just `create.mutate(input)`.
|
|
15
19
|
|
|
16
|
-
import { useMutation, useSubscription } from '@voltro/client'
|
|
17
20
|
import { useState, type FormEvent, type ReactNode } from 'react'
|
|
18
21
|
import type { PageMeta } from '@voltro/web'
|
|
19
22
|
import { T, useTFn } from '@voltro/i18n'
|
|
20
23
|
import { getCatalog } from '../locales'
|
|
24
|
+
import { useMutation, useSubscription } from '../lib/api'
|
|
21
25
|
|
|
22
26
|
// Locale-aware tab title: @voltro/web resolves meta({ locale }) from the
|
|
23
27
|
// active locale (the `voltro:locale` cookie in this cookie-i18n app).
|
|
@@ -31,21 +35,13 @@ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
|
31
35
|
// submitted tenantId matches the caller's (a typed TenantMismatch otherwise).
|
|
32
36
|
const TENANT = 'acme'
|
|
33
37
|
|
|
34
|
-
interface Note {
|
|
35
|
-
readonly id: string
|
|
36
|
-
readonly title: string
|
|
37
|
-
readonly body: string
|
|
38
|
-
readonly done: boolean
|
|
39
|
-
readonly tenantId: string
|
|
40
|
-
readonly createdAt: Date
|
|
41
|
-
readonly optimistic?: boolean
|
|
42
|
-
}
|
|
43
|
-
|
|
44
38
|
const IndexPage = (): ReactNode => {
|
|
39
|
+
// No row `interface` and no `<T>`: the shape comes from `notes.list`'s own
|
|
40
|
+
// output schema, including the client's `optimistic` marker.
|
|
45
41
|
const { data, revision, emittedAt, error, pendingPatches } =
|
|
46
|
-
useSubscription
|
|
42
|
+
useSubscription('notes.list')
|
|
47
43
|
|
|
48
|
-
const create = useMutation
|
|
44
|
+
const create = useMutation('notes.create')
|
|
49
45
|
const t = useTFn()
|
|
50
46
|
|
|
51
47
|
const notes = data ?? []
|
|
@@ -12,21 +12,23 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.34.0",
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/client": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/i18n": "0.34.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
21
|
+
"@voltro/web": "0.34.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.34.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"jsdom": "^30.0.0",
|
|
29
30
|
"typescript": "^6.0.3",
|
|
31
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
30
32
|
"vitest": "^4.1.10"
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -12,20 +12,22 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.34.0",
|
|
16
|
+
"@voltro/client": "0.34.0",
|
|
17
|
+
"@voltro/env": "0.34.0",
|
|
18
|
+
"@voltro/i18n": "0.34.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
20
|
+
"@voltro/web": "0.34.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.34.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"jsdom": "^30.0.0",
|
|
28
29
|
"typescript": "^6.0.3",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
29
31
|
"vitest": "^4.1.10"
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -12,22 +12,24 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/cms": "0.
|
|
19
|
-
"@voltro/env": "0.
|
|
20
|
-
"@voltro/i18n": "0.
|
|
21
|
-
"@voltro/ui-shadcn": "0.
|
|
22
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.34.0",
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/client": "0.34.0",
|
|
18
|
+
"@voltro/cms": "0.34.0",
|
|
19
|
+
"@voltro/env": "0.34.0",
|
|
20
|
+
"@voltro/i18n": "0.34.0",
|
|
21
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
22
|
+
"@voltro/web": "0.34.0",
|
|
23
23
|
"react": "^19.0.0",
|
|
24
24
|
"react-dom": "^19.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/devtools": "0.
|
|
27
|
+
"@voltro/devtools": "0.34.0",
|
|
28
28
|
"@types/react": "^19.0.0",
|
|
29
29
|
"@types/react-dom": "^19.0.0",
|
|
30
|
+
"jsdom": "^30.0.0",
|
|
30
31
|
"typescript": "^6.0.3",
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
31
33
|
"vitest": "^4.1.10"
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -12,22 +12,25 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/local-first": "0.
|
|
21
|
-
"@voltro/ui-shadcn": "0.
|
|
22
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.34.0",
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/client": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/i18n": "0.34.0",
|
|
20
|
+
"@voltro/local-first": "0.34.0",
|
|
21
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
22
|
+
"@voltro/web": "0.34.0",
|
|
23
23
|
"react": "^19.0.0",
|
|
24
24
|
"react-dom": "^19.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/devtools": "0.
|
|
27
|
+
"@voltro/devtools": "0.34.0",
|
|
28
|
+
"@voltro/testing": "0.34.0",
|
|
28
29
|
"@types/react": "^19.0.0",
|
|
29
30
|
"@types/react-dom": "^19.0.0",
|
|
31
|
+
"jsdom": "^30.0.0",
|
|
30
32
|
"typescript": "^6.0.3",
|
|
33
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
31
34
|
"vitest": "^4.1.10"
|
|
32
35
|
}
|
|
33
36
|
}
|