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

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 +98 -65
  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
package/src/pull-utils.ts CHANGED
@@ -1,41 +1,237 @@
1
1
  /**
2
- * Utilities for the `pull` commandextracted for unit testability.
2
+ * Utilities for `pull` and `introspect` map engine DatabaseState to Model<> scaffold.
3
3
  */
4
4
 
5
- export interface ColumnInfo {
5
+ export interface DbColumnState {
6
6
  name: string
7
- pgType: string
7
+ dataType: string
8
+ udtName: string
8
9
  nullable: boolean
10
+ default?: string | null
11
+ ordinalPosition?: number
12
+ }
13
+
14
+ export interface DbTableState {
15
+ schema?: string
16
+ name: string
17
+ columns: DbColumnState[]
18
+ }
19
+
20
+ export interface DbConstraintState {
21
+ table: string
22
+ name: string
23
+ constraintType: string
24
+ columns: string[]
25
+ foreignTable?: string | null
26
+ foreignColumns?: string[] | null
27
+ comment?: string | null
28
+ }
29
+
30
+ export interface DbIndexState {
31
+ table: string
32
+ name: string
33
+ columns: string[]
34
+ unique: boolean
35
+ method: string
9
36
  isPrimary: boolean
10
- isUnique: boolean
11
- hasDefault: boolean
37
+ comment?: string | null
12
38
  }
13
39
 
14
- /** Engine `/introspect` column shape (see {@link IntrospectResult} in engine-client). */
15
- export interface IntrospectColumn {
40
+ export interface DatabaseStateJson {
41
+ schema?: string
42
+ tables: DbTableState[]
43
+ constraints?: DbConstraintState[]
44
+ indexes?: DbIndexState[]
45
+ }
46
+
47
+ export interface ColumnInfo {
16
48
  name: string
17
- type: string
49
+ pgType: string
18
50
  nullable: boolean
19
- default?: string
20
- primaryKey?: boolean
21
- unique?: boolean
51
+ isPrimary: boolean
52
+ isUnique: boolean
53
+ hasDefault: boolean
22
54
  references?: { table: string; column: string }
23
55
  }
24
56
 
25
- /** Map engine introspection JSON to {@link ColumnInfo} for {@link pgTypeToField}. */
26
- export function introspectColumnToColumnInfo(col: IntrospectColumn): ColumnInfo {
27
- const def = col.default
57
+ /** Map engine introspection column to {@link ColumnInfo}. */
58
+ export function introspectColumnToColumnInfo(col: DbColumnState): ColumnInfo {
28
59
  return {
29
60
  name: col.name,
30
- pgType: col.type,
61
+ pgType: col.udtName || col.dataType,
31
62
  nullable: col.nullable,
32
- isPrimary: col.primaryKey ?? false,
33
- isUnique: col.unique ?? false,
34
- hasDefault: def !== undefined && def !== "",
63
+ isPrimary: col.name === "id" && (col.udtName === "uuid" || col.dataType.includes("uuid")),
64
+ isUnique: false,
65
+ hasDefault: col.default != null && col.default !== "",
66
+ }
67
+ }
68
+
69
+ function singleColumnUniques(
70
+ table: string,
71
+ constraints: DbConstraintState[] | undefined,
72
+ ): Map<string, boolean> {
73
+ const out = new Map<string, boolean>()
74
+ for (const c of constraints ?? []) {
75
+ if (c.table !== table || c.constraintType !== "unique" || c.columns.length !== 1) continue
76
+ out.set(c.columns[0]!, true)
35
77
  }
78
+ return out
36
79
  }
37
80
 
38
- /** Map a Postgres column type to the corresponding field.X() call string. */
81
+ function compositeIndexes(
82
+ table: string,
83
+ indexes: DbIndexState[] | undefined,
84
+ ): Array<{ fields: string[]; unique: boolean }> {
85
+ const out: Array<{ fields: string[]; unique: boolean }> = []
86
+ for (const idx of indexes ?? []) {
87
+ if (idx.table !== table || idx.isPrimary || idx.columns.length < 2) continue
88
+ out.push({ fields: idx.columns, unique: idx.unique })
89
+ }
90
+ return out
91
+ }
92
+
93
+ /** Map a Postgres column to a Model<> field type string (draft). */
94
+ export function pgTypeToModelField(col: ColumnInfo): string {
95
+ const name = col.name
96
+ const type = col.pgType.toLowerCase()
97
+ const optional = col.nullable ? "Optional<" : ""
98
+ const optionalClose = col.nullable ? ">" : ""
99
+
100
+ if (name === "id" && type.includes("uuid")) {
101
+ return `id: SupatypeAuthUserId`
102
+ }
103
+ if (name.endsWith("_id") && type.includes("uuid")) {
104
+ const base = name.replace(/_id$/, "")
105
+ const rel = base.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
106
+ const relName = rel.charAt(0).toUpperCase() + rel.slice(1)
107
+ return `${name}: UUID\n // TODO: relation — RelatedTo<"${relName}">`
108
+ }
109
+ if (name === "created_at" || name === "updated_at") {
110
+ return `${name}: Timestamp`
111
+ }
112
+ if (name === "deleted_at") {
113
+ return `${name}: Optional<Timestamp>`
114
+ }
115
+ if (type.includes("timestamptz") || type.includes("timestamp with time zone")) {
116
+ return `${name}: ${optional}Timestamp${optionalClose}`
117
+ }
118
+ if (type.includes("uuid")) {
119
+ const inner = col.isUnique ? "Unique<UUID>" : "UUID"
120
+ return `${name}: ${optional}${inner}${optionalClose}`
121
+ }
122
+ if (name.includes("email") && (type.includes("text") || type.includes("varchar"))) {
123
+ const inner = col.isUnique ? "Unique<Email>" : "Email"
124
+ return `${name}: ${optional}${inner}${optionalClose}`
125
+ }
126
+ if (name === "slug" || name.endsWith("_slug")) {
127
+ return `${name}: Slug<"title">`
128
+ }
129
+ if (type.includes("jsonb")) {
130
+ return `${name}: ${optional}JSON${optionalClose}`
131
+ }
132
+ if (type.includes("bool")) {
133
+ return `${name}: ${optional}boolean${optionalClose}`
134
+ }
135
+ if (type.includes("int8") || type.includes("bigint")) {
136
+ return `${name}: ${optional}BigInt${optionalClose}`
137
+ }
138
+ if (type.includes("int")) {
139
+ return `${name}: ${optional}Int${optionalClose}`
140
+ }
141
+ if (type.includes("text") || type.includes("varchar")) {
142
+ const inner = col.isUnique ? "Unique<string>" : "string"
143
+ return `${name}: ${optional}${inner}${optionalClose}`
144
+ }
145
+
146
+ return `${name}: string /* TODO: ${col.pgType} */`
147
+ }
148
+
149
+ /** Convert snake_case table name to PascalCase model export name. */
150
+ export function toModelName(s: string): string {
151
+ return s
152
+ .replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
153
+ .replace(/^([a-z])/, (c: string) => c.toUpperCase())
154
+ }
155
+
156
+ /** Generate draft schema/index.ts content from introspected DatabaseState. */
157
+ export function databaseStateToSchemaScaffold(state: DatabaseStateJson): string {
158
+ const constraints = state.constraints ?? []
159
+ const indexes = state.indexes ?? []
160
+ const skipTables = new Set(["spatial_ref_sys", "schema_migrations"])
161
+
162
+ const lines: string[] = [
163
+ "// DRAFT — generated by `supatype pull`. Review access rules, relations, and indexes before push.",
164
+ 'import type { Model, Public, UUID, Timestamp, Optional, Unique, Email, Slug, SupatypeAuthUserId, JSON, Int, BigInt } from "@supatype/types"',
165
+ "",
166
+ ]
167
+
168
+ for (const table of state.tables) {
169
+ if (skipTables.has(table.name) || table.name.startsWith("_")) continue
170
+
171
+ const modelName = toModelName(table.name)
172
+ const uniques = singleColumnUniques(table.name, constraints)
173
+ const modelIndexes = compositeIndexes(table.name, indexes)
174
+
175
+ const fieldLines: string[] = []
176
+ for (const col of table.columns) {
177
+ if (col.name === "created_at" || col.name === "updated_at") continue
178
+ const info = introspectColumnToColumnInfo(col)
179
+ info.isPrimary = col.name === "id"
180
+ info.isUnique = uniques.get(col.name) ?? false
181
+ fieldLines.push(` ${pgTypeToModelField(info)}`)
182
+ }
183
+
184
+ const hasTimestamps = table.columns.some((c) => c.name === "created_at")
185
+ if (hasTimestamps) {
186
+ fieldLines.push(" created_at: Timestamp")
187
+ fieldLines.push(" updated_at: Timestamp")
188
+ }
189
+
190
+ const indexBlock =
191
+ modelIndexes.length > 0
192
+ ? `\n indexes: [\n${modelIndexes
193
+ .map(
194
+ (idx) =>
195
+ ` { fields: [${idx.fields.map((f) => `"${f}"`).join(", ")}]${idx.unique ? ", unique: true" : ""} },`,
196
+ )
197
+ .join("\n")}\n ],`
198
+ : ""
199
+
200
+ lines.push(`export type ${modelName} = Model<{`)
201
+ lines.push(fieldLines.join("\n"))
202
+ lines.push(`}, {`)
203
+ lines.push(` access: {`)
204
+ lines.push(` read: Public`)
205
+ lines.push(` create: Public`)
206
+ lines.push(` update: Public`)
207
+ lines.push(` delete: Public`)
208
+ lines.push(` },${indexBlock}`)
209
+ lines.push(`}>`)
210
+ lines.push("")
211
+ }
212
+
213
+ return lines.join("\n")
214
+ }
215
+
216
+ /** Human-readable introspection summary for `supatype introspect`. */
217
+ export function printIntrospectSummary(state: DatabaseStateJson): void {
218
+ console.log(`Schema: ${state.schema ?? "public"}`)
219
+ console.log(`Tables: ${state.tables.length}`)
220
+ for (const table of state.tables) {
221
+ console.log(`\n ${table.name} (${table.columns.length} columns)`)
222
+ for (const col of table.columns) {
223
+ const nullMark = col.nullable ? "?" : ""
224
+ console.log(` ${col.name}${nullMark}: ${col.udtName || col.dataType}`)
225
+ }
226
+ }
227
+ const constraintCount = state.constraints?.length ?? 0
228
+ const indexCount = state.indexes?.length ?? 0
229
+ if (constraintCount + indexCount > 0) {
230
+ console.log(`\nConstraints: ${constraintCount}, Indexes: ${indexCount}`)
231
+ }
232
+ }
233
+
234
+ /** @deprecated Legacy pull helper — use {@link pgTypeToModelField}. */
39
235
  export function pgTypeToField(col: ColumnInfo): string {
40
236
  const opts: Record<string, unknown> = { required: !col.nullable }
41
237
  if (col.isPrimary) opts["primaryKey"] = true
@@ -73,9 +269,7 @@ export function pgTypeToField(col: ColumnInfo): string {
73
269
  return `field.text({ ...${optsStr} }) /* TODO: ${col.pgType} */`
74
270
  }
75
271
 
76
- /** Convert snake_case table name to PascalCase model export name. */
272
+ /** @deprecated Use {@link toModelName}. */
77
273
  export function toCamelCase(s: string): string {
78
- return s
79
- .replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
80
- .replace(/^([a-z])/, (c: string) => c.toUpperCase())
274
+ return toModelName(s)
81
275
  }
@@ -0,0 +1,419 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import { loadConfig } from "./config.js"
4
+ import type { DiffResult } from "./engine-client.js"
5
+ import { ensureEngine, engineRequest } from "./engine-client.js"
6
+ import type { SchemaSourcesPayload } from "./schema-sources.js"
7
+ import {
8
+ connectionString,
9
+ resolveRuntimeProvider,
10
+ schemaPathFromProject,
11
+ serverBaseUrl,
12
+ } from "./project-config.js"
13
+ import {
14
+ getEnvironmentTarget,
15
+ loadLocalEnvironment,
16
+ loadProjectLink,
17
+ resolveEnvironmentName,
18
+ resolveEnvironmentToken,
19
+ type BranchContext,
20
+ type LocalEnvironment,
21
+ type ProjectLink,
22
+ } from "./link.js"
23
+ import { targetFetch } from "./target-client.js"
24
+
25
+ export interface ResolveTargetFlags {
26
+ env?: string | undefined
27
+ direct?: boolean
28
+ local?: boolean
29
+ connection?: string | undefined
30
+ }
31
+
32
+ export type TargetMode = "cloud" | "self-host" | "local" | "direct"
33
+
34
+ export interface DeployTarget {
35
+ mode: TargetMode
36
+ environment: string
37
+ projectRef: string
38
+ apiBaseUrl: string
39
+ apiPrefix: "/api/v1" | "/platform/v1"
40
+ token?: string | undefined
41
+ orgId?: string | undefined
42
+ link: ProjectLink | null
43
+ /** Engine subprocess path when mode is direct or local without control plane. */
44
+ databaseUrl?: string
45
+ }
46
+
47
+ export function loadLocalEnvironmentFile(cwd: string): LocalEnvironment | null {
48
+ return loadLocalEnvironment(cwd)
49
+ }
50
+
51
+ function readServiceRoleKey(cwd: string): string | undefined {
52
+ return process.env["SERVICE_ROLE_KEY"] ?? process.env["SUPATYPE_SERVICE_ROLE_KEY"]
53
+ }
54
+
55
+ function resolveLocalToken(cwd: string): string | undefined {
56
+ return readServiceRoleKey(cwd)
57
+ }
58
+
59
+ export function loadBranchContext(cwd: string): BranchContext | null {
60
+ const path = resolve(cwd, ".supatype/branch.json")
61
+ if (!existsSync(path)) return null
62
+ return JSON.parse(readFileSync(path, "utf8")) as BranchContext
63
+ }
64
+
65
+ function resolveBranchDefaults(cwd: string, configEnvDefault?: string): string | undefined {
66
+ if (configEnvDefault) return configEnvDefault
67
+ try {
68
+ const config = loadConfig(cwd)
69
+ const branchDefaults = config.environments?.branchDefaults
70
+ if (!branchDefaults) return undefined
71
+ const { execSync } = require("node:child_process") as typeof import("node:child_process")
72
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf8" }).trim()
73
+ return branchDefaults[branch]
74
+ } catch {
75
+ return undefined
76
+ }
77
+ }
78
+
79
+ export function resolveTarget(cwd: string, flags: ResolveTargetFlags = {}): DeployTarget {
80
+ const config = loadConfig(cwd)
81
+ const projectRef = config.project?.name ?? config.project?.ref ?? "project"
82
+ const branchCtx = loadBranchContext(cwd)
83
+
84
+ if (branchCtx) {
85
+ return {
86
+ mode: "self-host",
87
+ environment: `branch:${branchCtx.branchId}`,
88
+ projectRef,
89
+ apiBaseUrl: branchCtx.apiUrl.replace(/\/$/, ""),
90
+ apiPrefix: "/platform/v1",
91
+ token: branchCtx.token,
92
+ link: loadProjectLink(cwd),
93
+ }
94
+ }
95
+
96
+ if (flags.direct || flags.local) {
97
+ const localEnv = loadLocalEnvironment(cwd)
98
+ return {
99
+ mode: "direct",
100
+ environment: "local",
101
+ projectRef,
102
+ apiBaseUrl: (serverBaseUrl(config) ?? "").replace(/\/$/, ""),
103
+ apiPrefix: "/platform/v1",
104
+ link: null,
105
+ databaseUrl:
106
+ flags.connection ??
107
+ localEnv?.databaseUrl ??
108
+ connectionString(config),
109
+ }
110
+ }
111
+
112
+ const link = loadProjectLink(cwd)
113
+ const localEnv = loadLocalEnvironment(cwd)
114
+
115
+ if (!link) {
116
+ if (localEnv && resolveRuntimeProvider(config) === "docker") {
117
+ return {
118
+ mode: "local",
119
+ environment: "local",
120
+ projectRef: localEnv.projectRef,
121
+ apiBaseUrl: localEnv.apiUrl.replace(/\/$/, ""),
122
+ apiPrefix: "/platform/v1",
123
+ token: resolveLocalToken(cwd),
124
+ link: null,
125
+ databaseUrl: localEnv.databaseUrl,
126
+ }
127
+ }
128
+ return {
129
+ mode: "direct",
130
+ environment: "local",
131
+ projectRef,
132
+ apiBaseUrl: (serverBaseUrl(config) ?? "").replace(/\/$/, ""),
133
+ apiPrefix: "/platform/v1",
134
+ link: null,
135
+ databaseUrl: flags.connection ?? connectionString(config),
136
+ }
137
+ }
138
+
139
+ const envName = resolveEnvironmentName(
140
+ link,
141
+ flags.env ?? resolveBranchDefaults(cwd, config.environments?.default),
142
+ )
143
+ const envTarget = getEnvironmentTarget(link, envName)
144
+ if (!envTarget) {
145
+ throw new Error(
146
+ `Environment "${envName}" not linked. Run: supatype link --env ${envName} ... or supatype envs list`,
147
+ )
148
+ }
149
+
150
+ const token = resolveEnvironmentToken(link, envTarget)
151
+ if (!token) {
152
+ throw new Error(`No token for environment "${envName}". Re-run supatype link --token ...`)
153
+ }
154
+
155
+ if (link.kind === "cloud") {
156
+ return {
157
+ mode: "cloud",
158
+ environment: envName,
159
+ projectRef: link.projectRef,
160
+ apiBaseUrl: (link.cloudApiUrl ?? envTarget.apiUrl).replace(/\/$/, ""),
161
+ apiPrefix: "/api/v1",
162
+ token,
163
+ link,
164
+ ...(link.orgId !== undefined ? { orgId: link.orgId } : {}),
165
+ }
166
+ }
167
+
168
+ return {
169
+ mode: link.kind === "local" ? "local" : "self-host",
170
+ environment: envName,
171
+ projectRef: link.projectRef,
172
+ apiBaseUrl: envTarget.apiUrl.replace(/\/$/, ""),
173
+ apiPrefix: "/platform/v1",
174
+ token,
175
+ link,
176
+ }
177
+ }
178
+
179
+ function projectPath(target: DeployTarget, subpath: string): string {
180
+ return `/projects/${target.projectRef}${subpath}`
181
+ }
182
+
183
+ export async function targetSchemaDiff(
184
+ target: DeployTarget,
185
+ ast: unknown,
186
+ opts?: { schema?: string },
187
+ ): Promise<DiffResult> {
188
+ if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
189
+ await ensureEngine()
190
+ return engineRequest<DiffResult>("/diff", {
191
+ ast,
192
+ database_url: target.databaseUrl!,
193
+ schema: opts?.schema ?? "public",
194
+ })
195
+ }
196
+
197
+ return targetFetch<DiffResult>(target.apiBaseUrl, target.apiPrefix, {
198
+ method: "POST",
199
+ path: projectPath(target, "/schema/diff"),
200
+ body: { ast, schema: opts?.schema ?? "public" },
201
+ token: target.token!,
202
+ orgId: target.orgId,
203
+ environment: target.mode === "cloud" ? target.environment : undefined,
204
+ })
205
+ }
206
+
207
+ export async function targetSchemaPush(
208
+ target: DeployTarget,
209
+ ast: unknown,
210
+ opts?: { force?: boolean; schema?: string; schemaSources?: SchemaSourcesPayload | null },
211
+ ): Promise<{ message?: string; status?: string; name?: string }> {
212
+ if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
213
+ await ensureEngine()
214
+ const body: Record<string, unknown> = {
215
+ ast,
216
+ database_url: target.databaseUrl!,
217
+ schema: opts?.schema ?? "public",
218
+ force: opts?.force ?? true,
219
+ }
220
+ if (opts?.schemaSources) {
221
+ body["schema_sources_gz_base64"] = opts.schemaSources.dataBase64
222
+ body["schema_sources_manifest"] = opts.schemaSources.manifest
223
+ }
224
+ return engineRequest("/push", body)
225
+ }
226
+
227
+ const pushBody: Record<string, unknown> = {
228
+ ast,
229
+ force: opts?.force ?? true,
230
+ schema: opts?.schema ?? "public",
231
+ }
232
+ if (opts?.schemaSources) {
233
+ pushBody["schemaSources"] = {
234
+ manifest: opts.schemaSources.manifest,
235
+ dataBase64: opts.schemaSources.dataBase64,
236
+ }
237
+ }
238
+
239
+ return targetFetch(target.apiBaseUrl, target.apiPrefix, {
240
+ method: "POST",
241
+ path: projectPath(target, "/schema/push"),
242
+ body: pushBody,
243
+ token: target.token!,
244
+ orgId: target.orgId,
245
+ environment: target.mode === "cloud" ? target.environment : undefined,
246
+ })
247
+ }
248
+
249
+ export interface SchemaRollbackResult {
250
+ status: string
251
+ name: string
252
+ message: string
253
+ restoredMigrationName?: string
254
+ schemaSourcesManifest?: SchemaSourcesManifestSummary
255
+ schemaSourcesBase64?: string
256
+ }
257
+
258
+ export interface SchemaSourcesManifestSummary {
259
+ entryPoint?: string
260
+ fileCount?: number
261
+ compressedBytes?: number
262
+ pushedBy?: string
263
+ files?: Array<{ path: string }>
264
+ }
265
+
266
+ export interface MigrationListEntry {
267
+ id: number
268
+ name: string
269
+ hash: string
270
+ appliedAt: string
271
+ rolledBack: boolean
272
+ engineVersion: string
273
+ status: string
274
+ schemaSourcesManifest?: SchemaSourcesManifestSummary | null
275
+ }
276
+
277
+ export async function targetSchemaRollback(
278
+ target: DeployTarget,
279
+ opts?: { schema?: string },
280
+ ): Promise<SchemaRollbackResult> {
281
+ if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
282
+ await ensureEngine()
283
+ return engineRequest<SchemaRollbackResult>("/rollback", {
284
+ database_url: target.databaseUrl!,
285
+ schema: opts?.schema ?? "public",
286
+ })
287
+ }
288
+
289
+ return targetFetch<SchemaRollbackResult>(target.apiBaseUrl, target.apiPrefix, {
290
+ method: "POST",
291
+ path: projectPath(target, "/schema/rollback"),
292
+ body: { schema: opts?.schema ?? "public" },
293
+ token: target.token!,
294
+ orgId: target.orgId,
295
+ environment: target.mode === "cloud" ? target.environment : undefined,
296
+ })
297
+ }
298
+
299
+ export async function targetListMigrations(
300
+ target: DeployTarget,
301
+ ): Promise<MigrationListEntry[]> {
302
+ if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
303
+ await ensureEngine()
304
+ const result = await engineRequest<{ migrations?: MigrationListEntry[] } | MigrationListEntry[]>(
305
+ "/migrations",
306
+ { database_url: target.databaseUrl!, action: "list" },
307
+ )
308
+ return Array.isArray(result) ? result : (result.migrations ?? [])
309
+ }
310
+
311
+ return targetFetch<MigrationListEntry[]>(target.apiBaseUrl, target.apiPrefix, {
312
+ method: "GET",
313
+ path: projectPath(target, "/schema/migrations"),
314
+ token: target.token!,
315
+ orgId: target.orgId,
316
+ environment: target.mode === "cloud" ? target.environment : undefined,
317
+ })
318
+ }
319
+
320
+ export async function targetSchemaDoctor(
321
+ target: DeployTarget,
322
+ ast: unknown,
323
+ opts?: { noCache?: boolean | undefined; schema?: string },
324
+ ): Promise<unknown> {
325
+ if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
326
+ await ensureEngine()
327
+ return engineRequest("/doctor", {
328
+ ast,
329
+ database_url: target.databaseUrl!,
330
+ schema: opts?.schema ?? "public",
331
+ no_cache: opts?.noCache ?? false,
332
+ })
333
+ }
334
+
335
+ return targetFetch(target.apiBaseUrl, target.apiPrefix, {
336
+ method: "POST",
337
+ path: projectPath(target, "/schema/doctor"),
338
+ body: { ast, no_cache: opts?.noCache ?? false, schema: opts?.schema ?? "public" },
339
+ token: target.token!,
340
+ orgId: target.orgId,
341
+ environment: target.mode === "cloud" ? target.environment : undefined,
342
+ })
343
+ }
344
+
345
+ export async function targetSchemaIntrospect(
346
+ target: DeployTarget,
347
+ opts?: { schema?: string },
348
+ ): Promise<unknown> {
349
+ if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
350
+ await ensureEngine()
351
+ return engineRequest("/introspect", {
352
+ database_url: target.databaseUrl!,
353
+ schema: opts?.schema ?? "public",
354
+ })
355
+ }
356
+
357
+ return targetFetch(target.apiBaseUrl, target.apiPrefix, {
358
+ method: "POST",
359
+ path: projectPath(target, "/schema/introspect"),
360
+ body: { schema: opts?.schema ?? "public" },
361
+ token: target.token!,
362
+ orgId: target.orgId,
363
+ environment: target.mode === "cloud" ? target.environment : undefined,
364
+ })
365
+ }
366
+
367
+ export async function targetSchemaAdopt(
368
+ target: DeployTarget,
369
+ ast: unknown,
370
+ opts?: { names?: string[]; schema?: string; yes?: boolean; noCache?: boolean },
371
+ ): Promise<unknown> {
372
+ if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
373
+ await ensureEngine()
374
+ return engineRequest("/adopt", {
375
+ ast,
376
+ database_url: target.databaseUrl!,
377
+ schema: opts?.schema ?? "public",
378
+ names: opts?.names,
379
+ yes: opts?.yes ?? false,
380
+ no_cache: opts?.noCache ?? false,
381
+ })
382
+ }
383
+
384
+ return targetFetch(target.apiBaseUrl, target.apiPrefix, {
385
+ method: "POST",
386
+ path: projectPath(target, "/schema/adopt"),
387
+ body: {
388
+ ast,
389
+ schema: opts?.schema ?? "public",
390
+ yes: opts?.yes ?? false,
391
+ no_cache: opts?.noCache ?? false,
392
+ ...(opts?.names !== undefined ? { names: opts.names } : {}),
393
+ },
394
+ token: target.token!,
395
+ orgId: target.orgId,
396
+ environment: target.mode === "cloud" ? target.environment : undefined,
397
+ })
398
+ }
399
+
400
+ export async function targetStatus(target: DeployTarget): Promise<unknown> {
401
+ if (target.mode === "direct") {
402
+ return { mode: "direct", environment: target.environment }
403
+ }
404
+
405
+ return targetFetch(target.apiBaseUrl, target.apiPrefix, {
406
+ method: "GET",
407
+ path: projectPath(target, "/status"),
408
+ token: target.token!,
409
+ orgId: target.orgId,
410
+ environment: target.mode === "cloud" ? target.environment : undefined,
411
+ })
412
+ }
413
+
414
+ export function schemaPgSchema(cwd: string): string {
415
+ const config = loadConfig(cwd)
416
+ return config.schema?.pg_schema ?? "public"
417
+ }
418
+
419
+ export { schemaPathFromProject }
@@ -92,6 +92,13 @@ function runtimeRouteSpecUnified(opts: RuntimeRouteOptions): RuntimeRoute[] {
92
92
  paths: ["/functions/v1/"],
93
93
  stripPath: false,
94
94
  },
95
+ {
96
+ name: "platform-v1",
97
+ serviceName: "supatype-server-platform",
98
+ serviceUrl: SERVER_GATEWAY,
99
+ paths: ["/platform/v1/"],
100
+ stripPath: false,
101
+ },
95
102
  {
96
103
  name: "graphql-v1",
97
104
  serviceName: "postgrest-graphql",