@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,985 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { afterEach, describe, expect, it } from "vitest"
5
+ import type { ModelAstV2 } from "../src/schema-ast-v2.js"
6
+ import { extractSchemaAstFromTypes } from "../src/type-extractor.js"
7
+
8
+ const dirs: string[] = []
9
+
10
+ function tableName(model: ModelAstV2 | undefined): string | undefined {
11
+ return model?.annotations.db.tableName
12
+ }
13
+
14
+ function modelAccess(model: ModelAstV2 | undefined): Record<string, unknown> {
15
+ return model?.annotations.platform.access ?? {}
16
+ }
17
+
18
+ afterEach(() => {
19
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
20
+ })
21
+
22
+ describe("extractSchemaAstFromTypes", () => {
23
+ it("extracts exported Model aliases into engine-compatible AST", () => {
24
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-"))
25
+ dirs.push(dir)
26
+ const schemaPath = join(dir, "schema.ts")
27
+ writeFileSync(
28
+ schemaPath,
29
+ `
30
+ import type { Model, UUID, Slug, Unique, RichText, Optional, Public, Owner, RelatedTo } from "@supatype/types"
31
+
32
+ export type Post = Model<{
33
+ id: UUID
34
+ slug: Unique<Slug>
35
+ title: string
36
+ body: RichText
37
+ publishedAt: Optional<Date>
38
+ }, {
39
+ access: { read: Public; update: Owner<"author_id">; delete: Owner<"author_id"> }
40
+ }>
41
+
42
+ export type Comment = Model<{
43
+ id: UUID
44
+ post: RelatedTo<Post>
45
+ }, {
46
+ access: { read: Public }
47
+ }>
48
+ `,
49
+ "utf8",
50
+ )
51
+
52
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
53
+ expect(ast).not.toBeNull()
54
+ expect(ast?.astVersion).toBe(2)
55
+ expect(ast?.models).toHaveLength(2)
56
+ const post = ast?.models.find((m) => m.name === "Post")
57
+ const comment = ast?.models.find((m) => m.name === "Comment")
58
+ expect(tableName(post)).toBe("post")
59
+ expect(comment?.fields["post"]).toMatchObject({
60
+ kind: "relation",
61
+ cardinality: "belongsTo",
62
+ target: "Post",
63
+ annotations: { db: { foreignKey: "post_id" } },
64
+ })
65
+ expect(post?.fields["id"]).toMatchObject({
66
+ kind: "uuid",
67
+ annotations: { db: { pgType: "UUID", unique: true } },
68
+ primaryKey: true,
69
+ required: true,
70
+ default: { kind: "genRandomUuid" },
71
+ })
72
+ expect(post?.fields["slug"]).toMatchObject({
73
+ kind: "slug",
74
+ from: "title",
75
+ annotations: { db: { unique: true } },
76
+ })
77
+ expect(modelAccess(post)["read"]).toEqual({ type: "public" })
78
+ expect(modelAccess(post)["update"]).toEqual({ type: "owner", field: "author_id" })
79
+ expect(modelAccess(post)["delete"]).toEqual({ type: "owner", field: "author_id" })
80
+ })
81
+
82
+ it("emits DEFAULT now for created_at / updated_at timestamp columns", () => {
83
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-audit-ts-"))
84
+ dirs.push(dir)
85
+ const schemaPath = join(dir, "schema.ts")
86
+ writeFileSync(
87
+ schemaPath,
88
+ `
89
+ import type { Model, UUID, Timestamp } from "@supatype/types"
90
+
91
+ export type Entry = Model<{ id: UUID; created_at: Timestamp; updated_at: Timestamp }>
92
+ `,
93
+ "utf8",
94
+ )
95
+
96
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
97
+ const entry = ast?.models.find((m) => m.name === "Entry")
98
+ expect(entry?.fields["created_at"]).toMatchObject({
99
+ kind: "datetime",
100
+ annotations: { db: { serverGenerated: true, pgType: "TIMESTAMP WITH TIME ZONE" } },
101
+ default: { kind: "now" },
102
+ })
103
+ expect(entry?.fields["updated_at"]).toMatchObject({
104
+ kind: "datetime",
105
+ annotations: { db: { serverGenerated: true, pgType: "TIMESTAMP WITH TIME ZONE" } },
106
+ default: { kind: "now" },
107
+ })
108
+ })
109
+
110
+ it("extracts Owner<Model, Key> using the key argument", () => {
111
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-owner-model-"))
112
+ dirs.push(dir)
113
+ const schemaPath = join(dir, "schema.ts")
114
+ writeFileSync(
115
+ schemaPath,
116
+ `
117
+ import type { Model, UUID, Public, Owner } from "@supatype/types"
118
+
119
+ export type User = Model<{
120
+ id: UUID
121
+ }, {
122
+ access: { read: Public; update: Owner<User, "id">; delete: Owner<User, "id"> }
123
+ }>
124
+ `,
125
+ "utf8",
126
+ )
127
+
128
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
129
+ const user = ast?.models.find((m) => m.name === "User")
130
+ expect(modelAccess(user)["update"]).toEqual({ type: "owner", field: "id" })
131
+ expect(modelAccess(user)["delete"]).toEqual({ type: "owner", field: "id" })
132
+ })
133
+
134
+ it("maps SupatypeAuthUser relations and OwnerFrom relation keys", () => {
135
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-auth-owner-"))
136
+ dirs.push(dir)
137
+ const schemaPath = join(dir, "schema.ts")
138
+ writeFileSync(
139
+ schemaPath,
140
+ `
141
+ import type { Model, UUID, RelatedTo, SupatypeAuthUser, OwnerFrom, LoggedIn } from "@supatype/types"
142
+
143
+ export type Post = Model<{
144
+ id: UUID
145
+ authUser: RelatedTo<SupatypeAuthUser>
146
+ }, {
147
+ access: { create: LoggedIn; update: OwnerFrom<"authUser">; delete: OwnerFrom<"authUser"> }
148
+ }>
149
+ `,
150
+ "utf8",
151
+ )
152
+
153
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
154
+ const post = ast?.models.find((m) => m.name === "Post")
155
+ expect(post?.fields["authUser"]).toMatchObject({
156
+ kind: "relation",
157
+ cardinality: "belongsTo",
158
+ target: "supatype:user",
159
+ annotations: { db: { foreignKey: "auth_user_id" } },
160
+ })
161
+ expect(modelAccess(post)["update"]).toEqual({ type: "owner", field: "authUser" })
162
+ expect(modelAccess(post)["delete"]).toEqual({ type: "owner", field: "authUser" })
163
+ })
164
+
165
+ it("unwraps Default<> so boolean fields stay boolean in the AST", () => {
166
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-"))
167
+ dirs.push(dir)
168
+ const schemaPath = join(dir, "schema.ts")
169
+ writeFileSync(
170
+ schemaPath,
171
+ `
172
+ import type { Model, UUID, Default } from "@supatype/types"
173
+
174
+ export type Flags = Model<{
175
+ id: UUID
176
+ isActive: Default<boolean, true>
177
+ }>
178
+ `,
179
+ "utf8",
180
+ )
181
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
182
+ expect(ast?.models[0]?.fields["isActive"]).toMatchObject({
183
+ kind: "boolean",
184
+ annotations: { db: { pgType: "BOOLEAN" } },
185
+ default: { kind: "value", value: true },
186
+ })
187
+ })
188
+
189
+ it("extracts Default<> literal values for scalars and RichText plain-string defaults", () => {
190
+ const dir = mkdtempSync(join(tmpdir(), "supatype-defaults-"))
191
+ dirs.push(dir)
192
+ const schemaPath = join(dir, "schema.ts")
193
+ writeFileSync(
194
+ schemaPath,
195
+ `
196
+ import type { Model, UUID, Default, Int, RichText } from "@supatype/types"
197
+
198
+ export type Product = Model<{
199
+ id: UUID
200
+ stock: Default<Int, 0>
201
+ blurb: Default<RichText, "Welcome to our shop.">
202
+ }>
203
+ `,
204
+ "utf8",
205
+ )
206
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
207
+ expect(ast?.models[0]?.fields["stock"]).toMatchObject({
208
+ kind: "integer",
209
+ default: { kind: "value", value: 0 },
210
+ })
211
+ expect(ast?.models[0]?.fields["blurb"]).toMatchObject({
212
+ kind: "richText",
213
+ annotations: { db: { pgType: "JSONB" }, platform: { editor: "rich" } },
214
+ default: { kind: "value", value: "Welcome to our shop." },
215
+ })
216
+ })
217
+
218
+ it("extracts RichText<\"…\"> inline default (equivalent to Default<RichText, \"…\">)", () => {
219
+ const dir = mkdtempSync(join(tmpdir(), "supatype-richtext-inline-"))
220
+ dirs.push(dir)
221
+ const schemaPath = join(dir, "schema.ts")
222
+ writeFileSync(
223
+ schemaPath,
224
+ `
225
+ import type { Model, UUID, RichText } from "@supatype/types"
226
+
227
+ export type Page = Model<{
228
+ id: UUID
229
+ intro: RichText<"Welcome to Elmside.">
230
+ }>
231
+ `,
232
+ "utf8",
233
+ )
234
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
235
+ expect(ast?.models[0]?.fields["intro"]).toMatchObject({
236
+ kind: "richText",
237
+ annotations: { db: { pgType: "JSONB" }, platform: { editor: "rich" } },
238
+ default: { kind: "value", value: "Welcome to Elmside." },
239
+ })
240
+ })
241
+
242
+ it("errors when RichText inline default and Default<> are both set", () => {
243
+ const dir = mkdtempSync(join(tmpdir(), "supatype-richtext-double-default-"))
244
+ dirs.push(dir)
245
+ const schemaPath = join(dir, "schema.ts")
246
+ writeFileSync(
247
+ schemaPath,
248
+ `
249
+ import type { Model, UUID, Default, RichText } from "@supatype/types"
250
+
251
+ export type Page = Model<{
252
+ id: UUID
253
+ intro: Default<RichText<"a">, "b">
254
+ }>
255
+ `,
256
+ "utf8",
257
+ )
258
+ expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(
259
+ /either Default<…> or an inline type default/,
260
+ )
261
+ })
262
+
263
+ it("extracts Bucket<> config into storageBuckets and field accessMode", () => {
264
+ const dir = mkdtempSync(join(tmpdir(), "supatype-bucket-"))
265
+ dirs.push(dir)
266
+ const schemaPath = join(dir, "schema.ts")
267
+ writeFileSync(
268
+ schemaPath,
269
+ `
270
+ import type { Model, UUID, Public, LoggedIn, ImageAsset, Bucket } from "@supatype/types"
271
+
272
+ export type covers = Bucket<
273
+ "covers",
274
+ {
275
+ accessMode: "public"
276
+ maxSize: "2MB"
277
+ accept: readonly ["image/jpeg", "image/png"]
278
+ access: { read: Public; create: LoggedIn; delete: LoggedIn }
279
+ }
280
+ >
281
+
282
+ export type Post = Model<{
283
+ id: UUID
284
+ hero: ImageAsset<covers>
285
+ }>
286
+ `,
287
+ "utf8",
288
+ )
289
+
290
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
291
+ expect(ast?.storageBuckets).toBeDefined()
292
+ const b = ast?.storageBuckets?.find((x) => x.id === "covers")
293
+ expect(b?.public).toBe(true)
294
+ expect(b?.accessMode).toBe("public")
295
+ expect(b?.fileSizeLimit).toBe(2 * 1024 * 1024)
296
+ expect(b?.allowedMimeTypes).toEqual(["image/jpeg", "image/png"])
297
+ expect(b?.access?.read).toEqual({ type: "public" })
298
+
299
+ const hero = ast?.models[0]?.fields["hero"]
300
+ expect(hero).toMatchObject({ kind: "image", bucket: "covers", accessMode: "public" })
301
+ })
302
+
303
+ it("extracts accessMode custom and s3BucketPolicy string", () => {
304
+ const dir = mkdtempSync(join(tmpdir(), "supatype-bucket-custom-"))
305
+ dirs.push(dir)
306
+ const schemaPath = join(dir, "schema.ts")
307
+ writeFileSync(
308
+ schemaPath,
309
+ `
310
+ import type { Model, UUID, ImageAsset, Bucket } from "@supatype/types"
311
+
312
+ export type assets = Bucket<"legacy", {
313
+ accessMode: "custom"
314
+ s3BucketPolicy: '{"Version":"2012-10-17"}'
315
+ }>
316
+
317
+ export type X = Model<{
318
+ id: UUID
319
+ f: ImageAsset<assets>
320
+ }>
321
+ `,
322
+ "utf8",
323
+ )
324
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
325
+ const b = ast?.storageBuckets?.find((x) => x.id === "legacy")
326
+ expect(b?.accessMode).toBe("custom")
327
+ expect(b?.public).toBe(false)
328
+ expect(b?.s3BucketPolicy).toBe('{"Version":"2012-10-17"}')
329
+ })
330
+
331
+ it("extracts slug source field from Slug<\"name\">", () => {
332
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-"))
333
+ dirs.push(dir)
334
+ const schemaPath = join(dir, "schema.ts")
335
+ writeFileSync(
336
+ schemaPath,
337
+ `
338
+ import type { Model, UUID, Slug, Unique } from "@supatype/types"
339
+
340
+ export type Article = Model<{
341
+ id: UUID
342
+ name: string
343
+ slug: Unique<Slug<"name">>
344
+ }>
345
+ `,
346
+ "utf8",
347
+ )
348
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
349
+ expect(ast?.models[0]?.fields["slug"]).toMatchObject({ kind: "slug", from: "name" })
350
+ })
351
+
352
+ it("normalizes RelatedTo foreign keys for fields ending in Id/ID", () => {
353
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-"))
354
+ dirs.push(dir)
355
+ const schemaPath = join(dir, "schema.ts")
356
+ writeFileSync(
357
+ schemaPath,
358
+ `
359
+ import type { Model, UUID, RelatedTo } from "@supatype/types"
360
+
361
+ export type Author = Model<{ id: UUID }>
362
+
363
+ export type Comment = Model<{
364
+ id: UUID
365
+ author: RelatedTo<Author>
366
+ userId: RelatedTo<Author>
367
+ customerID: RelatedTo<Author>
368
+ }>
369
+ `,
370
+ "utf8",
371
+ )
372
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
373
+ const comment = ast?.models.find((m) => m.name === "Comment")
374
+ expect(comment?.fields["author"]).toMatchObject({
375
+ annotations: { db: { foreignKey: "author_id" } },
376
+ })
377
+ expect(comment?.fields["userId"]).toMatchObject({
378
+ annotations: { db: { foreignKey: "user_id" } },
379
+ })
380
+ expect(comment?.fields["customerID"]).toMatchObject({
381
+ annotations: { db: { foreignKey: "customer_id" } },
382
+ })
383
+ })
384
+
385
+ it("extracts EditorReadOnly wrapper as readOnly field metadata", () => {
386
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-readonly-"))
387
+ dirs.push(dir)
388
+ const schemaPath = join(dir, "schema.ts")
389
+ writeFileSync(
390
+ schemaPath,
391
+ `
392
+ import type { Model, UUID, EditorReadOnly, RelatedTo } from "@supatype/types"
393
+
394
+ export type User = Model<{ id: UUID }>
395
+
396
+ export type Doc = Model<{
397
+ id: UUID
398
+ title: EditorReadOnly<string>
399
+ owner: EditorReadOnly<RelatedTo<User>>
400
+ }>
401
+ `,
402
+ "utf8",
403
+ )
404
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
405
+ const doc = ast?.models.find((m) => m.name === "Doc")
406
+ expect(doc?.fields["title"]).toMatchObject({
407
+ kind: "text",
408
+ annotations: { platform: { readOnly: true } },
409
+ })
410
+ expect(doc?.fields["owner"]).toMatchObject({
411
+ kind: "relation",
412
+ annotations: { platform: { readOnly: true } },
413
+ })
414
+ })
415
+
416
+ it("extracts Computed wrapper as readOnly + serverGenerated metadata", () => {
417
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-computed-"))
418
+ dirs.push(dir)
419
+ const schemaPath = join(dir, "schema.ts")
420
+ writeFileSync(
421
+ schemaPath,
422
+ `
423
+ import type { Model, UUID, Computed, Optional } from "@supatype/types"
424
+
425
+ export type Doc = Model<{
426
+ id: UUID
427
+ summary: Computed<Optional<string>>
428
+ }>
429
+ `,
430
+ "utf8",
431
+ )
432
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
433
+ const doc = ast?.models.find((m) => m.name === "Doc")
434
+ expect(doc?.fields["summary"]).toMatchObject({
435
+ kind: "text",
436
+ required: false,
437
+ annotations: { db: { serverGenerated: true }, platform: { readOnly: true } },
438
+ })
439
+ })
440
+
441
+ it("extracts ComputedFrom sources on text (single + tuple)", () => {
442
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-computed-from-"))
443
+ dirs.push(dir)
444
+ const schemaPath = join(dir, "schema.ts")
445
+ writeFileSync(
446
+ schemaPath,
447
+ `
448
+ import type { Model, UUID, ComputedFrom, Optional } from "@supatype/types"
449
+
450
+ export type Article = Model<{
451
+ id: UUID
452
+ title: string
453
+ subtitle: string
454
+ excerpt: Optional<ComputedFrom<string, "title">>
455
+ teaser: Optional<ComputedFrom<string, readonly ["title", "subtitle"]>>
456
+ }>
457
+ `,
458
+ "utf8",
459
+ )
460
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
461
+ const article = ast?.models.find((m) => m.name === "Article")
462
+ expect(article?.fields["excerpt"]).toMatchObject({
463
+ kind: "text",
464
+ required: false,
465
+ sources: ["title"],
466
+ })
467
+ expect(article?.fields["teaser"]).toMatchObject({
468
+ kind: "text",
469
+ required: false,
470
+ sources: ["title", "subtitle"],
471
+ })
472
+ })
473
+
474
+ it("extracts ComputedFrom template string and inferred sources", () => {
475
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-computed-tpl-"))
476
+ dirs.push(dir)
477
+ const schemaPath = join(dir, "schema.ts")
478
+ writeFileSync(
479
+ schemaPath,
480
+ `
481
+ import type { Model, UUID, ComputedFrom, Optional } from "@supatype/types"
482
+
483
+ export type Note = Model<{
484
+ id: UUID
485
+ title: string
486
+ author: string
487
+ published_at: string
488
+ description: string
489
+ summary: Optional<ComputedFrom<string, "Author: {author} | Date: {published_at}\\n{truncate(description, 100)}">>
490
+ }>
491
+ `,
492
+ "utf8",
493
+ )
494
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
495
+ const note = ast?.models.find((m) => m.name === "Note")
496
+ const summary = note?.fields["summary"] as { sources?: string[]; template?: string } | undefined
497
+ expect(summary).toMatchObject({
498
+ kind: "text",
499
+ required: false,
500
+ template: "Author: {author} | Date: {published_at}\n{truncate(description, 100)}",
501
+ })
502
+ expect(new Set(summary?.sources ?? [])).toEqual(new Set(["author", "published_at", "description"]))
503
+ })
504
+
505
+ it("extracts singleton: true with default _global_ table name", () => {
506
+ const dir = mkdtempSync(join(tmpdir(), "supatype-singleton-"))
507
+ dirs.push(dir)
508
+ const schemaPath = join(dir, "schema.ts")
509
+ writeFileSync(
510
+ schemaPath,
511
+ `
512
+ import type { Model, UUID, Public, Role, Timestamp } from "@supatype/types"
513
+
514
+ export type SiteSettings = Model<{
515
+ id: UUID
516
+ site_name: string
517
+ created_at: Timestamp
518
+ updated_at: Timestamp
519
+ }, {
520
+ singleton: true
521
+ access: { read: Public; update: Role<"supatype_admin"> }
522
+ }>
523
+ `,
524
+ "utf8",
525
+ )
526
+
527
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
528
+ const settings = ast?.models.find((m) => m.name === "SiteSettings")
529
+ expect(tableName(settings)).toBe("_global_site_settings")
530
+ expect(settings?.options).toMatchObject({ singleton: true, timestamps: true })
531
+ })
532
+
533
+ it("respects tableName override on singleton models", () => {
534
+ const dir = mkdtempSync(join(tmpdir(), "supatype-singleton-table-"))
535
+ dirs.push(dir)
536
+ const schemaPath = join(dir, "schema.ts")
537
+ writeFileSync(
538
+ schemaPath,
539
+ `
540
+ import type { Model, UUID, Public } from "@supatype/types"
541
+
542
+ export type Config = Model<{
543
+ id: UUID
544
+ }, {
545
+ singleton: true
546
+ tableName: "config"
547
+ access: { read: Public }
548
+ }>
549
+ `,
550
+ "utf8",
551
+ )
552
+
553
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
554
+ const config = ast?.models.find((m) => m.name === "Config")
555
+ expect(tableName(config)).toBe("config")
556
+ expect(config?.options.singleton).toBe(true)
557
+ })
558
+
559
+ it("infers timestamps from WithTimestamps wrapper", () => {
560
+ const dir = mkdtempSync(join(tmpdir(), "supatype-timestamps-"))
561
+ dirs.push(dir)
562
+ const schemaPath = join(dir, "schema.ts")
563
+ writeFileSync(
564
+ schemaPath,
565
+ `
566
+ import type { Model, UUID, WithTimestamps, Public } from "@supatype/types"
567
+
568
+ export type Post = Model<WithTimestamps<{
569
+ id: UUID
570
+ title: string
571
+ }>, {
572
+ access: { read: Public }
573
+ }>
574
+ `,
575
+ "utf8",
576
+ )
577
+
578
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
579
+ const post = ast?.models.find((m) => m.name === "Post")
580
+ expect(post?.options.timestamps).toBe(true)
581
+ expect(post?.options.singleton).toBeUndefined()
582
+ })
583
+
584
+ it("extracts LocaleConfig into schema AST locales", () => {
585
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-locale-config-"))
586
+ dirs.push(dir)
587
+ const schemaPath = join(dir, "schema.ts")
588
+ writeFileSync(
589
+ schemaPath,
590
+ `
591
+ import type { LocaleConfig, Model, UUID } from "@supatype/types"
592
+
593
+ export type localeConfig = LocaleConfig<["en", "de"], "en">
594
+
595
+ export type Page = Model<{ id: UUID; title: string }>
596
+ `,
597
+ "utf8",
598
+ )
599
+
600
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
601
+ expect(ast?.locales).toEqual(["en", "de"])
602
+ expect(ast?.defaultLocale).toBe("en")
603
+ })
604
+
605
+ it("marks Localized fields as JSONB with localized:true", () => {
606
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-localized-field-"))
607
+ dirs.push(dir)
608
+ const schemaPath = join(dir, "schema.ts")
609
+ writeFileSync(
610
+ schemaPath,
611
+ `
612
+ import type { Localized, Model, Optional, RichText, UUID } from "@supatype/types"
613
+
614
+ export type Page = Model<{
615
+ id: UUID
616
+ title: Localized<string>
617
+ body: Localized<RichText>
618
+ subtitle: Optional<Localized<string>>
619
+ }>
620
+ `,
621
+ "utf8",
622
+ )
623
+
624
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
625
+ const page = ast?.models.find((m) => m.name === "Page")
626
+ expect(page?.fields["title"]).toMatchObject({
627
+ kind: "text",
628
+ annotations: { db: { pgType: "JSONB" } },
629
+ localized: true,
630
+ required: true,
631
+ })
632
+ expect(page?.fields["body"]).toMatchObject({
633
+ kind: "richText",
634
+ annotations: { db: { pgType: "JSONB" }, platform: { editor: "rich" } },
635
+ localized: true,
636
+ })
637
+ expect(page?.fields["subtitle"]).toMatchObject({
638
+ kind: "text",
639
+ annotations: { db: { pgType: "JSONB" } },
640
+ localized: true,
641
+ required: false,
642
+ })
643
+ })
644
+
645
+ it("extracts LocalizedModel with auto-localized copy fields", () => {
646
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-localized-model-"))
647
+ dirs.push(dir)
648
+ const schemaPath = join(dir, "schema.ts")
649
+ writeFileSync(
650
+ schemaPath,
651
+ `
652
+ import type {
653
+ LocalizedModel,
654
+ UUID,
655
+ ImageAsset,
656
+ NotLocalized,
657
+ Blocks,
658
+ Block,
659
+ Bucket,
660
+ } from "@supatype/types"
661
+
662
+ export type marketing = Bucket<"marketing", { accessMode: "public" }>
663
+ export type RuleBlock = Block<"rule", { text: string }>
664
+
665
+ export type Homepage = LocalizedModel<{
666
+ id: UUID
667
+ hero_title: string
668
+ map_url: NotLocalized<string>
669
+ og_image: ImageAsset<marketing, { localized: true }>
670
+ hero_slides: Blocks<RuleBlock>
671
+ }>
672
+ `,
673
+ "utf8",
674
+ )
675
+
676
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
677
+ const homepage = ast?.models.find((m) => m.name === "Homepage")
678
+ expect(homepage?.fields["hero_title"]).toMatchObject({
679
+ kind: "text",
680
+ localized: true,
681
+ annotations: { db: { pgType: "JSONB" } },
682
+ })
683
+ expect(homepage?.fields["map_url"]?.localized).toBeUndefined()
684
+ expect(homepage?.fields["og_image"]).toMatchObject({
685
+ kind: "image",
686
+ localized: true,
687
+ })
688
+ const slides = homepage?.fields["hero_slides"] as { blocks?: { fields: Record<string, unknown> }[] }
689
+ expect(slides?.blocks?.[0]?.fields["text"]).toMatchObject({
690
+ kind: "text",
691
+ localized: true,
692
+ pgType: "JSONB",
693
+ })
694
+ })
695
+
696
+ it("marks Localized<Blocks<...>> as localized column", () => {
697
+ const dir = mkdtempSync(join(tmpdir(), "supatype-types-localized-blocks-col-"))
698
+ dirs.push(dir)
699
+ const schemaPath = join(dir, "schema.ts")
700
+ writeFileSync(
701
+ schemaPath,
702
+ `
703
+ import type { Localized, Model, UUID, Blocks, Block } from "@supatype/types"
704
+
705
+ export type Slide = Block<"slide", { image_path: string }>
706
+
707
+ export type Page = Model<{
708
+ id: UUID
709
+ slides: Localized<Blocks<Slide>>
710
+ }>
711
+ `,
712
+ "utf8",
713
+ )
714
+
715
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
716
+ const page = ast?.models.find((m) => m.name === "Page")
717
+ expect(page?.fields["slides"]).toMatchObject({
718
+ kind: "blocks",
719
+ localized: true,
720
+ annotations: { db: { pgType: "JSONB" } },
721
+ })
722
+ })
723
+
724
+ it("resolves type alias Nullable<T> = Optional<T>", () => {
725
+ const dir = mkdtempSync(join(tmpdir(), "supatype-alias-nullable-"))
726
+ dirs.push(dir)
727
+ const schemaPath = join(dir, "schema.ts")
728
+ writeFileSync(
729
+ schemaPath,
730
+ `
731
+ import type { Model, UUID, Email, Optional } from "@supatype/types"
732
+
733
+ type Nullable<T> = Optional<T>
734
+
735
+ export type User = Model<{
736
+ id: UUID
737
+ email: Nullable<Email>
738
+ }>
739
+ `,
740
+ "utf8",
741
+ )
742
+
743
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
744
+ const user = ast?.models.find((m) => m.name === "User")
745
+ expect(user?.fields["email"]).toMatchObject({
746
+ kind: "email",
747
+ required: false,
748
+ })
749
+ })
750
+
751
+ it("resolves multi-hop type aliases", () => {
752
+ const dir = mkdtempSync(join(tmpdir(), "supatype-alias-multihop-"))
753
+ dirs.push(dir)
754
+ const schemaPath = join(dir, "schema.ts")
755
+ writeFileSync(
756
+ schemaPath,
757
+ `
758
+ import type { Model, UUID, Email, Optional } from "@supatype/types"
759
+
760
+ type Nullable<T> = Optional<T>
761
+ type A = Nullable<Email>
762
+ type B = A
763
+
764
+ export type User = Model<{ id: UUID; email: B }>
765
+ `,
766
+ "utf8",
767
+ )
768
+
769
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
770
+ const user = ast?.models.find((m) => m.name === "User")
771
+ expect(user?.fields["email"]).toMatchObject({
772
+ kind: "email",
773
+ required: false,
774
+ })
775
+ })
776
+
777
+ it("resolves enum string-union type aliases", () => {
778
+ const dir = mkdtempSync(join(tmpdir(), "supatype-alias-enum-"))
779
+ dirs.push(dir)
780
+ const schemaPath = join(dir, "schema.ts")
781
+ writeFileSync(
782
+ schemaPath,
783
+ `
784
+ import type { Model, UUID } from "@supatype/types"
785
+
786
+ type Status = "draft" | "published" | "archived"
787
+
788
+ export type Post = Model<{ id: UUID; status: Status }>
789
+ `,
790
+ "utf8",
791
+ )
792
+
793
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
794
+ expect(ast?.models[0]?.fields["status"]).toMatchObject({
795
+ kind: "enum",
796
+ values: ["draft", "published", "archived"],
797
+ })
798
+ })
799
+
800
+ it("resolves import renames of @supatype/types primitives", () => {
801
+ const dir = mkdtempSync(join(tmpdir(), "supatype-import-rename-"))
802
+ dirs.push(dir)
803
+ const schemaPath = join(dir, "schema.ts")
804
+ writeFileSync(
805
+ schemaPath,
806
+ `
807
+ import type { Model, UUID, Email, Optional as Maybe } from "@supatype/types"
808
+
809
+ export type User = Model<{ id: UUID; email: Maybe<Email> }>
810
+ `,
811
+ "utf8",
812
+ )
813
+
814
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
815
+ const user = ast?.models.find((m) => m.name === "User")
816
+ expect(user?.fields["email"]).toMatchObject({
817
+ kind: "email",
818
+ required: false,
819
+ })
820
+ })
821
+
822
+ it("resolves cross-file type aliases via local import", () => {
823
+ const dir = mkdtempSync(join(tmpdir(), "supatype-cross-file-alias-"))
824
+ dirs.push(dir)
825
+ writeFileSync(
826
+ join(dir, "field-types.ts"),
827
+ `
828
+ import type { Optional } from "@supatype/types"
829
+
830
+ export type Nullable<T> = Optional<T>
831
+ `,
832
+ "utf8",
833
+ )
834
+ const schemaPath = join(dir, "schema.ts")
835
+ writeFileSync(
836
+ schemaPath,
837
+ `
838
+ import type { Model, UUID, Email } from "@supatype/types"
839
+ import type { Nullable } from "./field-types"
840
+
841
+ export type User = Model<{ id: UUID; email: Nullable<Email> }>
842
+ `,
843
+ "utf8",
844
+ )
845
+
846
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
847
+ const user = ast?.models.find((m) => m.name === "User")
848
+ expect(user?.fields["email"]).toMatchObject({
849
+ kind: "email",
850
+ required: false,
851
+ })
852
+ })
853
+
854
+ it("resolves import rename of a local type alias", () => {
855
+ const dir = mkdtempSync(join(tmpdir(), "supatype-rename-local-alias-"))
856
+ dirs.push(dir)
857
+ writeFileSync(
858
+ join(dir, "field-types.ts"),
859
+ `
860
+ import type { Optional } from "@supatype/types"
861
+
862
+ export type Nullable<T> = Optional<T>
863
+ `,
864
+ "utf8",
865
+ )
866
+ const schemaPath = join(dir, "schema.ts")
867
+ writeFileSync(
868
+ schemaPath,
869
+ `
870
+ import type { Model, UUID, Email } from "@supatype/types"
871
+ import type { Nullable as MaybeNull } from "./field-types"
872
+
873
+ export type User = Model<{ id: UUID; email: MaybeNull<Email> }>
874
+ `,
875
+ "utf8",
876
+ )
877
+
878
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
879
+ const user = ast?.models.find((m) => m.name === "User")
880
+ expect(user?.fields["email"]).toMatchObject({
881
+ kind: "email",
882
+ required: false,
883
+ })
884
+ })
885
+
886
+ it("resolves conditional type aliases via type checker", () => {
887
+ const dir = mkdtempSync(join(tmpdir(), "supatype-conditional-alias-"))
888
+ dirs.push(dir)
889
+ const schemaPath = join(dir, "schema.ts")
890
+ writeFileSync(
891
+ schemaPath,
892
+ `
893
+ import type { Model, UUID, Email, Optional } from "@supatype/types"
894
+
895
+ type NullableStr<T> = T extends string ? Optional<T> : T
896
+
897
+ export type User = Model<{ id: UUID; email: NullableStr<Email> }>
898
+ `,
899
+ "utf8",
900
+ )
901
+
902
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
903
+ const user = ast?.models.find((m) => m.name === "User")
904
+ expect(user?.fields["email"]).toMatchObject({
905
+ kind: "email",
906
+ required: false,
907
+ })
908
+ })
909
+
910
+ it("resolves mapped type aliases as Model fields argument", () => {
911
+ const dir = mkdtempSync(join(tmpdir(), "supatype-mapped-fields-"))
912
+ dirs.push(dir)
913
+ const schemaPath = join(dir, "schema.ts")
914
+ writeFileSync(
915
+ schemaPath,
916
+ `
917
+ import type { Model, UUID, Email, Optional } from "@supatype/types"
918
+
919
+ type AllOptional<T> = { [K in keyof T]: Optional<T[K]> }
920
+
921
+ export type User = Model<AllOptional<{ email: Email; name: string }>>
922
+ `,
923
+ "utf8",
924
+ )
925
+
926
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
927
+ const user = ast?.models.find((m) => m.name === "User")
928
+ expect(user?.fields["email"]).toMatchObject({ kind: "email", required: false })
929
+ expect(user?.fields["name"]).toMatchObject({ kind: "text", required: false })
930
+ })
931
+
932
+ it("throws on unknown Supatype types instead of silently mapping to TEXT", () => {
933
+ const dir = mkdtempSync(join(tmpdir(), "supatype-unknown-type-"))
934
+ dirs.push(dir)
935
+ const schemaPath = join(dir, "schema.ts")
936
+ writeFileSync(
937
+ schemaPath,
938
+ `
939
+ import type { Model, UUID } from "@supatype/types"
940
+
941
+ export type User = Model<{ id: UUID; email: SomeType }>
942
+ `,
943
+ "utf8",
944
+ )
945
+
946
+ expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(/Unknown Supatype type "SomeType"/)
947
+ })
948
+
949
+ it("throws on circular type alias chains", () => {
950
+ const dir = mkdtempSync(join(tmpdir(), "supatype-circular-alias-"))
951
+ dirs.push(dir)
952
+ const schemaPath = join(dir, "schema.ts")
953
+ writeFileSync(
954
+ schemaPath,
955
+ `
956
+ import type { Model, UUID } from "@supatype/types"
957
+
958
+ type A = B
959
+ type B = A
960
+
961
+ export type User = Model<{ id: UUID; email: A }>
962
+ `,
963
+ "utf8",
964
+ )
965
+
966
+ expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(/circular alias chain/)
967
+ })
968
+
969
+ it("throws on TypeScript utility types used as field types", () => {
970
+ const dir = mkdtempSync(join(tmpdir(), "supatype-utility-type-"))
971
+ dirs.push(dir)
972
+ const schemaPath = join(dir, "schema.ts")
973
+ writeFileSync(
974
+ schemaPath,
975
+ `
976
+ import type { Model, UUID } from "@supatype/types"
977
+
978
+ export type User = Model<{ id: UUID; email: NonNullable<string> }>
979
+ `,
980
+ "utf8",
981
+ )
982
+
983
+ expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(/Unknown Supatype type "NonNullable"/)
984
+ })
985
+ })