@voltro/cli 0.28.0 → 0.30.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 +655 -0
- package/THIRD-PARTY-NOTICES.md +90 -4
- package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
- package/dist/apiBuild-CxvR1Dwt.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-BMr-X48w.js +2427 -0
- package/dist/checkCommand-BOk9y264.js +2 -0
- package/dist/cliRuntime-Oh517vCV.js +96 -0
- package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
- package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
- package/dist/dbCommand-D8nhyppR.js +2 -0
- package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
- package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
- package/dist/fileConventions-Cof68_BL.js +33 -0
- package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
- package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
- package/dist/inspect-mmBuRXmy.js +2 -0
- package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
- package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
- package/dist/manifestBuild-Dj8Jjoto.js +2 -0
- package/dist/sdkgen-CqpOq_fy.js +582 -0
- package/dist/serveCommand-Ciq_wIY8.js +1577 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +50 -0
- package/templates/AGENTS.md +54 -2
- package/templates/agent-docs/_index.md +4 -2
- package/templates/agent-docs/_manifest.json +26 -5
- package/templates/agent-docs/ai.md +191 -0
- package/templates/agent-docs/cli.md +143 -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/internationalization.md +169 -8
- package/templates/agent-docs/local-first-mobile.md +426 -0
- package/templates/agent-docs/routing.md +94 -3
- package/templates/agent-docs/schema-driven-ui.md +12 -0
- package/templates/agent-docs/security.md +17 -0
- package/templates/agent-docs/templates/apibackends.md +210 -1
- package/templates/agent-docs/templates/appshells.md +144 -1
- package/templates/agent-docs/templates/mobile.md +60 -0
- package/templates/agent-docs/templates/overview.md +17 -7
- package/templates/agent-docs/whats-new.md +238 -158
- package/templates/agent-docs/workflows.md +376 -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-collab/README.md +70 -0
- package/templates/apps/api-collab/app.config.ts +28 -0
- package/templates/apps/api-collab/database/schema.ts +63 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
- package/templates/apps/api-collab/package.json +30 -0
- package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
- package/templates/apps/api-collab/queries/documents.query.ts +23 -0
- package/templates/apps/api-collab/template.json +6 -0
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
- package/templates/apps/api-collab/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/app.config.ts +1 -1
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/app.config.ts +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
- 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/app.config.ts +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
- 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-collab/README.md +80 -0
- package/templates/apps/frontend-collab/app.config.ts +38 -0
- package/templates/apps/frontend-collab/package.json +33 -0
- package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
- package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
- package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
- package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
- package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
- package/templates/apps/frontend-collab/template.json +6 -0
- package/templates/apps/frontend-collab/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/app.config.ts +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
- 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/app.config.ts +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr/app.config.ts +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
- 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/apps/mobile-app/README.md +58 -0
- package/templates/apps/mobile-app/app.config.ts +33 -0
- package/templates/apps/mobile-app/babel.config.js +5 -0
- package/templates/apps/mobile-app/metro.config.js +20 -0
- package/templates/apps/mobile-app/package.json +33 -0
- package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
- package/templates/apps/mobile-app/src/app/index.tsx +62 -0
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
- package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
- package/templates/apps/mobile-app/src/client.ts +74 -0
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
- package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
- package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
- package/templates/apps/mobile-app/src/persistence.ts +38 -0
- package/templates/apps/mobile-app/template.json +6 -0
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
- package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
- package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
- package/templates/apps/mobile-app/tsconfig.json +10 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
- package/templates/baselines/compose/docker-compose.prod.yml +15 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
- package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
- package/dist/apiBuild-DgBS9ayv.js +0 -2
- package/dist/dbCommand-DvguqlzF.js +0 -2
- 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-ZTn-dPFa.js +0 -1425
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Home screen — the reactive end-to-end loop, on a phone.
|
|
2
|
+
//
|
|
3
|
+
// The SAME hooks as the web frontend: `useSubscription('app', 'notes.list')`
|
|
4
|
+
// streams the server-side query; `useMutation('app', 'notes.create')` writes it.
|
|
5
|
+
// When the mutation lands, the server pushes a delta and this list re-renders —
|
|
6
|
+
// no refetch, no polling. (Requires the M0 client boot; see ../client.ts.)
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import { Link } from 'expo-router'
|
|
9
|
+
import { FlatList, Pressable, Text, TextInput, View } from 'react-native'
|
|
10
|
+
import { useMutation, useSubscription } from '@voltro/client'
|
|
11
|
+
|
|
12
|
+
interface Note {
|
|
13
|
+
readonly id: string
|
|
14
|
+
readonly title: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Home() {
|
|
18
|
+
const notes = useSubscription<ReadonlyArray<Note>>('app', 'notes.list')
|
|
19
|
+
const createNote = useMutation<{ title: string }, Note>('app', 'notes.create')
|
|
20
|
+
const [title, setTitle] = useState('')
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={{ flex: 1, padding: 16, gap: 12 }}>
|
|
24
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
25
|
+
<TextInput
|
|
26
|
+
value={title}
|
|
27
|
+
onChangeText={setTitle}
|
|
28
|
+
placeholder="New note…"
|
|
29
|
+
style={{ flex: 1, borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 10 }}
|
|
30
|
+
/>
|
|
31
|
+
<Pressable
|
|
32
|
+
onPress={() => {
|
|
33
|
+
if (title.trim().length === 0) return
|
|
34
|
+
// Auto-optimistic: the list updates instantly, reconciles on ack.
|
|
35
|
+
createNote.mutate({ title: title.trim() })
|
|
36
|
+
setTitle('')
|
|
37
|
+
}}
|
|
38
|
+
style={{ backgroundColor: '#2563eb', borderRadius: 8, paddingHorizontal: 16, justifyContent: 'center' }}
|
|
39
|
+
>
|
|
40
|
+
<Text style={{ color: 'white', fontWeight: '600' }}>Add</Text>
|
|
41
|
+
</Pressable>
|
|
42
|
+
</View>
|
|
43
|
+
|
|
44
|
+
<FlatList
|
|
45
|
+
data={notes.data ?? []}
|
|
46
|
+
keyExtractor={(n) => n.id}
|
|
47
|
+
ListEmptyComponent={<Text style={{ color: '#666' }}>No notes yet — add one above.</Text>}
|
|
48
|
+
renderItem={({ item }) => (
|
|
49
|
+
<Link href={`/orders/${item.id}` as never} asChild>
|
|
50
|
+
<Pressable style={{ paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#eee' }}>
|
|
51
|
+
<Text style={{ fontSize: 16 }}>{item.title}</Text>
|
|
52
|
+
</Pressable>
|
|
53
|
+
</Link>
|
|
54
|
+
)}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<Link href="/settings" style={{ color: '#2563eb', textAlign: 'center', paddingTop: 8 }}>
|
|
58
|
+
Settings & sync status
|
|
59
|
+
</Link>
|
|
60
|
+
</View>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A param screen — the target of the `/orders/:id` deep link (see
|
|
2
|
+
// ../../lib/deeplinks.ts). Tapping a note, a universal link, or a push whose
|
|
3
|
+
// payload carried `data.deepLink: '/orders/<id>'` all land here through the one
|
|
4
|
+
// typed link table.
|
|
5
|
+
import { useLocalSearchParams } from 'expo-router'
|
|
6
|
+
import { Text, View } from 'react-native'
|
|
7
|
+
|
|
8
|
+
export default function OrderDetail() {
|
|
9
|
+
const { id } = useLocalSearchParams<{ id: string }>()
|
|
10
|
+
return (
|
|
11
|
+
<View style={{ flex: 1, padding: 16, gap: 8 }}>
|
|
12
|
+
<Text style={{ fontSize: 20, fontWeight: '700' }}>Order {id}</Text>
|
|
13
|
+
<Text style={{ color: '#666' }}>
|
|
14
|
+
Reached via the typed deep-link table. Fetch this order with
|
|
15
|
+
{' '}useSubscription('app', 'orders.get', {'{'} id {'}'}).
|
|
16
|
+
</Text>
|
|
17
|
+
</View>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Settings — sync status (first-class on mobile) + device registration for push.
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { Pressable, Text, View } from 'react-native'
|
|
4
|
+
import { useMobileConnectionStatus } from '@voltro/react-native'
|
|
5
|
+
import { buildDeviceRegistration } from '../lib/notifications'
|
|
6
|
+
|
|
7
|
+
export default function Settings() {
|
|
8
|
+
const { status, reportSuccess, reportFailure } = useMobileConnectionStatus()
|
|
9
|
+
const [registered, setRegistered] = useState<string | null>(null)
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<View style={{ flex: 1, padding: 16, gap: 16 }}>
|
|
13
|
+
<View>
|
|
14
|
+
<Text style={{ fontSize: 18, fontWeight: '700' }}>Sync</Text>
|
|
15
|
+
<Text style={{ color: '#666' }}>Connection: {status}</Text>
|
|
16
|
+
<View style={{ flexDirection: 'row', gap: 8, marginTop: 8 }}>
|
|
17
|
+
{/* In a real app these are driven by NetInfo + the client's transport
|
|
18
|
+
errors (see plans/open/mobile/03). Buttons here make the seam visible. */}
|
|
19
|
+
<Pressable onPress={reportSuccess} style={btn}>
|
|
20
|
+
<Text style={btnText}>Mark reachable</Text>
|
|
21
|
+
</Pressable>
|
|
22
|
+
<Pressable onPress={reportFailure} style={btn}>
|
|
23
|
+
<Text style={btnText}>Mark unreachable</Text>
|
|
24
|
+
</Pressable>
|
|
25
|
+
</View>
|
|
26
|
+
</View>
|
|
27
|
+
|
|
28
|
+
<View>
|
|
29
|
+
<Text style={{ fontSize: 18, fontWeight: '700' }}>Push</Text>
|
|
30
|
+
<Text style={{ color: '#666' }}>
|
|
31
|
+
{registered ? `Registered token ${registered.slice(0, 12)}…` : 'Not registered'}
|
|
32
|
+
</Text>
|
|
33
|
+
<Pressable
|
|
34
|
+
onPress={() => {
|
|
35
|
+
// In a real app the token comes from expo-notifications'
|
|
36
|
+
// getExpoPushTokenAsync(); the SENDER is server-side + creds-gated
|
|
37
|
+
// (see plans/open/mobile/02). This builds the _voltro_devices row.
|
|
38
|
+
const row = buildDeviceRegistration('ExponentPushToken[demo]', 'ios')
|
|
39
|
+
setRegistered(row.deviceToken)
|
|
40
|
+
}}
|
|
41
|
+
style={[btn, { marginTop: 8 }]}
|
|
42
|
+
>
|
|
43
|
+
<Text style={btnText}>Register this device</Text>
|
|
44
|
+
</Pressable>
|
|
45
|
+
</View>
|
|
46
|
+
</View>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const btn = { backgroundColor: '#2563eb', borderRadius: 8, paddingHorizontal: 14, paddingVertical: 10 } as const
|
|
51
|
+
const btnText = { color: 'white', fontWeight: '600' } as const
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The Voltro client for React Native.
|
|
2
|
+
//
|
|
3
|
+
// The transport keystone now EXISTS: `@voltro/client`'s `buildApiRuntime` builds
|
|
4
|
+
// the runtime + subscription cache + rpc client over a WebSocket, and it takes
|
|
5
|
+
// the WebSocket constructor as an INJECTED dependency — so React Native passes
|
|
6
|
+
// its own `globalThis.WebSocket` (which RN provides) and gets the SAME client
|
|
7
|
+
// stack the web app uses, without pulling in `@voltro/web`. See
|
|
8
|
+
// plans/open/mobile/00-overview.md (M0).
|
|
9
|
+
//
|
|
10
|
+
// ┌─ THE ONE REMAINING SEAM ─────────────────────────────────────────────────┐
|
|
11
|
+
// │ `buildApiHandle` below is functional. What it still needs is the api's rpc │
|
|
12
|
+
// │ GROUP + descriptor map — on web these come from codegen's generated entry. │
|
|
13
|
+
// │ A `voltro codegen` mobile target that emits them for this app is the next │
|
|
14
|
+
// │ M-step (plans/open/mobile/01). Until it lands, `buildApiHandles()` returns │
|
|
15
|
+
// │ an empty map (the screens show their loading state) rather than faking it. │
|
|
16
|
+
// └──────────────────────────────────────────────────────────────────────────┘
|
|
17
|
+
|
|
18
|
+
import { buildApiRuntime, type ApiHandle, type BuildApiRuntimeOptions } from '@voltro/client'
|
|
19
|
+
import { offlineFirstDefaults } from '@voltro/react-native'
|
|
20
|
+
|
|
21
|
+
/** The api's rpc group type — sourced from @voltro/client so the template needs
|
|
22
|
+
* no direct @effect/rpc dependency. Codegen produces the concrete value. */
|
|
23
|
+
type ApiRpcGroup = BuildApiRuntimeOptions['group']
|
|
24
|
+
|
|
25
|
+
/** The api's base URL. On a simulator/device this must be your machine's LAN
|
|
26
|
+
* address (not `localhost`), e.g. `http://192.168.1.20:4000`. */
|
|
27
|
+
export const API_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:4000'
|
|
28
|
+
|
|
29
|
+
/** The offline-first posture spread into the app's client config. */
|
|
30
|
+
export const clientPosture = offlineFirstDefaults
|
|
31
|
+
|
|
32
|
+
/** http(s)://host:port → ws(s)://host:port/ws (the api's WebSocket endpoint). */
|
|
33
|
+
const toWsUrl = (httpUrl: string): string =>
|
|
34
|
+
`${httpUrl.replace(/^http/, 'ws').replace(/\/+$/, '')}/ws`
|
|
35
|
+
|
|
36
|
+
/** React Native provides a global WebSocket; no tracking/reconnect wrapper here
|
|
37
|
+
* yet (the RN reconnect supervisor is a later M-step — plans/open/mobile/03). */
|
|
38
|
+
const rnWebSocket = (url: string, protocols?: string | ReadonlyArray<string>): globalThis.WebSocket =>
|
|
39
|
+
new globalThis.WebSocket(url, protocols as string | string[] | undefined)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build one api's {@link ApiHandle} for the RN client provider. Fully functional
|
|
43
|
+
* — hand it the codegen-produced `group` + `descriptors` for the api and it
|
|
44
|
+
* returns a live handle. `descriptors` is `ClientDescriptor`-shaped
|
|
45
|
+
* (see @voltro/client); pass the generated map.
|
|
46
|
+
*/
|
|
47
|
+
export const buildApiHandle = async (
|
|
48
|
+
name: string,
|
|
49
|
+
group: ApiRpcGroup,
|
|
50
|
+
descriptors: ApiHandle['descriptors'],
|
|
51
|
+
): Promise<ApiHandle> => {
|
|
52
|
+
const wsUrl = toWsUrl(API_URL)
|
|
53
|
+
const built = await buildApiRuntime({ name, wsUrl, group, webSocketConstructor: rnWebSocket })
|
|
54
|
+
return {
|
|
55
|
+
runtime: built.runtime,
|
|
56
|
+
cache: built.cache,
|
|
57
|
+
client: built.client as ApiHandle['client'],
|
|
58
|
+
descriptors,
|
|
59
|
+
inspectBaseUrl: API_URL.replace(/\/+$/, ''),
|
|
60
|
+
errorBus: built.errorBus,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the `apis` map `FrameworkRuntimesProvider` needs. Returns an empty map
|
|
66
|
+
* until codegen emits this app's rpc group (the seam above) — then map each
|
|
67
|
+
* configured api through {@link buildApiHandle}.
|
|
68
|
+
*/
|
|
69
|
+
export const buildApiHandles = async (): Promise<ReadonlyMap<string, ApiHandle>> => {
|
|
70
|
+
// Once codegen targets mobile:
|
|
71
|
+
// const { rpcGroup, descriptors } = await import('./rpcGroup.generated')
|
|
72
|
+
// return new Map([['app', await buildApiHandle('app', rpcGroup, descriptors)]])
|
|
73
|
+
return new Map<string, ApiHandle>()
|
|
74
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The app's deep-link table + a pure resolver.
|
|
2
|
+
//
|
|
3
|
+
// A deep link maps an inbound URL — a universal link (`https://…/orders/42`), a
|
|
4
|
+
// custom-scheme URL (`{{appName}}://orders/42`), or a push payload's
|
|
5
|
+
// `data.deepLink` — to an in-app navigation. `defineDeepLink` + the pure matcher
|
|
6
|
+
// come from `@voltro/react-native`; this module is browser/RN-safe (no Metro, no
|
|
7
|
+
// screens), which is exactly why it is unit-testable without a simulator.
|
|
8
|
+
//
|
|
9
|
+
// Declaration order wins in `matchFirstDeepLink`, so list more-specific patterns
|
|
10
|
+
// FIRST. The `navigate` callback is injected (the screen layer passes
|
|
11
|
+
// expo-router's `router.push`), keeping this table pure + testable.
|
|
12
|
+
import {
|
|
13
|
+
defineDeepLink,
|
|
14
|
+
matchFirstDeepLink,
|
|
15
|
+
type DeepLinkDescriptor,
|
|
16
|
+
} from '@voltro/react-native'
|
|
17
|
+
|
|
18
|
+
/** Build the app's deep links against a navigate function (expo-router's `push`). */
|
|
19
|
+
export const makeDeepLinks = (
|
|
20
|
+
navigate: (href: string) => void,
|
|
21
|
+
): ReadonlyArray<DeepLinkDescriptor> => [
|
|
22
|
+
defineDeepLink({ pattern: '/orders/:id', handler: ({ id }) => navigate(`/orders/${id}`) }),
|
|
23
|
+
defineDeepLink({ pattern: '/settings', handler: () => navigate('/settings') }),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Route an inbound URL through the link table. Returns `true` when a link
|
|
28
|
+
* matched (its handler ran → navigation happened), `false` when nothing did so
|
|
29
|
+
* the caller can fall back to a default route.
|
|
30
|
+
*/
|
|
31
|
+
export const handleDeepLink = (
|
|
32
|
+
links: ReadonlyArray<DeepLinkDescriptor>,
|
|
33
|
+
url: string,
|
|
34
|
+
): boolean => {
|
|
35
|
+
const hit = matchFirstDeepLink(links, url)
|
|
36
|
+
if (hit === null) return false
|
|
37
|
+
hit.descriptor.handler(hit.params)
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Push device registration — the CREDENTIAL-FREE half.
|
|
2
|
+
//
|
|
3
|
+
// After the OS issues a push token (via `expo-notifications`), the app upserts a
|
|
4
|
+
// `_voltro_devices` row so the server can later fan a notification out to it. The
|
|
5
|
+
// SENDER (APNs/FCM/Expo push) is server-side and creds-gated — see
|
|
6
|
+
// plans/open/mobile/02-push-pipeline.md. This module only builds the row, purely,
|
|
7
|
+
// so it is testable without a device.
|
|
8
|
+
//
|
|
9
|
+
// `userId` / `tenantId` are deliberately NOT set here: the server stamps them
|
|
10
|
+
// from the authenticated request (never trusted from the client).
|
|
11
|
+
import {
|
|
12
|
+
resolveDeviceRegistration,
|
|
13
|
+
type DeviceRegistration,
|
|
14
|
+
type DevicePlatform,
|
|
15
|
+
type DeviceRegistrationEnv,
|
|
16
|
+
} from '@voltro/react-native'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the registration row from an Expo push token + platform. Pure given an
|
|
20
|
+
* `env` (inject `now` / `locale` / `timezone` in tests); in a real runtime the
|
|
21
|
+
* locale/timezone defaults come from the device.
|
|
22
|
+
*/
|
|
23
|
+
export const buildDeviceRegistration = (
|
|
24
|
+
expoPushToken: string,
|
|
25
|
+
platform: DevicePlatform,
|
|
26
|
+
env?: DeviceRegistrationEnv,
|
|
27
|
+
): DeviceRegistration =>
|
|
28
|
+
resolveDeviceRegistration(
|
|
29
|
+
{ deviceToken: expoPushToken, platform, metadata: { via: 'expo' } },
|
|
30
|
+
env,
|
|
31
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Background-sync decision layer — pure, on top of `@voltro/react-native`.
|
|
2
|
+
//
|
|
3
|
+
// The framework owns the DECISION (should this trigger actually sync, given
|
|
4
|
+
// what's in flight + when we last ran); the OS wiring (AppState / interval /
|
|
5
|
+
// background-fetch) is the screen's. Keeping the decision here + pure means a
|
|
6
|
+
// screen never re-implements the gate, and the gate is unit-testable.
|
|
7
|
+
import {
|
|
8
|
+
backgroundSyncReducer,
|
|
9
|
+
initialBackgroundSyncState,
|
|
10
|
+
shouldSync,
|
|
11
|
+
type BackgroundSyncState,
|
|
12
|
+
type ShouldSyncOptions,
|
|
13
|
+
} from '@voltro/react-native'
|
|
14
|
+
|
|
15
|
+
/** Sync every 5 minutes while foregrounded (matches `offlineFirstDefaults`). */
|
|
16
|
+
export const SYNC_OPTIONS: ShouldSyncOptions = { intervalMs: 5 * 60_000, enabled: true }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Should a trigger (foreground return, interval tick, manual pull) actually kick
|
|
20
|
+
* a sync now? `force` bypasses the foreground + interval gates (a manual pull).
|
|
21
|
+
*/
|
|
22
|
+
export const planSync = (
|
|
23
|
+
state: BackgroundSyncState,
|
|
24
|
+
now: number,
|
|
25
|
+
force = false,
|
|
26
|
+
): boolean => shouldSync(state, now, SYNC_OPTIONS, force)
|
|
27
|
+
|
|
28
|
+
export { backgroundSyncReducer, initialBackgroundSyncState }
|
|
29
|
+
export type { BackgroundSyncState }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// RN persistence — a key-value store with the security split M0 needs.
|
|
2
|
+
//
|
|
3
|
+
// The web client persists its cache/outbox via `window.localStorage`, which does
|
|
4
|
+
// not exist in RN. This adapter provides the RN equivalent, with a deliberate
|
|
5
|
+
// split (see plans/open/mobile/03-offline-first-and-sync.md):
|
|
6
|
+
// - SECRETS (session tokens) → `expo-secure-store` (Keychain / Keystore).
|
|
7
|
+
// - the query cache / outbox → `@react-native-async-storage/async-storage`.
|
|
8
|
+
// Putting a session token in AsyncStorage is a security bug; secure-store is its
|
|
9
|
+
// home.
|
|
10
|
+
//
|
|
11
|
+
// This is a generic async KV. M0 adapts it to whichever persistence CONTRACT it
|
|
12
|
+
// wires (the client cache and/or `@voltro/local-first`'s `PersistenceAdapter`) —
|
|
13
|
+
// it does not itself claim to satisfy a framework interface that would drift.
|
|
14
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
15
|
+
import * as SecureStore from 'expo-secure-store'
|
|
16
|
+
|
|
17
|
+
/** Keys carrying secrets — routed to the Keychain/Keystore, never AsyncStorage. */
|
|
18
|
+
const SECRET_PREFIX = 'secret:'
|
|
19
|
+
|
|
20
|
+
const isSecret = (key: string): boolean => key.startsWith(SECRET_PREFIX)
|
|
21
|
+
|
|
22
|
+
export const kv = {
|
|
23
|
+
get: async (key: string): Promise<string | null> =>
|
|
24
|
+
isSecret(key) ? SecureStore.getItemAsync(key) : AsyncStorage.getItem(key),
|
|
25
|
+
|
|
26
|
+
set: async (key: string, value: string): Promise<void> => {
|
|
27
|
+
if (isSecret(key)) await SecureStore.setItemAsync(key, value)
|
|
28
|
+
else await AsyncStorage.setItem(key, value)
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
remove: async (key: string): Promise<void> => {
|
|
32
|
+
if (isSecret(key)) await SecureStore.deleteItemAsync(key)
|
|
33
|
+
else await AsyncStorage.removeItem(key)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Prefix a key so it lands in secure storage. */
|
|
38
|
+
export const secretKey = (name: string): string => `${SECRET_PREFIX}${name}`
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "mobile-app",
|
|
3
|
+
"kind": "mobile",
|
|
4
|
+
"summary": "An Expo (React Native) app that is the THIRD consumer of your api — same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; `voltro dev` runs the sibling api. Scaffold both: `create-project --api=api-backend --mobile`.",
|
|
5
|
+
"tags": ["mobile", "react-native", "expo", "fullstack", "reactive", "offline-first", "deep-links", "push", "api-backend"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { handleDeepLink, makeDeepLinks } from '../src/lib/deeplinks'
|
|
3
|
+
|
|
4
|
+
describe('deep links', () => {
|
|
5
|
+
it('routes an order URL to the order screen with the captured id', () => {
|
|
6
|
+
const navigate = vi.fn()
|
|
7
|
+
const links = makeDeepLinks(navigate)
|
|
8
|
+
expect(handleDeepLink(links, '/orders/42')).toBe(true)
|
|
9
|
+
expect(navigate).toHaveBeenCalledWith('/orders/42')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('matches a full custom-scheme URL (scheme + host stripped) and a universal link', () => {
|
|
13
|
+
const navigate = vi.fn()
|
|
14
|
+
const links = makeDeepLinks(navigate)
|
|
15
|
+
expect(handleDeepLink(links, '{{appName}}://orders/7?ref=push')).toBe(true)
|
|
16
|
+
expect(handleDeepLink(links, 'https://example.com/orders/7')).toBe(true)
|
|
17
|
+
expect(navigate).toHaveBeenNthCalledWith(1, '/orders/7')
|
|
18
|
+
expect(navigate).toHaveBeenNthCalledWith(2, '/orders/7')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('routes the settings link', () => {
|
|
22
|
+
const navigate = vi.fn()
|
|
23
|
+
expect(handleDeepLink(makeDeepLinks(navigate), '/settings')).toBe(true)
|
|
24
|
+
expect(navigate).toHaveBeenCalledWith('/settings')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns false and navigates nowhere for an unknown URL', () => {
|
|
28
|
+
const navigate = vi.fn()
|
|
29
|
+
expect(handleDeepLink(makeDeepLinks(navigate), '/nope/nowhere')).toBe(false)
|
|
30
|
+
expect(navigate).not.toHaveBeenCalled()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildDeviceRegistration } from '../src/lib/notifications'
|
|
3
|
+
|
|
4
|
+
describe('device registration', () => {
|
|
5
|
+
it('builds a _voltro_devices row from an Expo push token (pure with injected env)', () => {
|
|
6
|
+
const row = buildDeviceRegistration('ExponentPushToken[abc]', 'ios', {
|
|
7
|
+
locale: 'de-DE',
|
|
8
|
+
timezone: 'Europe/Berlin',
|
|
9
|
+
now: () => 1_700_000_000_000,
|
|
10
|
+
})
|
|
11
|
+
expect(row).toMatchObject({
|
|
12
|
+
deviceToken: 'ExponentPushToken[abc]',
|
|
13
|
+
platform: 'ios',
|
|
14
|
+
locale: 'de-DE',
|
|
15
|
+
timezone: 'Europe/Berlin',
|
|
16
|
+
registeredAt: 1_700_000_000_000,
|
|
17
|
+
metadata: { via: 'expo' },
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('does NOT carry userId/tenantId — the server stamps those from the request', () => {
|
|
22
|
+
const row = buildDeviceRegistration('tok', 'android', { now: () => 0 })
|
|
23
|
+
expect(row).not.toHaveProperty('userId')
|
|
24
|
+
expect(row).not.toHaveProperty('tenantId')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
backgroundSyncReducer,
|
|
4
|
+
initialBackgroundSyncState,
|
|
5
|
+
planSync,
|
|
6
|
+
} from '../src/lib/sync'
|
|
7
|
+
|
|
8
|
+
describe('background-sync decision', () => {
|
|
9
|
+
it('syncs a fresh foregrounded state', () => {
|
|
10
|
+
expect(planSync(initialBackgroundSyncState, 1_000)).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('does not sync again while a sync is already in flight', () => {
|
|
14
|
+
const syncing = backgroundSyncReducer(initialBackgroundSyncState, { type: 'syncStarted', at: 1_000 })
|
|
15
|
+
expect(planSync(syncing, 1_100)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('respects the interval gate after a recent attempt, but a forced pull bypasses it', () => {
|
|
19
|
+
let s = backgroundSyncReducer(initialBackgroundSyncState, { type: 'syncStarted', at: 1_000 })
|
|
20
|
+
s = backgroundSyncReducer(s, { type: 'syncSucceeded', at: 1_050 })
|
|
21
|
+
// 1 minute later — inside the 5-minute interval → no auto sync…
|
|
22
|
+
expect(planSync(s, 1_050 + 60_000)).toBe(false)
|
|
23
|
+
// …but a manual pull forces it.
|
|
24
|
+
expect(planSync(s, 1_050 + 60_000, true)).toBe(true)
|
|
25
|
+
// 5+ minutes later the interval gate opens.
|
|
26
|
+
expect(planSync(s, 1_050 + 6 * 60_000)).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not auto-sync while backgrounded, but a forced sync still runs', () => {
|
|
30
|
+
const bg = backgroundSyncReducer(initialBackgroundSyncState, { type: 'background' })
|
|
31
|
+
expect(planSync(bg, 10_000)).toBe(false)
|
|
32
|
+
expect(planSync(bg, 10_000, true)).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The api(s) this mobile app consumes — mirrors a web app.config.ts's `apis:`
|
|
2
|
+
// map. The NAME ('app') is the lookup key the hooks use:
|
|
3
|
+
// `useSubscription('app', 'notes.list')`, `useMutation('app', 'notes.create')`.
|
|
4
|
+
// The `package` resolves the sibling api in this project so codegen can pull its
|
|
5
|
+
// typed rpc surface (end-to-end types on a phone). Scaffold the api with
|
|
6
|
+
// `--api=api-backend`.
|
|
7
|
+
export default {
|
|
8
|
+
apis: {
|
|
9
|
+
app: { package: '@{{projectName}}/api' },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -59,6 +59,21 @@ services:
|
|
|
59
59
|
NODE_ENV: production
|
|
60
60
|
DB_URL: postgres://{{projectNameSnake}}:${POSTGRES_PASSWORD:-change-me-in-prod}@postgres:5432/{{projectNameSnake}}
|
|
61
61
|
VOLTRO_SESSION_SECRET: ${VOLTRO_SESSION_SECRET:?must be set for prod compose}
|
|
62
|
+
# Only needed once an app turns on a plugin that encrypts columns at rest
|
|
63
|
+
# (`governance` with `fieldEncryption`, or any `.encrypted()` column). It
|
|
64
|
+
# is commented out because a generated file cannot know which plugins you
|
|
65
|
+
# will enable — and left here, named, because the alternative is finding
|
|
66
|
+
# out at `onActivate` in a container:
|
|
67
|
+
#
|
|
68
|
+
# governance: fieldEncryption is enabled but secret
|
|
69
|
+
# "VOLTRO_FIELD_ENCRYPTION_KEY" did not resolve
|
|
70
|
+
#
|
|
71
|
+
# Mint it with `voltro secret generate field-encryption` — same shape as
|
|
72
|
+
# `openssl rand -hex 32`, but it is the documented path and it cannot
|
|
73
|
+
# produce the wrong length. LOSE THE KEY, LOSE THE DATA: there is no
|
|
74
|
+
# recovery for a column encrypted under a key you no longer have, so back
|
|
75
|
+
# it up wherever you keep the session secret.
|
|
76
|
+
# VOLTRO_FIELD_ENCRYPTION_KEY: ${VOLTRO_FIELD_ENCRYPTION_KEY:?must be set once fieldEncryption is on}
|
|
62
77
|
ports:
|
|
63
78
|
- "4000:4000"
|
|
64
79
|
depends_on:
|
|
@@ -83,6 +83,21 @@ services:
|
|
|
83
83
|
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-minioadmin}
|
|
84
84
|
S3_FORCE_PATH_STYLE: "1"
|
|
85
85
|
VOLTRO_SESSION_SECRET: ${VOLTRO_SESSION_SECRET:?must be set for prod compose}
|
|
86
|
+
# Only needed once an app turns on a plugin that encrypts columns at rest
|
|
87
|
+
# (`governance` with `fieldEncryption`, or any `.encrypted()` column). It
|
|
88
|
+
# is commented out because a generated file cannot know which plugins you
|
|
89
|
+
# will enable — and left here, named, because the alternative is finding
|
|
90
|
+
# out at `onActivate` in a container:
|
|
91
|
+
#
|
|
92
|
+
# governance: fieldEncryption is enabled but secret
|
|
93
|
+
# "VOLTRO_FIELD_ENCRYPTION_KEY" did not resolve
|
|
94
|
+
#
|
|
95
|
+
# Mint it with `voltro secret generate field-encryption` — same shape as
|
|
96
|
+
# `openssl rand -hex 32`, but it is the documented path and it cannot
|
|
97
|
+
# produce the wrong length. LOSE THE KEY, LOSE THE DATA: there is no
|
|
98
|
+
# recovery for a column encrypted under a key you no longer have, so back
|
|
99
|
+
# it up wherever you keep the session secret.
|
|
100
|
+
# VOLTRO_FIELD_ENCRYPTION_KEY: ${VOLTRO_FIELD_ENCRYPTION_KEY:?must be set once fieldEncryption is on}
|
|
86
101
|
ports:
|
|
87
102
|
- "4000:4000"
|
|
88
103
|
depends_on:
|
|
@@ -10,8 +10,8 @@ index 77ebad1d2e940b73a494bf9ab0ad7e008daddf82..5e268da7b7923f454b7293eff2ee7932
|
|
|
10
10
|
+ const envelopeToRow = (envelope, message_id, deliver_at_in) => {
|
|
11
11
|
+ // Cast deliver_at (millisecond epoch) to BigInt so mssql's tedious
|
|
12
12
|
+ // driver binds it as BIGINT instead of inferring INT (which overflows
|
|
13
|
-
+ // for any post-2001 timestamp).
|
|
14
|
-
+ const deliver_at = deliver_at_in == null ? null : BigInt(deliver_at_in);
|
|
13
|
+
+ // for any post-2001 timestamp). ONLY mssql — sqlite runs safeIntegers(true) and hangs on a bigint.
|
|
14
|
+
+ const deliver_at = deliver_at_in == null ? null : sql.onDialectOrElse({ mssql: () => BigInt(deliver_at_in), orElse: () => deliver_at_in });
|
|
15
15
|
switch (envelope._tag) {
|
|
16
16
|
case "Request":
|
|
17
17
|
return {
|
|
@@ -96,8 +96,8 @@ index e41d91033992bb890bebb1a16a5ac9e6657a9c41..5ac3181298a020b4e748fc6433ec6f4a
|
|
|
96
96
|
+ const envelopeToRow = (envelope, message_id, deliver_at_in) => {
|
|
97
97
|
+ // Cast deliver_at (millisecond epoch) to BigInt so mssql's tedious
|
|
98
98
|
+ // driver binds it as BIGINT instead of inferring INT (which overflows
|
|
99
|
-
+ // for any post-2001 timestamp).
|
|
100
|
-
+ const deliver_at = deliver_at_in == null ? null : BigInt(deliver_at_in);
|
|
99
|
+
+ // for any post-2001 timestamp). ONLY mssql — sqlite runs safeIntegers(true) and hangs on a bigint.
|
|
100
|
+
+ const deliver_at = deliver_at_in == null ? null : sql.onDialectOrElse({ mssql: () => BigInt(deliver_at_in), orElse: () => deliver_at_in });
|
|
101
101
|
switch (envelope._tag) {
|
|
102
102
|
case "Request":
|
|
103
103
|
return {
|
|
@@ -183,8 +183,8 @@ index 4326c3832b425548dac135493a4a6cbec0856f63..430474fe47549170592fa32d5b0dcac6
|
|
|
183
183
|
): MessageRow => {
|
|
184
184
|
+ // Cast deliver_at (millisecond epoch) to BigInt so mssql's tedious
|
|
185
185
|
+ // driver binds it as BIGINT instead of inferring INT (which overflows
|
|
186
|
-
+ // for any post-2001 timestamp).
|
|
187
|
-
+ const deliver_at: bigint | null = deliver_at_in == null ? null : BigInt(deliver_at_in)
|
|
186
|
+
+ // for any post-2001 timestamp). ONLY mssql — sqlite runs safeIntegers(true) and hangs on a bigint.
|
|
187
|
+
+ const deliver_at: bigint | number | null = deliver_at_in == null ? null : sql.onDialectOrElse({ mssql: () => BigInt(deliver_at_in), orElse: () => deliver_at_in })
|
|
188
188
|
switch (envelope._tag) {
|
|
189
189
|
case "Request":
|
|
190
190
|
return {
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
//#region src/fileConventions.ts
|
|
2
|
-
var e = /\.(query|mutation|action|stream)\.ts$/, t = /\.(query|mutation|action|stream)\.server\.ts$/, n = /\.event\.ts$/, r = /\.workflow\.tsx$/, i = /\.workflow\.server\.tsx$/, a = /\.workflow\.tsx?$/, o = /\.trigger\.tsx?$/, s = /\.cron\.tsx?$/, c = /\.webhook\.tsx?$/, l = /\.outbox\.tsx?$/, u = /\.subscribe\.tsx?$/, d = /\.reaction\.tsx?$/, f = /\.aggregate\.tsx?$/, p = /\.startup\.tsx?$/, m = /\.seed\.ts$/, h = /\.relations\.ts$/, g = /\.entity\.ts$|\.schema\.ts$|^schema\.ts$/, _ = (e) => a.test(e) && !r.test(e) && !i.test(e), v = /(?:^|\/)page\.tsx$/, y = /\.island\.tsx?$/, b = /\.component\.tsx$/, x = /\.component\.ui\.tsx$/, S = /\.hook\.tsx?$/, C = /\.internal\.tsx?$/, w = /\.types\.tsx?$/, T = /\.fixture\.tsx?$/, E = /\.tracking\.tsx?$/, D = /\.store\.tsx?$/, O = /\.client\.tsx?$/, k = /\.(test|spec)\.tsx?$/, A = [
|
|
3
|
-
e,
|
|
4
|
-
t,
|
|
5
|
-
a,
|
|
6
|
-
i,
|
|
7
|
-
o,
|
|
8
|
-
s,
|
|
9
|
-
c,
|
|
10
|
-
l,
|
|
11
|
-
u,
|
|
12
|
-
d,
|
|
13
|
-
f,
|
|
14
|
-
p,
|
|
15
|
-
m,
|
|
16
|
-
h,
|
|
17
|
-
g,
|
|
18
|
-
/\.route\.tsx?$/,
|
|
19
|
-
n,
|
|
20
|
-
D,
|
|
21
|
-
O,
|
|
22
|
-
/\.(agent|tool|email|connection|serverless|server|config|generated)\.tsx?$/
|
|
23
|
-
], j = (e) => A.some((t) => t.test(e)), M = /* @__PURE__ */ new Set([
|
|
24
|
-
"layout.tsx",
|
|
25
|
-
"error.tsx",
|
|
26
|
-
"loading.tsx",
|
|
27
|
-
"not-found.tsx"
|
|
28
|
-
]);
|
|
29
|
-
//#endregion
|
|
30
|
-
export { j as A, E as C, r as D, c as E, a as O, k as S, w as T, g as _, n as a, D as b, C as c, v as d, M as f, h as g, d as h, s as i, _ as j, i as k, y as l, t as m, b as n, T as o, e as p, x as r, S as s, f as t, l as u, m as v, o as w, u as x, p as y };
|
package/dist/inspect-gt8bq-Tz.js
DELETED