@supatype/cli 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (350) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +208 -1
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app/proxy-dev-app.d.ts +13 -0
  5. package/dist/app/proxy-dev-app.d.ts.map +1 -0
  6. package/dist/app/proxy-dev-app.js +53 -0
  7. package/dist/app/proxy-dev-app.js.map +1 -0
  8. package/dist/app-config.d.ts +7 -0
  9. package/dist/app-config.d.ts.map +1 -0
  10. package/dist/app-config.js +113 -0
  11. package/dist/app-config.js.map +1 -0
  12. package/dist/augmentation-generator.d.ts +2 -0
  13. package/dist/augmentation-generator.d.ts.map +1 -0
  14. package/dist/augmentation-generator.js +111 -0
  15. package/dist/augmentation-generator.js.map +1 -0
  16. package/dist/binary-cache.d.ts +94 -0
  17. package/dist/binary-cache.d.ts.map +1 -0
  18. package/dist/binary-cache.js +669 -0
  19. package/dist/binary-cache.js.map +1 -0
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +13 -7
  22. package/dist/cli.js.map +1 -1
  23. package/dist/commands/admin.d.ts.map +1 -1
  24. package/dist/commands/admin.js +4 -3
  25. package/dist/commands/admin.js.map +1 -1
  26. package/dist/commands/app.d.ts.map +1 -1
  27. package/dist/commands/app.js +56 -209
  28. package/dist/commands/app.js.map +1 -1
  29. package/dist/commands/cache.d.ts +6 -0
  30. package/dist/commands/cache.d.ts.map +1 -0
  31. package/dist/commands/cache.js +105 -0
  32. package/dist/commands/cache.js.map +1 -0
  33. package/dist/commands/cloud.d.ts +20 -0
  34. package/dist/commands/cloud.d.ts.map +1 -1
  35. package/dist/commands/cloud.js +50 -52
  36. package/dist/commands/cloud.js.map +1 -1
  37. package/dist/commands/db.d.ts.map +1 -1
  38. package/dist/commands/db.js +47 -54
  39. package/dist/commands/db.js.map +1 -1
  40. package/dist/commands/deploy.d.ts +2 -1
  41. package/dist/commands/deploy.d.ts.map +1 -1
  42. package/dist/commands/deploy.js +79 -52
  43. package/dist/commands/deploy.js.map +1 -1
  44. package/dist/commands/dev.d.ts +11 -0
  45. package/dist/commands/dev.d.ts.map +1 -1
  46. package/dist/commands/dev.js +759 -385
  47. package/dist/commands/dev.js.map +1 -1
  48. package/dist/commands/diff.d.ts.map +1 -1
  49. package/dist/commands/diff.js +30 -15
  50. package/dist/commands/diff.js.map +1 -1
  51. package/dist/commands/engine.d.ts +1 -3
  52. package/dist/commands/engine.d.ts.map +1 -1
  53. package/dist/commands/engine.js +13 -85
  54. package/dist/commands/engine.js.map +1 -1
  55. package/dist/commands/functions.d.ts.map +1 -1
  56. package/dist/commands/functions.js +92 -105
  57. package/dist/commands/functions.js.map +1 -1
  58. package/dist/commands/generate.d.ts.map +1 -1
  59. package/dist/commands/generate.js +22 -12
  60. package/dist/commands/generate.js.map +1 -1
  61. package/dist/commands/init.d.ts +1 -1
  62. package/dist/commands/init.d.ts.map +1 -1
  63. package/dist/commands/init.js +137 -410
  64. package/dist/commands/init.js.map +1 -1
  65. package/dist/commands/migrate-from-v1.d.ts +5 -0
  66. package/dist/commands/migrate-from-v1.d.ts.map +1 -0
  67. package/dist/commands/migrate-from-v1.js +125 -0
  68. package/dist/commands/migrate-from-v1.js.map +1 -0
  69. package/dist/commands/migrate.d.ts.map +1 -1
  70. package/dist/commands/migrate.js +27 -23
  71. package/dist/commands/migrate.js.map +1 -1
  72. package/dist/commands/pg.d.ts +8 -0
  73. package/dist/commands/pg.d.ts.map +1 -0
  74. package/dist/commands/pg.js +102 -0
  75. package/dist/commands/pg.js.map +1 -0
  76. package/dist/commands/pull.d.ts.map +1 -1
  77. package/dist/commands/pull.js +5 -66
  78. package/dist/commands/pull.js.map +1 -1
  79. package/dist/commands/push.d.ts.map +1 -1
  80. package/dist/commands/push.js +128 -38
  81. package/dist/commands/push.js.map +1 -1
  82. package/dist/commands/seed.d.ts +2 -0
  83. package/dist/commands/seed.d.ts.map +1 -1
  84. package/dist/commands/seed.js +44 -11
  85. package/dist/commands/seed.js.map +1 -1
  86. package/dist/commands/self-host.d.ts +7 -1
  87. package/dist/commands/self-host.d.ts.map +1 -1
  88. package/dist/commands/self-host.js +272 -758
  89. package/dist/commands/self-host.js.map +1 -1
  90. package/dist/commands/self-update.d.ts +9 -0
  91. package/dist/commands/self-update.d.ts.map +1 -0
  92. package/dist/commands/self-update.js +33 -0
  93. package/dist/commands/self-update.js.map +1 -0
  94. package/dist/commands/status.d.ts.map +1 -1
  95. package/dist/commands/status.js +4 -3
  96. package/dist/commands/status.js.map +1 -1
  97. package/dist/commands/types.d.ts +3 -0
  98. package/dist/commands/types.d.ts.map +1 -0
  99. package/dist/commands/types.js +62 -0
  100. package/dist/commands/types.js.map +1 -0
  101. package/dist/commands/update.d.ts +7 -0
  102. package/dist/commands/update.d.ts.map +1 -0
  103. package/dist/commands/update.js +93 -0
  104. package/dist/commands/update.js.map +1 -0
  105. package/dist/components.d.ts +5 -0
  106. package/dist/components.d.ts.map +1 -0
  107. package/dist/components.js +3 -0
  108. package/dist/components.js.map +1 -0
  109. package/dist/config.d.ts +10 -51
  110. package/dist/config.d.ts.map +1 -1
  111. package/dist/config.js +101 -33
  112. package/dist/config.js.map +1 -1
  113. package/dist/dev-compose.d.ts +17 -0
  114. package/dist/dev-compose.d.ts.map +1 -0
  115. package/dist/dev-compose.js +374 -0
  116. package/dist/dev-compose.js.map +1 -0
  117. package/dist/diff-output.d.ts +4 -0
  118. package/dist/diff-output.d.ts.map +1 -0
  119. package/dist/diff-output.js +12 -0
  120. package/dist/diff-output.js.map +1 -0
  121. package/dist/docker-postgres.d.ts +57 -0
  122. package/dist/docker-postgres.d.ts.map +1 -0
  123. package/dist/docker-postgres.js +208 -0
  124. package/dist/docker-postgres.js.map +1 -0
  125. package/dist/engine-client.d.ts +69 -0
  126. package/dist/engine-client.d.ts.map +1 -0
  127. package/dist/engine-client.js +157 -0
  128. package/dist/engine-client.js.map +1 -0
  129. package/dist/ensure-binary.d.ts +7 -0
  130. package/dist/ensure-binary.d.ts.map +1 -0
  131. package/dist/ensure-binary.js +17 -0
  132. package/dist/ensure-binary.js.map +1 -0
  133. package/dist/functions-router-gen.d.ts +14 -0
  134. package/dist/functions-router-gen.d.ts.map +1 -0
  135. package/dist/functions-router-gen.js +199 -0
  136. package/dist/functions-router-gen.js.map +1 -0
  137. package/dist/index.d.ts +4 -5
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +2 -3
  140. package/dist/index.js.map +1 -1
  141. package/dist/kong-config.d.ts +25 -0
  142. package/dist/kong-config.d.ts.map +1 -0
  143. package/dist/kong-config.js +71 -0
  144. package/dist/kong-config.js.map +1 -0
  145. package/dist/local-gateway.d.ts +7 -0
  146. package/dist/local-gateway.d.ts.map +1 -0
  147. package/dist/local-gateway.js +9 -0
  148. package/dist/local-gateway.js.map +1 -0
  149. package/dist/local-storage.d.ts +8 -0
  150. package/dist/local-storage.d.ts.map +1 -0
  151. package/dist/local-storage.js +14 -0
  152. package/dist/local-storage.js.map +1 -0
  153. package/dist/pgbouncer-userlist.d.ts +5 -0
  154. package/dist/pgbouncer-userlist.d.ts.map +1 -0
  155. package/dist/pgbouncer-userlist.js +14 -0
  156. package/dist/pgbouncer-userlist.js.map +1 -0
  157. package/dist/postgres-ctl.d.ts +44 -0
  158. package/dist/postgres-ctl.d.ts.map +1 -0
  159. package/dist/postgres-ctl.js +137 -0
  160. package/dist/postgres-ctl.js.map +1 -0
  161. package/dist/process-manager.d.ts +43 -0
  162. package/dist/process-manager.d.ts.map +1 -0
  163. package/dist/process-manager.js +135 -0
  164. package/dist/process-manager.js.map +1 -0
  165. package/dist/project-config.d.ts +235 -0
  166. package/dist/project-config.d.ts.map +1 -0
  167. package/dist/project-config.js +160 -0
  168. package/dist/project-config.js.map +1 -0
  169. package/dist/pull-utils.d.ts +15 -0
  170. package/dist/pull-utils.d.ts.map +1 -1
  171. package/dist/pull-utils.js +12 -0
  172. package/dist/pull-utils.js.map +1 -1
  173. package/dist/release-pins.d.ts +7 -0
  174. package/dist/release-pins.d.ts.map +1 -0
  175. package/dist/release-pins.js +27 -0
  176. package/dist/release-pins.js.map +1 -0
  177. package/dist/release-public-key.d.ts +8 -0
  178. package/dist/release-public-key.d.ts.map +1 -0
  179. package/dist/release-public-key.js +13 -0
  180. package/dist/release-public-key.js.map +1 -0
  181. package/dist/runtime-routes.d.ts +34 -0
  182. package/dist/runtime-routes.d.ts.map +1 -0
  183. package/dist/runtime-routes.js +252 -0
  184. package/dist/runtime-routes.js.map +1 -0
  185. package/dist/schema-ast-v2.d.ts +127 -0
  186. package/dist/schema-ast-v2.d.ts.map +1 -0
  187. package/dist/schema-ast-v2.js +226 -0
  188. package/dist/schema-ast-v2.js.map +1 -0
  189. package/dist/scripts/postinstall.d.ts +5 -6
  190. package/dist/scripts/postinstall.d.ts.map +1 -1
  191. package/dist/scripts/postinstall.js +36 -20
  192. package/dist/scripts/postinstall.js.map +1 -1
  193. package/dist/self-host-compose.d.ts +22 -0
  194. package/dist/self-host-compose.d.ts.map +1 -0
  195. package/dist/self-host-compose.js +347 -0
  196. package/dist/self-host-compose.js.map +1 -0
  197. package/dist/storage-provision.d.ts +24 -0
  198. package/dist/storage-provision.d.ts.map +1 -0
  199. package/dist/storage-provision.js +44 -0
  200. package/dist/storage-provision.js.map +1 -0
  201. package/dist/studio-admin-roles.d.ts +7 -0
  202. package/dist/studio-admin-roles.d.ts.map +1 -0
  203. package/dist/studio-admin-roles.js +14 -0
  204. package/dist/studio-admin-roles.js.map +1 -0
  205. package/dist/studio-dev-server.d.ts +22 -0
  206. package/dist/studio-dev-server.d.ts.map +1 -0
  207. package/dist/studio-dev-server.js +28 -0
  208. package/dist/studio-dev-server.js.map +1 -0
  209. package/dist/systemd.d.ts +26 -0
  210. package/dist/systemd.d.ts.map +1 -0
  211. package/dist/systemd.js +102 -0
  212. package/dist/systemd.js.map +1 -0
  213. package/dist/tsx-runner.d.ts.map +1 -1
  214. package/dist/tsx-runner.js +9 -2
  215. package/dist/tsx-runner.js.map +1 -1
  216. package/dist/type-extractor.d.ts +4 -0
  217. package/dist/type-extractor.d.ts.map +1 -0
  218. package/dist/type-extractor.js +1213 -0
  219. package/dist/type-extractor.js.map +1 -0
  220. package/dist/type-resolver.d.ts +33 -0
  221. package/dist/type-resolver.d.ts.map +1 -0
  222. package/dist/type-resolver.js +338 -0
  223. package/dist/type-resolver.js.map +1 -0
  224. package/package.json +4 -3
  225. package/releases/deno/VERSION +1 -0
  226. package/scripts/mirror-deno-release.sh +76 -0
  227. package/src/TYPE-RESOLUTION.md +294 -0
  228. package/src/app/proxy-dev-app.ts +67 -0
  229. package/src/app-config.ts +128 -0
  230. package/src/augmentation-generator.ts +126 -0
  231. package/src/binary-cache.ts +822 -0
  232. package/src/cli.ts +13 -8
  233. package/src/commands/admin.ts +4 -3
  234. package/src/commands/app.ts +67 -231
  235. package/src/commands/cache.ts +117 -0
  236. package/src/commands/cloud.ts +63 -64
  237. package/src/commands/db.ts +54 -63
  238. package/src/commands/deploy.ts +96 -62
  239. package/src/commands/dev.ts +933 -405
  240. package/src/commands/diff.ts +31 -29
  241. package/src/commands/engine.ts +13 -116
  242. package/src/commands/functions.ts +97 -115
  243. package/src/commands/generate.ts +23 -10
  244. package/src/commands/init.ts +149 -414
  245. package/src/commands/migrate-from-v1.ts +131 -0
  246. package/src/commands/migrate.ts +27 -23
  247. package/src/commands/pg.ts +133 -0
  248. package/src/commands/pull.ts +6 -85
  249. package/src/commands/push.ts +161 -56
  250. package/src/commands/seed.ts +54 -12
  251. package/src/commands/self-host.ts +312 -880
  252. package/src/commands/self-update.ts +45 -0
  253. package/src/commands/status.ts +4 -3
  254. package/src/commands/types.ts +76 -0
  255. package/src/commands/update.ts +109 -0
  256. package/src/components.ts +6 -0
  257. package/src/config.ts +127 -94
  258. package/src/dev-compose.ts +455 -0
  259. package/src/diff-output.ts +12 -0
  260. package/src/docker-postgres.ts +295 -0
  261. package/src/engine-client.ts +236 -0
  262. package/src/ensure-binary.ts +28 -0
  263. package/src/functions-router-gen.ts +224 -0
  264. package/src/index.ts +4 -12
  265. package/src/kong-config.ts +93 -0
  266. package/src/local-gateway.ts +9 -0
  267. package/src/local-storage.ts +14 -0
  268. package/src/pgbouncer-userlist.ts +15 -0
  269. package/src/postgres-ctl.ts +171 -0
  270. package/src/process-manager.ts +168 -0
  271. package/src/project-config.ts +386 -0
  272. package/src/pull-utils.ts +24 -0
  273. package/src/release-pins.ts +31 -0
  274. package/src/release-public-key.ts +12 -0
  275. package/src/runtime-routes.ts +291 -0
  276. package/src/schema-ast-v2.ts +324 -0
  277. package/src/scripts/postinstall.ts +36 -25
  278. package/src/self-host-compose.ts +389 -0
  279. package/src/storage-provision.ts +58 -0
  280. package/src/studio-admin-roles.ts +16 -0
  281. package/src/studio-dev-server.ts +53 -0
  282. package/src/systemd.ts +137 -0
  283. package/src/tsx-runner.ts +11 -1
  284. package/src/type-extractor.ts +1479 -0
  285. package/src/type-resolver.ts +457 -0
  286. package/tests/app-command.test.ts +54 -0
  287. package/tests/augmentation-generator.test.ts +59 -0
  288. package/tests/binary-cache-cloud-overrides.test.ts +123 -0
  289. package/tests/cached-artifact-format.test.ts +84 -0
  290. package/tests/cli-help.test.ts +40 -14
  291. package/tests/config.test.ts +171 -37
  292. package/tests/docker-postgres.test.ts +39 -0
  293. package/tests/engine-distribution.test.ts +3 -3
  294. package/tests/ensure-binary.test.ts +59 -0
  295. package/tests/init.test.ts +28 -86
  296. package/tests/migrate-from-v1.test.ts +29 -0
  297. package/tests/normalize-admin-config.test.ts +48 -0
  298. package/tests/pg-spawn-env.test.ts +18 -0
  299. package/tests/postgres-archive-tag.test.ts +9 -0
  300. package/tests/proxy-dev-app.test.ts +33 -0
  301. package/tests/pull-utils.test.ts +36 -1
  302. package/tests/release-pins.test.ts +28 -0
  303. package/tests/runtime-contract.test.ts +351 -0
  304. package/tests/seed-discover.test.ts +31 -0
  305. package/tests/studio-admin-roles.test.ts +27 -0
  306. package/tests/tsconfig.json +9 -0
  307. package/tests/type-extractor.test.ts +985 -0
  308. package/tests/type-resolver.test.ts +59 -0
  309. package/tsconfig.tsbuildinfo +1 -1
  310. package/vitest.config.ts +12 -0
  311. package/dist/engine/cache.d.ts +0 -37
  312. package/dist/engine/cache.d.ts.map +0 -1
  313. package/dist/engine/cache.js +0 -121
  314. package/dist/engine/cache.js.map +0 -1
  315. package/dist/engine/download.d.ts +0 -19
  316. package/dist/engine/download.d.ts.map +0 -1
  317. package/dist/engine/download.js +0 -108
  318. package/dist/engine/download.js.map +0 -1
  319. package/dist/engine/platform.d.ts +0 -24
  320. package/dist/engine/platform.d.ts.map +0 -1
  321. package/dist/engine/platform.js +0 -50
  322. package/dist/engine/platform.js.map +0 -1
  323. package/dist/engine/resolve.d.ts +0 -37
  324. package/dist/engine/resolve.d.ts.map +0 -1
  325. package/dist/engine/resolve.js +0 -133
  326. package/dist/engine/resolve.js.map +0 -1
  327. package/dist/engine/update-notify.d.ts +0 -11
  328. package/dist/engine/update-notify.d.ts.map +0 -1
  329. package/dist/engine/update-notify.js +0 -43
  330. package/dist/engine/update-notify.js.map +0 -1
  331. package/dist/engine/verify.d.ts +0 -50
  332. package/dist/engine/verify.d.ts.map +0 -1
  333. package/dist/engine/verify.js +0 -161
  334. package/dist/engine/verify.js.map +0 -1
  335. package/dist/engine-version.d.ts +0 -35
  336. package/dist/engine-version.d.ts.map +0 -1
  337. package/dist/engine-version.js +0 -35
  338. package/dist/engine-version.js.map +0 -1
  339. package/dist/engine.d.ts +0 -34
  340. package/dist/engine.d.ts.map +0 -1
  341. package/dist/engine.js +0 -76
  342. package/dist/engine.js.map +0 -1
  343. package/src/engine/cache.ts +0 -135
  344. package/src/engine/download.ts +0 -143
  345. package/src/engine/platform.ts +0 -66
  346. package/src/engine/resolve.ts +0 -197
  347. package/src/engine/update-notify.ts +0 -50
  348. package/src/engine/verify.ts +0 -206
  349. package/src/engine-version.ts +0 -39
  350. package/src/engine.ts +0 -99
@@ -0,0 +1,294 @@
1
+ # Type Resolution in the Schema Extractor
2
+
3
+ `type-extractor.ts` converts TypeScript type definitions into `ExtractedSchemaAst` —
4
+ the JSON handed to the engine binary for SQL generation and client type output.
5
+
6
+ This document explains how type names are resolved, what patterns are supported,
7
+ and how the three-tier fallback chain works.
8
+
9
+ ---
10
+
11
+ ## The Problem
12
+
13
+ The extractor uses the TypeScript **parser only** (`ts.createSourceFile`), not the
14
+ full type checker. This is fast and requires no `tsconfig.json`, but it means the
15
+ extractor only sees raw source text — it cannot evaluate what a type alias resolves to.
16
+
17
+ The consequence is that any indirection breaks resolution:
18
+
19
+ ```typescript
20
+ // Works — extractor sees "Optional" literally
21
+ type Post = Model<{ email: Optional<Email> }>
22
+
23
+ // Previously broken — extractor sees "Nullable", not "Optional"
24
+ type Nullable<T> = Optional<T>
25
+ type Post = Model<{ email: Nullable<Email> }>
26
+
27
+ // Previously broken — import rename
28
+ import { Optional as Maybe } from "@supatype/types"
29
+ type Post = Model<{ email: Maybe<Email> }>
30
+ ```
31
+
32
+ Failures were silent — unknown types fell through to `{ kind: "text", pgType: "TEXT" }`
33
+ instead of throwing an error.
34
+
35
+ ---
36
+
37
+ ## Three-Tier Resolution
38
+
39
+ Every type name encountered in a field definition is resolved through three tiers
40
+ in order. The first tier to succeed wins. If all three fail, an error is thrown.
41
+
42
+ ```
43
+ Tier 1 — syntactic switch instant inline primitives and modifiers by name
44
+ Tier 2 — alias registry instant user-defined type aliases, import renames
45
+ Tier 3 — TypeScript checker ~300ms† conditional types, mapped types
46
+ ```
47
+
48
+ † Tier 3 is **lazy** — the `ts.Program` and `TypeChecker` are only created the first
49
+ time a conditional or mapped type is encountered. Schemas that use only tiers 1 and 2
50
+ pay no cost.
51
+
52
+ ---
53
+
54
+ ## Tier 1 — Syntactic Switch
55
+
56
+ The existing behaviour. The extractor walks the type reference chain and matches
57
+ names exactly against a hardcoded switch:
58
+
59
+ ```typescript
60
+ switch (typeName) {
61
+ case "Optional": flags.required = false; unwrap(); continue
62
+ case "Unique": flags.unique = true; unwrap(); continue
63
+ case "PrimaryKey": flags.primaryKey = true; unwrap(); continue
64
+ case "UUID": return { kind: "uuid", pgType: "UUID" }
65
+ case "Email": return { kind: "email", pgType: "TEXT" }
66
+ // ... all @supatype/types primitives and modifiers
67
+ }
68
+ ```
69
+
70
+ This covers all types used inline with their canonical names.
71
+
72
+ ---
73
+
74
+ ## Tier 2 — Alias Registry
75
+
76
+ Built once at startup from all source files loaded by `loadSchemaSourceFiles`.
77
+ Covers two sub-cases:
78
+
79
+ ### 2a — Type alias declarations
80
+
81
+ Any `type X = ...` that is not a `Model<>` declaration is indexed by name:
82
+
83
+ ```typescript
84
+ // These all become entries in the alias registry:
85
+ type Nullable<T> = Optional<T>
86
+ type UniqueSlug = Unique<Slug<"title">>
87
+ type AuditId = PrimaryKey<UUID>
88
+ type MyEnum = "draft" | "published" | "archived"
89
+ ```
90
+
91
+ When the extractor encounters an unknown name, it looks it up in the registry,
92
+ substitutes any type parameters via text replacement, and re-enters tier 1 with
93
+ the resolved node.
94
+
95
+ Multi-hop aliases work because the resolution recurses:
96
+
97
+ ```typescript
98
+ type A = B
99
+ type B = Optional<Email>
100
+ // A → B → Optional<Email> → resolved by tier 1
101
+ ```
102
+
103
+ Cycle detection via a `resolving: Set<string>` guard prevents infinite loops and
104
+ throws a descriptive error instead.
105
+
106
+ ### 2b — Import renames
107
+
108
+ Explicit `as` renames in import statements are indexed per file:
109
+
110
+ ```typescript
111
+ import { Optional as Maybe } from "@supatype/types"
112
+ import { Nullable as MaybeNull } from "./shared/field-types"
113
+ ```
114
+
115
+ Before the tier 1 switch runs, each name is checked against the rename map for
116
+ the current file. `Maybe` becomes `Optional`, `MaybeNull` becomes `Nullable`
117
+ (which is then resolved by 2a).
118
+
119
+ ### File loading
120
+
121
+ `loadSchemaSourceFiles` follows both `export` declarations (existing) and local
122
+ `import` declarations (new), ensuring that files referenced via import are loaded
123
+ into the source file set and their aliases are available in the registry.
124
+
125
+ Only relative specifiers (`.`-prefixed) are followed. Bare specifiers and scoped
126
+ packages (`@supatype/types`, `node_modules/*`) are not loaded — their exported
127
+ names are already covered by the tier 1 switch.
128
+
129
+ ---
130
+
131
+ ## Tier 3 — TypeScript Checker
132
+
133
+ Required for types that cannot be evaluated syntactically:
134
+
135
+ - **Conditional types**: `T extends U ? A : B` — requires evaluating the constraint
136
+ - **Mapped types**: `{ [K in keyof T]: F<T[K]> }` — requires enumerating `keyof T`
137
+
138
+ ### How it works
139
+
140
+ 1. A lazy `ts.Program` is created from the already-loaded source files using a
141
+ custom `CompilerHost` that serves them from memory (no disk re-reads).
142
+
143
+ 2. Because our lightweight parse tree and the Program's parse tree are different
144
+ objects (even for the same file), the node with the unresolvable type is located
145
+ in the Program's source file by matching `pos`/`end` character positions.
146
+
147
+ 3. `checker.getTypeAtLocation(programNode)` resolves the type fully.
148
+
149
+ 4. `checker.typeToString(type, ..., TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)`
150
+ converts it back to a string with **alias names preserved** — so `Optional<Email>`
151
+ stays as `Optional<Email>` rather than expanding to the underlying branded
152
+ intersection type.
153
+
154
+ 5. The string is re-parsed via `ts.createSourceFile` into a proper TypeNode with
155
+ valid `pos`/`end` values (so downstream `getText()` calls work).
156
+
157
+ 6. The resolved TypeNode is fed back into the tier 1 switch.
158
+
159
+ ### When tier 3 fires
160
+
161
+ - A field type is directly a conditional or mapped expression
162
+ - A tier 2 alias body contains a conditional or mapped type (detected by
163
+ `needsChecker()` before text substitution is attempted — the original node,
164
+ which has valid source positions, is passed to the checker instead)
165
+ - The `Model<>` fields argument is a mapped type alias:
166
+ ```typescript
167
+ type AllOptional<T> = { [K in keyof T]: Optional<T[K]> }
168
+ type Post = Model<AllOptional<{ email: Email; name: Text }>>
169
+ ```
170
+ This is handled in `unwrapModelFields`, which also participates in the
171
+ three-tier chain.
172
+
173
+ ---
174
+
175
+ ## What Is Supported
176
+
177
+ ```typescript
178
+ // Tier 1 — inline, canonical names
179
+ email: Optional<Email>
180
+ slug: Unique<Slug<"title">>
181
+ id: PrimaryKey<UUID>
182
+
183
+ // Tier 2a — simple alias
184
+ type Nullable<T> = Optional<T>
185
+ type UniqueSlug = Unique<Slug<"title">>
186
+ email: Nullable<Email>
187
+ slug: UniqueSlug
188
+
189
+ // Tier 2a — multi-hop
190
+ type A = B
191
+ type B = Nullable<Email>
192
+ email: A
193
+
194
+ // Tier 2b — import rename of primitive
195
+ import { Optional as Maybe } from "@supatype/types"
196
+ email: Maybe<Email>
197
+
198
+ // Tier 2b — import rename of local alias
199
+ import { Nullable as MaybeNull } from "./field-types"
200
+ email: MaybeNull<Email>
201
+
202
+ // Tier 2b — cross-file alias (no rename)
203
+ // helpers.ts: export type Nullable<T> = Optional<T>
204
+ import { Nullable } from "./helpers"
205
+ email: Nullable<Email>
206
+
207
+ // Tier 3 — conditional type
208
+ type NullableStr<T> = T extends string ? Optional<T> : T
209
+ email: NullableStr<Email>
210
+
211
+ // Tier 3 — mapped type as fields object
212
+ type AllOptional<T> = { [K in keyof T]: Optional<T[K]> }
213
+ type Post = Model<AllOptional<{ email: Email; name: Text }>>
214
+ ```
215
+
216
+ ---
217
+
218
+ ## What Is Not Supported
219
+
220
+ ```typescript
221
+ // Imports from node_modules other than @supatype/types
222
+ import { SomeHelper } from "some-library"
223
+
224
+ // Conditional / mapped types in alias bodies that reference
225
+ // symbols only available in node_modules (other than @supatype/types)
226
+
227
+ // TypeScript utility types used as field types
228
+ email: NonNullable<string> // error — not a @supatype/types primitive
229
+
230
+ // Namespace-qualified names
231
+ email: Types.Optional<Email> // error — only identifier references are resolved
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Error Behaviour
237
+
238
+ Unknown types now **throw** instead of silently falling back to
239
+ `{ kind: "text", pgType: "TEXT" }`:
240
+
241
+ ```
242
+ Error: Unknown Supatype type "SomeType" in field "email".
243
+ If this is a type alias, confirm the file defining it is reachable
244
+ from your schema entry point.
245
+ ```
246
+
247
+ Cycles in alias chains throw:
248
+
249
+ ```
250
+ Error: Field "email": circular alias chain detected resolving "A".
251
+ ```
252
+
253
+ Unresolvable conditional/mapped types throw:
254
+
255
+ ```
256
+ Error: Field "email": could not resolve conditional/mapped type via type checker.
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Data Structures
262
+
263
+ ```typescript
264
+ // One entry per non-Model type alias declaration across all loaded source files
265
+ type AliasEntry = {
266
+ typeParams: string[] // ["T"] for Nullable<T> = Optional<T>
267
+ body: ts.TypeNode // the RHS of the declaration
268
+ sourceFile: ts.SourceFile // getText() context for body
269
+ }
270
+
271
+ // Rename map: sf.fileName → (localName → canonicalName)
272
+ // Only populated for explicit `import { X as Y }` renames
273
+ type ImportRenameMap = Map<string, Map<string, string>>
274
+
275
+ // Passed to every resolution function; checker is lazy
276
+ type ResolveContext = {
277
+ aliasRegistry: Map<string, AliasEntry>
278
+ renameMap: ImportRenameMap
279
+ getChecker: () => CheckerContext
280
+ }
281
+
282
+ type CheckerContext = {
283
+ program: ts.Program
284
+ checker: ts.TypeChecker
285
+ }
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Adding a New Tier 1 Primitive
291
+
292
+ When a new type is added to `@supatype/types`, add a `case` to the `switch` in
293
+ `parseScalarType`. No other changes are needed — tier 2 and tier 3 handle
294
+ aliases and compositions of it automatically.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Spawn the frontend dev server during `supatype dev` when app.mode is proxy.
3
+ */
4
+
5
+ import { existsSync, readFileSync } from "node:fs"
6
+ import { join } from "node:path"
7
+ import { detectPackageManager } from "./framework.js"
8
+ import { ProcessManager } from "../process-manager.js"
9
+ import { projectRootFromConfig, type SupatypeProjectConfig } from "../project-config.js"
10
+
11
+ const DEFAULT_PROXY_DEV_SCRIPT = "start"
12
+
13
+ /** package.json script name to run (only when app.mode is proxy). */
14
+ export function resolveProxyDevScript(config: SupatypeProjectConfig): string | null {
15
+ if (config.app?.mode !== "proxy") return null
16
+ const script = config.app.start?.trim()
17
+ return script && script.length > 0 ? script : DEFAULT_PROXY_DEV_SCRIPT
18
+ }
19
+
20
+ /**
21
+ * Start the configured package.json script for proxy dev.
22
+ * Returns null when not in proxy mode or when the script is missing.
23
+ */
24
+ export function startProxyDevApp(
25
+ cwd: string,
26
+ config: SupatypeProjectConfig,
27
+ pidDir: string,
28
+ ): ProcessManager | null {
29
+ const script = resolveProxyDevScript(config)
30
+ if (!script) return null
31
+
32
+ const appDir = projectRootFromConfig(config, cwd)
33
+ const pkgPath = join(appDir, "package.json")
34
+ if (!existsSync(pkgPath)) {
35
+ console.warn(`[supatype] app.mode=proxy but no package.json at ${appDir}; skipping dev app`)
36
+ return null
37
+ }
38
+
39
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as {
40
+ scripts?: Record<string, string>
41
+ }
42
+ if (!pkg.scripts?.[script]) {
43
+ console.warn(
44
+ `[supatype] app.mode=proxy: package.json has no "${script}" script.\n` +
45
+ ` Add "scripts.${script}" or set app.start in supatype.config.ts`,
46
+ )
47
+ return null
48
+ }
49
+
50
+ const pm = detectPackageManager(appDir)
51
+ const bin = pm
52
+ const args = pm === "yarn" ? [script] : ["run", script]
53
+ // pnpm/npm/yarn are .cmd shims on Windows — spawn via shell so PATH resolution works.
54
+ const useShell = process.platform === "win32"
55
+
56
+ console.log(`[supatype] Proxy mode: running ${bin} ${args.join(" ")} (${appDir})`)
57
+
58
+ const manager = new ProcessManager(bin, args, {
59
+ label: "app",
60
+ pidDir,
61
+ cwd: appDir,
62
+ colour: "\x1b[33m",
63
+ shell: useShell,
64
+ })
65
+ manager.start()
66
+ return manager
67
+ }
@@ -0,0 +1,128 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import { Node, Project, QuoteKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from "ts-morph"
4
+
5
+ export interface UpdateAppConfigInput {
6
+ mode: "none" | "static" | "proxy"
7
+ staticDir?: string
8
+ upstream?: string
9
+ }
10
+
11
+ export function updateAppConfigInProject(cwd: string, input: UpdateAppConfigInput): string {
12
+ const configPath = resolve(cwd, "supatype.config.ts")
13
+ if (!existsSync(configPath)) {
14
+ throw new Error("supatype.config.ts not found. Run: supatype init")
15
+ }
16
+ const next = updateAppConfigAst(configPath, input)
17
+ writeFileSync(configPath, next, "utf8")
18
+ return configPath
19
+ }
20
+
21
+ function updateAppConfigAst(configPath: string, input: UpdateAppConfigInput): string {
22
+ const project = new Project({
23
+ useInMemoryFileSystem: false,
24
+ skipAddingFilesFromTsConfig: true,
25
+ manipulationSettings: {
26
+ quoteKind: QuoteKind.Double,
27
+ },
28
+ })
29
+ const srcText = readFileSync(configPath, "utf8")
30
+ const sourceFile = project.createSourceFile(configPath, srcText, { overwrite: true })
31
+ const rootObject = getRootConfigObject(sourceFile)
32
+
33
+ const appProperty = rootObject.getProperty("app")
34
+
35
+ if (appProperty === undefined) {
36
+ rootObject.addPropertyAssignment({
37
+ name: "app",
38
+ initializer: renderAppInitializer(input),
39
+ })
40
+ } else if (Node.isPropertyAssignment(appProperty)) {
41
+ const init = appProperty.getInitializer()
42
+ if (init && Node.isObjectLiteralExpression(init)) {
43
+ patchAppObject(init, input)
44
+ } else {
45
+ appProperty.setInitializer(renderAppInitializer(input))
46
+ }
47
+ } else {
48
+ appProperty.remove()
49
+ rootObject.addPropertyAssignment({
50
+ name: "app",
51
+ initializer: renderAppInitializer(input),
52
+ })
53
+ }
54
+
55
+ return sourceFile.getFullText()
56
+ }
57
+
58
+ function getRootConfigObject(sourceFile: SourceFile): ObjectLiteralExpression {
59
+ const exportAssignment = sourceFile.getFirstDescendantByKind(SyntaxKind.ExportAssignment)
60
+ if (!exportAssignment) {
61
+ throw new Error("Could not find default export in supatype.config.ts.")
62
+ }
63
+ const expr = exportAssignment.getExpression()
64
+ if (Node.isObjectLiteralExpression(expr)) {
65
+ return expr
66
+ }
67
+ if (Node.isCallExpression(expr)) {
68
+ const [firstArg] = expr.getArguments()
69
+ if (firstArg && Node.isObjectLiteralExpression(firstArg)) {
70
+ return firstArg
71
+ }
72
+ }
73
+ throw new Error(
74
+ "supatype.config.ts must export an object literal or defineConfig({...}).",
75
+ )
76
+ }
77
+
78
+ function renderAppInitializer(input: UpdateAppConfigInput): string {
79
+ if (input.mode === "proxy") {
80
+ return `{
81
+ mode: "proxy",
82
+ upstream: "${input.upstream ?? "http://localhost:3000"}",
83
+ }`
84
+ }
85
+ if (input.mode === "static") {
86
+ return `{
87
+ mode: "static",
88
+ static_dir: "${input.staticDir ?? "./dist"}",
89
+ }`
90
+ }
91
+ return `{
92
+ mode: "none",
93
+ }`
94
+ }
95
+
96
+ function patchAppObject(appObj: ObjectLiteralExpression, input: UpdateAppConfigInput): void {
97
+ upsertStringProperty(appObj, "mode", input.mode)
98
+
99
+ if (input.mode === "proxy") {
100
+ upsertStringProperty(appObj, "upstream", input.upstream ?? "http://localhost:3000")
101
+ removePropertyIfPresent(appObj, "static_dir")
102
+ return
103
+ }
104
+
105
+ if (input.mode === "static") {
106
+ upsertStringProperty(appObj, "static_dir", input.staticDir ?? "./dist")
107
+ removePropertyIfPresent(appObj, "upstream")
108
+ return
109
+ }
110
+
111
+ removePropertyIfPresent(appObj, "upstream")
112
+ removePropertyIfPresent(appObj, "static_dir")
113
+ }
114
+
115
+ function upsertStringProperty(obj: ObjectLiteralExpression, key: string, value: string): void {
116
+ const existing = obj.getProperty(key)
117
+ if (existing && Node.isPropertyAssignment(existing)) {
118
+ existing.setInitializer(`"${value}"`)
119
+ return
120
+ }
121
+ if (existing) existing.remove()
122
+ obj.addPropertyAssignment({ name: key, initializer: `"${value}"` })
123
+ }
124
+
125
+ function removePropertyIfPresent(obj: ObjectLiteralExpression, key: string): void {
126
+ const prop = obj.getProperty(key)
127
+ if (prop) prop.remove()
128
+ }
@@ -0,0 +1,126 @@
1
+ interface AstModel {
2
+ tableName: string
3
+ fields: Record<string, Record<string, unknown>>
4
+ }
5
+
6
+ interface AstShape {
7
+ models?: AstModel[]
8
+ }
9
+
10
+ export function generateClientAugmentation(ast: unknown): string {
11
+ const parsed = ast as AstShape
12
+ const models = [...(parsed.models ?? [])].sort((a, b) => a.tableName.localeCompare(b.tableName))
13
+
14
+ const lines: string[] = []
15
+ lines.push("// Generated by supatype CLI — do not edit manually.")
16
+ lines.push("")
17
+ lines.push("declare module \"@supatype/client\" {")
18
+ lines.push(" interface SupatypeModels {")
19
+
20
+ for (const model of models) {
21
+ lines.push(` ${quoteKey(model.tableName)}: {`)
22
+ lines.push(` Row: ${generateRowType(model.fields)}`)
23
+ lines.push(` Insert: ${generateInsertType(model.fields)}`)
24
+ lines.push(` Update: ${generateUpdateType(model.fields)}`)
25
+ lines.push(" }")
26
+ }
27
+
28
+ lines.push(" }")
29
+ lines.push("}")
30
+ lines.push("")
31
+ lines.push("export {}")
32
+ lines.push("")
33
+
34
+ return lines.join("\n")
35
+ }
36
+
37
+ function generateRowType(fields: Record<string, Record<string, unknown>>): string {
38
+ const entries = Object.entries(fields).sort(([a], [b]) => a.localeCompare(b))
39
+ if (entries.length === 0) return "Record<string, unknown>"
40
+ const body = entries
41
+ .map(([name, meta]) => ` ${quoteKey(name)}: ${toTsType(meta)}`)
42
+ .join("\n")
43
+ return `{\n${body}\n}`
44
+ }
45
+
46
+ function insertColumnOptionalOnInsert(meta: Record<string, unknown>): boolean {
47
+ return meta["default"] !== undefined || meta["serverGenerated"] === true
48
+ }
49
+
50
+ function generateInsertType(fields: Record<string, Record<string, unknown>>): string {
51
+ const entries = Object.entries(fields).sort(([a], [b]) => a.localeCompare(b))
52
+ if (entries.length === 0) return "Record<string, unknown>"
53
+ const body = entries
54
+ .map(([name, meta]) => {
55
+ const required = meta["required"] === true && !insertColumnOptionalOnInsert(meta)
56
+ return ` ${quoteKey(name)}${required ? "" : "?"}: ${toTsType(meta)}`
57
+ })
58
+ .join("\n")
59
+ return `{\n${body}\n}`
60
+ }
61
+
62
+ function generateUpdateType(fields: Record<string, Record<string, unknown>>): string {
63
+ const entries = Object.entries(fields).sort(([a], [b]) => a.localeCompare(b))
64
+ if (entries.length === 0) return "Record<string, unknown>"
65
+ const body = entries
66
+ .map(([name, meta]) => ` ${quoteKey(name)}?: ${toTsType(meta)}`)
67
+ .join("\n")
68
+ return `{\n${body}\n}`
69
+ }
70
+
71
+ function toTsType(meta: Record<string, unknown>): string {
72
+ const kind = typeof meta["kind"] === "string" ? meta["kind"] : "json"
73
+ const required = meta["required"] === true
74
+ const base = (() => {
75
+ switch (kind) {
76
+ case "uuid":
77
+ case "slug":
78
+ case "text":
79
+ case "email":
80
+ case "url":
81
+ case "date":
82
+ case "timestamp":
83
+ case "datetime":
84
+ case "money":
85
+ case "xml":
86
+ case "interval":
87
+ return "string"
88
+ case "integer":
89
+ case "smallInt":
90
+ case "serial":
91
+ case "float":
92
+ case "decimal":
93
+ return "number"
94
+ case "bigSerial":
95
+ case "bigInt":
96
+ return "bigint"
97
+ case "boolean":
98
+ return "boolean"
99
+ case "json":
100
+ case "blocks":
101
+ case "geo":
102
+ case "vector":
103
+ case "relation":
104
+ case "array":
105
+ case "image":
106
+ case "file":
107
+ return "Record<string, unknown>"
108
+ case "richText":
109
+ return "(import(\"@supatype/types/lexical\").SerializedEditorState | string)"
110
+ case "enum": {
111
+ const values = Array.isArray(meta["values"]) ? meta["values"] as unknown[] : []
112
+ const quoted = values
113
+ .filter((v): v is string => typeof v === "string")
114
+ .map((v) => JSON.stringify(v))
115
+ return quoted.length > 0 ? quoted.join(" | ") : "string"
116
+ }
117
+ default:
118
+ return "unknown"
119
+ }
120
+ })()
121
+ return required ? base : `${base} | null`
122
+ }
123
+
124
+ function quoteKey(key: string): string {
125
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key)
126
+ }