@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
@@ -293,13 +293,14 @@ export function emitModel(
293
293
  options: Record<string, unknown>,
294
294
  tableName: string,
295
295
  access: Record<string, unknown>,
296
+ indexes: unknown[] = [],
296
297
  ): ModelAstV2 {
297
298
  return {
298
299
  name,
299
300
  fields,
300
301
  options,
301
302
  annotations: {
302
- db: { tableName, indexes: [] },
303
+ db: { tableName, indexes },
303
304
  platform: { access },
304
305
  },
305
306
  }
@@ -0,0 +1,248 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ writeFileSync,
7
+ cpSync,
8
+ } from "node:fs"
9
+ import { join, resolve, relative } from "node:path"
10
+ import { gzipSync, gunzipSync } from "node:zlib"
11
+ import { collectSchemaSourcePaths, type SchemaSourceGraph } from "./type-extractor.js"
12
+ import { loadConfig } from "./config.js"
13
+ import { projectRootFromConfig, schemaPathFromProject } from "./project-config.js"
14
+
15
+ const MAX_COMPRESSED_BYTES = 2 * 1024 * 1024
16
+ const WARN_COMPRESSED_BYTES = 512 * 1024
17
+
18
+ export interface SchemaSourcesManifest {
19
+ version: number
20
+ format: "tar+gzip"
21
+ entryPoint: string
22
+ fileCount: number
23
+ uncompressedBytes: number
24
+ compressedBytes: number
25
+ files: Array<{ path: string; sha256: string; bytes: number }>
26
+ pushedBy?: string
27
+ }
28
+
29
+ export interface SchemaSourcesPayload {
30
+ manifest: SchemaSourcesManifest
31
+ dataBase64: string
32
+ gz: Buffer
33
+ }
34
+
35
+ function padTarField(value: string, len: number): string {
36
+ return value.slice(0, len).padEnd(len, "\0")
37
+ }
38
+
39
+ function tarChecksum(header: Buffer): number {
40
+ let sum = 0
41
+ for (let i = 0; i < 512; i++) {
42
+ sum += i >= 148 && i < 156 ? 32 : header[i]!
43
+ }
44
+ return sum
45
+ }
46
+
47
+ function writeTarEntry(name: string, content: Buffer): Buffer {
48
+ const header = Buffer.alloc(512, 0)
49
+ const size = content.length
50
+ header.write(padTarField(name, 100), 0, 100, "ascii")
51
+ header.write("0000644\0", 100, 8, "ascii")
52
+ header.write("0000000\0", 108, 8, "ascii")
53
+ header.write("0000000\0", 116, 8, "ascii")
54
+ header.write(padTarField(size.toString(8), 11), 124, 12, "ascii")
55
+ header.write("00000000000\0", 136, 12, "ascii")
56
+ header.write(" ", 148, 8, "ascii")
57
+ header.write("ustar\0", 257, 6, "ascii")
58
+ header.write("00", 263, 2, "ascii")
59
+ const chk = tarChecksum(header)
60
+ header.write(chk.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii")
61
+
62
+ const pad = (512 - (size % 512)) % 512
63
+ return Buffer.concat([header, content, Buffer.alloc(pad)])
64
+ }
65
+
66
+ export function packSchemaSources(graph: SchemaSourceGraph): Buffer {
67
+ const chunks: Buffer[] = []
68
+ for (const file of graph.files) {
69
+ const content = readFileSync(file.absolutePath)
70
+ chunks.push(writeTarEntry(file.relativePath.replace(/\\/g, "/"), content))
71
+ }
72
+ chunks.push(Buffer.alloc(1024, 0))
73
+ return Buffer.concat(chunks)
74
+ }
75
+
76
+ export function unpackSchemaSources(tar: Buffer, projectRoot: string): Map<string, Buffer> {
77
+ const root = resolve(projectRoot)
78
+ const out = new Map<string, Buffer>()
79
+ let offset = 0
80
+ while (offset + 512 <= tar.length) {
81
+ const header = tar.subarray(offset, offset + 512)
82
+ offset += 512
83
+ if (header.every((b) => b === 0)) break
84
+ const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "").trim()
85
+ if (!name) break
86
+ const sizeOct = header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim()
87
+ const size = parseInt(sizeOct, 8) || 0
88
+ const content = tar.subarray(offset, offset + size)
89
+ offset += size + ((512 - (size % 512)) % 512)
90
+ const rel = name.replace(/\\/g, "/")
91
+ if (rel.startsWith("..") || rel.includes("/../")) {
92
+ throw new Error(`Invalid tar path: ${name}`)
93
+ }
94
+ const dest = resolve(root, rel)
95
+ if (!dest.startsWith(root)) {
96
+ throw new Error(`Tar path escapes project root: ${name}`)
97
+ }
98
+ out.set(rel, Buffer.from(content))
99
+ }
100
+ return out
101
+ }
102
+
103
+ export function buildSchemaSourcesPayload(
104
+ cwd: string,
105
+ pushedBy?: string,
106
+ ): SchemaSourcesPayload | null {
107
+ const config = loadConfig(cwd)
108
+ const root = projectRootFromConfig(config, cwd)
109
+ const entry = schemaPathFromProject(config, cwd)
110
+ const graph = collectSchemaSourcePaths(entry, root)
111
+ if (graph.files.length === 0) return null
112
+
113
+ const tarBuf = packSchemaSources(graph)
114
+ const gz = gzipSync(tarBuf, { level: 9 })
115
+
116
+ if (gz.length > MAX_COMPRESSED_BYTES) {
117
+ throw new Error(
118
+ `Schema snapshot too large (${(gz.length / 1024).toFixed(0)} KB compressed, max 2 MB). Split schema modules.`,
119
+ )
120
+ }
121
+ if (gz.length > WARN_COMPRESSED_BYTES) {
122
+ console.warn(`Warning: large schema snapshot (${(gz.length / 1024).toFixed(0)} KB compressed).`)
123
+ }
124
+
125
+ const manifest: SchemaSourcesManifest = {
126
+ version: 1,
127
+ format: "tar+gzip",
128
+ entryPoint: graph.entryPoint,
129
+ fileCount: graph.files.length,
130
+ uncompressedBytes: graph.files.reduce((s, f) => s + f.bytes, 0),
131
+ compressedBytes: gz.length,
132
+ files: graph.files.map((f) => ({ path: f.relativePath, sha256: f.sha256, bytes: f.bytes })),
133
+ ...(pushedBy ? { pushedBy } : {}),
134
+ }
135
+
136
+ return {
137
+ manifest,
138
+ dataBase64: gz.toString("base64"),
139
+ gz,
140
+ }
141
+ }
142
+
143
+ export function cacheSchemaSourcesLocally(cwd: string, migrationName: string, gz: Buffer): void {
144
+ const dir = join(cwd, ".supatype", "schema-snapshots")
145
+ mkdirSync(dir, { recursive: true })
146
+ writeFileSync(join(dir, `${migrationName}.tar.gz`), gz)
147
+ }
148
+
149
+ export function restoreSchemaSourcesFromGz(
150
+ gz: Buffer,
151
+ manifest: SchemaSourcesManifest,
152
+ projectRoot: string,
153
+ opts?: { backupDir?: string },
154
+ ): string[] {
155
+ const tar = gunzipSync(gz)
156
+ const files = unpackSchemaSources(tar, projectRoot)
157
+ const root = resolve(projectRoot)
158
+ const restored: string[] = []
159
+
160
+ if (opts?.backupDir) {
161
+ mkdirSync(opts.backupDir, { recursive: true })
162
+ for (const path of manifest.files) {
163
+ const abs = join(root, path.path)
164
+ if (existsSync(abs)) {
165
+ const dest = join(opts.backupDir, path.path)
166
+ mkdirSync(join(dest, ".."), { recursive: true })
167
+ cpSync(abs, dest)
168
+ }
169
+ }
170
+ }
171
+
172
+ for (const [rel, content] of files) {
173
+ const abs = join(root, rel)
174
+ mkdirSync(join(abs, ".."), { recursive: true })
175
+ writeFileSync(abs, content)
176
+ restored.push(rel)
177
+ }
178
+
179
+ return restored
180
+ }
181
+
182
+ export function findOrphanSchemaFiles(
183
+ projectRoot: string,
184
+ entryPoint: string,
185
+ manifestPaths: Set<string>,
186
+ ): string[] {
187
+ const entry = resolve(projectRoot, entryPoint)
188
+ const graph = collectSchemaSourcePaths(entry, projectRoot)
189
+ const orphans: string[] = []
190
+ const schemaDir = join(projectRoot, graph.entryPoint.split("/").slice(0, -1).join("/") || ".")
191
+ if (!existsSync(schemaDir)) return orphans
192
+
193
+ const walk = (dir: string): void => {
194
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
195
+ const abs = join(dir, ent.name)
196
+ if (ent.isDirectory()) {
197
+ walk(abs)
198
+ continue
199
+ }
200
+ if (!/\.(ts|tsx)$/.test(ent.name)) continue
201
+ const rel = relative(projectRoot, abs).replace(/\\/g, "/")
202
+ if (!manifestPaths.has(rel) && !graph.files.some((f) => f.relativePath === rel)) {
203
+ orphans.push(rel)
204
+ }
205
+ }
206
+ }
207
+ walk(schemaDir)
208
+ return orphans
209
+ }
210
+
211
+ export function resolvePushedBy(): string {
212
+ return (
213
+ process.env["SUPATYPE_PUSHED_BY"] ??
214
+ process.env["USER"] ??
215
+ process.env["USERNAME"] ??
216
+ "local"
217
+ )
218
+ }
219
+
220
+ export interface SchemaSourcePushArtifacts {
221
+ gzPath: string
222
+ manifestPath: string
223
+ /** Paths inside the Docker /project bind mount. */
224
+ dockerGzPath: string
225
+ dockerManifestPath: string
226
+ payload: SchemaSourcesPayload
227
+ }
228
+
229
+ /** Write schema source blob + manifest under `.supatype/` for engine push (CLI + dev watch). */
230
+ export function writeSchemaSourcePushArtifacts(cwd: string): SchemaSourcePushArtifacts | null {
231
+ const payload = buildSchemaSourcesPayload(cwd, resolvePushedBy())
232
+ if (!payload) return null
233
+
234
+ const dir = join(cwd, ".supatype")
235
+ mkdirSync(dir, { recursive: true })
236
+ const gzPath = join(dir, "schema-sources-push.gz")
237
+ const manifestPath = join(dir, "schema-sources-manifest.json")
238
+ writeFileSync(gzPath, payload.gz)
239
+ writeFileSync(manifestPath, JSON.stringify(payload.manifest))
240
+
241
+ return {
242
+ gzPath,
243
+ manifestPath,
244
+ dockerGzPath: "/project/.supatype/schema-sources-push.gz",
245
+ dockerManifestPath: "/project/.supatype/schema-sources-manifest.json",
246
+ payload,
247
+ }
248
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs"
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
2
2
  import { dirname, join, relative, resolve } from "node:path"
3
3
  import { spawnSync } from "node:child_process"
4
4
  import { preferredFunctionsPathFromProject, type SupatypeProjectConfig } from "./project-config.js"
@@ -12,6 +12,16 @@ export const COMPOSE_PINNED_IMAGE_ENV_KEYS = [
12
12
  "SUPATYPE_POSTGRES_IMAGE",
13
13
  ] as const
14
14
 
15
+ /** Compose image env vars that may be overridden manually in `.env`. */
16
+ export const COMPOSE_IMAGE_ENV_KEYS = [
17
+ ...COMPOSE_PINNED_IMAGE_ENV_KEYS,
18
+ "SUPATYPE_CONTROL_PLANE_IMAGE",
19
+ "SUPATYPE_AUTH_IMAGE",
20
+ "SUPATYPE_STUDIO_IMAGE",
21
+ "SUPATYPE_STORAGE_IMAGE",
22
+ "SUPATYPE_FUNCTIONS_WORKER_IMAGE",
23
+ ] as const
24
+
15
25
  type DockerPinComponent = "engine" | "server" | "postgres"
16
26
 
17
27
  /** Map a config version pin to a Docker Hub image reference. */
@@ -57,6 +67,52 @@ export function composeDockerImageEnv(config: SupatypeProjectConfig): Record<str
57
67
  return env
58
68
  }
59
69
 
70
+ /** True when a Docker image tag is a semver/latest ref we expect `docker pull` to resolve. */
71
+ export function isRegistryPullableImageRef(ref: string): boolean {
72
+ const trimmed = ref.trim()
73
+ if (!trimmed) return true
74
+ const tag = trimmed.includes(":") ? trimmed.slice(trimmed.lastIndexOf(":") + 1) : "latest"
75
+ if (tag === "latest") return true
76
+ if (/^v?\d+\.\d+/.test(tag)) return true
77
+ if (/^\d+-latest$/.test(tag)) return true
78
+ return false
79
+ }
80
+
81
+ export function hasLocalVersionPins(config: SupatypeProjectConfig): boolean {
82
+ const versions = config.versions
83
+ if (!versions) return false
84
+ return (
85
+ versions.engine === VERSION_PIN_LOCAL ||
86
+ versions.server === VERSION_PIN_LOCAL ||
87
+ versions.postgres === VERSION_PIN_LOCAL ||
88
+ versions.deno === VERSION_PIN_LOCAL
89
+ )
90
+ }
91
+
92
+ function readComposeImageEnvValues(cwd: string): string[] {
93
+ const envPath = resolve(cwd, ".env")
94
+ if (!existsSync(envPath)) return []
95
+ const text = readFileSync(envPath, "utf8")
96
+ const values: string[] = []
97
+ for (const key of COMPOSE_IMAGE_ENV_KEYS) {
98
+ const match = text.match(new RegExp(`^${key}=(.+)$`, "m"))
99
+ if (match?.[1]) values.push(match[1].trim())
100
+ }
101
+ return values
102
+ }
103
+
104
+ /**
105
+ * Use `docker compose pull --ignore-pull-failures` only when the project may
106
+ * reference local-only images (config `versions: local` or custom `.env` tags).
107
+ */
108
+ export function composePullNeedsIgnoreFailures(
109
+ config: SupatypeProjectConfig,
110
+ cwd: string = process.cwd(),
111
+ ): boolean {
112
+ if (hasLocalVersionPins(config)) return true
113
+ return readComposeImageEnvValues(cwd).some((ref) => !isRegistryPullableImageRef(ref))
114
+ }
115
+
60
116
  /**
61
117
  * Schema-engine image for a one-off `docker compose run` when pushing schema.
62
118
  * Uses config pin when set; otherwise CDN engine semver (Docker Hub `:latest` can lag).
@@ -174,6 +230,8 @@ export function renderSelfHostCompose(
174
230
  const devLocal = options?.devLocal === true
175
231
  const studioHostDev = devLocal && hasStudioOverride(config)
176
232
  const appEnv = serverAppEnvForCompose(config, devLocal)
233
+ const staticDir = staticDirForCompose(config) ?? "./dist"
234
+ const composeProject = composeProjectName(config.project.name)
177
235
  const studioService = studioServiceBlock()
178
236
  const studioBlock = studioHostDev
179
237
  ? ""
@@ -186,9 +244,11 @@ ${studioService}
186
244
  - "3002"
187
245
  `
188
246
  const kongDependsOn = studioHostDev
189
- ? ` - server`
247
+ ? ` - server
248
+ - control-plane`
190
249
  : ` - server
191
- - studio`
250
+ - studio
251
+ - control-plane`
192
252
  const publishDbToHost = !devLocal || hasEngineOverride(config)
193
253
  const dbPorts = publishDbToHost
194
254
  ? devLocal
@@ -280,6 +340,27 @@ ${dbPorts} volumes:
280
340
  db:
281
341
  condition: service_healthy
282
342
 
343
+ control-plane:
344
+ image: \${SUPATYPE_CONTROL_PLANE_IMAGE:-supatype/control-plane:latest}
345
+ expose:
346
+ - "8080"
347
+ volumes:
348
+ - ${projectMount}:/project
349
+ - /var/run/docker.sock:/var/run/docker.sock
350
+ environment:
351
+ PORT: "8080"
352
+ SUPATYPE_PROJECT_REF: ${JSON.stringify(config.project.name)}
353
+ SUPATYPE_PROJECT_ROOT: /project
354
+ DATABASE_URL: "postgresql://\${POSTGRES_USER:-supatype_admin}:\${POSTGRES_PASSWORD:-postgres}@db:5432/\${POSTGRES_DB:-supatype}"
355
+ SUPATYPE_FUNCTIONS_ROOT: /project/functions
356
+ SUPATYPE_STATIC_ROOT: /project/${staticDir.replace(/^\.\//, "")}
357
+ SUPATYPE_DEPLOYMENTS_DIR: /project/.supatype/deployments
358
+ COMPOSE_PROJECT_NAME: ${composeProject}
359
+ SUPATYPE_ENGINE_BIN: supatype-engine
360
+ depends_on:
361
+ db:
362
+ condition: service_healthy
363
+
283
364
  server:
284
365
  image: \${SUPATYPE_SERVER_IMAGE:-\${SUPATYPE_AUTH_IMAGE:-supatype/server:latest}}
285
366
  ${serverPorts} volumes:
@@ -299,6 +380,7 @@ ${serverPorts} volumes:
299
380
  SUPATYPE_SQL_DATABASE_URL: "postgresql://\${POSTGRES_USER:-supatype_admin}:\${POSTGRES_PASSWORD:-postgres}@db:5432/\${POSTGRES_DB:-supatype}"
300
381
  SUPATYPE_DENO_FUNCTIONS_DIR: /project/functions
301
382
  SUPATYPE_FUNCTIONS_WORKER_URL: http://functions-worker:8001
383
+ SUPATYPE_CONTROL_PLANE_URL: http://control-plane:8080
302
384
  ${appEnv}
303
385
  GOTRUE_API_HOST: 0.0.0.0
304
386
  GOTRUE_API_PORT: 9999
@@ -324,6 +406,8 @@ ${devLocal ? " STUDIO_OPEN_DEV: \"1\"\n" : ""}
324
406
  condition: service_started
325
407
  functions-worker:
326
408
  condition: service_started
409
+ control-plane:
410
+ condition: service_started
327
411
 
328
412
  minio:
329
413
  image: minio/minio:RELEASE.2024-11-07T00-52-20Z
@@ -6,6 +6,10 @@
6
6
  * Buckets already registered return 409 Conflict, which is treated as success.
7
7
  */
8
8
 
9
+ import type { ExtractedSchemaAstV2 } from "./schema-ast-v2.js"
10
+
11
+ export type { ExtractedStorageBucketAst as SchemaStorageBucketAst } from "./schema-ast-v2.js"
12
+
9
13
  export interface BucketSpec {
10
14
  id: string
11
15
  public: boolean
@@ -15,6 +19,29 @@ export interface BucketSpec {
15
19
  s3_bucket_policy?: string | null
16
20
  }
17
21
 
22
+ export function bucketSpecsFromAst(ast: Pick<ExtractedSchemaAstV2, "storageBuckets">): BucketSpec[] {
23
+ return (ast.storageBuckets ?? []).map((b) => ({
24
+ id: b.id,
25
+ public: b.public,
26
+ ...(b.accessMode !== undefined && { access_mode: b.accessMode }),
27
+ ...(b.allowedMimeTypes != null && { allowed_mime_types: b.allowedMimeTypes }),
28
+ ...(b.fileSizeLimit != null && { file_size_limit: b.fileSizeLimit }),
29
+ ...(b.s3BucketPolicy != null &&
30
+ b.s3BucketPolicy !== "" && { s3_bucket_policy: b.s3BucketPolicy }),
31
+ }))
32
+ }
33
+
34
+ export async function provisionBucketsFromAst(
35
+ ast: Pick<ExtractedSchemaAstV2, "storageBuckets">,
36
+ storageApiUrl: string,
37
+ serviceRoleKey: string,
38
+ ): Promise<void> {
39
+ const buckets = bucketSpecsFromAst(ast)
40
+ if (buckets.length === 0) return
41
+ console.log("[supatype] Provisioning storage buckets...")
42
+ await provisionBuckets(storageApiUrl, serviceRoleKey, buckets)
43
+ }
44
+
18
45
  /**
19
46
  * Ensure all declared buckets exist in the storage server.
20
47
  *
@@ -48,11 +75,16 @@ export async function provisionBuckets(
48
75
  const res = await fetch(`${base}/bucket`, { method: "POST", headers, body })
49
76
  .catch(() => null)
50
77
 
51
- if (res === null) continue // server not reachable — skip silently
78
+ if (res === null) {
79
+ console.warn(`[storage] Storage API unreachable — skipped bucket "${bucket.id}"`)
80
+ continue
81
+ }
52
82
  if (res.status === 409) continue // already exists — fine
53
83
  if (!res.ok) {
54
84
  const msg = await res.text().catch(() => res.statusText)
55
85
  console.warn(`[storage] Failed to provision bucket "${bucket.id}": ${msg}`)
86
+ continue
56
87
  }
88
+ console.log(`[supatype] Storage bucket "${bucket.id}" ready.`)
57
89
  }
58
90
  }
@@ -0,0 +1,40 @@
1
+ export interface TargetFetchOptions {
2
+ method: string
3
+ path: string
4
+ body?: unknown
5
+ token: string
6
+ orgId?: string | undefined
7
+ environment?: string | undefined
8
+ }
9
+
10
+ export async function targetFetch<T>(
11
+ baseUrl: string,
12
+ apiPrefix: "/api/v1" | "/platform/v1",
13
+ opts: TargetFetchOptions,
14
+ ): Promise<T> {
15
+ const headers: Record<string, string> = {
16
+ "Content-Type": "application/json",
17
+ Authorization: `Bearer ${opts.token}`,
18
+ }
19
+ if (opts.orgId) headers["X-Org-Id"] = opts.orgId
20
+ if (opts.environment) headers["X-Supatype-Environment"] = opts.environment
21
+
22
+ const url = `${baseUrl.replace(/\/$/, "")}${apiPrefix}${opts.path}`
23
+ const res = await fetch(url, {
24
+ method: opts.method,
25
+ headers,
26
+ ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
27
+ })
28
+
29
+ const json = (await res.json().catch(() => ({}))) as {
30
+ data?: T
31
+ error?: string
32
+ message?: string
33
+ }
34
+
35
+ if (!res.ok) {
36
+ throw new Error(json.message ?? json.error ?? `API error: ${res.status} ${url}`)
37
+ }
38
+
39
+ return (json.data !== undefined ? json.data : json) as T
40
+ }
@@ -1,5 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
- import { dirname, isAbsolute, resolve } from "node:path"
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs"
2
+ import { createHash } from "node:crypto"
3
+ import { dirname, isAbsolute, relative, resolve } from "node:path"
3
4
  import ts from "typescript"
4
5
  import {
5
6
  applyImportRename,
@@ -103,7 +104,7 @@ export function extractSchemaAstFromTypes(
103
104
  )
104
105
  }
105
106
 
106
- const { tableName, access, options } = parseModelMeta(
107
+ const { tableName, access, options, indexes } = parseModelMeta(
107
108
  metaArg,
108
109
  sourceFile,
109
110
  stmt.name.text,
@@ -112,7 +113,7 @@ export function extractSchemaAstFromTypes(
112
113
  )
113
114
 
114
115
  models.push(
115
- emitModel(stmt.name.text, fields, options, tableName, access),
116
+ emitModel(stmt.name.text, fields, options, tableName, access, indexes),
116
117
  )
117
118
  }
118
119
  }
@@ -143,9 +144,51 @@ export function extractSchemaAstFromTypes(
143
144
  })
144
145
  }
145
146
 
146
- function loadSchemaSourceFiles(entryPath: string): ts.SourceFile[] {
147
+ export interface SchemaSourceFile {
148
+ relativePath: string
149
+ absolutePath: string
150
+ sha256: string
151
+ bytes: number
152
+ }
153
+
154
+ export interface SchemaSourceGraph {
155
+ entryPoint: string
156
+ files: SchemaSourceFile[]
157
+ }
158
+
159
+ export function collectSchemaSourcePaths(entryAbsPath: string, projectRoot: string): SchemaSourceGraph {
160
+ const root = resolve(projectRoot)
161
+ const entryReal = realpathSync(entryAbsPath)
162
+ const entryPoint = relative(root, entryReal).replace(/\\/g, "/")
163
+ if (entryPoint.startsWith("..")) {
164
+ throw new Error(`Schema entry must be under project root: ${entryAbsPath}`)
165
+ }
166
+
167
+ const absolutePaths = walkSchemaSourceAbsPaths(entryReal)
168
+ const files: SchemaSourceFile[] = []
169
+
170
+ for (const abs of absolutePaths) {
171
+ const real = realpathSync(abs)
172
+ const rel = relative(root, real).replace(/\\/g, "/")
173
+ if (rel.startsWith("..")) {
174
+ throw new Error(`Schema source escapes project root: ${abs}`)
175
+ }
176
+ const content = readFileSync(real)
177
+ files.push({
178
+ relativePath: rel,
179
+ absolutePath: real,
180
+ sha256: createHash("sha256").update(content).digest("hex"),
181
+ bytes: content.length,
182
+ })
183
+ }
184
+
185
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))
186
+ return { entryPoint, files }
187
+ }
188
+
189
+ function walkSchemaSourceAbsPaths(entryPath: string): string[] {
147
190
  const visited = new Set<string>()
148
- const sourceFiles: ts.SourceFile[] = []
191
+ const paths: string[] = []
149
192
  const queue: string[] = [entryPath]
150
193
 
151
194
  while (queue.length > 0) {
@@ -153,12 +196,11 @@ function loadSchemaSourceFiles(entryPath: string): ts.SourceFile[] {
153
196
  if (!currentPath) continue
154
197
  if (visited.has(currentPath)) continue
155
198
  visited.add(currentPath)
156
-
157
199
  if (!existsSync(currentPath)) continue
200
+ paths.push(currentPath)
201
+
158
202
  const sourceText = readFileSync(currentPath, "utf8")
159
203
  const sourceFile = ts.createSourceFile(currentPath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
160
- sourceFiles.push(sourceFile)
161
-
162
204
  const baseDir = dirname(currentPath)
163
205
  for (const stmt of sourceFile.statements) {
164
206
  let specifier: string | undefined
@@ -178,7 +220,14 @@ function loadSchemaSourceFiles(entryPath: string): ts.SourceFile[] {
178
220
  }
179
221
  }
180
222
 
181
- return sourceFiles
223
+ return paths
224
+ }
225
+
226
+ function loadSchemaSourceFiles(entryPath: string): ts.SourceFile[] {
227
+ return walkSchemaSourceAbsPaths(entryPath).map((abs) => {
228
+ const sourceText = readFileSync(abs, "utf8")
229
+ return ts.createSourceFile(abs, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
230
+ })
182
231
  }
183
232
 
184
233
  function resolveTypeModulePath(fromDir: string, specifier: string): string | null {
@@ -1372,7 +1421,12 @@ function parseModelMeta(
1372
1421
  modelName: string,
1373
1422
  fieldsArg: ts.TypeNode,
1374
1423
  fields: Record<string, FieldAstV2>,
1375
- ): { tableName: string; access: Record<string, unknown>; options: Record<string, unknown> } {
1424
+ ): {
1425
+ tableName: string
1426
+ access: Record<string, unknown>
1427
+ options: Record<string, unknown>
1428
+ indexes: unknown[]
1429
+ } {
1376
1430
  const literal = parseMetaLiteral(metaArg, sourceFile)
1377
1431
  const singleton = literal.singleton === true
1378
1432
  const tableName =
@@ -1397,7 +1451,66 @@ function parseModelMeta(
1397
1451
  tableName,
1398
1452
  access: parseModelAccess(metaArg, sourceFile),
1399
1453
  options,
1454
+ indexes: parseModelIndexes(metaArg, sourceFile, fields),
1455
+ }
1456
+ }
1457
+
1458
+ function parseModelIndexes(
1459
+ metaArg: ts.TypeNode | undefined,
1460
+ sourceFile: ts.SourceFile,
1461
+ fields: Record<string, FieldAstV2>,
1462
+ ): unknown[] {
1463
+ if (!metaArg || !ts.isTypeLiteralNode(metaArg)) return []
1464
+
1465
+ const indexesProp = metaArg.members.find(
1466
+ (member) => ts.isPropertySignature(member) && getPropertyName(member.name) === "indexes",
1467
+ )
1468
+ if (
1469
+ !indexesProp ||
1470
+ !ts.isPropertySignature(indexesProp) ||
1471
+ !indexesProp.type ||
1472
+ !ts.isTupleTypeNode(indexesProp.type)
1473
+ ) {
1474
+ return []
1475
+ }
1476
+
1477
+ const indexes: unknown[] = []
1478
+ for (const element of indexesProp.type.elements) {
1479
+ if (!ts.isTypeLiteralNode(element)) continue
1480
+ const indexDef: Record<string, unknown> = { using: "btree" }
1481
+ for (const member of element.members) {
1482
+ if (!ts.isPropertySignature(member) || !member.type) continue
1483
+ const key = getPropertyName(member.name)
1484
+ if (!key) continue
1485
+ if (key === "name" && ts.isLiteralTypeNode(member.type) && ts.isStringLiteral(member.type.literal)) {
1486
+ indexDef.name = member.type.literal.text
1487
+ } else if (key === "unique" && isBooleanLiteralType(member.type, true)) {
1488
+ indexDef.unique = true
1489
+ } else if (key === "fields" && ts.isTupleTypeNode(member.type)) {
1490
+ indexDef.fields = member.type.elements
1491
+ .map((fieldNode) => {
1492
+ if (!ts.isLiteralTypeNode(fieldNode) || !ts.isStringLiteral(fieldNode.literal)) return null
1493
+ return resolveIndexFieldName(fieldNode.literal.text, fields)
1494
+ })
1495
+ .filter((field): field is string => field !== null)
1496
+ }
1497
+ }
1498
+ if (Array.isArray(indexDef.fields) && indexDef.fields.length > 0) {
1499
+ indexes.push(indexDef)
1500
+ }
1501
+ }
1502
+ return indexes
1503
+ }
1504
+
1505
+ function resolveIndexFieldName(fieldName: string, fields: Record<string, FieldAstV2>): string | null {
1506
+ if (fields[fieldName] !== undefined) {
1507
+ const field = fields[fieldName]
1508
+ if (field.kind === "relation" && field.annotations?.db?.foreignKey) {
1509
+ return field.annotations.db.foreignKey as string
1510
+ }
1511
+ return fieldName
1400
1512
  }
1513
+ return fieldName
1401
1514
  }
1402
1515
 
1403
1516
  function parseModelAccess(metaArg: ts.TypeNode | undefined, sourceFile: ts.SourceFile): Record<string, unknown> {