@voltro/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (626) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +57 -0
  3. package/README.md +26 -0
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +20236 -0
  6. package/bin/voltro.mjs +43 -0
  7. package/dist/bin.d.ts +1 -0
  8. package/dist/bin.js +9 -0
  9. package/dist/commands-CfPH2Wf4.js +18061 -0
  10. package/dist/frameworkInspectState-CX2250XB.js +86 -0
  11. package/dist/index.d.ts +48 -0
  12. package/dist/index.js +5 -0
  13. package/dist/inspectState.d.ts +116 -0
  14. package/dist/inspectState.js +2 -0
  15. package/dist/startup.d.ts +26 -0
  16. package/dist/startup.js +2 -0
  17. package/dist/startupRunner-CRhuUl91.js +71 -0
  18. package/package.json +88 -0
  19. package/templates/AGENTS.core.md +258 -0
  20. package/templates/AGENTS.md +351 -0
  21. package/templates/agent-docs/_index.md +93 -0
  22. package/templates/agent-docs/_manifest.json +655 -0
  23. package/templates/agent-docs/ai.md +1845 -0
  24. package/templates/agent-docs/authentication.md +1788 -0
  25. package/templates/agent-docs/caching.md +624 -0
  26. package/templates/agent-docs/cli.md +1650 -0
  27. package/templates/agent-docs/configuration.md +295 -0
  28. package/templates/agent-docs/data.md +2172 -0
  29. package/templates/agent-docs/database/advancedqueries.md +1583 -0
  30. package/templates/agent-docs/database/columntypes.md +1200 -0
  31. package/templates/agent-docs/database/hosting.md +881 -0
  32. package/templates/agent-docs/database/migrations.md +2938 -0
  33. package/templates/agent-docs/database/misc.md +270 -0
  34. package/templates/agent-docs/database/overview.md +108 -0
  35. package/templates/agent-docs/database/querying.md +1622 -0
  36. package/templates/agent-docs/database/scaling.md +331 -0
  37. package/templates/agent-docs/database/schema.md +1458 -0
  38. package/templates/agent-docs/database/seedsdialects.md +1235 -0
  39. package/templates/agent-docs/database/transactions.md +285 -0
  40. package/templates/agent-docs/deployment.md +999 -0
  41. package/templates/agent-docs/internationalization.md +359 -0
  42. package/templates/agent-docs/introduction.md +438 -0
  43. package/templates/agent-docs/multi-tenancy.md +610 -0
  44. package/templates/agent-docs/observability.md +350 -0
  45. package/templates/agent-docs/plugins.md +1273 -0
  46. package/templates/agent-docs/reference.md +978 -0
  47. package/templates/agent-docs/routing.md +1553 -0
  48. package/templates/agent-docs/scheduling.md +666 -0
  49. package/templates/agent-docs/schema-driven-ui.md +607 -0
  50. package/templates/agent-docs/security.md +42 -0
  51. package/templates/agent-docs/templates/apibackends.md +3214 -0
  52. package/templates/agent-docs/templates/appshells.md +2062 -0
  53. package/templates/agent-docs/templates/custom.md +128 -0
  54. package/templates/agent-docs/templates/overview.md +122 -0
  55. package/templates/agent-docs/templates/serverless.md +315 -0
  56. package/templates/agent-docs/testing.md +376 -0
  57. package/templates/agent-docs/workflows.md +1351 -0
  58. package/templates/apps/api-ai/README.md +105 -0
  59. package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
  60. package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
  61. package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
  62. package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
  63. package/templates/apps/api-ai/app.config.ts +57 -0
  64. package/templates/apps/api-ai/database/schema.ts +67 -0
  65. package/templates/apps/api-ai/package.json +27 -0
  66. package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
  67. package/templates/apps/api-ai/template.json +6 -0
  68. package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
  69. package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
  70. package/templates/apps/api-ai/tsconfig.json +5 -0
  71. package/templates/apps/api-auth/.env +17 -0
  72. package/templates/apps/api-auth/README.md +109 -0
  73. package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
  74. package/templates/apps/api-auth/actions/me.action.ts +19 -0
  75. package/templates/apps/api-auth/app.config.ts +59 -0
  76. package/templates/apps/api-auth/database/schema.ts +26 -0
  77. package/templates/apps/api-auth/package.json +28 -0
  78. package/templates/apps/api-auth/template.json +6 -0
  79. package/templates/apps/api-auth/tests/me.test.ts +37 -0
  80. package/templates/apps/api-auth/tsconfig.json +5 -0
  81. package/templates/apps/api-backend/README.md +25 -0
  82. package/templates/apps/api-backend/app.config.ts +39 -0
  83. package/templates/apps/api-backend/database/schema.ts +57 -0
  84. package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
  85. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
  86. package/templates/apps/api-backend/package.json +28 -0
  87. package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
  88. package/templates/apps/api-backend/queries/notes.query.ts +20 -0
  89. package/templates/apps/api-backend/template.json +6 -0
  90. package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
  91. package/templates/apps/api-backend/tsconfig.json +5 -0
  92. package/templates/apps/api-backend-deactivation/README.md +49 -0
  93. package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
  94. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
  95. package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
  96. package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
  97. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
  98. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
  99. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
  100. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
  101. package/templates/apps/api-backend-deactivation/package.json +28 -0
  102. package/templates/apps/api-backend-deactivation/template.json +6 -0
  103. package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
  104. package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
  105. package/templates/apps/api-backend-mail/README.md +39 -0
  106. package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
  107. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
  108. package/templates/apps/api-backend-mail/app.config.ts +34 -0
  109. package/templates/apps/api-backend-mail/database/schema.ts +57 -0
  110. package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
  111. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
  112. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
  113. package/templates/apps/api-backend-mail/package.json +30 -0
  114. package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
  115. package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
  116. package/templates/apps/api-backend-mail/template.json +6 -0
  117. package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
  118. package/templates/apps/api-backend-mail/tsconfig.json +5 -0
  119. package/templates/apps/api-backend-mariadb/.env.example +35 -0
  120. package/templates/apps/api-backend-mariadb/README.md +31 -0
  121. package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
  122. package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
  123. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
  124. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
  125. package/templates/apps/api-backend-mariadb/package.json +30 -0
  126. package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
  127. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
  128. package/templates/apps/api-backend-mariadb/template.json +6 -0
  129. package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
  130. package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
  131. package/templates/apps/api-backend-storage/README.md +86 -0
  132. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
  133. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
  134. package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
  135. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
  136. package/templates/apps/api-backend-storage/app.config.ts +38 -0
  137. package/templates/apps/api-backend-storage/database/schema.ts +57 -0
  138. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
  139. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
  140. package/templates/apps/api-backend-storage/package.json +27 -0
  141. package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
  142. package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
  143. package/templates/apps/api-backend-storage/template.json +6 -0
  144. package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
  145. package/templates/apps/api-backend-storage/tsconfig.json +5 -0
  146. package/templates/apps/api-data-advanced/.env +17 -0
  147. package/templates/apps/api-data-advanced/README.md +88 -0
  148. package/templates/apps/api-data-advanced/app.config.ts +56 -0
  149. package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
  150. package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
  151. package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
  152. package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
  153. package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
  154. package/templates/apps/api-data-advanced/database/index.ts +23 -0
  155. package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
  156. package/templates/apps/api-data-advanced/package.json +28 -0
  157. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
  158. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
  159. package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
  160. package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
  161. package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
  162. package/templates/apps/api-data-advanced/template.json +6 -0
  163. package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
  164. package/templates/apps/api-data-advanced/tsconfig.json +5 -0
  165. package/templates/apps/api-durable/README.md +81 -0
  166. package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
  167. package/templates/apps/api-durable/app.config.ts +41 -0
  168. package/templates/apps/api-durable/database/schema.ts +65 -0
  169. package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
  170. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
  171. package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
  172. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
  173. package/templates/apps/api-durable/package.json +28 -0
  174. package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
  175. package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
  176. package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
  177. package/templates/apps/api-durable/template.json +6 -0
  178. package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
  179. package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
  180. package/templates/apps/api-durable/tsconfig.json +5 -0
  181. package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
  182. package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
  183. package/templates/apps/api-feature-flags/README.md +63 -0
  184. package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
  185. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
  186. package/templates/apps/api-feature-flags/app.config.ts +41 -0
  187. package/templates/apps/api-feature-flags/database/schema.ts +37 -0
  188. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
  189. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
  190. package/templates/apps/api-feature-flags/package.json +29 -0
  191. package/templates/apps/api-feature-flags/template.json +6 -0
  192. package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
  193. package/templates/apps/api-feature-flags/tsconfig.json +5 -0
  194. package/templates/apps/api-governance/.env +4 -0
  195. package/templates/apps/api-governance/README.md +61 -0
  196. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
  197. package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
  198. package/templates/apps/api-governance/app.config.ts +35 -0
  199. package/templates/apps/api-governance/database/schema.ts +41 -0
  200. package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
  201. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
  202. package/templates/apps/api-governance/package.json +29 -0
  203. package/templates/apps/api-governance/template.json +6 -0
  204. package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
  205. package/templates/apps/api-governance/tsconfig.json +5 -0
  206. package/templates/apps/api-kv/README.md +100 -0
  207. package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
  208. package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
  209. package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
  210. package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
  211. package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
  212. package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
  213. package/templates/apps/api-kv/app.config.ts +48 -0
  214. package/templates/apps/api-kv/database/schema.ts +62 -0
  215. package/templates/apps/api-kv/package.json +28 -0
  216. package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
  217. package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
  218. package/templates/apps/api-kv/template.json +6 -0
  219. package/templates/apps/api-kv/tests/sync.test.ts +103 -0
  220. package/templates/apps/api-kv/tsconfig.json +5 -0
  221. package/templates/apps/api-moderation/README.md +46 -0
  222. package/templates/apps/api-moderation/app.config.ts +33 -0
  223. package/templates/apps/api-moderation/database/schema.ts +45 -0
  224. package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
  225. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
  226. package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
  227. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
  228. package/templates/apps/api-moderation/package.json +29 -0
  229. package/templates/apps/api-moderation/template.json +6 -0
  230. package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
  231. package/templates/apps/api-moderation/tsconfig.json +5 -0
  232. package/templates/apps/api-observability/README.md +106 -0
  233. package/templates/apps/api-observability/app.config.ts +32 -0
  234. package/templates/apps/api-observability/database/schema.ts +38 -0
  235. package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
  237. package/templates/apps/api-observability/package.json +29 -0
  238. package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
  239. package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
  240. package/templates/apps/api-observability/template.json +6 -0
  241. package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
  242. package/templates/apps/api-observability/tsconfig.json +5 -0
  243. package/templates/apps/api-ratelimit/README.md +49 -0
  244. package/templates/apps/api-ratelimit/app.config.ts +42 -0
  245. package/templates/apps/api-ratelimit/database/schema.ts +37 -0
  246. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
  247. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
  248. package/templates/apps/api-ratelimit/package.json +29 -0
  249. package/templates/apps/api-ratelimit/template.json +6 -0
  250. package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
  251. package/templates/apps/api-ratelimit/tsconfig.json +5 -0
  252. package/templates/apps/api-rbac/README.md +59 -0
  253. package/templates/apps/api-rbac/app.config.ts +48 -0
  254. package/templates/apps/api-rbac/database/schema.ts +37 -0
  255. package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
  256. package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
  257. package/templates/apps/api-rbac/package.json +29 -0
  258. package/templates/apps/api-rbac/template.json +6 -0
  259. package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
  260. package/templates/apps/api-rbac/tsconfig.json +5 -0
  261. package/templates/apps/api-rest/README.md +85 -0
  262. package/templates/apps/api-rest/app.config.ts +70 -0
  263. package/templates/apps/api-rest/database/schema.ts +52 -0
  264. package/templates/apps/api-rest/lib/product.ts +30 -0
  265. package/templates/apps/api-rest/package.json +27 -0
  266. package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
  267. package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
  268. package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
  269. package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
  270. package/templates/apps/api-rest/template.json +6 -0
  271. package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
  272. package/templates/apps/api-rest/tsconfig.json +5 -0
  273. package/templates/apps/api-saas/README.md +106 -0
  274. package/templates/apps/api-saas/app.config.ts +51 -0
  275. package/templates/apps/api-saas/database/schema.ts +50 -0
  276. package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
  277. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
  278. package/templates/apps/api-saas/package.json +31 -0
  279. package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
  280. package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
  281. package/templates/apps/api-saas/template.json +6 -0
  282. package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
  283. package/templates/apps/api-saas/tsconfig.json +5 -0
  284. package/templates/apps/api-search/README.md +68 -0
  285. package/templates/apps/api-search/app.config.ts +31 -0
  286. package/templates/apps/api-search/database/schema.ts +47 -0
  287. package/templates/apps/api-search/lib/search.ts +27 -0
  288. package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
  289. package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
  290. package/templates/apps/api-search/package.json +29 -0
  291. package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
  292. package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
  293. package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
  294. package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
  295. package/templates/apps/api-search/template.json +6 -0
  296. package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
  297. package/templates/apps/api-search/tsconfig.json +5 -0
  298. package/templates/apps/api-versioning/README.md +51 -0
  299. package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
  300. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
  301. package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
  302. package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
  303. package/templates/apps/api-versioning/app.config.ts +24 -0
  304. package/templates/apps/api-versioning/database/schema.ts +38 -0
  305. package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
  306. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
  307. package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
  308. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
  309. package/templates/apps/api-versioning/package.json +29 -0
  310. package/templates/apps/api-versioning/template.json +6 -0
  311. package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
  312. package/templates/apps/api-versioning/tsconfig.json +5 -0
  313. package/templates/apps/api-webhooks/.env +6 -0
  314. package/templates/apps/api-webhooks/README.md +106 -0
  315. package/templates/apps/api-webhooks/app.config.ts +16 -0
  316. package/templates/apps/api-webhooks/database/schema.ts +49 -0
  317. package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
  318. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
  319. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
  320. package/templates/apps/api-webhooks/package.json +28 -0
  321. package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
  322. package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
  323. package/templates/apps/api-webhooks/template.json +6 -0
  324. package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
  325. package/templates/apps/api-webhooks/tsconfig.json +5 -0
  326. package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
  327. package/templates/apps/changelog/README.md +77 -0
  328. package/templates/apps/changelog/app.config.ts +27 -0
  329. package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
  330. package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
  331. package/templates/apps/changelog/package.json +30 -0
  332. package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
  333. package/templates/apps/changelog/src/globals.css +66 -0
  334. package/templates/apps/changelog/src/globals.d.ts +16 -0
  335. package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
  336. package/templates/apps/changelog/src/lib/locale.ts +55 -0
  337. package/templates/apps/changelog/src/lib/releases.ts +21 -0
  338. package/templates/apps/changelog/src/locales/de.ts +22 -0
  339. package/templates/apps/changelog/src/locales/en.ts +30 -0
  340. package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
  341. package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
  342. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
  343. package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
  344. package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
  345. package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
  346. package/templates/apps/changelog/src/pages/index.tsx +55 -0
  347. package/templates/apps/changelog/src/pages/layout.tsx +84 -0
  348. package/templates/apps/changelog/template.json +6 -0
  349. package/templates/apps/changelog/tsconfig.json +11 -0
  350. package/templates/apps/edge-functions/README.md +55 -0
  351. package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
  352. package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
  353. package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
  354. package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
  355. package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
  356. package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
  357. package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
  358. package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
  359. package/templates/apps/edge-functions/package.json +21 -0
  360. package/templates/apps/edge-functions/template.json +6 -0
  361. package/templates/apps/edge-functions/tsconfig.json +10 -0
  362. package/templates/apps/frontend-admin/README.md +40 -0
  363. package/templates/apps/frontend-admin/app.config.ts +37 -0
  364. package/templates/apps/frontend-admin/package.json +30 -0
  365. package/templates/apps/frontend-admin/src/config.ts +8 -0
  366. package/templates/apps/frontend-admin/src/globals.css +76 -0
  367. package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
  368. package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
  369. package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
  370. package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
  371. package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
  372. package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
  373. package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
  374. package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
  375. package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
  376. package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
  377. package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
  378. package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
  379. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
  380. package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
  381. package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
  382. package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
  383. package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
  384. package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
  385. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
  386. package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
  387. package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
  388. package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
  389. package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
  390. package/templates/apps/frontend-admin/template.json +6 -0
  391. package/templates/apps/frontend-admin/tsconfig.json +11 -0
  392. package/templates/apps/frontend-app/README.md +98 -0
  393. package/templates/apps/frontend-app/app.config.ts +43 -0
  394. package/templates/apps/frontend-app/package.json +30 -0
  395. package/templates/apps/frontend-app/src/locales/de.ts +36 -0
  396. package/templates/apps/frontend-app/src/locales/en.ts +43 -0
  397. package/templates/apps/frontend-app/src/locales/index.ts +15 -0
  398. package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
  399. package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
  400. package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
  401. package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
  402. package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
  403. package/templates/apps/frontend-app/template.json +6 -0
  404. package/templates/apps/frontend-app/tsconfig.json +11 -0
  405. package/templates/apps/frontend-blank/README.md +18 -0
  406. package/templates/apps/frontend-blank/app.config.ts +29 -0
  407. package/templates/apps/frontend-blank/package.json +29 -0
  408. package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
  409. package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
  410. package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
  411. package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
  412. package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
  413. package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
  414. package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
  415. package/templates/apps/frontend-blank/template.json +6 -0
  416. package/templates/apps/frontend-blank/tsconfig.json +11 -0
  417. package/templates/apps/frontend-contact/README.md +65 -0
  418. package/templates/apps/frontend-contact/app.config.ts +25 -0
  419. package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
  420. package/templates/apps/frontend-contact/package.json +33 -0
  421. package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
  422. package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
  423. package/templates/apps/frontend-contact/src/config.ts +12 -0
  424. package/templates/apps/frontend-contact/src/globals.css +84 -0
  425. package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
  426. package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
  427. package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
  428. package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
  429. package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
  430. package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
  431. package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
  432. package/templates/apps/frontend-contact/template.json +6 -0
  433. package/templates/apps/frontend-contact/tsconfig.json +11 -0
  434. package/templates/apps/frontend-dashboard/README.md +54 -0
  435. package/templates/apps/frontend-dashboard/app.config.ts +39 -0
  436. package/templates/apps/frontend-dashboard/package.json +29 -0
  437. package/templates/apps/frontend-dashboard/src/config.ts +8 -0
  438. package/templates/apps/frontend-dashboard/src/globals.css +72 -0
  439. package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
  440. package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
  441. package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
  442. package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
  443. package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
  444. package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
  445. package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
  446. package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
  447. package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
  448. package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
  449. package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
  450. package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
  451. package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
  452. package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
  453. package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
  454. package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
  455. package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
  456. package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
  457. package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
  458. package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
  459. package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
  460. package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
  461. package/templates/apps/frontend-dashboard/template.json +6 -0
  462. package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
  463. package/templates/apps/frontend-docs/README.md +19 -0
  464. package/templates/apps/frontend-docs/app.config.ts +25 -0
  465. package/templates/apps/frontend-docs/package.json +29 -0
  466. package/templates/apps/frontend-docs/src/globals.css +32 -0
  467. package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
  468. package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
  469. package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
  470. package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
  471. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
  472. package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
  473. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
  474. package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
  475. package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
  476. package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
  477. package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
  478. package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
  479. package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
  480. package/templates/apps/frontend-docs/template.json +6 -0
  481. package/templates/apps/frontend-docs/tsconfig.json +11 -0
  482. package/templates/apps/frontend-i18n/README.md +61 -0
  483. package/templates/apps/frontend-i18n/app.config.ts +33 -0
  484. package/templates/apps/frontend-i18n/package.json +28 -0
  485. package/templates/apps/frontend-i18n/src/globals.css +46 -0
  486. package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
  487. package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
  488. package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
  489. package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
  490. package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
  491. package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
  492. package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
  493. package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
  494. package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
  495. package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
  496. package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
  497. package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
  498. package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
  499. package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
  500. package/templates/apps/frontend-i18n/template.json +6 -0
  501. package/templates/apps/frontend-i18n/tsconfig.json +11 -0
  502. package/templates/apps/frontend-landing/README.md +17 -0
  503. package/templates/apps/frontend-landing/app.config.ts +25 -0
  504. package/templates/apps/frontend-landing/package.json +29 -0
  505. package/templates/apps/frontend-landing/src/globals.css +23 -0
  506. package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
  507. package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
  508. package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
  509. package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
  510. package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
  511. package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
  512. package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
  513. package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
  514. package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
  515. package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
  516. package/templates/apps/frontend-landing/template.json +6 -0
  517. package/templates/apps/frontend-landing/tsconfig.json +11 -0
  518. package/templates/apps/frontend-spa/README.md +45 -0
  519. package/templates/apps/frontend-spa/app.config.ts +27 -0
  520. package/templates/apps/frontend-spa/package.json +29 -0
  521. package/templates/apps/frontend-spa/src/globals.css +84 -0
  522. package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
  523. package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
  524. package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
  525. package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
  526. package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
  527. package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
  528. package/templates/apps/frontend-spa/template.json +6 -0
  529. package/templates/apps/frontend-spa/tsconfig.json +11 -0
  530. package/templates/apps/frontend-ssr/README.md +68 -0
  531. package/templates/apps/frontend-ssr/app.config.ts +32 -0
  532. package/templates/apps/frontend-ssr/package.json +29 -0
  533. package/templates/apps/frontend-ssr/src/globals.css +67 -0
  534. package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
  535. package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
  536. package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
  537. package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
  538. package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
  539. package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
  540. package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
  541. package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
  542. package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
  543. package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
  544. package/templates/apps/frontend-ssr/template.json +6 -0
  545. package/templates/apps/frontend-ssr/tsconfig.json +11 -0
  546. package/templates/apps/frontend-ssr-api/README.md +50 -0
  547. package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
  548. package/templates/apps/frontend-ssr-api/package.json +30 -0
  549. package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
  550. package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
  551. package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
  552. package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
  553. package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
  554. package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
  555. package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
  556. package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
  557. package/templates/apps/frontend-ssr-api/template.json +6 -0
  558. package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
  559. package/templates/apps/frontend-static-blog/README.md +49 -0
  560. package/templates/apps/frontend-static-blog/app.config.ts +34 -0
  561. package/templates/apps/frontend-static-blog/package.json +28 -0
  562. package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
  563. package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
  564. package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
  565. package/templates/apps/frontend-static-blog/src/globals.css +75 -0
  566. package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
  567. package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
  568. package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
  569. package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
  570. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
  571. package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
  572. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
  573. package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
  574. package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
  575. package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
  576. package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
  577. package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
  578. package/templates/apps/frontend-static-blog/template.json +6 -0
  579. package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
  580. package/templates/baselines/bare/.env.example +37 -0
  581. package/templates/baselines/bare/README.md +47 -0
  582. package/templates/baselines/bare/baseline.json +31 -0
  583. package/templates/baselines/bare/deploy/README.md +43 -0
  584. package/templates/baselines/bare/deploy/voltro.service.example +36 -0
  585. package/templates/baselines/compose/.env.example +46 -0
  586. package/templates/baselines/compose/README.md +69 -0
  587. package/templates/baselines/compose/baseline.json +51 -0
  588. package/templates/baselines/compose/docker/.dockerignore +38 -0
  589. package/templates/baselines/compose/docker/api.Dockerfile +57 -0
  590. package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
  591. package/templates/baselines/compose/docker/web.Dockerfile +59 -0
  592. package/templates/baselines/compose/docker-compose.dev.yml +89 -0
  593. package/templates/baselines/compose/docker-compose.prod.yml +87 -0
  594. package/templates/baselines/compose/docker-compose.yml +41 -0
  595. package/templates/baselines/compose-mariadb/.env.example +57 -0
  596. package/templates/baselines/compose-mariadb/README.md +78 -0
  597. package/templates/baselines/compose-mariadb/baseline.json +51 -0
  598. package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
  599. package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
  600. package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
  601. package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
  602. package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
  603. package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
  604. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
  605. package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
  606. package/templates/baselines/helm/.env.example +39 -0
  607. package/templates/baselines/helm/README.md +98 -0
  608. package/templates/baselines/helm/baseline.json +53 -0
  609. package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
  610. package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
  611. package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
  612. package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
  613. package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
  614. package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
  615. package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
  616. package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
  617. package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
  618. package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
  619. package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
  620. package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
  621. package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
  622. package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
  623. package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
  624. package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
  625. package/templates/baselines/helm/deploy/README.md +94 -0
  626. package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
@@ -0,0 +1,1583 @@
1
+ # database.advancedQueries
2
+
3
+ > Btree, GIN, GiST, BRIN, HNSW — when to use what, plus partial + expression + composite indexes.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/database/indexes.md -->
10
+ ## Indexes
11
+
12
+ _Btree, GIN, GiST, BRIN, HNSW — when to use what, plus partial + expression + composite indexes._
13
+
14
+ Indexes accelerate reads at the cost of write throughput + storage. Voltro's DSL lets you declare every Postgres index type with explicit intent.
15
+
16
+ ## Single-column index
17
+
18
+ Indexes are declared at the table level — there is no column-level
19
+ `.index()` modifier. The single surface is `.index(...)`:
20
+
21
+ ```ts
22
+ const notes = table('notes', {
23
+ id: id(),
24
+ authorId: text(),
25
+ tenantId: text(),
26
+ })
27
+ .index(['authorId']) // auto-named → "notes_authorId_idx"
28
+ .index(['tenantId'])
29
+ ```
30
+
31
+ Each generates `CREATE INDEX notes_<col>_idx ON notes (<col>);` — btree, default opclass.
32
+
33
+ ## Named composite index
34
+
35
+ For multi-column lookups + ORDER BY pagination:
36
+
37
+ ```ts
38
+ import { table, id, text, timestamp } from '@voltro/database'
39
+
40
+ const notes = table('notes', {
41
+ id: id(),
42
+ tenantId: text(),
43
+ createdAt: timestamp(),
44
+ })
45
+ // Chained `.index(name, [cols], options?)` on the table — leftmost column
46
+ // is the most-selective filter. Index fields are plain column names
47
+ // (or `{ expr: '…' }` for an expression).
48
+ .index('notes_tenant_created', ['tenantId', 'createdAt'])
49
+ ```
50
+
51
+ Column order matters — leftmost is the most-selective filter. The framework warns when an index's leftmost column is also covered by a single-column index (redundancy).
52
+
53
+ ## Indexing mixin-contributed columns
54
+
55
+ `.index([...])` / `.unique([...])` keys accept the table's **own** columns
56
+ plus the columns each **directly applied** mixin contributes — `tenant()` →
57
+ `tenantId`, `audit()` → `createdAt` / `updatedAt` / `createdBy` / `updatedBy`,
58
+ `softDelete()` → `deletedAt` / `deletedBy`. **Order matters:** declare the
59
+ index or unique constraint **after** `.with(...)`, so the mixin columns are in
60
+ scope. Calling `.index([...])` *before* `.with(...)` sees only the own columns
61
+ (the mixin hasn't been applied yet).
62
+
63
+ ```ts
64
+ export const memberships = table('memberships', {
65
+ id: id(),
66
+ userId: reference(() => users),
67
+ })
68
+ .with(audit(), tenant()) // applies tenantId + audit columns
69
+ .unique(['tenantId', 'userId']) // ✅ mixin column tenantId is addressable
70
+ .index(['createdAt']) // ✅ audit column, applied above
71
+ ```
72
+
73
+ Typos are still caught — the key is the real merged column set, not an
74
+ "accept any string" escape hatch. `tenant()` already ships its OWN `tenantId`
75
+ index, so a plain per-tenant lookup needs nothing extra; this only matters for
76
+ the additional composite keys you author yourself.
77
+
78
+ **Transitive caveat.** Only **directly applied** mixin columns enter the key
79
+ union. `.with(tenant())` adds `tenantId` but NOT `audit()`'s columns — even
80
+ though `tenant()` requires `audit()` and those columns exist at runtime. To
81
+ index a transitively-required mixin's column, apply that mixin explicitly:
82
+ `.with(audit(), tenant())`.
83
+
84
+ ## Index types
85
+
86
+ Every index kind is selected with the `{ kind }` option on the
87
+ table-level `.index(name, [cols], { kind })` (or `.expressionIndex(...)`).
88
+ The default is `btree`.
89
+
90
+ ### Btree *(default)*
91
+
92
+ Equality + range + ordering. The right choice 90% of the time. Leave
93
+ `kind` off to get it:
94
+
95
+ ```ts
96
+ table('notes', { id: id(), authorId: text() })
97
+ .index('notes_author', ['authorId']) // btree, default opclass
98
+ ```
99
+
100
+ ### GIN
101
+
102
+ For arrays, JSONB containment, and full-text search:
103
+
104
+ ```ts
105
+ import { table, id, json } from '@voltro/database'
106
+
107
+ table('docs', { id: id(), tags: json<string[]>() })
108
+ .index('docs_tags', ['tags'], { kind: 'gin' })
109
+ ```
110
+
111
+ On postgres this emits `USING GIN`. GIN is postgres-only — on
112
+ mysql / mariadb a btree can't serve containment so the migrator skips
113
+ the index + warns (denormalise the path you query into a generated
114
+ column and index THAT instead); on mssql / sqlite it falls back to a
115
+ plain btree + a warning.
116
+
117
+ ### GiST
118
+
119
+ For range types, geometric types (PostGIS), and full-text search where
120
+ you need ranking:
121
+
122
+ ```ts
123
+ table('venues', { id: id(), location: text() }) // geography(...) in real PostGIS code
124
+ .expressionIndex('venues_loc_gist', [{ expr: '"location"' }], { kind: 'gist' })
125
+ ```
126
+
127
+ Postgres-only; the migrator warns + falls back to btree on other
128
+ dialects. See [PostGIS](/docs/database/postgis) for the spatial story.
129
+
130
+ ### BRIN
131
+
132
+ Block-range index — extremely cheap for very large append-mostly tables
133
+ sorted by an indexed column (time-series, logs):
134
+
135
+ ```ts
136
+ table('events', { id: id(), occurredAt: timestamp() })
137
+ .index('events_occurred', ['occurredAt'], { kind: 'brin' })
138
+ ```
139
+
140
+ On postgres this emits `USING BRIN` — tiny (~1% of the table size) +
141
+ fast for range scans on append-ordered data, useless for random-access
142
+ lookups. Other dialects have no block-range method; the migrator falls
143
+ back to a plain btree + a warning (the range scan still works, just
144
+ without BRIN's size win).
145
+
146
+ ### HNSW (pgvector)
147
+
148
+ For vector similarity search. The index must cover exactly one
149
+ `vector()` column — declaring `kind: 'hnsw'` on anything else throws at
150
+ declaration time:
151
+
152
+ ```ts
153
+ table('embeddings', { id: id(), embedding: vector(1536) })
154
+ .index('emb_hnsw', ['embedding'], { kind: 'hnsw' })
155
+ ```
156
+
157
+ Defaults to `m=16`, `ef_construction=64`, opclass `vector_cosine_ops`.
158
+ Override the tuning knobs via `kindOptions.hnsw`:
159
+
160
+ ```ts
161
+ table('embeddings', { id: id(), embedding: vector(1536) })
162
+ .index('emb_hnsw', ['embedding'], {
163
+ kind: 'hnsw',
164
+ kindOptions: { hnsw: { m: 24, efConstruction: 128 } },
165
+ })
166
+ ```
167
+
168
+ Postgres + pgvector only; on every other dialect a btree on a vector is
169
+ meaningless, so the migrator emits no index + warns (ANN queries fall
170
+ back to a sequential scan). The distance metric → opclass mapping
171
+ (`vector_cosine_ops` / `vector_l2_ops` / `vector_ip_ops`) is covered on
172
+ the [Vector columns](/docs/database/vectors) page.
173
+
174
+ ### Per-dialect support matrix
175
+
176
+ `warn+btree` = the migrator emits a plain btree index + a
177
+ `[voltro:migrate]` warning (the intended method was unavailable but a
178
+ btree still helps). `skip+warn` = no index is emitted + a warning (a
179
+ btree on that column would be useless).
180
+
181
+ | `kind` | postgres | mysql | mariadb | mssql | sqlite |
182
+ |--------------------|-------------------------------------------------------|-------------|-------------|------------|------------|
183
+ | `btree` *(default)*| `USING btree` (implicit) | implicit | implicit | implicit | implicit |
184
+ | `gist` | `USING GIST` | warn+btree | warn+btree | warn+btree | warn+btree |
185
+ | `gin` | `USING GIN` | skip+warn | skip+warn | warn+btree | warn+btree |
186
+ | `brin` | `USING BRIN` | warn+btree | warn+btree | warn+btree | warn+btree |
187
+ | `hnsw` | `USING hnsw (col <opclass>) WITH (m=…, ef_construction=…)` | skip+warn | skip+warn | skip+warn | skip+warn |
188
+
189
+ A "jsonb index" is not a separate kind — it's a `json()` column (already
190
+ `JSONB` on postgres) plus `{ kind: 'gin' }`, i.e. the `gin` row above.
191
+
192
+ ## Partial indexes
193
+
194
+ Index only rows matching a predicate — drastically smaller + faster when most rows wouldn't match. Pass `{ where }`:
195
+
196
+ ```ts
197
+ table('notes', { id: id(), updatedAt: timestamp(), archived: boolean() })
198
+ .index('notes_unarchived', ['updatedAt'], { where: `"archived" = false` })
199
+ ```
200
+
201
+ Useful for soft-delete tables (`WHERE deletedAt IS NULL`) and status-filtered queries. See [Partial indexes (WHERE clause)](#partial-indexes-where-clause) below for the full cross-dialect story.
202
+
203
+ ## Expression indexes
204
+
205
+ Index a computed value, not a column — use `.expressionIndex(...)` with an `{ expr }` entry:
206
+
207
+ ```ts
208
+ table('users', { id: id(), email: text() })
209
+ .expressionIndex('users_email_lower', [{ expr: 'lower("email")' }])
210
+ ```
211
+
212
+ Then `WHERE lower("email") = ?` uses the index. The query builder doesn't auto-rewrite `WHERE email ILIKE 'foo'` to use this — you call out the expression explicitly.
213
+
214
+ ## Unique indexes
215
+
216
+ ```ts
217
+ text().unique() // single-column, on the column
218
+ ```
219
+
220
+ Multi-column uniqueness is declared at the table level with `.unique(name, [cols])` — see [Composite UNIQUE constraints](#composite-unique-constraints) below.
221
+
222
+ ## When NOT to index
223
+
224
+ - Tables with <10k rows on a fast disk — the cost of maintaining the index outweighs the seq-scan cost.
225
+ - Columns with very low selectivity (boolean flags, `status` enums where one value dominates). Use a partial index instead.
226
+ - Write-heavy hot paths. Every index is a synchronous write on every insert/update.
227
+
228
+ ## Partial indexes (WHERE clause)
229
+
230
+ ```ts
231
+ table('orders', {
232
+ id: id(),
233
+ orgId: text(),
234
+ status: text(),
235
+ createdAt: timestamp(),
236
+ })
237
+ .index('byOpenStatus', ['orgId', 'createdAt'], {
238
+ where: `"status" IN ('pending', 'approved')`,
239
+ })
240
+ ```
241
+
242
+ The DDL emits `CREATE INDEX ... WHERE ...` — only rows matching the
243
+ predicate participate in the index. Use cases:
244
+
245
+ - **Index only non-soft-deleted rows**: `where: \`"deletedAt" IS NULL\``
246
+ - **Index only active users**: `where: \`"banned" = false\``
247
+ - **Index only open tickets**: `where: \`"status" IN ('open', 'pending')\``
248
+
249
+ The result: a much smaller B-tree (faster reads, smaller cache
250
+ footprint) at the cost of one extra `WHERE` clause the query
251
+ planner has to match against.
252
+
253
+ Cross-dialect:
254
+
255
+ | Dialect | Support |
256
+ |---------------------|----------------------------------------------------|
257
+ | postgres / sqlite | native `CREATE INDEX ... WHERE ...` |
258
+ | mssql | native "filtered index" |
259
+ | mysql / mariadb | NOT supported → drops the WHERE + warns at migrate |
260
+
261
+ The `where` clause is emitted verbatim — caller is responsible for
262
+ quoting identifiers per the target dialect.
263
+
264
+ ## Expression indexes (functions on columns)
265
+
266
+ For predicates that compute on the column rather than match exact
267
+ values, use `.expressionIndex()`:
268
+
269
+ ```ts
270
+ table('users', { id: id(), email: text() })
271
+ .expressionIndex('byEmailCi', [{ expr: 'lower("email")' }])
272
+ ```
273
+
274
+ Now `WHERE lower("email") = ?` uses the index. Without it,
275
+ case-insensitive email lookup is a sequential scan.
276
+
277
+ ```ts
278
+ // Mix columns + expressions
279
+ .expressionIndex('byOrgCreatedMonth', [
280
+ 'orgId',
281
+ { expr: \`date_trunc('month', "createdAt")\` },
282
+ ])
283
+
284
+ // Combine with partial-where
285
+ .expressionIndex('byActiveEmailCi',
286
+ [{ expr: 'lower("email")' }],
287
+ { where: \`"active" = true\` },
288
+ )
289
+ ```
290
+
291
+ Why a separate method from `.index([...])`:
292
+
293
+ - `.index([cols])` validates the column names against the row type
294
+ at compile time. Typos fail at `tsc --noEmit`.
295
+ - `.expressionIndex(name, [...])` accepts arbitrary expression
296
+ strings — by definition the framework can't type-check them.
297
+ Keeping the two methods separate preserves the compile-time
298
+ safety of the regular form.
299
+
300
+ ## Index names are unique per SCHEMA, not per table
301
+
302
+ Every dialect keys index names per schema (postgres `pg_class`, mysql /
303
+ mariadb `information_schema`, mssql `sys.indexes`, sqlite `sqlite_master`)
304
+ — **not per table**. So a hand-picked name reused on two tables collides:
305
+
306
+ ```ts
307
+ table('ab_tests', { /* … */ }).index('byStatusStart', ['status', 'startAt'])
308
+ table('tournaments', { /* … */ }).index('byStatusStart', ['status', 'startAt'])
309
+ // ❌ throws — 'byStatusStart' would exist twice in one schema
310
+ ```
311
+
312
+ The DB creates only the first; every later `CREATE INDEX … IF NOT EXISTS
313
+ <name>` is a silent no-op, so `db plan` re-emits the un-created ones
314
+ forever and never reaches "up to date". The framework catches this when
315
+ the full schema is snapshotted (boot / `db plan`) and fails loud, naming
316
+ both tables + a suggested fix. **Auto-named** indexes (`.index([col])` →
317
+ `<table>_<col>_idx`) are table-prefixed and can't collide — only
318
+ explicit names can. Give each a distinct, table-scoped name
319
+ (`abTestsByStatusStart`, `tournamentsByStatusStart`).
320
+
321
+ ## Composite UNIQUE constraints
322
+
323
+ Multi-column uniqueness — `(orgId, slug)` must be unique so two
324
+ orgs can both have a `/dashboard` slug but neither can have two of
325
+ their own:
326
+
327
+ ```ts
328
+ table('org_slugs', {
329
+ id: id(),
330
+ orgId: reference(() => orgs),
331
+ slug: text(),
332
+ })
333
+ .unique(['orgId', 'slug']) // auto-named
334
+ .unique('byOrgSlug', ['orgId', 'slug']) // explicit name
335
+ .unique('byOrgSlug', ['orgId', 'slug'], { dedup: 'suffix-counter' }) // with backfill policy
336
+ ```
337
+
338
+ Distinct from the column-level `.unique()` modifier (single-column
339
+ only, lives on the column). Composite UNIQUE MUST be declared at
340
+ the table level.
341
+
342
+ The `dedup` policy tells the migration planner what to do when the
343
+ constraint is added to a populated table with duplicates:
344
+
345
+ - `'fail'` (default) — refuse with the conflicting rows surfaced
346
+ - `'suffix-counter'` — UPDATE conflicts to `<value>-2`, `<value>-3`, ...
347
+ - `sql\`...\`` — custom SQL fragment
348
+
349
+ Emits `CONSTRAINT <name> UNIQUE (col1, col2, ...)` inline in CREATE
350
+ TABLE on every dialect. Standard SQL.
351
+
352
+ This is what backs `ctx.store.upsert(..., { conflictColumns: ['a', 'b'] })`
353
+ — see [Bulk operations](/docs/database/bulk-operations#upsert).
354
+
355
+ ## GiST indexes (PostGIS spatial)
356
+
357
+ `{ kind: 'gist' }` on `.expressionIndex()` emits `USING GIST`:
358
+
359
+ ```ts
360
+ table('venues', {
361
+ id: id(),
362
+ location: geography('Point', 4326), // from @voltro/plugin-postgis
363
+ }).expressionIndex(
364
+ 'venues_loc_gist',
365
+ [{ expr: '"location"' }],
366
+ { kind: 'gist' },
367
+ )
368
+ ```
369
+
370
+ GiST is the access method PostGIS needs for spatial predicates
371
+ (`ST_DWithin`, `ST_Contains`, etc.) — a regular B-tree index can't
372
+ serve them. Postgres-only; the migrator warns + falls back to
373
+ B-tree on other dialects.
374
+
375
+ See [PostGIS](/docs/database/postgis) for the full spatial story.
376
+
377
+ ## Full-text indexes
378
+
379
+ ```ts
380
+ table('posts', { id: id(), title: text(), body: text() })
381
+ .fullTextIndex('postSearch', ['title', 'body'], {
382
+ config: 'english',
383
+ weights: { title: 'A', body: 'B' },
384
+ })
385
+ ```
386
+
387
+ Higher-level abstraction over expression indexes — see
388
+ [Full-text search](/docs/database/full-text-search) for the full
389
+ shape.
390
+
391
+ ## Inspecting
392
+
393
+ ```sh
394
+ # The migrations introspection endpoint carries the full live schema —
395
+ # every table's columns + indexes as the runtime sees them.
396
+ curl http://localhost:5191/_voltro/inspect/migrations
397
+ ```
398
+
399
+ Or via the dashboard's Database tab.
400
+
401
+ For Postgres-side inspection: `pg_stat_user_indexes` shows scans + tuples read per index. Indexes with `idx_scan = 0` after weeks of traffic are deadweight — drop them.
402
+
403
+
404
+
405
+ ---
406
+
407
+ <!-- source: en/database/json.md -->
408
+ ## JSON columns
409
+
410
+ _Typed jsonb columns, path queries, GIN indexing, and when to denormalise into real columns._
411
+
412
+ `json<T>()` declares a `jsonb` column whose JSON shape is typed by `T`. The runtime decodes on read + validates on write — typos in your object literals fail the TypeScript check, not at runtime.
413
+
414
+ ## Declaring a typed JSON column
415
+
416
+ ```ts
417
+ import { table, id, json } from '@voltro/database'
418
+
419
+ interface NotePrefs {
420
+ readonly fontSize: 'sm' | 'md' | 'lg'
421
+ readonly collapsed: ReadonlyArray<string>
422
+ readonly autoSave?: boolean
423
+ }
424
+
425
+ const notes = table('notes', {
426
+ id: id(),
427
+ prefs: json<NotePrefs>().default({ fontSize: 'md', collapsed: [] }),
428
+ })
429
+ ```
430
+
431
+ `ctx.store.select('notes')` returns `prefs: NotePrefs` — autocomplete works in your IDE, mutation inputs are checked too.
432
+
433
+ ## Storage + validation per dialect
434
+
435
+ `json()` maps to each engine's native JSON type, and Voltro enforces JSON validity **on write, on every dialect** — invalid JSON is rejected by the database, not only by TypeScript:
436
+
437
+ | Dialect | Column type | Validated on write |
438
+ |---|---|---|
439
+ | postgres | `JSONB` (binary, GIN-indexable) | yes (native) |
440
+ | mysql / mariadb | `JSON` | yes (mariadb's `JSON` is `LONGTEXT` + an auto `json_valid` CHECK) |
441
+ | mssql | `NVARCHAR(MAX)` + `CHECK (ISJSON(col)=1)` | yes |
442
+ | sqlite | `TEXT` + `CHECK (json_valid(col))` | yes |
443
+
444
+ On postgres the binary `JSONB` form is what makes path queries + GIN indexing fast; the other engines store JSON as text (mariadb's `information_schema` reports the `JSON` column as `longtext` — that *is* what the JSON type is there). Either way, the `jsonField(...)` filters below and write-validation behave identically across all of them, and reads always come back as parsed objects/arrays — never raw strings.
445
+
446
+ > On postgres, prefer `jsonb` over `json` (Voltro always emits `jsonb`): binary storage, GIN-indexable, faster. Plain `json` only preserves exact byte / whitespace / key-order — never what you want for app data.
447
+
448
+ ## Path filters
449
+
450
+ Filter on a value *inside* a `json()` column with `jsonField(column, ...path)`:
451
+
452
+ ```ts
453
+ import { jsonField } from '@voltro/database'
454
+
455
+ database.users.where(jsonField('preferences', 'theme').eq('dark'))
456
+ database.events.where(jsonField('payload', 'amount').gt(1000)) // numeric, not lexical
457
+ database.docs.where(jsonField('meta', 'tags', 0).eq('urgent')) // nested key + array index
458
+ database.users.where(jsonField('preferences', 'locale').inSet(['de', 'en']))
459
+ database.users.where(jsonField('preferences', 'theme').isNotNull())
460
+ ```
461
+
462
+ Path segments are object keys (string) or array indices (number) → `$.theme` / `$.tags[0]`.
463
+
464
+ | Operators | Compare the extracted value as |
465
+ |---|---|
466
+ | `eq` `neq` `inSet` `notInSet` `contains` `isNull` `isNotNull` | **text** |
467
+ | `gt` `gte` `lt` `lte` | **number** (numeric ordering, not lexical) |
468
+
469
+ The same `jsonField(...)` expression is **portable across every backend** — it lowers to each dialect's json accessor: postgres `#>>`, mysql/mariadb `JSON_EXTRACT` (+ `JSON_UNQUOTE`), mssql `JSON_VALUE`, sqlite / turso `json_extract`.
470
+
471
+ Reactive subscriptions filtered by a JSON path stay live, but the matcher treats the leaf as **non-indexable**: it's re-checked on every change to the table (the same way `contains` is). For a high-traffic filter, denormalise into a real indexed column — or index the specific path (see [Indexing JSON columns](#indexing-json-columns) below).
472
+
473
+ > **Scope.** JSON-path filtering is a **server-side** query-DSL feature (query / mutation handlers). Clients can't yet send JSON-path filters over rpc — that's a deliberate later phase (it needs path allow-listing + validation). For arbitrary expressions the DSL doesn't model, drop to the `store.raw` escape hatch (see the [query builder](/docs/database/query-builder#raw-sql-escape-hatch)).
474
+
475
+ ## Indexing JSON columns
476
+
477
+ ### GIN index — broad coverage
478
+
479
+ A GIN index covers arbitrary containment queries on the whole `jsonb`
480
+ document. Declare it with the table-level `.index(name, [col], { kind: 'gin' })`:
481
+
482
+ ```ts
483
+ import { table, id, json } from '@voltro/database'
484
+
485
+ const events = table('events', {
486
+ id: id(),
487
+ payload: json<unknown>(),
488
+ })
489
+ .index('events_payload', ['payload'], { kind: 'gin' })
490
+ ```
491
+
492
+ On postgres this emits `CREATE INDEX "events_payload" ON "events" USING GIN ("payload")`,
493
+ so any containment query on `payload` is indexed.
494
+
495
+ GIN is postgres-only. On mysql / mariadb there is no GIN access method
496
+ and a btree on a JSON column can't serve containment — the migrator
497
+ **skips** the index and warns, pointing you at the single-path approach
498
+ below. On mssql / sqlite it falls back to a plain btree + a warning.
499
+ GIN is also large (often 30-50% of the table size for wide JSON); use
500
+ the next option when you only filter on one specific path.
501
+
502
+ ### Index a JSON path — `jsonIndex`
503
+
504
+ When 99% of your queries look like `jsonField('payload', 'kind').eq('X')`,
505
+ index just that path with `jsonIndex(column, ...path)` inside
506
+ `.expressionIndex(...)`. It mirrors `jsonField` exactly — same column, same
507
+ segments, lowered to the same per-dialect accessor — so a filter on that
508
+ path can use the index:
509
+
510
+ ```ts
511
+ import { table, id, json, jsonIndex } from '@voltro/database'
512
+
513
+ const events = table('events', { id: id(), payload: json<unknown>() })
514
+ .expressionIndex('events_kind', [jsonIndex('payload', 'kind')])
515
+ .expressionIndex('events_amount', [jsonIndex('payload', 'amount').numeric()])
516
+ ```
517
+
518
+ Text extraction (the default) backs `eq` / `neq` / `inSet` / `contains`;
519
+ `.numeric()` backs the range ops (`gt` / `gte` / `lt` / `lte`). Because the
520
+ index expression is byte-identical to the WHERE expression `jsonField(...)`
521
+ compiles to, the optimiser actually picks it up.
522
+
523
+ > **Dialect support is uneven — this is a hard engine limit, not a Voltro
524
+ > choice.** Only some engines can index an expression directly:
525
+ >
526
+ > | Dialect | `jsonIndex` | Notes |
527
+ > |---|---|---|
528
+ > | postgres | ✓ expression index | `((col #>> '{path}'::text[]))` |
529
+ > | mysql (8.0.13+) | ✓ functional index | `((JSON_UNQUOTE(JSON_EXTRACT(...))))` |
530
+ > | sqlite | ✓ expression index | `(json_extract(col, '$.path'))` |
531
+ > | mariadb | — skipped + warned | no expression-index support |
532
+ > | mssql | — skipped + warned | no expression-index support |
533
+ >
534
+ > On **mariadb / mssql** the migrator skips a `jsonIndex` and warns rather
535
+ > than emit DDL the engine rejects. The idiomatic indexed-JSON path there is
536
+ > a **generated / computed column** (which Voltro already supports), then
537
+ > filter on that column instead of `jsonField(...)`:
538
+ >
539
+ > ```ts
540
+ > table('events', {
541
+ > id: id(),
542
+ > payload: json<unknown>(),
543
+ > // materialise the path into a real, indexable column
544
+ > kind: text().generatedAs(`JSON_UNQUOTE(JSON_EXTRACT(\`payload\`, '$.kind'))`),
545
+ > }).index('events_kind', ['kind'])
546
+ > // query: ctx.store.select('events').where('kind', 'click').all()
547
+ > ```
548
+
549
+ For an arbitrary expression the `jsonIndex` shorthand doesn't model, drop to
550
+ the raw `.expressionIndex(...)` form with an `{ expr }` entry:
551
+
552
+ ```ts
553
+ table('events', { id: id(), payload: json<unknown>() })
554
+ .expressionIndex('events_kind', [{ expr: `(payload->>'kind')` }])
555
+ ```
556
+
557
+ There you own per-dialect correctness (the `expr` string is emitted
558
+ verbatim; the postgres `->>` form is shown).
559
+
560
+ ## Writing JSON
561
+
562
+ ```ts
563
+ ctx.store.insert('notes', {
564
+ prefs: { fontSize: 'lg', collapsed: ['archive'] },
565
+ })
566
+ ```
567
+
568
+ A plain `update` writes the WHOLE JSON value. To change one field
569
+ without rewriting the rest, use **`patchJson`** — a server-side in-place
570
+ JSON merge (no read-modify-write round-trip):
571
+
572
+ ```ts
573
+ // Merge an object over the top level of the column:
574
+ await ctx.store.patchJson('notes', id, 'prefs', { autoSave: true })
575
+
576
+ // Set a nested path (dot-separated; the column is the first segment):
577
+ await ctx.store.patchJson('notes', id, 'prefs.theme', 'dark')
578
+ ```
579
+
580
+ `patchJson(table, pk, path, value)` returns the post-image (or `null`
581
+ when the row doesn't exist) and emits an `update` ChangeEvent so reactive
582
+ subscribers see the change. The merge is server-side on every dialect —
583
+ postgres `jsonb_set` / `||`, mysql + mariadb `JSON_SET`, mssql
584
+ `JSON_MODIFY`, sqlite `json_set` / `json_patch`.
585
+
586
+ When you need a full read-modify-write (e.g. computing the new value from
587
+ the old in JS), read the row, change the object, and write it back inside
588
+ the mutation's transaction:
589
+
590
+ ```ts
591
+ const note = await ctx.store.select('notes').where('id', id).one()
592
+ await ctx.store.update('notes', id, {
593
+ prefs: { ...note.prefs, autoSave: true },
594
+ })
595
+ ```
596
+
597
+ ## Validating JSON shape
598
+
599
+ Two layers of validation apply:
600
+
601
+ 1. **JSON validity** — that the stored bytes are well-formed JSON — is enforced automatically by the database on every dialect (see [Storage + validation per dialect](#storage--validation-per-dialect)). You don't declare anything.
602
+ 2. **JSON *shape*** — that the value matches your expected structure — is up to you: enforce it at the table level with `table().validate(Schema)`:
603
+
604
+ ```ts
605
+ import { Schema } from 'effect'
606
+
607
+ table('notes', { id: id(), prefs: json<NotePrefs>() })
608
+ .validate(Schema.Struct({
609
+ prefs: Schema.Struct({ fontSize: Schema.Literal('sm', 'md', 'lg') }),
610
+ }))
611
+ ```
612
+
613
+ Decode failure throws a typed `TableValidationFailed` before the INSERT runs. There is no `.check()` modifier on a JSON column.
614
+
615
+ ## When JSON is the wrong choice
616
+
617
+ JSON is great for:
618
+
619
+ - Free-form user-configurable data (preferences, layout configs)
620
+ - Sparse extensions (every row has different shape)
621
+ - Foreign-system payloads (Stripe webhook bodies, Slack message JSON)
622
+
623
+ JSON is a footgun for:
624
+
625
+ - **Anything you filter on heavily.** Denormalise into real columns — they're cheaper to query, easier to index, simpler to constrain.
626
+ - **Anything with strict schema.** A real column with a NOT NULL + CHECK is stronger than a JSON path constraint.
627
+ - **Joining / relating to other tables.** You can't FK from a JSON path.
628
+
629
+ Rule of thumb: if you'd write a migration to add a new field, it's a real column. If users add fields without your code changing, it's JSON.
630
+
631
+ ## Anti-patterns
632
+
633
+ - **`json<any>()` everywhere.** Defeats the type-safety. Be specific.
634
+ - **Putting a foreign key inside JSON.** No FK constraint, no cascade, no clean join. Use a real `reference(() => table)` column.
635
+ - **Storing big binary as JSON.** Use the storage plugin (`@voltro/plugin-storage`) for blobs > a few KB.
636
+
637
+
638
+
639
+ ---
640
+
641
+ <!-- source: en/database/recursive-cte.md -->
642
+ ## Recursive CTEs
643
+
644
+ _WITH RECURSIVE for org hierarchies, comment threads, file-folder trees, category graphs — single query, no app-side loops._
645
+
646
+ `WITH RECURSIVE` lets a query reference itself. Used for tree-walks
647
+ and graph traversals that would otherwise need an app-side loop with
648
+ N round-trips. Voltro's `.recursiveCte()` ships the SQL standard
649
+ form on every supported dialect (postgres, mysql 8+, mariadb 10.2+,
650
+ mssql, sqlite 3.8+).
651
+
652
+ ## When to use
653
+
654
+ - **Org hierarchy**: "find every descendant of org X" / "find every
655
+ parent up to the root"
656
+ - **Comment threads**: "fetch a comment + every reply, recursively"
657
+ - **File-folder tree**: "list everything inside this folder, any depth"
658
+ - **Category graphs**: "products in this category OR any sub-category"
659
+ - **Dependency graphs**: "what migrations does plan X transitively
660
+ depend on?"
661
+
662
+ When you don't have a recursive structure, plain
663
+ [`withCte()`](/docs/database/query-builder#ctes) is enough.
664
+
665
+ ## Shape
666
+
667
+ Every recursive CTE has two arms joined by `UNION` (or `UNION ALL`):
668
+
669
+ 1. **Anchor** — the seed query. Picks the starting rows
670
+ non-recursively. Typically `WHERE id = <root>`.
671
+ 2. **Recursion** — references the CTE by name, joining itself to the
672
+ parent table to walk one level. Composed via `.innerJoin()`.
673
+
674
+ Compose with `union(anchor, recursion)` or `unionAll(...)` and pass
675
+ the combined descriptor to `.recursiveCte(name, ...)`.
676
+
677
+ ## Example: org hierarchy
678
+
679
+ ```ts
680
+ import { eq, queryFor, union } from '@voltro/database'
681
+ // `database` is YOUR project's handle — `export const database =
682
+ // databaseHandle({ ...tables })` in `database/index.ts`.
683
+ import { database } from '../database/index'
684
+
685
+ const rootId = 'org_root'
686
+
687
+ // Anchor: the root org itself
688
+ const anchor = queryFor(database.orgs).where(eq('id', rootId))
689
+
690
+ // Recursion: every org whose parentId is in the running set
691
+ const recursion = queryFor(database.orgs).as('child')
692
+ .innerJoin('descendants', 'parent', eq('parent.id', 'child.parentId'))
693
+
694
+ const tree = await ctx.store.query(
695
+ queryFor(database.orgs)
696
+ .recursiveCte('descendants', union(anchor, recursion).descriptor)
697
+ .where(eq('id', rootId))
698
+ .descriptor,
699
+ )
700
+ // tree: every org reachable from rootId, transitively
701
+ ```
702
+
703
+ Compiles to:
704
+
705
+ ```sql
706
+ WITH RECURSIVE "descendants" AS (
707
+ -- anchor
708
+ (SELECT * FROM "orgs" WHERE "id" = $1)
709
+ UNION
710
+ -- recursion
711
+ (SELECT * FROM "orgs" AS "child"
712
+ INNER JOIN "descendants" AS "parent" ON "parent"."id" = "child"."parentId")
713
+ )
714
+ SELECT * FROM "orgs" WHERE "id" = $1
715
+ ```
716
+
717
+ ## Example: walk UP a tree
718
+
719
+ The recursion direction is yours to choose — join `child.parentId`
720
+ to walk up, or `parent.id` to walk down.
721
+
722
+ ```ts
723
+ const node = queryFor(folders).where(eq('id', leafFolderId))
724
+ const ancestors = queryFor(folders).as('parent')
725
+ .innerJoin('chain', 'child', eq('child.parentId', 'parent.id'))
726
+
727
+ const path = await ctx.store.query(
728
+ queryFor(folders)
729
+ .recursiveCte('chain', union(node, ancestors).descriptor)
730
+ .descriptor,
731
+ )
732
+ ```
733
+
734
+ ## Cycle handling
735
+
736
+ Standard `UNION` semantics dedupe across iterations — if your graph
737
+ contains cycles, the recursion stops naturally when no new rows
738
+ appear in a step. For very large graphs with cycles, prefer
739
+ `unionAll` only when you've verified the graph is acyclic OR you
740
+ have a `WHERE` predicate in the recursion that prevents infinite
741
+ loops (e.g. a depth limit).
742
+
743
+ ## Cross-dialect notes
744
+
745
+ | Dialect | Supported | Notes |
746
+ |------------|-----------|-------|
747
+ | postgres | ✓ native | Best optimizer for recursive CTEs |
748
+ | mysql 8+ | ✓ native | Recursion depth limited by `cte_max_recursion_depth` (default 1000) — set per-session for deeper trees |
749
+ | mariadb 10.2+ | ✓ native | Same as mysql |
750
+ | mssql | ✓ native | `OPTION (MAXRECURSION N)` hint NOT auto-emitted — set if you need >100-level recursion |
751
+ | sqlite 3.8+ | ✓ native | Smaller default recursion limit; check `PRAGMA recursive_triggers` |
752
+
753
+ Voltro's compiler emits identical syntax across dialects; only the
754
+ runtime defaults differ.
755
+
756
+ ## Reactivity
757
+
758
+ Recursive CTE queries are reactive — coarsely. The engine registers
759
+ the subscription against every table the recursion reads (the anchor +
760
+ the recursive arm), so a write to any of them re-runs the tree-walk.
761
+ It can't pre-filter per column — a change to one ancestor can reshape
762
+ the whole result — so it re-queries on any contributing-table change.
763
+ Correct, but it re-runs the full recursion each time: fine for bounded
764
+ trees (org hierarchies, comment threads). For very hot or very large
765
+ trees, model the relationship via [Relations](/docs/database/joins) and
766
+ eager-load with `.with({...})` to get the per-field pre-filter.
767
+
768
+ ## Limitations
769
+
770
+ - **No mutual recursion** between two CTEs in the same block. Each
771
+ recursive CTE references only itself.
772
+ - **Cycle detection without `UNION` dedup**: if you use `unionAll`,
773
+ ensure your recursion has a termination predicate. Voltro doesn't
774
+ inject a default depth limit.
775
+ - **No reactivity**: see above.
776
+
777
+ ## See also
778
+
779
+ - [Plain CTEs](/docs/database/query-builder#ctes) — `withCte()` for
780
+ non-recursive named sub-queries
781
+ - [Joins](/docs/database/joins) — relation-based traversal when the
782
+ graph depth is fixed (e.g. parent + immediate children)
783
+ - [Aggregations](/docs/database/query-builder#aggregations) —
784
+ COUNT/SUM/AVG over a recursive CTE's result set
785
+
786
+
787
+
788
+ ---
789
+
790
+ <!-- source: en/database/set-operations.md -->
791
+ ## Set operations (UNION / INTERSECT / EXCEPT)
792
+
793
+ _Combine the results of multiple queries — UNION dedups, UNION ALL doesn't, INTERSECT keeps rows in both, EXCEPT subtracts._
794
+
795
+ `union` / `unionAll` / `intersect` / `except` combine two or more
796
+ queries that produce the **same column shape**. The result is one
797
+ unified row set you can sort, paginate, and aggregate against.
798
+
799
+ ## Quick start
800
+
801
+ ```ts
802
+ import { union, eq, queryFor } from '@voltro/database'
803
+
804
+ // Active + archived tickets for an org, treated as one list
805
+ const all = await ctx.store.query(
806
+ union(
807
+ queryFor(database.activeTickets).where(eq('orgId', oid)),
808
+ queryFor(database.archivedTickets).where(eq('orgId', oid)),
809
+ ).orderBy('createdAt', 'desc').limit(50).descriptor,
810
+ )
811
+ ```
812
+
813
+ Compiles to:
814
+
815
+ ```sql
816
+ (SELECT * FROM "activeTickets" WHERE "orgId" = $1)
817
+ UNION
818
+ (SELECT * FROM "archivedTickets" WHERE "orgId" = $2)
819
+ ORDER BY "createdAt" DESC
820
+ LIMIT 50
821
+ ```
822
+
823
+ The outer `.orderBy()` / `.limit()` apply to the combined result —
824
+ each inner query keeps its own predicate but loses its own
825
+ ordering.
826
+
827
+ ## The four operations
828
+
829
+ | Helper | Semantics |
830
+ |---------------|------------------------------------------------------------|
831
+ | `union(...)` | Rows from any input, deduplicated |
832
+ | `unionAll(...)` | Rows from any input, NO dedup (faster + preserves duplicates) |
833
+ | `intersect(...)` | Rows present in EVERY input |
834
+ | `except(...)` | Rows in the FIRST input, NOT in any subsequent input |
835
+
836
+ Each accepts 2+ queries. One input throws (`requires at least two
837
+ queries`).
838
+
839
+ ## When to reach for each
840
+
841
+ - **`union`** — "show this user's items from two different sources,
842
+ deduped" (a notifications feed mixed with system messages).
843
+ - **`unionAll`** — same as `union` but you know there are no
844
+ duplicates OR you specifically want to keep them. Skipping the
845
+ dedup pass is meaningfully faster on large inputs.
846
+ - **`intersect`** — "users who exist in BOTH the paying-customers
847
+ list AND the active-this-week list".
848
+ - **`except`** — "all users EXCEPT those who unsubscribed". Use
849
+ `notInSubquery` if you only need a column-level check; `except`
850
+ when you're operating on full row shapes.
851
+
852
+ ## Three-way and beyond
853
+
854
+ All four accept any number of inputs (≥ 2). The compiler chains them
855
+ with the appropriate keyword:
856
+
857
+ ```ts
858
+ const combined = union(
859
+ queryFor(database.eventsA),
860
+ queryFor(database.eventsB),
861
+ queryFor(database.eventsC),
862
+ )
863
+ // (SELECT * FROM "eventsA") UNION (SELECT * FROM "eventsB") UNION (SELECT * FROM "eventsC")
864
+ ```
865
+
866
+ ## Column shape requirement
867
+
868
+ Every input MUST produce the same column shape. The framework
869
+ doesn't enforce this at TypeScript level — the DB throws at query
870
+ time if shapes don't line up. To narrow each input, use
871
+ `.select(...cols)` on the inner queries so they project the same
872
+ column set.
873
+
874
+ ## Cross-dialect
875
+
876
+ Standard SQL — every dialect we ship supports the four set ops with
877
+ identical syntax. No per-dialect dispatch.
878
+
879
+ ## Reactivity
880
+
881
+ Reactive — coarsely. The engine registers the subscription against
882
+ every branch's source table, so a write to any branch (the UNION /
883
+ INTERSECT / EXCEPT side) re-runs the combined query. It re-queries on
884
+ any contributing-table change rather than pre-filtering per column, so
885
+ keep the branches' result sets bounded.
886
+
887
+ ## See also
888
+
889
+ - [Sub-queries](/docs/database/sub-queries) — `notInSubquery` for
890
+ the column-level "in A but not in B" case
891
+ - [Aggregations](/docs/database/aggregations) — `count()` etc. on
892
+ a set-op result is a common pattern
893
+ - [CTEs](/docs/database/query-builder#ctes) — name a complex set-op
894
+ result so you can reference it in a larger query
895
+
896
+
897
+
898
+ ---
899
+
900
+ <!-- source: en/database/sub-queries.md -->
901
+ ## Sub-queries (IN / NOT IN / EXISTS)
902
+
903
+ _Predicates that reference other queries — col IN (SELECT ...), EXISTS (SELECT ...)._
904
+
905
+ `inSubquery` / `notInSubquery` / `exists` / `notExists` let a WHERE
906
+ predicate reference the results of another query. Use when you'd
907
+ otherwise pull a list of IDs to the app and filter client-side.
908
+
909
+ ## Quick start
910
+
911
+ ```ts
912
+ import { inSubquery, eq, queryFor } from '@voltro/database'
913
+
914
+ // Find every post by an actively-banned user
915
+ const blockedUserIds = queryFor(database.blockedUsers).select('userId')
916
+
917
+ const banned = await ctx.store.query(
918
+ queryFor(database.posts)
919
+ .where(inSubquery('userId', blockedUserIds))
920
+ .descriptor,
921
+ )
922
+ ```
923
+
924
+ Compiles to:
925
+
926
+ ```sql
927
+ SELECT * FROM "posts" WHERE "userId" IN (SELECT "userId" FROM "blockedUsers")
928
+ ```
929
+
930
+ The sub-query runs as part of the same SQL statement — one round-
931
+ trip, the DB optimiser decides whether to materialise the inner set
932
+ or use a hash semi-join.
933
+
934
+ ## `inSubquery` / `notInSubquery`
935
+
936
+ Both expect the sub-query to project a **single column** (use
937
+ `.select('colName')` on the inner query). The predicate matches rows
938
+ whose specified outer column appears (or doesn't) in the inner
939
+ result set.
940
+
941
+ ```ts
942
+ // Not in
943
+ const visibleUsers = await ctx.store.query(
944
+ queryFor(database.users)
945
+ .where(notInSubquery('id', queryFor(database.blockedUsers).select('userId')))
946
+ .descriptor,
947
+ )
948
+ ```
949
+
950
+ **Empty inner set semantics:** `inSubquery` against an empty set
951
+ matches NOTHING (no row's column is "in" an empty list).
952
+ `notInSubquery` against an empty set matches EVERYTHING (every row's
953
+ column is "not in" an empty list). The framework matches SQL
954
+ exactly.
955
+
956
+ ## `exists` / `notExists`
957
+
958
+ Doesn't bind to a specific outer column — the predicate's truth
959
+ depends only on whether the sub-query produces any rows at all.
960
+
961
+ ```ts
962
+ import { exists, queryFor } from '@voltro/database'
963
+
964
+ // Users who have at least one post
965
+ const authors = await ctx.store.query(
966
+ queryFor(database.users)
967
+ .where(exists(queryFor(database.posts).where(eq('userId', 'placeholder'))))
968
+ .descriptor,
969
+ )
970
+ ```
971
+
972
+ `exists` is typically faster than `inSubquery` when the inner set is
973
+ large but you only need yes-or-no — the DB stops after the first
974
+ match.
975
+
976
+ ## Composing with `and` / `or`
977
+
978
+ Sub-query predicates compose with the regular boolean combinators:
979
+
980
+ ```ts
981
+ import { and, or, eq, inSubquery } from '@voltro/database'
982
+
983
+ queryFor(database.users).where(
984
+ and(
985
+ eq('tenantId', tenantId),
986
+ inSubquery('id', queryFor(database.bannedUsers).select('userId')),
987
+ ),
988
+ )
989
+ ```
990
+
991
+ ## Limitations (v1)
992
+
993
+ Non-correlated only. The inner query can NOT reference outer-row
994
+ columns like `WHERE inner.userId = users.id`. For correlated
995
+ sub-queries (a common shape: "user who has at least one post created
996
+ in the last hour") use a [Self-join](/docs/database/self-joins) or
997
+ an [Eager-load](/docs/database/joins#eager-loading) — both can
998
+ express the same query without the correlation reference.
999
+
1000
+ ## Reactivity
1001
+
1002
+ Sub-query predicates ARE reactive. The matcher tracks BOTH the outer
1003
+ descriptor's table AND every sub-query's table. A write to the inner
1004
+ table re-evaluates the outer query.
1005
+
1006
+ In the in-memory store the framework pre-materialises every
1007
+ sub-query before evaluating the outer predicate (one pass per
1008
+ sub-query, not per row). Same fast-path applies to SQL stores via
1009
+ the standard `IN (SELECT ...)` query plan.
1010
+
1011
+ ## Cross-dialect
1012
+
1013
+ `IN (SELECT ...)` and `EXISTS (SELECT ...)` are standard SQL on
1014
+ every dialect we ship. No per-dialect dispatch.
1015
+
1016
+ ## See also
1017
+
1018
+ - [Aggregations](/docs/database/aggregations) — sub-queries paired
1019
+ with `count()` etc. for "count of X where Y belongs to Z"
1020
+ - [Self-joins](/docs/database/self-joins) — when the relationship
1021
+ can be expressed as a join instead
1022
+ - [CTEs](/docs/database/query-builder#ctes) — for naming a
1023
+ sub-query you reuse multiple times in the same outer query
1024
+
1025
+
1026
+
1027
+ ---
1028
+
1029
+ <!-- source: en/database/distinct.md -->
1030
+ ## DISTINCT + DISTINCT ON
1031
+
1032
+ _Dedupe row sets. .distinct() is universal; .distinctOn() picks one row per group._
1033
+
1034
+ `.distinct()` dedups the result row set. `.distinctOn([cols])` picks
1035
+ ONE row per unique value of the listed columns — the "latest per
1036
+ channel" / "best per user" pattern.
1037
+
1038
+ ## `.distinct()` — basic dedup
1039
+
1040
+ ```ts
1041
+ await ctx.store.query(
1042
+ queryFor(database.messages)
1043
+ .select('userId')
1044
+ .distinct()
1045
+ .descriptor,
1046
+ )
1047
+ // → distinct user IDs that have any message
1048
+ ```
1049
+
1050
+ Cross-dialect: standard SQL, supported on every dialect we ship.
1051
+
1052
+ ## `.distinctOn([cols])` — pick the first per group
1053
+
1054
+ ```ts
1055
+ // Latest message per channel
1056
+ await ctx.store.query(
1057
+ queryFor(database.messages)
1058
+ .distinctOn(['channelId'])
1059
+ .orderBy('channelId', 'asc')
1060
+ .orderBy('createdAt', 'desc')
1061
+ .descriptor,
1062
+ )
1063
+ ```
1064
+
1065
+ The order matters: postgres picks the FIRST row per
1066
+ `distinctOn`-column combination AS DETERMINED BY THE FULL `orderBy`.
1067
+ Always set:
1068
+
1069
+ 1. The `distinctOn` columns first in `orderBy`
1070
+ 2. The tie-breaker column second (which row to pick when multiple
1071
+ match — typically a timestamp or id)
1072
+
1073
+ Without that order, postgres still picks one row but the choice is
1074
+ unstable.
1075
+
1076
+ ## Cross-dialect
1077
+
1078
+ - **Postgres** — native `DISTINCT ON` clause.
1079
+ - **MySQL / MariaDB / MSSQL / SQLite** — no native `DISTINCT ON`.
1080
+ The framework falls back to plain `DISTINCT` and logs no warning
1081
+ because the fallback covers most use cases. If you specifically
1082
+ need one-row-per-group semantics on these dialects, use a
1083
+ [window function](/docs/database/aggregations#window-functions)
1084
+ with `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` and
1085
+ filter to `rank = 1` in a sub-query.
1086
+
1087
+ ## When NOT to use `.distinct()`
1088
+
1089
+ Distinct on a row set with multiple non-unique columns is rarely
1090
+ what you want — it dedups by the full row shape, including columns
1091
+ you might not have intended to constrain. If you want one row per
1092
+ some-key, use `.distinctOn([key])` (postgres) or `groupBy([key])`
1093
+ with `aggregate({})` to be explicit.
1094
+
1095
+ ## See also
1096
+
1097
+ - [Aggregations](/docs/database/aggregations) — `.groupBy()` + window
1098
+ functions for cases distinct can't express
1099
+ - [Sub-queries](/docs/database/sub-queries) — `notInSubquery` for
1100
+ "rows not appearing elsewhere"
1101
+
1102
+
1103
+
1104
+ ---
1105
+
1106
+ <!-- source: en/database/self-joins.md -->
1107
+ ## Self-joins + aliased projections
1108
+
1109
+ _Join a table to itself or to a CTE. selectJoined({...}) for aliased projections with full TypeScript inference._
1110
+
1111
+ When you need to reference the same table twice in one query —
1112
+ parent/child trees, before/after comparisons, follower/followee
1113
+ graphs — use `.as(alias)` + `.innerJoin(table, alias, on)`.
1114
+
1115
+ For non-self joins (a table joined to a DIFFERENT table) the
1116
+ preferred pattern is [eager-loading via relations](/docs/database/joins).
1117
+ This page covers the cases where eager-load doesn't fit:
1118
+ self-joins, joining against a CTE, or joining against a dynamically-
1119
+ named table.
1120
+
1121
+ ## Quick start — self-join
1122
+
1123
+ ```ts
1124
+ import { eq, queryFor } from '@voltro/database'
1125
+
1126
+ // Posts and their parent posts
1127
+ const tree = await ctx.store.query(
1128
+ queryFor(database.posts).as('children')
1129
+ .innerJoin(database.posts, 'parents', eq('parents.id', 'children.parentId'))
1130
+ .selectJoined({
1131
+ childId: 'children.id',
1132
+ childTitle: 'children.title',
1133
+ parentTitle: 'parents.title',
1134
+ })
1135
+ .descriptor,
1136
+ )
1137
+ // rows: Array<{ childId: string; childTitle: string; parentTitle: string }>
1138
+ ```
1139
+
1140
+ Compiles to:
1141
+
1142
+ ```sql
1143
+ SELECT
1144
+ "children"."id" AS "childId",
1145
+ "children"."title" AS "childTitle",
1146
+ "parents"."title" AS "parentTitle"
1147
+ FROM "posts" AS "children"
1148
+ INNER JOIN "posts" AS "parents" ON "parents"."id" = "children"."parentId"
1149
+ ```
1150
+
1151
+ ## `.as(alias)` — aliasing the FROM table
1152
+
1153
+ `.as('children')` emits `FROM "posts" AS "children"`. Required when
1154
+ combining with `.innerJoin(...)` so the JOIN's ON condition can
1155
+ disambiguate columns from both sides.
1156
+
1157
+ ## `.innerJoin()` vs `.leftJoin()`
1158
+
1159
+ Same shape, different semantics:
1160
+
1161
+ - `.innerJoin(target, alias, on)` — drops rows from the FROM side
1162
+ that have no JOIN-side match.
1163
+ - `.leftJoin(target, alias, on)` — keeps rows from the FROM side
1164
+ even when no JOIN-side match exists. The aliased row's columns
1165
+ will be `null` for those rows.
1166
+
1167
+ Both have **two overloads**:
1168
+
1169
+ ### Pass a `Table` descriptor → full type inference
1170
+
1171
+ ```ts
1172
+ queryFor(database.posts).as('children')
1173
+ .innerJoin(database.posts, 'parents', eq('parents.id', 'children.parentId'))
1174
+ .selectJoined({
1175
+ parentTitle: 'parents.title', // ← type inferred as string
1176
+ })
1177
+ // rows[0].parentTitle is typed `string`, not `unknown`
1178
+ ```
1179
+
1180
+ The framework tracks the joined Table's row type in a hidden `Joins`
1181
+ type parameter. `.selectJoined({...})` reads it to resolve each
1182
+ `'alias.column'` source to the joined column's actual type.
1183
+
1184
+ ### Pass a string name → opaque type fallback
1185
+
1186
+ ```ts
1187
+ queryFor(database.posts).as('children')
1188
+ .innerJoin('some_cte', 'cte', eq('cte.userId', 'children.userId'))
1189
+ .selectJoined({
1190
+ fromCte: 'cte.value', // ← type stays `unknown`
1191
+ })
1192
+ ```
1193
+
1194
+ Use the string form for CTE references (the framework doesn't have
1195
+ the CTE's row type at compile time) or when joining against a name
1196
+ that's only known at runtime.
1197
+
1198
+ ## `.selectJoined({...})` — picking columns
1199
+
1200
+ The chain method has TWO mutually-exclusive projection forms:
1201
+
1202
+ - `.select('col1', 'col2')` — for FROM-side rows only. Result row
1203
+ type narrows to `Pick<RowOf, 'col1' | 'col2'>`.
1204
+ - `.selectJoined({outKey: 'alias.col', ...})` — for queries with
1205
+ joins. Each spec entry maps a `'<alias>.<column>'` source to an
1206
+ output key on the result row.
1207
+
1208
+ Mixing isn't supported; pick one. The framework defaults to
1209
+ `SELECT *` when neither is set (returns the FROM-side row).
1210
+
1211
+ ## Reading without `.selectJoined`
1212
+
1213
+ If you skip `.selectJoined`, the result row stays the FROM-side row
1214
+ type. The joined columns ARE on the row at runtime (postgres returns
1215
+ them flat with `alias.col` keys) but TypeScript doesn't see them.
1216
+ For ad-hoc reads:
1217
+
1218
+ ```ts
1219
+ const rows = await ctx.store.query(
1220
+ queryFor(database.posts).as('c')
1221
+ .innerJoin(database.posts, 'p', eq('p.id', 'c.parentId'))
1222
+ .descriptor,
1223
+ )
1224
+ // rows[0] is typed Post
1225
+ // rows[0]['p.title'] exists at runtime but needs an explicit cast
1226
+ ```
1227
+
1228
+ Use `.selectJoined({...})` for the typed path; the cast escape-hatch
1229
+ is for one-off reads.
1230
+
1231
+ ## Cross-dialect
1232
+
1233
+ Standard SQL — `INNER JOIN ... AS ... ON ...` is supported on every
1234
+ dialect we ship with identical syntax.
1235
+
1236
+ ## When NOT to use this
1237
+
1238
+ - **Joining different tables related via FK** — use
1239
+ [eager-loading](/docs/database/joins) with relations + `.with({})`.
1240
+ More ergonomic, gives you the nested-object result shape, and
1241
+ reactively subscribes to the joined tables.
1242
+ - **Walking trees more than 1 level deep** — use a
1243
+ [recursive CTE](/docs/database/recursive-cte). Self-join only
1244
+ covers parent + immediate child.
1245
+
1246
+ ## See also
1247
+
1248
+ - [Joins (eager-load)](/docs/database/joins) — the preferred way
1249
+ for relations-based joins
1250
+ - [Recursive CTE](/docs/database/recursive-cte) — for multi-level
1251
+ tree traversal
1252
+ - [Sub-queries](/docs/database/sub-queries) — for the
1253
+ "rows where column matches another query" pattern
1254
+
1255
+
1256
+
1257
+ ---
1258
+
1259
+ <!-- source: en/database/aggregations.md -->
1260
+ ## Aggregations (count / sum / avg / min / max + GROUP BY + window functions)
1261
+
1262
+ _On-demand counts, sums, group-by, having, and window functions — without escaping to raw SQL._
1263
+
1264
+ The query builder's `.count()` / `.aggregate({...})` / `.groupBy()` /
1265
+ `.having()` covers nearly every analytic query a typical SaaS app
1266
+ needs without raw SQL. Window functions (`rowNumber`, `rank`, `lag`,
1267
+ `lead`, `sumOver`) live in the same surface for percentile-style and
1268
+ running-total reads.
1269
+
1270
+ These are **on-demand reads** — they run when called, not on a
1271
+ schedule. For pre-computed read models that refresh periodically see
1272
+ [Aggregates](/docs/data/aggregates) (the `*.aggregate.ts` file
1273
+ convention).
1274
+
1275
+ Live — an on-demand aggregation as a computed reactive query: add or toggle a
1276
+ todo and the `{ open, done, total }` counts update with no refetch:
1277
+
1278
+ ```tsx
1279
+ const stats = useSubscription('app', 'todos.stats') // reactive count roll-up
1280
+ ```
1281
+
1282
+ ## Quick start
1283
+
1284
+ ```ts
1285
+ import { count, sum, avg, max, eq, queryFor } from '@voltro/database'
1286
+
1287
+ // How many open todos does this user have right now?
1288
+ const rows = await ctx.store.query(
1289
+ queryFor(database.todos).where(eq('done', false)).count().descriptor,
1290
+ )
1291
+ const open = rows[0]!.count // → number
1292
+ ```
1293
+
1294
+ Result rows from any aggregate are always **an array with one entry
1295
+ per group** (or exactly one entry when there's no `groupBy`). Reach
1296
+ in via `[0]` for the ungrouped case.
1297
+
1298
+ ## `count()` — the most common case
1299
+
1300
+ ```ts
1301
+ // Count every row in the filtered set
1302
+ queryFor(database.posts).where(eq('userId', uid)).count()
1303
+
1304
+ // Count distinct values of a column
1305
+ import { countDistinct } from '@voltro/database'
1306
+ queryFor(database.posts).aggregate({
1307
+ authors: countDistinct('userId'),
1308
+ })
1309
+ ```
1310
+
1311
+ `count()` defaults to `COUNT(*)` — every matching row, including
1312
+ NULLs. `countDistinct(column)` emits `COUNT(DISTINCT column)`.
1313
+
1314
+ ## `aggregate({...})` — bundle multiple aggregates in one query
1315
+
1316
+ ```ts
1317
+ import { count, sum, avg, max, min } from '@voltro/database'
1318
+
1319
+ const rows = await ctx.store.query(
1320
+ queryFor(database.orders).where(eq('orgId', oid)).aggregate({
1321
+ total: sum('amount'),
1322
+ avgAmt: avg('amount'),
1323
+ peak: max('createdAt'),
1324
+ earliest: min('createdAt'),
1325
+ cnt: count(),
1326
+ }).descriptor,
1327
+ )
1328
+
1329
+ const stats = rows[0]!
1330
+ // { total: 12_345, avgAmt: 89.5, peak: Date, earliest: Date, cnt: 138 }
1331
+ ```
1332
+
1333
+ The keys of the spec map become the column names on the result row.
1334
+ Each value is a helper:
1335
+
1336
+ | Helper | SQL | Returns |
1337
+ |---------------------------------|---------------------------|---------|
1338
+ | `count()` | `COUNT(*)` | number |
1339
+ | `count('col')` | `COUNT(col)` (non-null) | number |
1340
+ | `countDistinct('col')` | `COUNT(DISTINCT col)` | number |
1341
+ | `sum('col')` | `SUM(col)` | number / null |
1342
+ | `avg('col')` | `AVG(col)` | number / null |
1343
+ | `min('col')` | `MIN(col)` | column type / null |
1344
+ | `max('col')` | `MAX(col)` | column type / null |
1345
+
1346
+ **SQL semantics on empty result sets**: `count` returns 0;
1347
+ `sum/avg/min/max` return `null`. The framework matches this — don't
1348
+ write `if (rows.length === 0)` defensive code, the row is always
1349
+ there.
1350
+
1351
+ ## Fast path: `.exists()` for "is there any?"
1352
+
1353
+ When you only need a yes/no, avoid `count() > 0` — `.exists()`
1354
+ short-circuits with `SELECT 1 ... LIMIT 1`:
1355
+
1356
+ ```ts
1357
+ const rows = await ctx.store.query(
1358
+ queryFor(database.users).where(eq('email', e)).exists().descriptor,
1359
+ )
1360
+ const emailTaken = rows[0]!.exists // → boolean
1361
+ ```
1362
+
1363
+ On a 10M-row table this is the difference between an index-only scan
1364
+ that stops at the first match and a full count.
1365
+
1366
+ ## `groupBy(cols)` — one row per group
1367
+
1368
+ ```ts
1369
+ const rows = await ctx.store.query(
1370
+ queryFor(database.orders).where(eq('orgId', oid))
1371
+ .groupBy(['status'])
1372
+ .aggregate({ cnt: count(), total: sum('amount') })
1373
+ .descriptor,
1374
+ )
1375
+ // rows: [{ cnt: 12, total: 4500 }, { cnt: 5, total: 1800 }] — one row per group
1376
+ ```
1377
+
1378
+ > **Project the group key with `column()`.** By default the result rows
1379
+ > carry the **aggregate aliases only**. To fold a `GROUP BY` key column
1380
+ > into the same row, add a `column(name)` entry to the spec — it must also
1381
+ > appear in `.groupBy([...])` (standard SQL):
1382
+ >
1383
+ > ```ts
1384
+ > import { column, count, sum } from '@voltro/database'
1385
+ >
1386
+ > queryFor(database.orders).where(eq('orgId', oid))
1387
+ > .groupBy(['status'])
1388
+ > .aggregate({ status: column<string>('status'), cnt: count(), total: sum('amount') })
1389
+ > // rows: [{ status: 'open', cnt: 12, total: 4500 }, …]
1390
+ > ```
1391
+ > Annotate the type (`column<string>(...)`) for a precise result row, or
1392
+ > rely on the default `string | number | boolean | Date | null` union.
1393
+
1394
+ Chained `.groupBy()` calls append columns — group by `orgId × status`:
1395
+
1396
+ ```ts
1397
+ queryFor(database.orders)
1398
+ .groupBy(['orgId'])
1399
+ .groupBy(['status'])
1400
+ .aggregate({ cnt: count() })
1401
+ ```
1402
+
1403
+ Group-by works without `aggregate()` too (returns distinct combos),
1404
+ but the typical pattern is grouping + aggregating together.
1405
+
1406
+ ## `having(predicate)` — filter groups after aggregation
1407
+
1408
+ `having` predicates run AFTER aggregation; they reference aggregate
1409
+ aliases, not raw columns. Distinct from `.where()` (which becomes
1410
+ `WHERE` and runs BEFORE grouping):
1411
+
1412
+ ```ts
1413
+ import { gt } from '@voltro/database'
1414
+
1415
+ interface PostCount { userId: string; postCount: number }
1416
+
1417
+ // "How many users posted more than 10 times in the last 30 days"
1418
+ const heavy = await ctx.store.query(
1419
+ queryFor(database.posts)
1420
+ .where(gt('createdAt', daysAgo(30))) // WHERE — pre-aggregate
1421
+ .groupBy(['userId'])
1422
+ .having(gt<PostCount, 'postCount'>('postCount', 10)) // HAVING — post-aggregate
1423
+ .aggregate({ postCount: count() })
1424
+ .descriptor,
1425
+ )
1426
+ // rows: [{ postCount: 14 }, { postCount: 11 }, …] — one per qualifying user
1427
+ ```
1428
+
1429
+ Chained `.having()` AND-merges, same as `.where()`.
1430
+
1431
+ ## Window functions
1432
+
1433
+ Window functions compute per-row aggregates over a "window" of rows
1434
+ without collapsing the row set — different from `groupBy`, which
1435
+ collapses. Use for ranking, running totals, "previous-row" deltas.
1436
+
1437
+ ### `rowNumber()` / `rank()` / `denseRank()`
1438
+
1439
+ ```ts
1440
+ import { rank, queryFor } from '@voltro/database'
1441
+
1442
+ const ranked = await ctx.store.query(
1443
+ queryFor(database.players).aggregate({
1444
+ rank: rank().over({
1445
+ partitionBy: ['teamId'],
1446
+ orderBy: [{ column: 'score', direction: 'desc' }],
1447
+ }),
1448
+ }).descriptor,
1449
+ )
1450
+ // Each row: { rank } — rank restarts at 1 per team
1451
+ ```
1452
+
1453
+ > **Note:** to carry a plain column (`id`, `name`) alongside the window
1454
+ > expression, add a `column(name)` entry to the spec — it projects
1455
+ > `"col" AS "alias"` into the same result row.
1456
+
1457
+ `rowNumber()` always returns sequential 1, 2, 3 — ties get distinct
1458
+ numbers. `rank()` gives ties the same number, then skips
1459
+ (1, 1, 3, 4…). `denseRank()` gives ties the same number, no skip
1460
+ (1, 1, 2, 3…).
1461
+
1462
+ ### `lag()` / `lead()` — previous / next row
1463
+
1464
+ ```ts
1465
+ import { lag, lead } from '@voltro/database'
1466
+
1467
+ queryFor(database.scores).aggregate({
1468
+ prevScore: lag('score').over({ partitionBy: ['userId'], orderBy: [{ column: 'createdAt', direction: 'asc' }] }),
1469
+ nextScore: lead('score').over({ partitionBy: ['userId'], orderBy: [{ column: 'createdAt', direction: 'asc' }] }),
1470
+ })
1471
+ ```
1472
+
1473
+ Both accept an optional `offset` — `lag('score', 3)` looks 3 rows
1474
+ back.
1475
+
1476
+ ### `sumOver()` / `avgOver()` — running totals
1477
+
1478
+ ```ts
1479
+ import { sumOver } from '@voltro/database'
1480
+
1481
+ queryFor(database.ledger).aggregate({
1482
+ runningSum: sumOver('amount').over({
1483
+ orderBy: [{ column: 'createdAt', direction: 'asc' }],
1484
+ }),
1485
+ })
1486
+ // Each row: running total of `amount` up to and including this row
1487
+ ```
1488
+
1489
+ Combine `partitionBy` + `orderBy` for per-group running totals
1490
+ ("total spend per user, sorted by date").
1491
+
1492
+ ## Reactivity
1493
+
1494
+ `count()` and `aggregate()` subscriptions ARE reactive. The
1495
+ dispatcher subscribes to the source table; any write triggers a
1496
+ re-run of the same aggregate SQL.
1497
+
1498
+ Window-function queries are reactive — coarsely. The matcher can't
1499
+ bucket on a window result (a rank shifts when ANY partition peer
1500
+ changes), so the engine widens the dependency to the whole source
1501
+ table: a write to it re-runs the query. That's correct but coarser
1502
+ than a plain query's per-field pre-filter — every write to the source
1503
+ table re-evaluates the window. Keep the source set bounded (a top-N
1504
+ leaderboard, not a 10M-row scan), or materialise it on a schedule with
1505
+ an [aggregate](/docs/data/aggregates).
1506
+
1507
+ ## Cross-dialect
1508
+
1509
+ Standard SQL — postgres, mysql 8+, mariadb 10.2+, mssql, sqlite 3.25+, turso all support these forms with identical syntax. The framework's
1510
+ compiler doesn't dispatch per dialect for any of the helpers above.
1511
+
1512
+ ## See also
1513
+
1514
+ - [Aggregates (scheduled)](/docs/data/aggregates) — `*.aggregate.ts`
1515
+ for precomputed read models that refresh periodically
1516
+ - [Query builder](/docs/database/query-builder) — the chain methods
1517
+ these aggregates compose with (where, orderBy, limit, etc.)
1518
+ - [Sub-queries](/docs/database/sub-queries) — `inSubquery` /
1519
+ `exists` for predicates that reference other tables
1520
+ - [Set operations](/docs/database/set-operations) — UNION / INTERSECT /
1521
+ EXCEPT for combining multiple aggregates
1522
+
1523
+
1524
+
1525
+ ---
1526
+
1527
+ <!-- source: en/database/views.md -->
1528
+ ## SQL views
1529
+
1530
+ _Declare a read-only SQL VIEW with view(name, columns, select) — discovered + applied by the migrator, queried by-name like a table._
1531
+
1532
+ A **view** is a named, server-side `SELECT` you query by name exactly like a table, but which is never written to. Declare one with `view(name, columns, select)`: `columns` describes the projected row shape (types the result + drives the read decoder), and `select` is the raw SELECT body emitted verbatim into `CREATE VIEW`.
1533
+
1534
+ ```ts
1535
+ import { view, id, text, boolean, timestamp } from '@voltro/database'
1536
+
1537
+ export const activeUsers = view(
1538
+ 'active_users',
1539
+ {
1540
+ id: id(),
1541
+ email: text(),
1542
+ active: boolean(),
1543
+ createdAt: timestamp(),
1544
+ },
1545
+ `SELECT id, email, active, created_at AS "createdAt"
1546
+ FROM users
1547
+ WHERE deleted_at IS NULL`,
1548
+ )
1549
+ ```
1550
+
1551
+ The migrator discovers the view alongside your tables and emits it **after** the base tables it reads from — no ordering wiring needed. Pass it into the schema entity list the same way you pass tables.
1552
+
1553
+ ## Querying a view
1554
+
1555
+ A view is read-only. Query it with `queryForView(...)`, which returns a query with the full `.where(...)` / `.orderBy(...)` / `.take(...)` / `.with(...)` surface but **no** mutation path (there's no INSERT/UPDATE/DELETE on a view).
1556
+
1557
+ ```ts
1558
+ import { queryForView, eq } from '@voltro/database'
1559
+
1560
+ const rows = await ctx.store.query(
1561
+ queryForView(activeUsers).where(eq('email', someEmail)),
1562
+ )
1563
+ ```
1564
+
1565
+ The projected columns are decoded to their canonical JS shapes — a `boolean()` projection comes back a real boolean, a `json<T>()` projection a parsed object, a `decimal()` projection a string — the same read codec that runs for tables.
1566
+
1567
+ ## Idempotent per dialect
1568
+
1569
+ `CREATE VIEW` is emitted idempotently so re-running a migration is a no-op:
1570
+
1571
+ | Dialect | Emission |
1572
+ |---|---|
1573
+ | Postgres / MySQL / MariaDB | `CREATE OR REPLACE VIEW` |
1574
+ | MSSQL | `CREATE OR ALTER VIEW` (SQL Server 2016 SP1+) |
1575
+ | SQLite / Turso | `DROP VIEW IF EXISTS` + `CREATE VIEW` (no `CREATE OR REPLACE`; a view holds no data, so dropping is free) |
1576
+
1577
+ ## When to use a view
1578
+
1579
+ - Collapse a recurring filter/join into a named entity your handlers query directly (`active_users`, `open_orders`).
1580
+ - Expose a stable read shape while the underlying tables evolve.
1581
+ - Hand a reporting/read path a denormalized projection without duplicating the join logic in every query.
1582
+
1583
+ You own the SELECT body's cross-dialect portability — the framework emits it verbatim, the same contract as a `raw()` column or an `expressionIndex(...)` expression. Keep to standard SQL, or gate dialect-specific views behind your deployment's known backend.