@supatype/cli 0.1.6 → 0.1.9

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 (477) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +152 -102
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/LICENSE +202 -0
  5. package/deno.d.ts +52 -0
  6. package/dist/app/framework.js +1 -1
  7. package/dist/app/framework.js.map +1 -1
  8. package/dist/augmentation-generator.d.ts +22 -0
  9. package/dist/augmentation-generator.d.ts.map +1 -1
  10. package/dist/augmentation-generator.js +19 -5
  11. package/dist/augmentation-generator.js.map +1 -1
  12. package/dist/binary-cache.d.ts +42 -6
  13. package/dist/binary-cache.d.ts.map +1 -1
  14. package/dist/binary-cache.js +76 -44
  15. package/dist/binary-cache.js.map +1 -1
  16. package/dist/cache-clean.d.ts +15 -0
  17. package/dist/cache-clean.d.ts.map +1 -0
  18. package/dist/cache-clean.js +76 -0
  19. package/dist/cache-clean.js.map +1 -0
  20. package/dist/cache-pins.d.ts +51 -0
  21. package/dist/cache-pins.d.ts.map +1 -0
  22. package/dist/cache-pins.js +184 -0
  23. package/dist/cache-pins.js.map +1 -0
  24. package/dist/cli-artifacts.d.ts +32 -0
  25. package/dist/cli-artifacts.d.ts.map +1 -0
  26. package/dist/cli-artifacts.js +49 -0
  27. package/dist/cli-artifacts.js.map +1 -0
  28. package/dist/cli-install-method.d.ts +27 -0
  29. package/dist/cli-install-method.d.ts.map +1 -0
  30. package/dist/cli-install-method.js +78 -0
  31. package/dist/cli-install-method.js.map +1 -0
  32. package/dist/cli-package-version.d.ts +1 -1
  33. package/dist/cli-package-version.d.ts.map +1 -1
  34. package/dist/cli-package-version.js +6 -1
  35. package/dist/cli-package-version.js.map +1 -1
  36. package/dist/cli-standalone.d.ts +13 -4
  37. package/dist/cli-standalone.d.ts.map +1 -1
  38. package/dist/cli-standalone.js +86 -31
  39. package/dist/cli-standalone.js.map +1 -1
  40. package/dist/cli-version-embedded.d.ts +10 -0
  41. package/dist/cli-version-embedded.d.ts.map +1 -0
  42. package/dist/cli-version-embedded.js +13 -0
  43. package/dist/cli-version-embedded.js.map +1 -0
  44. package/dist/cli.d.ts.map +1 -1
  45. package/dist/cli.js +7 -1
  46. package/dist/cli.js.map +1 -1
  47. package/dist/cloud-credentials.d.ts +34 -0
  48. package/dist/cloud-credentials.d.ts.map +1 -0
  49. package/dist/cloud-credentials.js +139 -0
  50. package/dist/cloud-credentials.js.map +1 -0
  51. package/dist/commands/add.js +1 -1
  52. package/dist/commands/add.js.map +1 -1
  53. package/dist/commands/admin.d.ts +11 -0
  54. package/dist/commands/admin.d.ts.map +1 -1
  55. package/dist/commands/admin.js +123 -21
  56. package/dist/commands/admin.js.map +1 -1
  57. package/dist/commands/adopt.js +1 -1
  58. package/dist/commands/adopt.js.map +1 -1
  59. package/dist/commands/app.js +2 -2
  60. package/dist/commands/app.js.map +1 -1
  61. package/dist/commands/cache.d.ts +1 -1
  62. package/dist/commands/cache.d.ts.map +1 -1
  63. package/dist/commands/cache.js +69 -73
  64. package/dist/commands/cache.js.map +1 -1
  65. package/dist/commands/cloud.d.ts.map +1 -1
  66. package/dist/commands/cloud.js +32 -8
  67. package/dist/commands/cloud.js.map +1 -1
  68. package/dist/commands/db.d.ts +3 -2
  69. package/dist/commands/db.d.ts.map +1 -1
  70. package/dist/commands/db.js +149 -3
  71. package/dist/commands/db.js.map +1 -1
  72. package/dist/commands/deploy.d.ts +9 -9
  73. package/dist/commands/deploy.js +12 -12
  74. package/dist/commands/deploy.js.map +1 -1
  75. package/dist/commands/dev.d.ts +3 -3
  76. package/dist/commands/dev.d.ts.map +1 -1
  77. package/dist/commands/dev.js +63 -23
  78. package/dist/commands/dev.js.map +1 -1
  79. package/dist/commands/doctor.d.ts +28 -0
  80. package/dist/commands/doctor.d.ts.map +1 -1
  81. package/dist/commands/doctor.js +60 -3
  82. package/dist/commands/doctor.js.map +1 -1
  83. package/dist/commands/engine.d.ts +1 -1
  84. package/dist/commands/engine.js +1 -1
  85. package/dist/commands/functions.d.ts +7 -0
  86. package/dist/commands/functions.d.ts.map +1 -1
  87. package/dist/commands/functions.js +45 -11
  88. package/dist/commands/functions.js.map +1 -1
  89. package/dist/commands/generate.d.ts.map +1 -1
  90. package/dist/commands/generate.js +5 -1
  91. package/dist/commands/generate.js.map +1 -1
  92. package/dist/commands/hooks.d.ts +12 -0
  93. package/dist/commands/hooks.d.ts.map +1 -0
  94. package/dist/commands/hooks.js +118 -0
  95. package/dist/commands/hooks.js.map +1 -0
  96. package/dist/commands/init.d.ts.map +1 -1
  97. package/dist/commands/init.js +64 -30
  98. package/dist/commands/init.js.map +1 -1
  99. package/dist/commands/internal.d.ts +6 -0
  100. package/dist/commands/internal.d.ts.map +1 -0
  101. package/dist/commands/internal.js +22 -0
  102. package/dist/commands/internal.js.map +1 -0
  103. package/dist/commands/link-helpers.d.ts.map +1 -1
  104. package/dist/commands/link-helpers.js +8 -2
  105. package/dist/commands/link-helpers.js.map +1 -1
  106. package/dist/commands/login.d.ts +7 -0
  107. package/dist/commands/login.d.ts.map +1 -0
  108. package/dist/commands/login.js +110 -0
  109. package/dist/commands/login.js.map +1 -0
  110. package/dist/commands/logs.d.ts +1 -1
  111. package/dist/commands/migrate.js +5 -5
  112. package/dist/commands/migrate.js.map +1 -1
  113. package/dist/commands/pg.d.ts +1 -1
  114. package/dist/commands/pg.js +1 -1
  115. package/dist/commands/plugins.js +1 -1
  116. package/dist/commands/plugins.js.map +1 -1
  117. package/dist/commands/pull.js +2 -2
  118. package/dist/commands/pull.js.map +1 -1
  119. package/dist/commands/push.d.ts.map +1 -1
  120. package/dist/commands/push.js +110 -7
  121. package/dist/commands/push.js.map +1 -1
  122. package/dist/commands/self-host.d.ts +1 -1
  123. package/dist/commands/self-host.js +1 -1
  124. package/dist/commands/self-update.d.ts +3 -2
  125. package/dist/commands/self-update.d.ts.map +1 -1
  126. package/dist/commands/self-update.js +31 -16
  127. package/dist/commands/self-update.js.map +1 -1
  128. package/dist/commands/status.d.ts +1 -1
  129. package/dist/commands/update.d.ts +1 -1
  130. package/dist/commands/update.js +1 -1
  131. package/dist/config.d.ts +1 -1
  132. package/dist/config.d.ts.map +1 -1
  133. package/dist/config.js +19 -4
  134. package/dist/config.js.map +1 -1
  135. package/dist/db-preflight.d.ts +78 -0
  136. package/dist/db-preflight.d.ts.map +1 -0
  137. package/dist/db-preflight.js +452 -0
  138. package/dist/db-preflight.js.map +1 -0
  139. package/dist/dev-compose.d.ts +4 -4
  140. package/dist/dev-compose.d.ts.map +1 -1
  141. package/dist/dev-compose.js +143 -88
  142. package/dist/dev-compose.js.map +1 -1
  143. package/dist/dev-log-filter.d.ts +1 -1
  144. package/dist/dev-log-filter.d.ts.map +1 -1
  145. package/dist/dev-log-filter.js +1 -1
  146. package/dist/dev-log-filter.js.map +1 -1
  147. package/dist/dev-ports.d.ts +2 -2
  148. package/dist/dev-ports.d.ts.map +1 -1
  149. package/dist/dev-ports.js +2 -2
  150. package/dist/dev-ports.js.map +1 -1
  151. package/dist/dev-session.d.ts +5 -5
  152. package/dist/dev-session.d.ts.map +1 -1
  153. package/dist/dev-session.js +6 -6
  154. package/dist/dev-session.js.map +1 -1
  155. package/dist/dev-shutdown.d.ts +5 -5
  156. package/dist/dev-shutdown.d.ts.map +1 -1
  157. package/dist/dev-shutdown.js +7 -7
  158. package/dist/dev-shutdown.js.map +1 -1
  159. package/dist/dev-task-colors.d.ts +1 -1
  160. package/dist/dev-task-colors.d.ts.map +1 -1
  161. package/dist/dev-task-colors.js +1 -1
  162. package/dist/dev-task-colors.js.map +1 -1
  163. package/dist/docker-postgres.d.ts +5 -5
  164. package/dist/docker-postgres.d.ts.map +1 -1
  165. package/dist/docker-postgres.js +5 -5
  166. package/dist/docker-postgres.js.map +1 -1
  167. package/dist/docker-runtime.d.ts +2 -2
  168. package/dist/docker-runtime.d.ts.map +1 -1
  169. package/dist/docker-runtime.js +5 -5
  170. package/dist/docker-runtime.js.map +1 -1
  171. package/dist/engine-client.d.ts +1 -1
  172. package/dist/engine-client.js +3 -3
  173. package/dist/engine-client.js.map +1 -1
  174. package/dist/engine-push-output.js +1 -1
  175. package/dist/engine-push-output.js.map +1 -1
  176. package/dist/ensure-binary.d.ts.map +1 -1
  177. package/dist/ensure-binary.js +17 -0
  178. package/dist/ensure-binary.js.map +1 -1
  179. package/dist/ensure-component-binaries.js +1 -1
  180. package/dist/ensure-component-binaries.js.map +1 -1
  181. package/dist/env-file.d.ts +40 -2
  182. package/dist/env-file.d.ts.map +1 -1
  183. package/dist/env-file.js +132 -11
  184. package/dist/env-file.js.map +1 -1
  185. package/dist/field-masking-tier.d.ts +50 -0
  186. package/dist/field-masking-tier.d.ts.map +1 -0
  187. package/dist/field-masking-tier.js +91 -0
  188. package/dist/field-masking-tier.js.map +1 -0
  189. package/dist/functions-deno-types.d.ts +24 -0
  190. package/dist/functions-deno-types.d.ts.map +1 -0
  191. package/dist/functions-deno-types.js +155 -0
  192. package/dist/functions-deno-types.js.map +1 -0
  193. package/dist/functions-router-gen.js +1 -1
  194. package/dist/gitignore.d.ts +2 -2
  195. package/dist/gitignore.d.ts.map +1 -1
  196. package/dist/gitignore.js +3 -3
  197. package/dist/gitignore.js.map +1 -1
  198. package/dist/hook-upload.d.ts +30 -0
  199. package/dist/hook-upload.d.ts.map +1 -0
  200. package/dist/hook-upload.js +86 -0
  201. package/dist/hook-upload.js.map +1 -0
  202. package/dist/hooks-generator.d.ts +8 -0
  203. package/dist/hooks-generator.d.ts.map +1 -0
  204. package/dist/hooks-generator.js +332 -0
  205. package/dist/hooks-generator.js.map +1 -0
  206. package/dist/index.d.ts +1 -1
  207. package/dist/index.js +1 -1
  208. package/dist/init-project-detect.d.ts +1 -1
  209. package/dist/init-project-detect.d.ts.map +1 -1
  210. package/dist/init-project-detect.js +1 -1
  211. package/dist/init-project-detect.js.map +1 -1
  212. package/dist/link.d.ts +4 -0
  213. package/dist/link.d.ts.map +1 -1
  214. package/dist/link.js +2 -0
  215. package/dist/link.js.map +1 -1
  216. package/dist/local-secrets.d.ts +71 -0
  217. package/dist/local-secrets.d.ts.map +1 -0
  218. package/dist/local-secrets.js +100 -0
  219. package/dist/local-secrets.js.map +1 -0
  220. package/dist/main.d.ts +2 -0
  221. package/dist/main.d.ts.map +1 -0
  222. package/dist/main.js +20 -0
  223. package/dist/main.js.map +1 -0
  224. package/dist/model-hooks.d.ts +73 -0
  225. package/dist/model-hooks.d.ts.map +1 -0
  226. package/dist/model-hooks.js +211 -0
  227. package/dist/model-hooks.js.map +1 -0
  228. package/dist/pgbouncer-userlist.d.ts +1 -1
  229. package/dist/pgbouncer-userlist.d.ts.map +1 -1
  230. package/dist/pgbouncer-userlist.js +2 -2
  231. package/dist/pgbouncer-userlist.js.map +1 -1
  232. package/dist/postgres-ctl.d.ts +12 -1
  233. package/dist/postgres-ctl.d.ts.map +1 -1
  234. package/dist/postgres-ctl.js +28 -5
  235. package/dist/postgres-ctl.js.map +1 -1
  236. package/dist/process-manager.d.ts +2 -2
  237. package/dist/process-manager.d.ts.map +1 -1
  238. package/dist/process-manager.js +4 -4
  239. package/dist/process-manager.js.map +1 -1
  240. package/dist/project-config.d.ts +122 -2
  241. package/dist/project-config.d.ts.map +1 -1
  242. package/dist/project-config.js +150 -1
  243. package/dist/project-config.js.map +1 -1
  244. package/dist/pull-utils.d.ts +2 -2
  245. package/dist/pull-utils.d.ts.map +1 -1
  246. package/dist/pull-utils.js +4 -4
  247. package/dist/pull-utils.js.map +1 -1
  248. package/dist/realtime-launch.js +1 -1
  249. package/dist/realtime-launch.js.map +1 -1
  250. package/dist/release-pins.d.ts +1 -1
  251. package/dist/release-pins.js +2 -2
  252. package/dist/release-pins.js.map +1 -1
  253. package/dist/resolve-api-url.js +1 -1
  254. package/dist/resolve-api-url.js.map +1 -1
  255. package/dist/resolve-target.d.ts +4 -0
  256. package/dist/resolve-target.d.ts.map +1 -1
  257. package/dist/resolve-target.js +69 -73
  258. package/dist/resolve-target.js.map +1 -1
  259. package/dist/rest-cache-admin.js +2 -2
  260. package/dist/rest-cache-admin.js.map +1 -1
  261. package/dist/runtime-routes.d.ts +1 -1
  262. package/dist/runtime-routes.js +1 -1
  263. package/dist/schema-ast-v2.d.ts +5 -4
  264. package/dist/schema-ast-v2.d.ts.map +1 -1
  265. package/dist/schema-ast-v2.js +5 -3
  266. package/dist/schema-ast-v2.js.map +1 -1
  267. package/dist/schema-push-lock.d.ts.map +1 -1
  268. package/dist/schema-push-lock.js +43 -16
  269. package/dist/schema-push-lock.js.map +1 -1
  270. package/dist/scripts/postinstall.d.ts +1 -1
  271. package/dist/scripts/postinstall.js +1 -1
  272. package/dist/self-host-compose.d.ts +26 -2
  273. package/dist/self-host-compose.d.ts.map +1 -1
  274. package/dist/self-host-compose.js +305 -73
  275. package/dist/self-host-compose.js.map +1 -1
  276. package/dist/service-role-check.d.ts +21 -0
  277. package/dist/service-role-check.d.ts.map +1 -0
  278. package/dist/service-role-check.js +85 -0
  279. package/dist/service-role-check.js.map +1 -0
  280. package/dist/storage-provision.d.ts +1 -1
  281. package/dist/storage-provision.js +3 -3
  282. package/dist/storage-provision.js.map +1 -1
  283. package/dist/studio-dev-server.d.ts +1 -1
  284. package/dist/studio-dev-server.d.ts.map +1 -1
  285. package/dist/systemd.d.ts +1 -1
  286. package/dist/systemd.js +1 -1
  287. package/dist/target-client.d.ts +17 -0
  288. package/dist/target-client.d.ts.map +1 -1
  289. package/dist/target-client.js +69 -17
  290. package/dist/target-client.js.map +1 -1
  291. package/dist/tsx-runner.js +1 -1
  292. package/dist/tsx-runner.js.map +1 -1
  293. package/dist/type-extractor.d.ts.map +1 -1
  294. package/dist/type-extractor.js +738 -34
  295. package/dist/type-extractor.js.map +1 -1
  296. package/dist/type-resolver.js +1 -1
  297. package/dist/type-resolver.js.map +1 -1
  298. package/dist/ui/brand.d.ts +1 -1
  299. package/dist/ui/brand.js +1 -1
  300. package/dist/ui/components/LogoWordmark.d.ts +1 -1
  301. package/dist/ui/components/LogoWordmark.d.ts.map +1 -1
  302. package/dist/ui/components/LogoWordmark.js +1 -1
  303. package/dist/ui/components/LogoWordmark.js.map +1 -1
  304. package/dist/ui/components/SelectField.js +1 -1
  305. package/dist/ui/components/SelectField.js.map +1 -1
  306. package/dist/ui/confirm.d.ts +1 -1
  307. package/dist/ui/confirm.js +1 -1
  308. package/dist/ui/fatal.d.ts +1 -1
  309. package/dist/ui/fatal.d.ts.map +1 -1
  310. package/dist/ui/fatal.js +1 -1
  311. package/dist/ui/fatal.js.map +1 -1
  312. package/dist/ui/flows/prompt-fields.js +1 -1
  313. package/dist/ui/flows/prompt-fields.js.map +1 -1
  314. package/dist/ui/interactive.d.ts +1 -1
  315. package/dist/ui/interactive.d.ts.map +1 -1
  316. package/dist/ui/interactive.js +1 -1
  317. package/dist/ui/interactive.js.map +1 -1
  318. package/dist/ui/progress.d.ts +1 -1
  319. package/dist/ui/progress.js +1 -1
  320. package/dist/ui/runtime/command-chrome.js +2 -2
  321. package/dist/ui/runtime/command-chrome.js.map +1 -1
  322. package/dist/ui/runtime/run-clack-flow.js +1 -1
  323. package/dist/ui/runtime/run-clack-flow.js.map +1 -1
  324. package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
  325. package/dist/ui/runtime/stdin-after-ink.js +4 -2
  326. package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
  327. package/dist/ui/theme.d.ts +1 -1
  328. package/dist/ui/theme.js +1 -1
  329. package/dist/valkey-sidecar.d.ts +1 -1
  330. package/dist/valkey-sidecar.d.ts.map +1 -1
  331. package/dist/valkey-sidecar.js +2 -2
  332. package/dist/valkey-sidecar.js.map +1 -1
  333. package/package.json +6 -2
  334. package/scripts/build-release-artifacts.sh +112 -0
  335. package/scripts/generate-homebrew-formula.mjs +123 -0
  336. package/scripts/mirror-deno-release.sh +1 -1
  337. package/src/TYPE-RESOLUTION.md +31 -31
  338. package/src/app/framework.ts +1 -1
  339. package/src/augmentation-generator.ts +19 -6
  340. package/src/binary-cache.ts +94 -47
  341. package/src/cache-clean.ts +81 -0
  342. package/src/cache-pins.ts +243 -0
  343. package/src/cli-artifacts.ts +65 -0
  344. package/src/cli-install-method.ts +87 -0
  345. package/src/cli-package-version.ts +5 -1
  346. package/src/cli-standalone.ts +116 -34
  347. package/src/cli-version-embedded.ts +12 -0
  348. package/src/cli.ts +7 -1
  349. package/src/cloud-credentials.ts +183 -0
  350. package/src/commands/add.ts +1 -1
  351. package/src/commands/admin.ts +142 -27
  352. package/src/commands/adopt.ts +1 -1
  353. package/src/commands/app.ts +2 -2
  354. package/src/commands/cache.ts +92 -77
  355. package/src/commands/cloud.ts +32 -8
  356. package/src/commands/db.ts +164 -3
  357. package/src/commands/deploy.ts +12 -12
  358. package/src/commands/dev.ts +72 -23
  359. package/src/commands/doctor.ts +69 -3
  360. package/src/commands/engine.ts +1 -1
  361. package/src/commands/functions.ts +47 -10
  362. package/src/commands/generate.ts +5 -1
  363. package/src/commands/hooks.ts +138 -0
  364. package/src/commands/init.ts +65 -31
  365. package/src/commands/internal.ts +23 -0
  366. package/src/commands/link-helpers.ts +9 -2
  367. package/src/commands/login.ts +127 -0
  368. package/src/commands/logs.ts +1 -1
  369. package/src/commands/migrate.ts +5 -5
  370. package/src/commands/pg.ts +1 -1
  371. package/src/commands/plugins.ts +1 -1
  372. package/src/commands/pull.ts +2 -2
  373. package/src/commands/push.ts +124 -7
  374. package/src/commands/self-host.ts +1 -1
  375. package/src/commands/self-update.ts +41 -19
  376. package/src/commands/status.ts +1 -1
  377. package/src/commands/update.ts +1 -1
  378. package/src/config.ts +23 -4
  379. package/src/db-preflight.ts +575 -0
  380. package/src/dev-compose.ts +158 -89
  381. package/src/dev-log-filter.ts +1 -1
  382. package/src/dev-ports.ts +3 -3
  383. package/src/dev-session.ts +6 -6
  384. package/src/dev-shutdown.ts +8 -8
  385. package/src/dev-task-colors.ts +1 -1
  386. package/src/docker-postgres.ts +6 -6
  387. package/src/docker-runtime.ts +5 -5
  388. package/src/engine-client.ts +3 -3
  389. package/src/engine-push-output.ts +1 -1
  390. package/src/ensure-binary.ts +20 -0
  391. package/src/ensure-component-binaries.ts +1 -1
  392. package/src/env-file.ts +157 -11
  393. package/src/field-masking-tier.ts +107 -0
  394. package/src/functions-deno-types.ts +188 -0
  395. package/src/functions-router-gen.ts +1 -1
  396. package/src/gitignore.ts +3 -3
  397. package/src/hook-upload.ts +107 -0
  398. package/src/hooks-generator.ts +352 -0
  399. package/src/index.ts +1 -1
  400. package/src/init-project-detect.ts +1 -1
  401. package/src/link.ts +7 -0
  402. package/src/local-secrets.ts +104 -0
  403. package/src/main.ts +20 -0
  404. package/src/model-hooks.ts +260 -0
  405. package/src/pgbouncer-userlist.ts +2 -2
  406. package/src/postgres-ctl.ts +32 -5
  407. package/src/process-manager.ts +4 -4
  408. package/src/project-config.ts +237 -3
  409. package/src/pull-utils.ts +4 -4
  410. package/src/realtime-launch.ts +1 -1
  411. package/src/release-pins.ts +2 -2
  412. package/src/resolve-api-url.ts +1 -1
  413. package/src/resolve-target.ts +113 -68
  414. package/src/rest-cache-admin.ts +2 -2
  415. package/src/runtime-routes.ts +2 -2
  416. package/src/schema-ast-v2.ts +8 -5
  417. package/src/schema-push-lock.ts +56 -17
  418. package/src/scripts/postinstall.ts +1 -1
  419. package/src/self-host-compose.ts +348 -72
  420. package/src/service-role-check.ts +111 -0
  421. package/src/storage-provision.ts +3 -3
  422. package/src/studio-dev-server.ts +1 -1
  423. package/src/systemd.ts +1 -1
  424. package/src/target-client.ts +105 -18
  425. package/src/tsx-runner.ts +1 -1
  426. package/src/type-extractor.ts +885 -30
  427. package/src/type-resolver.ts +1 -1
  428. package/src/ui/README.md +1 -1
  429. package/src/ui/brand.ts +1 -1
  430. package/src/ui/components/LogoWordmark.tsx +1 -1
  431. package/src/ui/components/SelectField.tsx +1 -1
  432. package/src/ui/confirm.ts +1 -1
  433. package/src/ui/fatal.ts +1 -1
  434. package/src/ui/flows/prompt-fields.tsx +1 -1
  435. package/src/ui/interactive.ts +1 -1
  436. package/src/ui/progress.ts +1 -1
  437. package/src/ui/runtime/command-chrome.ts +2 -2
  438. package/src/ui/runtime/run-clack-flow.tsx +1 -1
  439. package/src/ui/runtime/stdin-after-ink.ts +4 -2
  440. package/src/ui/theme.ts +1 -1
  441. package/src/valkey-sidecar.ts +2 -2
  442. package/tests/access-composition.test.ts +667 -0
  443. package/tests/admin-studio-membership.test.ts +121 -0
  444. package/tests/api-schemas.test.ts +83 -0
  445. package/tests/archive-name-alias.test.ts +42 -0
  446. package/tests/augmentation-generator.test.ts +28 -0
  447. package/tests/cache-clean.test.ts +206 -0
  448. package/tests/cli-help.test.ts +11 -2
  449. package/tests/cli-install-method.test.ts +66 -0
  450. package/tests/command-chrome-coverage.test.ts +31 -26
  451. package/tests/command-chrome-wrap.test.ts +1 -0
  452. package/tests/db-preflight.test.ts +72 -0
  453. package/tests/doctor-render.test.ts +62 -0
  454. package/tests/doctor-service-role.test.ts +55 -0
  455. package/tests/engine-push-output.test.ts +1 -1
  456. package/tests/env-file.test.ts +121 -0
  457. package/tests/external-database-compose.test.ts +184 -0
  458. package/tests/external-database-config.test.ts +154 -0
  459. package/tests/field-masking-tier.test.ts +184 -0
  460. package/tests/functions-deno-types.test.ts +197 -0
  461. package/tests/homebrew-formula.test.ts +100 -0
  462. package/tests/hook-upload.test.ts +125 -0
  463. package/tests/hooks-generator.test.ts +212 -0
  464. package/tests/init.test.ts +5 -2
  465. package/tests/keys.test.ts +8 -2
  466. package/tests/local-secrets.test.ts +92 -0
  467. package/tests/model-hooks.test.ts +322 -0
  468. package/tests/runtime-contract.test.ts +113 -1
  469. package/tests/service-role-check.test.ts +109 -0
  470. package/tests/service-role-routes.test.ts +51 -0
  471. package/tests/storage-provision.test.ts +1 -1
  472. package/tests/target-client-refresh.test.ts +94 -0
  473. package/tests/type-extractor.test.ts +187 -0
  474. package/tsconfig.tsbuildinfo +1 -1
  475. package/vitest.config.ts +7 -1
  476. package/scripts/build-release-bundle.mjs +0 -27
  477. package/tests/engine-distribution.test.ts +0 -418
@@ -46,7 +46,7 @@ export function extractSchemaAstFromTypes(
46
46
  const bucketAliases = new Map<string, string>()
47
47
  const bucketsById = new Map<string, ExtractedStorageBucketAst>()
48
48
  for (const sourceFile of sourceFiles) {
49
- const bucketContext = collectBucketContext(sourceFile)
49
+ const bucketContext = collectBucketContext(sourceFile, resolveCtx)
50
50
  for (const [alias, bucketId] of bucketContext.aliases) {
51
51
  bucketAliases.set(alias, bucketId)
52
52
  }
@@ -105,16 +105,17 @@ export function extractSchemaAstFromTypes(
105
105
  )
106
106
  }
107
107
 
108
- const { tableName, access, options, indexes } = parseModelMeta(
108
+ const { tableName, access, options, indexes, hooks } = parseModelMeta(
109
109
  metaArg,
110
110
  sourceFile,
111
111
  stmt.name.text,
112
112
  fieldsArg,
113
113
  fields,
114
+ resolveCtx,
114
115
  )
115
116
 
116
117
  models.push(
117
- emitModel(stmt.name.text, fields, options, tableName, access, indexes),
118
+ emitModel(stmt.name.text, fields, options, tableName, access, indexes, hooks),
118
119
  )
119
120
  }
120
121
  }
@@ -312,7 +313,7 @@ function mergeIntersectionParts(
312
313
  continue
313
314
  }
314
315
  }
315
- // Unresolvable parts are skipped the model still extracts with whatever fields were found
316
+ // Unresolvable parts are skipped, the model still extracts with whatever fields were found
316
317
  }
317
318
  if (allMembers.length === 0) return null
318
319
  return ts.factory.createTypeLiteralNode(allMembers)
@@ -907,8 +908,19 @@ function parseScalarType(
907
908
  return scalar("tsVector")
908
909
  case "Money":
909
910
  return scalar("money")
910
- case "Decimal":
911
- return scalar("decimal")
911
+ case "Decimal": {
912
+ // `Decimal<10, 2>` names a precision and a scale, and the engine renders `NUMERIC(p, s)`
913
+ // from them: but nothing used to read the type arguments, so every Decimal became an
914
+ // unbounded NUMERIC. Silently, which is the worst way to lose a constraint on a money column.
915
+ const precision = parseNumericTypeArg(typeNode.typeArguments?.[0], sourceFile)
916
+ const scale = parseNumericTypeArg(typeNode.typeArguments?.[1], sourceFile)
917
+ return scalar("decimal", {
918
+ kernel: {
919
+ ...(precision !== undefined && { precision }),
920
+ ...(scale !== undefined && { scale }),
921
+ },
922
+ })
923
+ }
912
924
  case "DateOnly":
913
925
  return scalar("date")
914
926
  case "Date":
@@ -930,7 +942,35 @@ function parseScalarType(
930
942
  case "Button":
931
943
  return scalar("button", { db: { pgType: "JSONB" } })
932
944
  case "Duration":
933
- return scalar("json", { db: { pgType: "JSONB" } })
945
+ return scalar("json", {
946
+ db: { pgType: "JSONB" },
947
+ platform: { editor: "duration" },
948
+ kernel: { tsType: "{ ms: number }" },
949
+ })
950
+ // `Code` and `Currency` each carry two values, so a scalar column would have to drop one:
951
+ // JSONB keeps `lang` with its `source`, and an amount with the currency it is denominated in.
952
+ // `tsType` is what stops the generated client row from flattening them to an opaque object.
953
+ case "Code": {
954
+ const lang = literalStringType(typeNode.typeArguments?.[0])
955
+ return scalar("json", {
956
+ db: { pgType: "JSONB" },
957
+ // `editor` picks the Studio widget. Without it a code snippet and a money amount both get
958
+ // the raw JSON editor, because the widget is otherwise chosen from the column kind alone
959
+ // and both of these are JSONB.
960
+ platform: { editor: "code" },
961
+ kernel: { tsType: `{ lang: ${lang !== null ? JSON.stringify(lang) : "string"}; source: string }` },
962
+ })
963
+ }
964
+ case "Currency": {
965
+ const currencyCode = literalStringType(typeNode.typeArguments?.[0])
966
+ return scalar("json", {
967
+ db: { pgType: "JSONB" },
968
+ platform: { editor: "currency" },
969
+ kernel: {
970
+ tsType: `{ amount: string; code: ${currencyCode !== null ? JSON.stringify(currencyCode) : "string"} }`,
971
+ },
972
+ })
973
+ }
934
974
  case "GeoPoint":
935
975
  case "Geo":
936
976
  return scalar("geo", { kernel: { geoType: "point", srid: 4326 } })
@@ -1066,7 +1106,10 @@ function parseStringLiteralTuple(node: ts.TypeNode, sourceFile: ts.SourceFile):
1066
1106
  return out
1067
1107
  }
1068
1108
 
1069
- function collectBucketContext(sourceFile: ts.SourceFile): {
1109
+ function collectBucketContext(
1110
+ sourceFile: ts.SourceFile,
1111
+ resolveCtx: ResolveContext,
1112
+ ): {
1070
1113
  aliases: Map<string, string>
1071
1114
  bucketsById: Map<string, ExtractedStorageBucketAst>
1072
1115
  } {
@@ -1084,7 +1127,7 @@ function collectBucketContext(sourceFile: ts.SourceFile): {
1084
1127
 
1085
1128
  const parsed =
1086
1129
  configArg && ts.isTypeLiteralNode(configArg)
1087
- ? parseBucketTypeLiteral(configArg, sourceFile)
1130
+ ? parseBucketTypeLiteral(configArg, sourceFile, id, resolveCtx)
1088
1131
  : {}
1089
1132
 
1090
1133
  const next = buildExtractedBucketAst(id, parsed)
@@ -1133,6 +1176,8 @@ interface ParsedBucketLiteral {
1133
1176
  function parseBucketTypeLiteral(
1134
1177
  lit: ts.TypeLiteralNode,
1135
1178
  sourceFile: ts.SourceFile,
1179
+ bucketId: string,
1180
+ resolveCtx: ResolveContext,
1136
1181
  ): Partial<ParsedBucketLiteral> {
1137
1182
  const out: Partial<ParsedBucketLiteral> = {}
1138
1183
  for (const member of lit.members) {
@@ -1159,7 +1204,7 @@ function parseBucketTypeLiteral(
1159
1204
  continue
1160
1205
  }
1161
1206
  if (key === "access") {
1162
- const acc = parsePartialBucketAccess(member.type, sourceFile)
1207
+ const acc = parsePartialBucketAccess(member.type, sourceFile, bucketId, resolveCtx)
1163
1208
  if (acc !== undefined && Object.keys(acc).length > 0) out.access = acc
1164
1209
  continue
1165
1210
  }
@@ -1221,14 +1266,28 @@ function parseMimeAcceptList(typeNode: ts.TypeNode, sourceFile: ts.SourceFile):
1221
1266
  function parsePartialBucketAccess(
1222
1267
  typeNode: ts.TypeNode,
1223
1268
  sourceFile: ts.SourceFile,
1269
+ bucketId: string,
1270
+ resolveCtx: ResolveContext,
1224
1271
  ): Record<string, unknown> | undefined {
1225
- if (!ts.isTypeLiteralNode(typeNode)) return undefined
1272
+ // Same hazard as model access: silently dropping these rules publishes a
1273
+ // bucket with no read/write restrictions, so an alias must resolve and an
1274
+ // unresolvable annotation must fail rather than degrade to "no rules".
1275
+ const literal = resolveAccessLiteral(typeNode, sourceFile, resolveCtx)
1276
+ if (!literal) {
1277
+ throw new Error(
1278
+ `Bucket "${bucketId}": could not resolve its \`access\` rules from ` +
1279
+ `\`${typeNode.getText(sourceFile)}\`.\n` +
1280
+ ` access must be an object type, or a type alias for one, e.g.\n` +
1281
+ ` export type MediaAccess = { read: Public; create: Authenticated }\n` +
1282
+ ` Refusing to continue: an unresolved access block would leave this bucket unrestricted.`,
1283
+ )
1284
+ }
1226
1285
  const access: Record<string, unknown> = {}
1227
- for (const member of typeNode.members) {
1286
+ for (const member of literal.members) {
1228
1287
  if (!ts.isPropertySignature(member) || !member.type) continue
1229
1288
  const key = getPropertyName(member.name)
1230
1289
  if (key !== "read" && key !== "create" && key !== "delete") continue
1231
- access[key] = parseAccessRule(member.type, sourceFile)
1290
+ access[key] = parseAccessRule(member.type, sourceFile, resolveCtx)
1232
1291
  }
1233
1292
  return access
1234
1293
  }
@@ -1394,7 +1453,7 @@ function looksLikeComputedTemplateLiteral(lit: string): boolean {
1394
1453
  return /\{truncate\s*\(/i.test(lit) || /\{[a-zA-Z_]\w*\}/g.test(lit)
1395
1454
  }
1396
1455
 
1397
- /** Resolves second type arg of `ComputedFrom<Value, Sources>` tuple concat, single field, or template literal. */
1456
+ /** Resolves second type arg of `ComputedFrom<Value, Sources>`, tuple concat, single field, or template literal. */
1398
1457
  function parseComputedFromSecondArg(
1399
1458
  sourcesArg: ts.TypeNode | undefined,
1400
1459
  sourceFile: ts.SourceFile,
@@ -1554,11 +1613,13 @@ function parseModelMeta(
1554
1613
  modelName: string,
1555
1614
  fieldsArg: ts.TypeNode,
1556
1615
  fields: Record<string, FieldAstV2>,
1616
+ resolveCtx: ResolveContext,
1557
1617
  ): {
1558
1618
  tableName: string
1559
1619
  access: Record<string, unknown>
1560
1620
  options: Record<string, unknown>
1561
1621
  indexes: unknown[]
1622
+ hooks: Record<string, ParsedModelHook>
1562
1623
  } {
1563
1624
  const literal = parseMetaLiteral(metaArg, sourceFile)
1564
1625
  const singleton = literal.singleton === true
@@ -1582,9 +1643,87 @@ function parseModelMeta(
1582
1643
 
1583
1644
  return {
1584
1645
  tableName,
1585
- access: parseModelAccess(metaArg, sourceFile),
1646
+ access: parseModelAccess(metaArg, sourceFile, modelName, fields, resolveCtx),
1586
1647
  options,
1587
1648
  indexes: parseModelIndexes(metaArg, sourceFile, fields),
1649
+ hooks: parseModelHooks(metaArg, sourceFile),
1650
+ }
1651
+ }
1652
+
1653
+ /** One lifecycle hook: `"fn-name"` or `{ function: "fn-name", timeout: 5000 }`. */
1654
+ interface ParsedModelHook {
1655
+ function: string
1656
+ timeout?: number
1657
+ onUnavailable?: "reject" | "log"
1658
+ }
1659
+
1660
+ const HOOK_EVENTS = ["beforeChange", "afterChange", "beforeDelete", "afterDelete"] as const
1661
+
1662
+ /**
1663
+ * Read `hooks` from a model's meta.
1664
+ *
1665
+ * Deliberately strict: an entry that is neither a string nor an object with a `function` name is
1666
+ * **dropped**, and `validateModelHooks` then reports the model whose hook did not survive. A hook
1667
+ * silently not firing is the failure this feature cannot have, so an unreadable declaration must
1668
+ * fail the push rather than extract to nothing.
1669
+ */
1670
+ function parseModelHooks(
1671
+ metaArg: ts.TypeNode | undefined,
1672
+ sourceFile: ts.SourceFile,
1673
+ ): Record<string, ParsedModelHook> {
1674
+ if (!metaArg || !ts.isTypeLiteralNode(metaArg)) return {}
1675
+
1676
+ const hooksProp = metaArg.members.find(
1677
+ (member) => ts.isPropertySignature(member) && getPropertyName(member.name) === "hooks",
1678
+ )
1679
+ if (!hooksProp || !ts.isPropertySignature(hooksProp) || !hooksProp.type) return {}
1680
+ if (!ts.isTypeLiteralNode(hooksProp.type)) return {}
1681
+
1682
+ const hooks: Record<string, ParsedModelHook> = {}
1683
+ for (const member of hooksProp.type.members) {
1684
+ if (!ts.isPropertySignature(member) || !member.type) continue
1685
+ const event = getPropertyName(member.name)
1686
+ if (!event || !(HOOK_EVENTS as readonly string[]).includes(event)) continue
1687
+
1688
+ const parsed = parseModelHookValue(member.type, sourceFile)
1689
+ if (parsed !== null) hooks[event] = parsed
1690
+ }
1691
+ return hooks
1692
+ }
1693
+
1694
+ function parseModelHookValue(
1695
+ node: ts.TypeNode,
1696
+ sourceFile: ts.SourceFile,
1697
+ ): ParsedModelHook | null {
1698
+ const literal = literalStringType(node)
1699
+ if (literal !== null) {
1700
+ return literal.trim().length > 0 ? { function: literal } : null
1701
+ }
1702
+ if (!ts.isTypeLiteralNode(node)) return null
1703
+
1704
+ let fn: string | undefined
1705
+ let timeout: number | undefined
1706
+ let onUnavailable: "reject" | "log" | undefined
1707
+
1708
+ for (const member of node.members) {
1709
+ if (!ts.isPropertySignature(member) || !member.type) continue
1710
+ const key = getPropertyName(member.name)
1711
+ if (key === "function") {
1712
+ const value = literalStringType(member.type)
1713
+ if (value !== null && value.trim().length > 0) fn = value
1714
+ } else if (key === "timeout") {
1715
+ timeout = parseNumericTypeArg(member.type, sourceFile)
1716
+ } else if (key === "onUnavailable") {
1717
+ const value = literalStringType(member.type)
1718
+ if (value === "reject" || value === "log") onUnavailable = value
1719
+ }
1720
+ }
1721
+
1722
+ if (fn === undefined) return null
1723
+ return {
1724
+ function: fn,
1725
+ ...(timeout !== undefined && { timeout }),
1726
+ ...(onUnavailable !== undefined && { onUnavailable }),
1588
1727
  }
1589
1728
  }
1590
1729
 
@@ -1646,28 +1785,289 @@ function resolveIndexFieldName(fieldName: string, fields: Record<string, FieldAs
1646
1785
  return fieldName
1647
1786
  }
1648
1787
 
1649
- function parseModelAccess(metaArg: ts.TypeNode | undefined, sourceFile: ts.SourceFile): Record<string, unknown> {
1788
+ /**
1789
+ * Resolve an `access:` annotation to its rule object.
1790
+ *
1791
+ * `access` may be written inline (`access: { read: Public }`) or factored into a
1792
+ * shared type alias (`access: CmsPublicReadAdminWrite`), the latter is the
1793
+ * natural way to reuse one policy set across many models, so it has to resolve
1794
+ * through aliases and intersections, not just literals.
1795
+ *
1796
+ * Anything unresolvable throws. Returning `{}` here means "no access rules",
1797
+ * which the differ treats as "emit no RLS policies", so a typo or an
1798
+ * unsupported shape silently published a table with no row-level protection at
1799
+ * all. Failing the extract is the only safe outcome.
1800
+ */
1801
+ function parseModelAccess(
1802
+ metaArg: ts.TypeNode | undefined,
1803
+ sourceFile: ts.SourceFile,
1804
+ modelName: string,
1805
+ fields: Record<string, FieldAstV2>,
1806
+ resolveCtx: ResolveContext,
1807
+ ): Record<string, unknown> {
1650
1808
  if (!metaArg || !ts.isTypeLiteralNode(metaArg)) return {}
1651
1809
  const accessProp = metaArg.members.find(
1652
1810
  (member) => ts.isPropertySignature(member) && getPropertyName(member.name) === "access",
1653
1811
  )
1654
- if (!accessProp || !ts.isPropertySignature(accessProp) || !accessProp.type || !ts.isTypeLiteralNode(accessProp.type)) {
1812
+ if (!accessProp || !ts.isPropertySignature(accessProp) || !accessProp.type) {
1655
1813
  return {}
1656
1814
  }
1657
1815
 
1816
+ const literal = resolveAccessLiteral(accessProp.type, sourceFile, resolveCtx)
1817
+ if (!literal) {
1818
+ throw new Error(
1819
+ `Model "${modelName}": could not resolve its \`access\` rules from ` +
1820
+ `\`${accessProp.type.getText(sourceFile)}\`.\n` +
1821
+ ` access must be an object type, or a type alias for one:\n` +
1822
+ ` export type PublicReadAdminWrite = { read: Public; create: Role<"admin"> }\n` +
1823
+ ` A \`const … as const\` value cannot be used here, access is read from types, and\n` +
1824
+ ` rule names like Public / Role are types, not values.\n` +
1825
+ ` Refusing to continue: an unresolved access block would publish this table with no RLS.`,
1826
+ )
1827
+ }
1828
+
1658
1829
  const access: Record<string, unknown> = {}
1659
- for (const member of accessProp.type.members) {
1830
+ for (const member of literal.members) {
1660
1831
  if (!ts.isPropertySignature(member) || !member.type) continue
1661
1832
  const key = getPropertyName(member.name)
1662
1833
  if (!key) continue
1663
- access[key] = parseAccessRule(member.type, sourceFile)
1834
+
1835
+ // `update: { using, check }` splits which rows may be changed from what they
1836
+ // may be changed into. Flattened here into the two AST fields the engine
1837
+ // renders, so the engine never has to know about the sugar.
1838
+ if (key === "update") {
1839
+ const split = parseUpdateSplit(member.type, sourceFile, resolveCtx)
1840
+ if (split) {
1841
+ access["update"] = split.using
1842
+ if (split.check !== undefined) access["updateCheck"] = split.check
1843
+ continue
1844
+ }
1845
+ }
1846
+
1847
+ if (key === "fields") {
1848
+ const fieldRules = parseFieldAccess(member.type, sourceFile, modelName, fields, resolveCtx)
1849
+ if (Object.keys(fieldRules).length > 0) access["fields"] = fieldRules
1850
+ continue
1851
+ }
1852
+
1853
+ access[key] = parseAccessRule(member.type, sourceFile, resolveCtx)
1854
+ }
1855
+
1856
+ if (Object.keys(access).length === 0) {
1857
+ throw new Error(
1858
+ `Model "${modelName}": \`access\` resolved to an empty rule set ` +
1859
+ `(from \`${accessProp.type.getText(sourceFile)}\`). Remove \`access\` if the table is ` +
1860
+ `intentionally unrestricted, or declare at least one of read/create/update/delete.`,
1861
+ )
1664
1862
  }
1665
1863
  return access
1666
1864
  }
1667
1865
 
1668
- function parseAccessRule(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): Record<string, unknown> {
1866
+ /**
1867
+ * `fields: { [column]: { read?, write? } }`, per-column narrowing of the table rules.
1868
+ *
1869
+ * Any rule the DSL can express is allowed. Enforcement is a query rewrite that
1870
+ * evaluates the rule per row with the caller's claims, so ownership, membership and
1871
+ * application roles all work here, none of which a column privilege could express.
1872
+ */
1873
+ function parseFieldAccess(
1874
+ typeNode: ts.TypeNode,
1875
+ sourceFile: ts.SourceFile,
1876
+ modelName: string,
1877
+ fields: Record<string, FieldAstV2>,
1878
+ resolveCtx?: ResolveContext,
1879
+ ): Record<string, Record<string, unknown>> {
1880
+ const literal = resolveCtx
1881
+ ? resolveAccessLiteral(typeNode, sourceFile, resolveCtx)
1882
+ : ts.isTypeLiteralNode(typeNode)
1883
+ ? typeNode
1884
+ : null
1885
+ if (!literal) {
1886
+ throw new Error(
1887
+ `Model "${modelName}": could not resolve \`access.fields\` from ` +
1888
+ `\`${typeNode.getText(sourceFile)}\`. It must be an object type, or a type ` +
1889
+ `alias for one: \`fields: { salary: { read: Private } }\`.`,
1890
+ )
1891
+ }
1892
+
1893
+ const out: Record<string, Record<string, unknown>> = {}
1894
+ for (const member of literal.members) {
1895
+ if (!ts.isPropertySignature(member) || !member.type) continue
1896
+ const declared = getPropertyName(member.name)
1897
+ if (!declared) continue
1898
+ // A privilege is granted on a column, and a relation field is not one --
1899
+ // `author` is stored as `author_id`. Same mapping the index builder uses.
1900
+ // Names that match nothing are passed through rather than rejected: composite
1901
+ // wrappers expand into columns this map never sees, so a hard error here would
1902
+ // refuse valid schemas. Postgres rejects the unknown column at push instead.
1903
+ const column = resolveIndexFieldName(declared, fields) ?? declared
1904
+
1905
+ const columnLiteral = resolveCtx
1906
+ ? resolveAccessLiteral(member.type, sourceFile, resolveCtx)
1907
+ : ts.isTypeLiteralNode(member.type)
1908
+ ? member.type
1909
+ : null
1910
+ if (!columnLiteral) {
1911
+ throw new Error(
1912
+ `Model "${modelName}": \`access.fields.${column}\` must be an object with ` +
1913
+ `\`read\` and/or \`write\`, found \`${member.type.getText(sourceFile)}\`.`,
1914
+ )
1915
+ }
1916
+
1917
+ const rules: Record<string, unknown> = {}
1918
+ for (const opMember of columnLiteral.members) {
1919
+ if (!ts.isPropertySignature(opMember) || !opMember.type) continue
1920
+ const operation = getPropertyName(opMember.name)
1921
+ if (operation !== "read" && operation !== "write") {
1922
+ throw new Error(
1923
+ `Model "${modelName}": \`access.fields.${column}\` may only contain ` +
1924
+ `\`read\` and \`write\`, found "${operation}".`,
1925
+ )
1926
+ }
1927
+ rules[operation] = parseAccessRule(opMember.type, sourceFile, resolveCtx)
1928
+ }
1929
+
1930
+ // An empty `{}` reads as "this column is restricted" but restricts nothing.
1931
+ if (Object.keys(rules).length === 0) {
1932
+ throw new Error(
1933
+ `Model "${modelName}": \`access.fields.${column}\` declares no rules. ` +
1934
+ `Give it a \`read\` or a \`write\`, or remove the entry.`,
1935
+ )
1936
+ }
1937
+ out[column] = rules
1938
+ }
1939
+ return out
1940
+ }
1941
+
1942
+ /**
1943
+ * `update: { using: …, check: … }`, or null when `update` is a plain rule.
1944
+ *
1945
+ * An update policy has two halves: `USING` picks which existing rows may be
1946
+ * modified, `WITH CHECK` constrains what they may become. One rule for both is the
1947
+ * right default and what every existing schema means, but it makes "an editor may
1948
+ * move a post between their own sites" inexpressible, because the row they are
1949
+ * changing and the row they are changing it into are judged by the same predicate.
1950
+ */
1951
+ function parseUpdateSplit(
1952
+ typeNode: ts.TypeNode,
1953
+ sourceFile: ts.SourceFile,
1954
+ resolveCtx?: ResolveContext,
1955
+ ): { using: Record<string, unknown>; check?: Record<string, unknown> } | null {
1956
+ const literal = resolveCtx
1957
+ ? resolveAccessLiteral(typeNode, sourceFile, resolveCtx)
1958
+ : ts.isTypeLiteralNode(typeNode)
1959
+ ? typeNode
1960
+ : null
1961
+ if (!literal) return null
1962
+
1963
+ let using: ts.TypeNode | undefined
1964
+ let check: ts.TypeNode | undefined
1965
+ for (const member of literal.members) {
1966
+ if (!ts.isPropertySignature(member) || !member.type) continue
1967
+ const name = getPropertyName(member.name)
1968
+ if (name === "using") using = member.type
1969
+ else if (name === "check") check = member.type
1970
+ else if (name !== undefined) {
1971
+ throw new Error(
1972
+ `\`update\` may only contain \`using\` and \`check\`, found "${name}".`,
1973
+ )
1974
+ }
1975
+ }
1976
+
1977
+ // Not the split form at all, an object with neither key is some other shape.
1978
+ if (!using && !check) return null
1979
+ if (!using) {
1980
+ throw new Error(
1981
+ "`update: { check }` needs a `using` rule too, without it no row is " +
1982
+ "selectable for update, so the check can never apply.",
1983
+ )
1984
+ }
1985
+
1986
+ const parsed: { using: Record<string, unknown>; check?: Record<string, unknown> } = {
1987
+ using: parseAccessRule(using, sourceFile, resolveCtx),
1988
+ }
1989
+ if (check) parsed.check = parseAccessRule(check, sourceFile, resolveCtx)
1990
+ return parsed
1991
+ }
1992
+
1993
+ /** Walk aliases/intersections down to the object type holding the access rules. */
1994
+ function resolveAccessLiteral(
1995
+ typeNode: ts.TypeNode,
1996
+ sourceFile: ts.SourceFile,
1997
+ resolveCtx: ResolveContext,
1998
+ depth = 0,
1999
+ ): ts.TypeLiteralNode | null {
2000
+ if (depth > 16) return null
2001
+ if (ts.isTypeLiteralNode(typeNode)) return typeNode
2002
+
2003
+ if (ts.isIntersectionTypeNode(typeNode)) {
2004
+ const members: ts.TypeElement[] = []
2005
+ for (const part of typeNode.types) {
2006
+ const resolved = resolveAccessLiteral(part, sourceFile, resolveCtx, depth + 1)
2007
+ if (!resolved) return null
2008
+ members.push(...resolved.members)
2009
+ }
2010
+ return ts.factory.createTypeLiteralNode(members)
2011
+ }
2012
+
2013
+ if (ts.isTypeReferenceNode(typeNode)) {
2014
+ const expanded = tryResolveTypeReference(typeNode, sourceFile, resolveCtx)
2015
+ if (expanded) return resolveAccessLiteral(expanded, sourceFile, resolveCtx, depth + 1)
2016
+ }
2017
+
2018
+ if (needsChecker(typeNode)) {
2019
+ const resolved = resolveTypeNode(typeNode, sourceFile, resolveCtx)
2020
+ if (resolved !== typeNode) {
2021
+ return resolveAccessLiteral(resolved, sourceFile, resolveCtx, depth + 1)
2022
+ }
2023
+ }
2024
+
2025
+ return null
2026
+ }
2027
+
2028
+ /**
2029
+ * `access` rules may be declared in a different file from the model (a shared
2030
+ * alias), and `getText(file)` reads the given file's text at the node's offsets
2031
+ *- so passing the model's file for a node owned by another one yielded garbage
2032
+ * and every rule silently fell through to `private`. Always read a node's text
2033
+ * from the file that actually owns it.
2034
+ */
2035
+ function ownerText(node: ts.Node, fallback: ts.SourceFile): string {
2036
+ const own = node.pos >= 0 ? node.getSourceFile() : undefined
2037
+ return node.getText(own ?? fallback)
2038
+ }
2039
+
2040
+ function parseAccessRule(
2041
+ typeNode: ts.TypeNode,
2042
+ sourceFile: ts.SourceFile,
2043
+ resolveCtx?: ResolveContext,
2044
+ depth = 0,
2045
+ ): Record<string, unknown> {
2046
+ // Reuse a single rule from a shared set: `access: { read: Rules["read"] }`.
2047
+ if (ts.isIndexedAccessTypeNode(typeNode) && resolveCtx) {
2048
+ const member = resolveIndexedAccessRule(typeNode, sourceFile, resolveCtx)
2049
+ if (member) return parseAccessRule(member, sourceFile, resolveCtx)
2050
+ throw new Error(
2051
+ `Could not resolve access rule from \`${ownerText(typeNode, sourceFile)}\`. ` +
2052
+ `Reference a rule set member as \`Rules["read"]\`, where Rules is a type alias.`,
2053
+ )
2054
+ }
2055
+
1669
2056
  if (!ts.isTypeReferenceNode(typeNode)) return { type: "private" }
1670
- const ref = typeNode.typeName.getText(sourceFile)
2057
+
2058
+ // `Rules.read` is a qualified name (namespace lookup), not a member of a type
2059
+ // alias: TypeScript rejects it, and it used to fall through to `private`,
2060
+ // silently denying an operation the author meant to grant.
2061
+ if (ts.isQualifiedName(typeNode.typeName)) {
2062
+ const text = ownerText(typeNode.typeName, sourceFile)
2063
+ const [head, ...rest] = text.split(".")
2064
+ throw new Error(
2065
+ `Access rule \`${text}\` is not valid: a member of a rule-set type must be written ` +
2066
+ `\`${head}["${rest.join(".")}"]\`, not \`${text}\`.`,
2067
+ )
2068
+ }
2069
+
2070
+ const ref = ownerText(typeNode.typeName, sourceFile)
1671
2071
  switch (ref) {
1672
2072
  case "Public":
1673
2073
  case "BucketPublic":
@@ -1684,24 +2084,459 @@ function parseAccessRule(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): Reco
1684
2084
  const args = typeNode.typeArguments ?? []
1685
2085
  const keyArg = args.length >= 2 ? args[1] : args[0]
1686
2086
  // Must match engine `AccessRule::Owner { field }` (see supatype-schema-engine parser/ast.rs).
1687
- return { type: "owner", field: keyArg?.getText(sourceFile).replace(/['"]/g, "") ?? "user_id" }
2087
+ return {
2088
+ type: "owner",
2089
+ field: keyArg ? ownerText(keyArg, sourceFile).replace(/['"]/g, "") : "user_id",
2090
+ }
1688
2091
  }
1689
2092
  case "OwnerFrom": {
1690
2093
  const relationArg = typeNode.typeArguments?.[0]
1691
- const relationField = relationArg?.getText(sourceFile).replace(/['"]/g, "") ?? "owner"
2094
+ const relationField = relationArg
2095
+ ? ownerText(relationArg, sourceFile).replace(/['"]/g, "")
2096
+ : "owner"
1692
2097
  return { type: "owner", field: relationField }
1693
2098
  }
1694
- case "Role": {
1695
- const roleArg = typeNode.typeArguments?.[0]
1696
- return { type: "role", roles: [roleArg?.getText(sourceFile).replace(/['"]/g, "") ?? "admin"] }
1697
- }
2099
+ case "Role":
1698
2100
  case "BucketRole": {
1699
2101
  const roleArg = typeNode.typeArguments?.[0]
1700
- return { type: "role", roles: [roleArg?.getText(sourceFile).replace(/['"]/g, "") ?? "admin"] }
2102
+ return {
2103
+ type: "role",
2104
+ roles: [roleArg ? ownerText(roleArg, sourceFile).replace(/['"]/g, "") : "admin"],
2105
+ }
1701
2106
  }
1702
- default:
1703
- return { type: "private" }
2107
+ case "Any": {
2108
+ const listArg = typeNode.typeArguments?.[0]
2109
+ if (!listArg || !ts.isTupleTypeNode(listArg)) {
2110
+ throw new Error(
2111
+ `\`Any<>\` takes a tuple of rules, as in ` +
2112
+ `\`Any<[Role<"admin">, Owner<"author_id">]>\`.`,
2113
+ )
2114
+ }
2115
+ // An empty list would compile to a policy that grants nothing while
2116
+ // reading like a grant, the exact silent-denial failure the unknown-rule
2117
+ // branch below exists to prevent.
2118
+ if (listArg.elements.length === 0) {
2119
+ throw new Error(
2120
+ "`Any<[]>` grants nothing. List the rules that should allow access, or " +
2121
+ "use `Private` if denying is the intent.",
2122
+ )
2123
+ }
2124
+ return {
2125
+ type: "any",
2126
+ rules: listArg.elements.map((element) =>
2127
+ parseAccessRule(element, sourceFile, resolveCtx),
2128
+ ),
2129
+ }
2130
+ }
2131
+ case "All": {
2132
+ const listArg = typeNode.typeArguments?.[0]
2133
+ if (!listArg || !ts.isTupleTypeNode(listArg)) {
2134
+ throw new Error(
2135
+ '`All<>` takes a tuple of rules, as in `All<[Role<"editor">, NotNull<"published_at">]>`.',
2136
+ )
2137
+ }
2138
+ // Unlike `Any<[]>`, an empty `All` grants *everything*, an empty AND is
2139
+ // true. Reading as a restriction while imposing none is worse than an error.
2140
+ if (listArg.elements.length === 0) {
2141
+ throw new Error(
2142
+ "`All<[]>` restricts nothing and grants everything. List the rules that " +
2143
+ "must hold, or use `Public` if that is the intent.",
2144
+ )
2145
+ }
2146
+ return {
2147
+ type: "all",
2148
+ rules: listArg.elements.map((element) =>
2149
+ parseAccessRule(element, sourceFile, resolveCtx),
2150
+ ),
2151
+ }
2152
+ }
2153
+ case "Not": {
2154
+ const inner = typeNode.typeArguments?.[0]
2155
+ if (!inner) {
2156
+ throw new Error('`Not<>` takes one rule, as in `Not<Role<"banned">>`.')
2157
+ }
2158
+ return { type: "not", rule: parseAccessRule(inner, sourceFile, resolveCtx) }
2159
+ }
2160
+ case "Eq":
2161
+ case "Neq":
2162
+ case "Gt":
2163
+ case "Gte":
2164
+ case "Lt":
2165
+ case "Lte":
2166
+ case "Like": {
2167
+ const args = typeNode.typeArguments ?? []
2168
+ if (args.length !== 2) {
2169
+ throw new Error(
2170
+ `\`${ref}<>\` takes two operands, as in \`${ref}<"author_id", AuthUid>\`.`,
2171
+ )
2172
+ }
2173
+ return {
2174
+ type: "compare",
2175
+ op: ref.toLowerCase(),
2176
+ left: parseAccessOperand(args[0]!, sourceFile),
2177
+ right: parseAccessOperand(args[1]!, sourceFile),
2178
+ }
2179
+ }
2180
+ case "IsNull":
2181
+ case "NotNull": {
2182
+ const operand = typeNode.typeArguments?.[0]
2183
+ if (!operand) {
2184
+ throw new Error(`\`${ref}<>\` takes one operand, as in \`${ref}<"deleted_at">\`.`)
2185
+ }
2186
+ return {
2187
+ type: "nullCheck",
2188
+ operand: parseAccessOperand(operand, sourceFile),
2189
+ isNull: ref === "IsNull",
2190
+ }
2191
+ }
2192
+ case "Exists": {
2193
+ const source = typeNode.typeArguments?.[0]
2194
+ if (!source) {
2195
+ throw new Error('`Exists<>` takes a source, as in `Exists<MySites>`.')
2196
+ }
2197
+ return {
2198
+ type: "exists",
2199
+ source: parseMembershipSource(source, sourceFile, resolveCtx),
2200
+ }
2201
+ }
2202
+ case "In": {
2203
+ const args = typeNode.typeArguments ?? []
2204
+ if (args.length !== 2) {
2205
+ throw new Error(
2206
+ '`In<>` takes a column and a source, as in `In<"site_id", MySites>`.',
2207
+ )
2208
+ }
2209
+ const column = parseAccessOperand(args[0]!, sourceFile)
2210
+ if (column["kind"] !== "column") {
2211
+ throw new Error(
2212
+ "`In<>`'s first argument must be a column name, the membership test is " +
2213
+ "about a column of the row being checked.",
2214
+ )
2215
+ }
2216
+ return {
2217
+ type: "in",
2218
+ column: column["name"],
2219
+ source: parseMembershipSource(args[1]!, sourceFile, resolveCtx),
2220
+ }
2221
+ }
2222
+ default: {
2223
+ // A named alias: possibly parameterised, standing for a rule. Expanded
2224
+ // before giving up, so `SiteAccess<"site_id">` works.
2225
+ if (resolveCtx && depth < 16) {
2226
+ const expanded = resolveAccessAliasNode(typeNode, sourceFile, resolveCtx)
2227
+ if (expanded) return parseAccessRule(expanded, sourceFile, resolveCtx, depth + 1)
2228
+ }
2229
+
2230
+ // Falling back to `private` here would silently deny an operation the
2231
+ // author believed they had granted, and with deny-by-default there is no
2232
+ // longer any need for a permissive guess. Name the offending rule instead.
2233
+ throw new Error(
2234
+ `Unknown access rule "${ref}". Supported: Public, Private, LoggedIn, ` +
2235
+ `Owner<"field">, OwnerFrom<"relation">, Role<"name">, ` +
2236
+ `Any<[rule, …]>, All<[rule, …]>, Not<rule>, ` +
2237
+ `Eq/Neq/Gt/Gte/Lt/Lte/Like<left, right>, IsNull<operand>, NotNull<operand>` +
2238
+ ` (buckets also accept BucketPublic, BucketPrivate, BucketLoggedIn, BucketOwner, BucketRole).`,
2239
+ )
2240
+ }
2241
+ }
2242
+ }
2243
+
2244
+ /**
2245
+ * Expands a named alias to the type it stands for.
2246
+ *
2247
+ * This is what makes rules parameterisable, the type-level equivalent of
2248
+ * Payload's access-control factory:
2249
+ *
2250
+ * ```typescript
2251
+ * type SiteAccess<F extends string> = Any<[Role<"admin">, In<F, MySites>]>
2252
+ * access: { update: SiteAccess<"site_id"> }
2253
+ * ```
2254
+ *
2255
+ * Without it a rule had to be written inline at every use, and any alias, even an
2256
+ * unparameterised one: was reported as an unknown rule.
2257
+ */
2258
+ function resolveAccessAliasNode(
2259
+ typeNode: ts.TypeReferenceNode,
2260
+ sourceFile: ts.SourceFile,
2261
+ resolveCtx: ResolveContext,
2262
+ ): ts.TypeNode | undefined {
2263
+ const expanded = tryResolveTypeReference(typeNode, sourceFile, resolveCtx)
2264
+ if (expanded && expanded !== typeNode) return expanded
2265
+
2266
+ if (needsChecker(typeNode)) {
2267
+ const resolved = resolveTypeNode(typeNode, sourceFile, resolveCtx)
2268
+ if (resolved !== typeNode) return resolved
1704
2269
  }
2270
+ return undefined
2271
+ }
2272
+
2273
+ /**
2274
+ * The set on the right of an `In<>`.
2275
+ *
2276
+ * Resolved through the same alias machinery as rules, so a shared
2277
+ * `type MySites = Rows<…>` can be reused across models, the parameterisation the
2278
+ * plan calls the type-level equivalent of Payload's access factory.
2279
+ */
2280
+ function parseMembershipSource(
2281
+ typeNode: ts.TypeNode,
2282
+ sourceFile: ts.SourceFile,
2283
+ resolveCtx?: ResolveContext,
2284
+ ): Record<string, unknown> {
2285
+ if (ts.isIndexedAccessTypeNode(typeNode) && resolveCtx) {
2286
+ const member = resolveIndexedAccessRule(typeNode, sourceFile, resolveCtx)
2287
+ if (member) return parseMembershipSource(member, sourceFile, resolveCtx)
2288
+ }
2289
+
2290
+ if (ts.isTypeReferenceNode(typeNode) && !ts.isQualifiedName(typeNode.typeName)) {
2291
+ const ref = ownerText(typeNode.typeName, sourceFile)
2292
+ const args = typeNode.typeArguments ?? []
2293
+
2294
+ if (ref === "Claim") {
2295
+ return parseAccessOperand(typeNode, sourceFile)
2296
+ }
2297
+
2298
+ if (ref === "Rows") {
2299
+ const table = stringLiteralArg(args[0], "Rows", "table name")
2300
+ const column = stringLiteralArg(args[1], "Rows", "column name")
2301
+ const source: Record<string, unknown> = { kind: "rows", table, column }
2302
+ // The third argument narrows the source rows. Absent means "every row",
2303
+ // which is a lookup rather than a membership check, allowed, but rarely
2304
+ // what was meant, so it stays explicit rather than being defaulted.
2305
+ if (args[2]) {
2306
+ source["where"] = parseAccessRule(args[2], sourceFile, resolveCtx)
2307
+ }
2308
+ return source
2309
+ }
2310
+
2311
+ if (ref === "Values") {
2312
+ const listArg = args[0]
2313
+ if (!listArg || !ts.isTupleTypeNode(listArg)) {
2314
+ throw new Error('`Values<>` takes a tuple, as in `Values<["draft", "review"]>`.')
2315
+ }
2316
+ if (listArg.elements.length === 0) {
2317
+ throw new Error("`Values<[]>` matches nothing. List the values, or use `Private`.")
2318
+ }
2319
+ // Not `parseAccessOperand`: there, a bare string is a *column*, which is
2320
+ // right for a comparison and wrong here. Inside a value list a string can
2321
+ // only be a value, so it is read as one, no `Literal<>` wrapper needed.
2322
+ return {
2323
+ kind: "literal",
2324
+ values: listArg.elements.map((element) => literalFromNode(element, sourceFile)),
2325
+ }
2326
+ }
2327
+
2328
+ // A named alias for a source: `type MySites = Rows<…>`.
2329
+ if (resolveCtx) {
2330
+ const resolved = resolveAccessAliasNode(typeNode, sourceFile, resolveCtx)
2331
+ if (resolved) return parseMembershipSource(resolved, sourceFile, resolveCtx)
2332
+ }
2333
+ }
2334
+
2335
+ throw new Error(
2336
+ `\`${ownerText(typeNode, sourceFile)}\` is not a valid membership source. Use ` +
2337
+ `Rows<"table", "column", Where>, Claim<"path"> or Values<[…]>.`,
2338
+ )
2339
+ }
2340
+
2341
+ /** Interval units, matching `TimeUnit` in `@supatype/types` and the engine. */
2342
+ const TIME_UNITS = [
2343
+ "seconds",
2344
+ "minutes",
2345
+ "hours",
2346
+ "days",
2347
+ "weeks",
2348
+ "months",
2349
+ "years",
2350
+ ] as const
2351
+
2352
+ /** Granularities for `StartOf<>`. */
2353
+ const TRUNC_UNITS = ["day", "week", "month", "year"] as const
2354
+
2355
+ /**
2356
+ * `Ago<30, "days">` / `FromNow<7, "days">`.
2357
+ *
2358
+ * The amount and unit are validated here and the interval is re-assembled from the
2359
+ * parsed integer and the matched keyword, so no author-supplied text reaches
2360
+ * Postgres. That is what keeps this from being a small escape hatch: a permissive
2361
+ * `"30 days"` string would be raw SQL by another name.
2362
+ */
2363
+ function parseDurationOperand(
2364
+ ref: "Ago" | "FromNow",
2365
+ args: readonly ts.TypeNode[],
2366
+ sourceFile: ts.SourceFile,
2367
+ ): Record<string, unknown> {
2368
+ if (args.length !== 2) {
2369
+ throw new Error(`\`${ref}<>\` takes an amount and a unit, as in \`${ref}<30, "days">\`.`)
2370
+ }
2371
+
2372
+ const amountNode = args[0]!
2373
+ const opposite = ref === "Ago" ? "FromNow" : "Ago"
2374
+
2375
+ // A negative literal is a prefix-unary expression in the TypeScript AST, not a
2376
+ // numeric literal, so it has to be recognised separately, otherwise `Ago<-5, …>`
2377
+ // is refused for the wrong reason and the message misses the real advice.
2378
+ if (
2379
+ ts.isLiteralTypeNode(amountNode) &&
2380
+ ts.isPrefixUnaryExpression(amountNode.literal) &&
2381
+ amountNode.literal.operator === ts.SyntaxKind.MinusToken
2382
+ ) {
2383
+ throw new Error(
2384
+ `\`${ref}<>\` does not take a negative amount. For the other direction use ` +
2385
+ `\`${opposite}<>\`, which reads correctly instead of relying on a double negative.`,
2386
+ )
2387
+ }
2388
+
2389
+ if (!ts.isLiteralTypeNode(amountNode) || !ts.isNumericLiteral(amountNode.literal)) {
2390
+ throw new Error(`\`${ref}<>\` needs a number literal amount, as in \`${ref}<30, "days">\`.`)
2391
+ }
2392
+ const amount = Number(amountNode.literal.text)
2393
+ if (!Number.isInteger(amount)) {
2394
+ throw new Error(
2395
+ `\`${ref}<${amount}, …>\` must be a whole number of units, Postgres intervals ` +
2396
+ `take integers, so a fraction would be silently truncated or rejected.`,
2397
+ )
2398
+ }
2399
+
2400
+ const unit = stringLiteralArg(args[1], ref, "unit")
2401
+ if (!TIME_UNITS.includes(unit as (typeof TIME_UNITS)[number])) {
2402
+ throw new Error(
2403
+ `\`${ref}<${amount}, "${unit}">\` is not a valid unit. Use one of: ` +
2404
+ `${TIME_UNITS.join(", ")} (plural).`,
2405
+ )
2406
+ }
2407
+
2408
+ void sourceFile
2409
+ return { kind: ref === "Ago" ? "ago" : "fromNow", amount, unit }
2410
+ }
2411
+
2412
+ /** A string, number or boolean literal as its plain JS value. */
2413
+ function literalFromNode(
2414
+ node: ts.TypeNode,
2415
+ sourceFile: ts.SourceFile,
2416
+ ): string | number | boolean {
2417
+ if (ts.isLiteralTypeNode(node)) {
2418
+ const literal = node.literal
2419
+ if (ts.isStringLiteral(literal)) return literal.text
2420
+ if (ts.isNumericLiteral(literal)) return Number(literal.text)
2421
+ if (literal.kind === ts.SyntaxKind.TrueKeyword) return true
2422
+ if (literal.kind === ts.SyntaxKind.FalseKeyword) return false
2423
+ }
2424
+ throw new Error(
2425
+ `\`${ownerText(node, sourceFile)}\` is not a string, number or boolean literal.`,
2426
+ )
2427
+ }
2428
+
2429
+ function stringLiteralArg(
2430
+ node: ts.TypeNode | undefined,
2431
+ owner: string,
2432
+ what: string,
2433
+ ): string {
2434
+ if (!node || !ts.isLiteralTypeNode(node) || !ts.isStringLiteral(node.literal)) {
2435
+ throw new Error(`\`${owner}<>\` needs a ${what} as a string literal.`)
2436
+ }
2437
+ const value = node.literal.text.trim()
2438
+ // Rendered as a SQL identifier, so refuse anything that is not one.
2439
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
2440
+ throw new Error(`"${value}" is not a valid ${what}.`)
2441
+ }
2442
+ return value
2443
+ }
2444
+
2445
+ /**
2446
+ * One side of a comparison.
2447
+ *
2448
+ * A bare string literal is a **column** on the model the rule is attached to.
2449
+ * That is the common case by a wide margin, and making it the default keeps
2450
+ * `Eq<"author_id", AuthUid>` readable; a constant is written `Literal<"x">` so the
2451
+ * two can never be confused. Numbers and booleans are unambiguous, so they are
2452
+ * taken as constants directly.
2453
+ */
2454
+ function parseAccessOperand(
2455
+ typeNode: ts.TypeNode,
2456
+ sourceFile: ts.SourceFile,
2457
+ ): Record<string, unknown> {
2458
+ if (ts.isLiteralTypeNode(typeNode)) {
2459
+ const literal = typeNode.literal
2460
+ if (ts.isStringLiteral(literal)) {
2461
+ const name = literal.text.trim()
2462
+ if (name === "") {
2463
+ throw new Error("An empty column name is not a valid operand.")
2464
+ }
2465
+ // Rendered into SQL as an identifier, so refuse anything that is not one
2466
+ // rather than letting it through to the policy.
2467
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2468
+ throw new Error(
2469
+ `"${name}" is not a valid column name. Use \`Literal<"${name}">\` for a ` +
2470
+ `constant, or quote a real column.`,
2471
+ )
2472
+ }
2473
+ return { kind: "column", name }
2474
+ }
2475
+ if (ts.isNumericLiteral(literal)) {
2476
+ return { kind: "literal", value: Number(literal.text) }
2477
+ }
2478
+ if (literal.kind === ts.SyntaxKind.TrueKeyword) return { kind: "literal", value: true }
2479
+ if (literal.kind === ts.SyntaxKind.FalseKeyword) return { kind: "literal", value: false }
2480
+ }
2481
+
2482
+ if (ts.isTypeReferenceNode(typeNode) && !ts.isQualifiedName(typeNode.typeName)) {
2483
+ const ref = ownerText(typeNode.typeName, sourceFile)
2484
+ const operandArgs = typeNode.typeArguments ?? []
2485
+ switch (ref) {
2486
+ case "AuthUid":
2487
+ return { kind: "authUid" }
2488
+ case "AuthRole":
2489
+ return { kind: "authRole" }
2490
+ case "Now":
2491
+ return { kind: "now" }
2492
+ case "StartOf": {
2493
+ const unit = stringLiteralArg(operandArgs[0], "StartOf", "unit")
2494
+ if (!TRUNC_UNITS.includes(unit as (typeof TRUNC_UNITS)[number])) {
2495
+ throw new Error(
2496
+ `\`StartOf<"${unit}">\` is not a valid granularity. Use one of: ` +
2497
+ `${TRUNC_UNITS.join(", ")}.`,
2498
+ )
2499
+ }
2500
+ return { kind: "startOf", unit }
2501
+ }
2502
+ case "Ago":
2503
+ case "FromNow":
2504
+ return parseDurationOperand(ref, operandArgs, sourceFile)
2505
+ case "Claim": {
2506
+ const pathArg = typeNode.typeArguments?.[0]
2507
+ if (!pathArg || !ts.isLiteralTypeNode(pathArg) || !ts.isStringLiteral(pathArg.literal)) {
2508
+ throw new Error('`Claim<>` takes a dotted path literal, as in `Claim<"app_metadata.tier">`.')
2509
+ }
2510
+ const path = pathArg.literal.text.trim()
2511
+ // The path is split on dots to walk the claims object, so an empty
2512
+ // segment would silently look up a key that cannot exist.
2513
+ if (path === "" || path.split(".").some((segment) => segment.trim() === "")) {
2514
+ throw new Error(
2515
+ `\`Claim<"${path}">\` is not a valid claim path, use dotted segments, ` +
2516
+ `as in \`Claim<"app_metadata.tier">\`.`,
2517
+ )
2518
+ }
2519
+ return { kind: "claim", path }
2520
+ }
2521
+ case "Literal": {
2522
+ const valueArg = typeNode.typeArguments?.[0]
2523
+ if (!valueArg || !ts.isLiteralTypeNode(valueArg)) {
2524
+ throw new Error('`Literal<>` takes a string, number or boolean, as in `Literal<"published">`.')
2525
+ }
2526
+ const literal = valueArg.literal
2527
+ if (ts.isStringLiteral(literal)) return { kind: "literal", value: literal.text }
2528
+ if (ts.isNumericLiteral(literal)) return { kind: "literal", value: Number(literal.text) }
2529
+ if (literal.kind === ts.SyntaxKind.TrueKeyword) return { kind: "literal", value: true }
2530
+ if (literal.kind === ts.SyntaxKind.FalseKeyword) return { kind: "literal", value: false }
2531
+ throw new Error('`Literal<>` takes a string, number or boolean.')
2532
+ }
2533
+ }
2534
+ }
2535
+
2536
+ throw new Error(
2537
+ `\`${ownerText(typeNode, sourceFile)}\` is not a valid operand. Use a column name ` +
2538
+ `("author_id"), AuthUid, AuthRole, Claim<"path"> or Literal<value>.`,
2539
+ )
1705
2540
  }
1706
2541
 
1707
2542
  interface ParsedRelationOptions {
@@ -1790,3 +2625,23 @@ function relationForeignKeyFromField(fieldName: string): string {
1790
2625
  const base = snake.replace(/_id$/i, "")
1791
2626
  return `${base}_id`
1792
2627
  }
2628
+
2629
+ /** Resolve `Rules["read"]` to the rule type declared for that member. */
2630
+ function resolveIndexedAccessRule(
2631
+ node: ts.IndexedAccessTypeNode,
2632
+ sourceFile: ts.SourceFile,
2633
+ resolveCtx: ResolveContext,
2634
+ ): ts.TypeNode | undefined {
2635
+ const literal = resolveAccessLiteral(node.objectType, sourceFile, resolveCtx)
2636
+ if (!literal) return undefined
2637
+ if (!ts.isLiteralTypeNode(node.indexType) || !ts.isStringLiteral(node.indexType.literal)) {
2638
+ return undefined
2639
+ }
2640
+ const key = node.indexType.literal.text
2641
+ for (const member of literal.members) {
2642
+ if (ts.isPropertySignature(member) && getPropertyName(member.name) === key) {
2643
+ return member.type
2644
+ }
2645
+ }
2646
+ return undefined
2647
+ }