@supatype/cli 0.1.0-alpha.10 → 0.1.0-alpha.11

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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +104 -71
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app/framework.js +1 -3
  5. package/dist/app/framework.js.map +1 -1
  6. package/dist/app/proxy-dev-app.d.ts +14 -0
  7. package/dist/app/proxy-dev-app.d.ts.map +1 -1
  8. package/dist/app/proxy-dev-app.js +109 -6
  9. package/dist/app/proxy-dev-app.js.map +1 -1
  10. package/dist/binary-cache.d.ts +1 -1
  11. package/dist/binary-cache.d.ts.map +1 -1
  12. package/dist/binary-cache.js +6 -1
  13. package/dist/binary-cache.js.map +1 -1
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +6 -0
  16. package/dist/cli.js.map +1 -1
  17. package/dist/commands/adopt.d.ts +3 -0
  18. package/dist/commands/adopt.d.ts.map +1 -0
  19. package/dist/commands/adopt.js +58 -0
  20. package/dist/commands/adopt.js.map +1 -0
  21. package/dist/commands/cloud.d.ts +4 -9
  22. package/dist/commands/cloud.d.ts.map +1 -1
  23. package/dist/commands/cloud.js +49 -91
  24. package/dist/commands/cloud.js.map +1 -1
  25. package/dist/commands/db.d.ts.map +1 -1
  26. package/dist/commands/db.js +25 -47
  27. package/dist/commands/db.js.map +1 -1
  28. package/dist/commands/deploy.d.ts.map +1 -1
  29. package/dist/commands/deploy.js +117 -74
  30. package/dist/commands/deploy.js.map +1 -1
  31. package/dist/commands/dev.d.ts.map +1 -1
  32. package/dist/commands/dev.js +21 -3
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/diff.d.ts.map +1 -1
  35. package/dist/commands/diff.js +37 -37
  36. package/dist/commands/diff.js.map +1 -1
  37. package/dist/commands/doctor.d.ts +3 -0
  38. package/dist/commands/doctor.d.ts.map +1 -0
  39. package/dist/commands/doctor.js +77 -0
  40. package/dist/commands/doctor.js.map +1 -0
  41. package/dist/commands/functions.d.ts.map +1 -1
  42. package/dist/commands/functions.js +80 -33
  43. package/dist/commands/functions.js.map +1 -1
  44. package/dist/commands/init.d.ts +1 -0
  45. package/dist/commands/init.d.ts.map +1 -1
  46. package/dist/commands/init.js +26 -4
  47. package/dist/commands/init.js.map +1 -1
  48. package/dist/commands/introspect.d.ts +3 -0
  49. package/dist/commands/introspect.d.ts.map +1 -0
  50. package/dist/commands/introspect.js +34 -0
  51. package/dist/commands/introspect.js.map +1 -0
  52. package/dist/commands/link-helpers.d.ts +15 -0
  53. package/dist/commands/link-helpers.d.ts.map +1 -0
  54. package/dist/commands/link-helpers.js +187 -0
  55. package/dist/commands/link-helpers.js.map +1 -0
  56. package/dist/commands/migrate.d.ts.map +1 -1
  57. package/dist/commands/migrate.js +116 -14
  58. package/dist/commands/migrate.js.map +1 -1
  59. package/dist/commands/pull.d.ts.map +1 -1
  60. package/dist/commands/pull.js +32 -5
  61. package/dist/commands/pull.js.map +1 -1
  62. package/dist/commands/push.d.ts.map +1 -1
  63. package/dist/commands/push.js +102 -129
  64. package/dist/commands/push.js.map +1 -1
  65. package/dist/commands/status.d.ts +1 -1
  66. package/dist/commands/status.d.ts.map +1 -1
  67. package/dist/commands/status.js +93 -29
  68. package/dist/commands/status.js.map +1 -1
  69. package/dist/commands/update.d.ts.map +1 -1
  70. package/dist/commands/update.js +6 -2
  71. package/dist/commands/update.js.map +1 -1
  72. package/dist/config.d.ts +2 -1
  73. package/dist/config.d.ts.map +1 -1
  74. package/dist/config.js.map +1 -1
  75. package/dist/dev-compose.d.ts +23 -0
  76. package/dist/dev-compose.d.ts.map +1 -1
  77. package/dist/dev-compose.js +183 -6
  78. package/dist/dev-compose.js.map +1 -1
  79. package/dist/diff-output.d.ts +5 -1
  80. package/dist/diff-output.d.ts.map +1 -1
  81. package/dist/diff-output.js +69 -0
  82. package/dist/diff-output.js.map +1 -1
  83. package/dist/engine-client.d.ts +10 -1
  84. package/dist/engine-client.d.ts.map +1 -1
  85. package/dist/engine-client.js +64 -13
  86. package/dist/engine-client.js.map +1 -1
  87. package/dist/engine-push-output.d.ts +1 -0
  88. package/dist/engine-push-output.d.ts.map +1 -1
  89. package/dist/engine-push-output.js +4 -1
  90. package/dist/engine-push-output.js.map +1 -1
  91. package/dist/gitignore.d.ts +8 -0
  92. package/dist/gitignore.d.ts.map +1 -0
  93. package/dist/gitignore.js +41 -0
  94. package/dist/gitignore.js.map +1 -0
  95. package/dist/link.d.ts +66 -0
  96. package/dist/link.d.ts.map +1 -0
  97. package/dist/link.js +159 -0
  98. package/dist/link.js.map +1 -0
  99. package/dist/process-manager.d.ts +2 -0
  100. package/dist/process-manager.d.ts.map +1 -1
  101. package/dist/process-manager.js +2 -0
  102. package/dist/process-manager.js.map +1 -1
  103. package/dist/project-config.d.ts +8 -0
  104. package/dist/project-config.d.ts.map +1 -1
  105. package/dist/project-config.js.map +1 -1
  106. package/dist/pull-utils.d.ts +50 -14
  107. package/dist/pull-utils.d.ts.map +1 -1
  108. package/dist/pull-utils.js +152 -12
  109. package/dist/pull-utils.js.map +1 -1
  110. package/dist/resolve-target.d.ts +86 -0
  111. package/dist/resolve-target.d.ts.map +1 -0
  112. package/dist/resolve-target.js +291 -0
  113. package/dist/resolve-target.js.map +1 -0
  114. package/dist/runtime-routes.d.ts.map +1 -1
  115. package/dist/runtime-routes.js +7 -0
  116. package/dist/runtime-routes.js.map +1 -1
  117. package/dist/schema-ast-v2.d.ts +1 -1
  118. package/dist/schema-ast-v2.d.ts.map +1 -1
  119. package/dist/schema-ast-v2.js +2 -2
  120. package/dist/schema-ast-v2.js.map +1 -1
  121. package/dist/schema-sources.d.ts +40 -0
  122. package/dist/schema-sources.d.ts.map +1 -0
  123. package/dist/schema-sources.js +183 -0
  124. package/dist/schema-sources.js.map +1 -0
  125. package/dist/self-host-compose.d.ts +10 -0
  126. package/dist/self-host-compose.d.ts.map +1 -1
  127. package/dist/self-host-compose.js +85 -3
  128. package/dist/self-host-compose.js.map +1 -1
  129. package/dist/storage-provision.d.ts +4 -0
  130. package/dist/storage-provision.d.ts.map +1 -1
  131. package/dist/storage-provision.js +24 -2
  132. package/dist/storage-provision.js.map +1 -1
  133. package/dist/target-client.d.ts +10 -0
  134. package/dist/target-client.d.ts.map +1 -0
  135. package/dist/target-client.js +22 -0
  136. package/dist/target-client.js.map +1 -0
  137. package/dist/type-extractor.d.ts +11 -0
  138. package/dist/type-extractor.d.ts.map +1 -1
  139. package/dist/type-extractor.js +95 -8
  140. package/dist/type-extractor.js.map +1 -1
  141. package/package.json +1 -1
  142. package/src/app/framework.ts +1 -3
  143. package/src/app/proxy-dev-app.ts +113 -6
  144. package/src/binary-cache.ts +6 -1
  145. package/src/cli.ts +6 -0
  146. package/src/commands/adopt.ts +83 -0
  147. package/src/commands/cloud.ts +66 -108
  148. package/src/commands/db.ts +28 -52
  149. package/src/commands/deploy.ts +162 -104
  150. package/src/commands/dev.ts +24 -10
  151. package/src/commands/diff.ts +40 -41
  152. package/src/commands/doctor.ts +102 -0
  153. package/src/commands/functions.ts +95 -37
  154. package/src/commands/init.ts +25 -4
  155. package/src/commands/introspect.ts +47 -0
  156. package/src/commands/link-helpers.ts +228 -0
  157. package/src/commands/migrate.ts +163 -15
  158. package/src/commands/pull.ts +37 -9
  159. package/src/commands/push.ts +132 -166
  160. package/src/commands/status.ts +100 -33
  161. package/src/commands/update.ts +6 -2
  162. package/src/config.ts +2 -1
  163. package/src/dev-compose.ts +240 -6
  164. package/src/diff-output.ts +79 -1
  165. package/src/engine-client.ts +70 -13
  166. package/src/engine-push-output.ts +7 -3
  167. package/src/gitignore.ts +48 -0
  168. package/src/link.ts +242 -0
  169. package/src/process-manager.ts +4 -0
  170. package/src/project-config.ts +8 -0
  171. package/src/pull-utils.ts +217 -23
  172. package/src/resolve-target.ts +419 -0
  173. package/src/runtime-routes.ts +7 -0
  174. package/src/schema-ast-v2.ts +2 -1
  175. package/src/schema-sources.ts +248 -0
  176. package/src/self-host-compose.ts +87 -3
  177. package/src/storage-provision.ts +33 -1
  178. package/src/target-client.ts +40 -0
  179. package/src/type-extractor.ts +124 -11
  180. package/tests/cli-help.test.ts +27 -2
  181. package/tests/init.test.ts +1 -1
  182. package/tests/link.test.ts +148 -0
  183. package/tests/proxy-dev-app.test.ts +45 -1
  184. package/tests/pull-utils.test.ts +5 -4
  185. package/tests/runtime-contract.test.ts +44 -1
  186. package/tests/schema-sources.test.ts +119 -0
  187. package/tests/storage-provision.test.ts +100 -0
  188. package/tsconfig.tsbuildinfo +1 -1
@@ -33,6 +33,9 @@ describe("CLI binary (requires built dist/)", () => {
33
33
  "push",
34
34
  "diff",
35
35
  "pull",
36
+ "doctor",
37
+ "introspect",
38
+ "adopt",
36
39
  "generate",
37
40
  "migrate",
38
41
  "rollback",
@@ -79,10 +82,32 @@ describe("CLI binary (requires built dist/)", () => {
79
82
  expect(stdout).toContain("dry run")
80
83
  })
81
84
 
82
- it("pull --help describes deprecated pull command", () => {
85
+ it("pull --help describes scaffold pull command", () => {
83
86
  const { stdout, exitCode } = runCli(["pull", "--help"])
84
87
  expect(exitCode).toBe(0)
85
- expect(stdout).toContain("deprecated")
88
+ expect(stdout).toContain("Scaffold")
89
+ expect(stdout).toContain("--dry-run")
90
+ })
91
+
92
+ it("doctor --help describes drift report command", () => {
93
+ const { stdout, exitCode } = runCli(["doctor", "--help"])
94
+ expect(exitCode).toBe(0)
95
+ expect(stdout).toContain("drift")
96
+ expect(stdout).toContain("--strict")
97
+ })
98
+
99
+ it("introspect --help describes database introspection command", () => {
100
+ const { stdout, exitCode } = runCli(["introspect", "--help"])
101
+ expect(exitCode).toBe(0)
102
+ expect(stdout).toContain("Introspect")
103
+ expect(stdout).toContain("--json")
104
+ })
105
+
106
+ it("adopt --help describes adoption ceremony command", () => {
107
+ const { stdout, exitCode } = runCli(["adopt", "--help"])
108
+ expect(exitCode).toBe(0)
109
+ expect(stdout).toContain("adopt")
110
+ expect(stdout).toContain("--yes")
86
111
  })
87
112
 
88
113
  it("reset --help shows --yes flag", () => {
@@ -97,7 +97,7 @@ describe("scaffold()", () => {
97
97
  const content = readFileSync(join(tmpRoot, ".gitignore"), "utf8")
98
98
  expect(content).toContain(".env")
99
99
  expect(content).toContain("node_modules/")
100
- expect(content).toContain(".supatype/engine/")
100
+ expect(content).toContain(".supatype/")
101
101
  expect(content).toContain("supatype.local.config.ts")
102
102
  })
103
103
 
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import { resolveTarget, targetSchemaRollback } from "../src/resolve-target.js"
6
+ import { loadProjectLink, migrateLegacyLinkFiles } from "../src/link.js"
7
+ import { scaffold } from "../src/commands/init.js"
8
+
9
+ function writePlainConfig(dir: string): void {
10
+ writeFileSync(
11
+ join(dir, "supatype.config.ts"),
12
+ `export default ${JSON.stringify({
13
+ project: { name: "demo" },
14
+ database: { provider: "docker" },
15
+ server: { mode: "dev" },
16
+ app: { mode: "none" },
17
+ schema: { path: "schema/index.ts", pg_schema: "public" },
18
+ })}
19
+ `,
20
+ )
21
+ }
22
+
23
+ describe("link model", () => {
24
+ let tmp: string
25
+
26
+ beforeEach(() => {
27
+ tmp = mkdtempSync(join(tmpdir(), "supatype-link-"))
28
+ })
29
+
30
+ afterEach(() => {
31
+ rmSync(tmp, { recursive: true, force: true })
32
+ })
33
+
34
+ it("migrates cloud.json into link.json", () => {
35
+ scaffold(tmp, "demo")
36
+ mkdirSync(join(tmp, ".supatype"), { recursive: true })
37
+ writeFileSync(join(tmp, ".supatype", "cloud.json"), JSON.stringify({
38
+ apiUrl: "https://api.example.com",
39
+ token: "pat",
40
+ projectSlug: "myproj",
41
+ orgId: "org1",
42
+ }), { encoding: "utf8" })
43
+
44
+ migrateLegacyLinkFiles(tmp)
45
+ const link = loadProjectLink(tmp)
46
+ expect(link?.kind).toBe("cloud")
47
+ expect(link?.projectRef).toBe("myproj")
48
+ expect(link?.environments.production?.apiUrl).toBe("https://api.example.com")
49
+ })
50
+
51
+ it("resolveTarget returns cloud mode with /api/v1 prefix", () => {
52
+ writePlainConfig(tmp)
53
+ mkdirSync(join(tmp, ".supatype"), { recursive: true })
54
+ writeFileSync(join(tmp, ".supatype", "link.json"), JSON.stringify({
55
+ version: 1,
56
+ kind: "cloud",
57
+ projectRef: "myproj",
58
+ defaultEnvironment: "production",
59
+ token: "pat",
60
+ orgId: "org1",
61
+ cloudApiUrl: "https://api.example.com",
62
+ linkedAt: new Date().toISOString(),
63
+ environments: {
64
+ production: {
65
+ name: "production",
66
+ apiUrl: "https://myproj.supatype.dev",
67
+ linkedAt: new Date().toISOString(),
68
+ },
69
+ },
70
+ }))
71
+
72
+ const target = resolveTarget(tmp)
73
+ expect(target.mode).toBe("cloud")
74
+ expect(target.apiPrefix).toBe("/api/v1")
75
+ expect(target.apiBaseUrl).toBe("https://api.example.com")
76
+ })
77
+
78
+ it("resolveTarget returns self-host mode with /platform/v1 prefix", () => {
79
+ writePlainConfig(tmp)
80
+ mkdirSync(join(tmp, ".supatype"), { recursive: true })
81
+ writeFileSync(join(tmp, ".supatype", "link.json"), JSON.stringify({
82
+ version: 1,
83
+ kind: "self-host",
84
+ projectRef: "demo",
85
+ defaultEnvironment: "production",
86
+ linkedAt: new Date().toISOString(),
87
+ environments: {
88
+ production: {
89
+ name: "production",
90
+ apiUrl: "https://app.example.com",
91
+ token: "srk",
92
+ linkedAt: new Date().toISOString(),
93
+ },
94
+ },
95
+ }))
96
+
97
+ const target = resolveTarget(tmp)
98
+ expect(target.mode).toBe("self-host")
99
+ expect(target.apiPrefix).toBe("/platform/v1")
100
+ expect(target.token).toBe("srk")
101
+ })
102
+
103
+ it("targetSchemaRollback posts to /platform/v1 when linked", async () => {
104
+ writePlainConfig(tmp)
105
+ mkdirSync(join(tmp, ".supatype"), { recursive: true })
106
+ writeFileSync(join(tmp, ".supatype", "link.json"), JSON.stringify({
107
+ version: 1,
108
+ kind: "self-host",
109
+ projectRef: "demo",
110
+ defaultEnvironment: "production",
111
+ linkedAt: new Date().toISOString(),
112
+ environments: {
113
+ production: {
114
+ name: "production",
115
+ apiUrl: "https://app.example.com",
116
+ token: "srk",
117
+ linkedAt: new Date().toISOString(),
118
+ },
119
+ },
120
+ }))
121
+
122
+ const fetchMock = vi.fn().mockResolvedValue({
123
+ ok: true,
124
+ json: async () => ({
125
+ data: {
126
+ status: "rolled_back",
127
+ name: "push_test",
128
+ message: "Rolled back migration push_test.",
129
+ },
130
+ }),
131
+ })
132
+ vi.stubGlobal("fetch", fetchMock)
133
+
134
+ const target = resolveTarget(tmp)
135
+ await targetSchemaRollback(target, { schema: "public" })
136
+
137
+ expect(fetchMock).toHaveBeenCalledOnce()
138
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
139
+ expect(url).toBe("https://app.example.com/platform/v1/projects/demo/schema/rollback")
140
+ expect(init.method).toBe("POST")
141
+ expect(init.headers).toMatchObject({
142
+ Authorization: "Bearer srk",
143
+ "Content-Type": "application/json",
144
+ })
145
+
146
+ vi.unstubAllGlobals()
147
+ })
148
+ })
@@ -1,5 +1,12 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { tmpdir } from "node:os"
1
4
  import { describe, expect, it } from "vitest"
2
- import { resolveProxyDevScript } from "../src/app/proxy-dev-app.js"
5
+ import {
6
+ portFromUpstream,
7
+ resolveProxyDevScript,
8
+ resolveViteDirectSpawn,
9
+ } from "../src/app/proxy-dev-app.js"
3
10
  import type { SupatypeProjectConfig } from "../src/project-config.js"
4
11
 
5
12
  const base = {
@@ -31,3 +38,40 @@ describe("resolveProxyDevScript()", () => {
31
38
  expect(resolveProxyDevScript(config)).toBe("dev:site")
32
39
  })
33
40
  })
41
+
42
+ describe("portFromUpstream()", () => {
43
+ it("parses port from upstream URL", () => {
44
+ const config = {
45
+ ...base,
46
+ app: { mode: "proxy" as const, upstream: "http://localhost:5285" },
47
+ } satisfies SupatypeProjectConfig
48
+ expect(portFromUpstream(config)).toBe(5285)
49
+ })
50
+
51
+ it("returns null when upstream has no port", () => {
52
+ const config = {
53
+ ...base,
54
+ app: { mode: "proxy" as const, upstream: "http://localhost" },
55
+ } satisfies SupatypeProjectConfig
56
+ expect(portFromUpstream(config)).toBeNull()
57
+ })
58
+ })
59
+
60
+ describe("resolveViteDirectSpawn()", () => {
61
+ it("returns node + vite.js for a plain vite script", () => {
62
+ const appDir = join(tmpdir(), `supatype-vite-spawn-${Date.now()}`)
63
+ const viteJs = join(appDir, "node_modules", "vite", "bin", "vite.js")
64
+ mkdirSync(join(appDir, "node_modules", "vite", "bin"), { recursive: true })
65
+ writeFileSync(viteJs, "")
66
+ const result = resolveViteDirectSpawn(appDir, "dev:vite", { "dev:vite": "vite" })
67
+ expect(result).not.toBeNull()
68
+ expect(result?.bin).toBe(process.execPath)
69
+ expect(result?.args[0]).toBe(viteJs)
70
+ expect(result?.shell).toBe(false)
71
+ })
72
+
73
+ it("returns null for non-vite scripts", () => {
74
+ const result = resolveViteDirectSpawn(process.cwd(), "dev", { dev: "next dev" })
75
+ expect(result).toBeNull()
76
+ })
77
+ })
@@ -106,12 +106,12 @@ describe("pgTypeToField()", () => {
106
106
  })
107
107
 
108
108
  describe("introspectColumnToColumnInfo()", () => {
109
- it("maps engine type → pgType and flags", () => {
109
+ it("maps engine column → pgType and flags", () => {
110
110
  const out = introspectColumnToColumnInfo({
111
111
  name: "id",
112
- type: "uuid",
112
+ dataType: "uuid",
113
+ udtName: "uuid",
113
114
  nullable: false,
114
- primaryKey: true,
115
115
  default: "gen_random_uuid()",
116
116
  })
117
117
  expect(out).toEqual({
@@ -127,7 +127,8 @@ describe("introspectColumnToColumnInfo()", () => {
127
127
  it("treats empty default as no default", () => {
128
128
  const out = introspectColumnToColumnInfo({
129
129
  name: "x",
130
- type: "text",
130
+ dataType: "text",
131
+ udtName: "text",
131
132
  nullable: true,
132
133
  default: "",
133
134
  })
@@ -4,7 +4,7 @@ import { join } from "node:path"
4
4
  import { tmpdir } from "node:os"
5
5
  import { runtimeRouteSpec } from "../src/runtime-routes.js"
6
6
  import { buildKongDeclarative } from "../src/kong-config.js"
7
- import { composeDockerImageEnv, renderSelfHostCompose, writeSelfHostCompose } from "../src/self-host-compose.js"
7
+ import { composeDockerImageEnv, composePullNeedsIgnoreFailures, hasLocalVersionPins, isRegistryPullableImageRef, renderSelfHostCompose, writeSelfHostCompose } from "../src/self-host-compose.js"
8
8
  import { updateAppConfigInProject } from "../src/app-config.js"
9
9
  import type { SupatypeProjectConfig } from "../src/project-config.js"
10
10
  import { DENO_RELEASE_PIN } from "../src/release-pins.js"
@@ -128,6 +128,49 @@ describe("runtime contract", () => {
128
128
  })
129
129
  })
130
130
 
131
+ it("isRegistryPullableImageRef accepts semver and latest tags", () => {
132
+ expect(isRegistryPullableImageRef("supatype/server:latest")).toBe(true)
133
+ expect(isRegistryPullableImageRef("supatype/schema-engine:v0.4.2")).toBe(true)
134
+ expect(isRegistryPullableImageRef("supatype/postgres:17-latest")).toBe(true)
135
+ expect(isRegistryPullableImageRef("supatype/control-plane:local")).toBe(false)
136
+ expect(isRegistryPullableImageRef("supatype/server:keepsake-local")).toBe(false)
137
+ })
138
+
139
+ it("composePullNeedsIgnoreFailures is false for semver pins only", () => {
140
+ const cwd = mkdtempSync(join(tmpdir(), "supatype-pull-"))
141
+ try {
142
+ writeFileSync(
143
+ join(cwd, ".env"),
144
+ "SUPATYPE_ENGINE_IMAGE=supatype/schema-engine:v0.4.2\n",
145
+ "utf8",
146
+ )
147
+ expect(hasLocalVersionPins(baseConfig)).toBe(false)
148
+ expect(composePullNeedsIgnoreFailures(baseConfig, cwd)).toBe(false)
149
+ } finally {
150
+ rmSync(cwd, { recursive: true, force: true })
151
+ }
152
+ })
153
+
154
+ it("composePullNeedsIgnoreFailures is true for versions.local or custom tags", () => {
155
+ const cwd = mkdtempSync(join(tmpdir(), "supatype-pull-local-"))
156
+ try {
157
+ writeFileSync(
158
+ join(cwd, ".env"),
159
+ "SUPATYPE_CONTROL_PLANE_IMAGE=supatype/control-plane:local\n",
160
+ "utf8",
161
+ )
162
+ expect(
163
+ composePullNeedsIgnoreFailures(
164
+ { ...baseConfig, versions: { engine: "local" } },
165
+ cwd,
166
+ ),
167
+ ).toBe(true)
168
+ expect(composePullNeedsIgnoreFailures(baseConfig, cwd)).toBe(true)
169
+ } finally {
170
+ rmSync(cwd, { recursive: true, force: true })
171
+ }
172
+ })
173
+
131
174
  it("self-host compose mounts project root at /project (project-directory relative)", () => {
132
175
  const compose = renderSelfHostCompose(baseConfig)
133
176
  expect(compose).toContain("- .:/project")
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest"
2
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import { collectSchemaSourcePaths } from "../src/type-extractor.js"
6
+ import {
7
+ packSchemaSources,
8
+ unpackSchemaSources,
9
+ buildSchemaSourcesPayload,
10
+ restoreSchemaSourcesFromGz,
11
+ findOrphanSchemaFiles,
12
+ } from "../src/schema-sources.js"
13
+
14
+ function writeMultiFileSchema(root: string): void {
15
+ mkdirSync(join(root, "schema", "models"), { recursive: true })
16
+ mkdirSync(join(root, "schema", "shared"), { recursive: true })
17
+ writeFileSync(
18
+ join(root, "schema", "index.ts"),
19
+ `export type { Album } from "./models/album"\nexport type { localeConfig } from "./shared/locale"\n`,
20
+ )
21
+ writeFileSync(
22
+ join(root, "schema", "models", "album.ts"),
23
+ `import type { Nullable } from "../shared/field-types"\nexport type Album = { id: string; title: Nullable<string> }\n`,
24
+ )
25
+ writeFileSync(
26
+ join(root, "schema", "shared", "field-types.ts"),
27
+ `export type Nullable<T> = T | null\n`,
28
+ )
29
+ writeFileSync(
30
+ join(root, "schema", "shared", "locale.ts"),
31
+ `export type localeConfig = { default: string }\n`,
32
+ )
33
+ }
34
+
35
+ describe("schema-sources", () => {
36
+ let tmp: string
37
+
38
+ beforeEach(() => {
39
+ tmp = mkdtempSync(join(tmpdir(), "schema-sources-"))
40
+ })
41
+
42
+ afterEach(() => {
43
+ rmSync(tmp, { recursive: true, force: true })
44
+ })
45
+
46
+ it("discovers multi-file graph from entry point", () => {
47
+ writeMultiFileSchema(tmp)
48
+ const entry = join(tmp, "schema", "index.ts")
49
+ const graph = collectSchemaSourcePaths(entry, tmp)
50
+ const paths = graph.files.map((f) => f.relativePath).sort()
51
+ expect(paths).toEqual([
52
+ "schema/index.ts",
53
+ "schema/models/album.ts",
54
+ "schema/shared/field-types.ts",
55
+ "schema/shared/locale.ts",
56
+ ])
57
+ expect(graph.entryPoint).toBe("schema/index.ts")
58
+ })
59
+
60
+ it("roundtrips tar+gzip bytes", () => {
61
+ writeMultiFileSchema(tmp)
62
+ const graph = collectSchemaSourcePaths(join(tmp, "schema", "index.ts"), tmp)
63
+ const tar = packSchemaSources(graph)
64
+ const files = unpackSchemaSources(tar, tmp)
65
+ for (const file of graph.files) {
66
+ const original = readFileSync(file.absolutePath)
67
+ const restored = files.get(file.relativePath.replace(/\\/g, "/"))
68
+ expect(restored?.equals(original)).toBe(true)
69
+ }
70
+ })
71
+
72
+ it("buildSchemaSourcesPayload includes manifest metadata", () => {
73
+ writeMultiFileSchema(tmp)
74
+ writeFileSync(
75
+ join(tmp, "supatype.config.ts"),
76
+ `export default ${JSON.stringify({
77
+ project: { name: "demo" },
78
+ database: { provider: "docker" },
79
+ server: { mode: "dev" },
80
+ app: { mode: "none" },
81
+ schema: { path: "schema/index.ts", pg_schema: "public" },
82
+ })}\n`,
83
+ )
84
+ const payload = buildSchemaSourcesPayload(tmp, "test@example.com")
85
+ expect(payload).not.toBeNull()
86
+ expect(payload!.manifest.fileCount).toBe(4)
87
+ expect(payload!.manifest.pushedBy).toBe("test@example.com")
88
+ expect(payload!.manifest.compressedBytes).toBeGreaterThan(0)
89
+ })
90
+
91
+ it("restore overwrites modified local files", () => {
92
+ writeMultiFileSchema(tmp)
93
+ writeFileSync(
94
+ join(tmp, "supatype.config.ts"),
95
+ `export default ${JSON.stringify({
96
+ project: { name: "demo" },
97
+ database: { provider: "docker" },
98
+ server: { mode: "dev" },
99
+ app: { mode: "none" },
100
+ schema: { path: "schema/index.ts", pg_schema: "public" },
101
+ })}\n`,
102
+ )
103
+ const payload = buildSchemaSourcesPayload(tmp)!
104
+ const albumPath = join(tmp, "schema", "models", "album.ts")
105
+ writeFileSync(albumPath, "// modified\n")
106
+
107
+ restoreSchemaSourcesFromGz(payload.gz, payload.manifest, tmp)
108
+ expect(readFileSync(albumPath, "utf8")).toContain("Nullable")
109
+ })
110
+
111
+ it("warns about orphan files not in snapshot", () => {
112
+ writeMultiFileSchema(tmp)
113
+ writeFileSync(join(tmp, "schema", "models", "experimental.ts"), "export type X = {}\n")
114
+ const graph = collectSchemaSourcePaths(join(tmp, "schema", "index.ts"), tmp)
115
+ const manifestPaths = new Set(graph.files.map((f) => f.relativePath))
116
+ const orphans = findOrphanSchemaFiles(tmp, graph.entryPoint, manifestPaths)
117
+ expect(orphans).toContain("schema/models/experimental.ts")
118
+ })
119
+ })
@@ -0,0 +1,100 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest"
2
+ import {
3
+ bucketSpecsFromAst,
4
+ provisionBuckets,
5
+ provisionBucketsFromAst,
6
+ type SchemaStorageBucketAst,
7
+ } from "../src/storage-provision.js"
8
+
9
+ describe("bucketSpecsFromAst", () => {
10
+ it("maps storage bucket AST fields to API bucket specs", () => {
11
+ const buckets: SchemaStorageBucketAst[] = [
12
+ {
13
+ id: "photos",
14
+ public: false,
15
+ accessMode: "private",
16
+ allowedMimeTypes: ["image/jpeg"],
17
+ fileSizeLimit: 5_000_000,
18
+ },
19
+ { id: "avatars", public: true },
20
+ ]
21
+
22
+ expect(bucketSpecsFromAst({ storageBuckets: buckets })).toEqual([
23
+ {
24
+ id: "photos",
25
+ public: false,
26
+ access_mode: "private",
27
+ allowed_mime_types: ["image/jpeg"],
28
+ file_size_limit: 5_000_000,
29
+ },
30
+ { id: "avatars", public: true },
31
+ ])
32
+ })
33
+
34
+ it("returns an empty list when no buckets are declared", () => {
35
+ expect(bucketSpecsFromAst({})).toEqual([])
36
+ })
37
+ })
38
+
39
+ describe("provisionBuckets", () => {
40
+ afterEach(() => {
41
+ vi.restoreAllMocks()
42
+ })
43
+
44
+ it("POSTs each bucket to the storage API", async () => {
45
+ const fetchMock = vi.fn().mockResolvedValue({ status: 200, ok: true })
46
+ vi.stubGlobal("fetch", fetchMock)
47
+
48
+ await provisionBuckets("http://localhost:54321/storage/v1/", "service-key", [
49
+ { id: "photos", public: false },
50
+ { id: "avatars", public: true },
51
+ ])
52
+
53
+ expect(fetchMock).toHaveBeenCalledTimes(2)
54
+ expect(fetchMock).toHaveBeenNthCalledWith(
55
+ 1,
56
+ "http://localhost:54321/storage/v1/bucket",
57
+ expect.objectContaining({
58
+ method: "POST",
59
+ headers: {
60
+ Authorization: "Bearer service-key",
61
+ "Content-Type": "application/json",
62
+ },
63
+ body: JSON.stringify({ id: "photos", name: "photos", public: false }),
64
+ }),
65
+ )
66
+ })
67
+
68
+ it("treats 409 Conflict as success (bucket already exists)", async () => {
69
+ const fetchMock = vi.fn().mockResolvedValue({ status: 409, ok: false })
70
+ vi.stubGlobal("fetch", fetchMock)
71
+
72
+ await expect(
73
+ provisionBuckets("http://localhost:54321/storage/v1", "key", [{ id: "photos", public: false }]),
74
+ ).resolves.toBeUndefined()
75
+ })
76
+
77
+ it("warns and continues when the storage API is unreachable", async () => {
78
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined)
79
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNREFUSED")))
80
+
81
+ await provisionBuckets("http://localhost:54321/storage/v1", "key", [{ id: "photos", public: false }])
82
+
83
+ expect(warn).toHaveBeenCalledWith('[storage] Storage API unreachable — skipped bucket "photos"')
84
+ })
85
+ })
86
+
87
+ describe("provisionBucketsFromAst", () => {
88
+ afterEach(() => {
89
+ vi.restoreAllMocks()
90
+ })
91
+
92
+ it("skips provisioning when the AST declares no buckets", async () => {
93
+ const fetchMock = vi.fn()
94
+ vi.stubGlobal("fetch", fetchMock)
95
+
96
+ await provisionBucketsFromAst({}, "http://localhost:54321/storage/v1", "key")
97
+
98
+ expect(fetchMock).not.toHaveBeenCalled()
99
+ })
100
+ })