@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +98 -65
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app/framework.js +1 -3
- package/dist/app/framework.js.map +1 -1
- package/dist/app/proxy-dev-app.d.ts +14 -0
- package/dist/app/proxy-dev-app.d.ts.map +1 -1
- package/dist/app/proxy-dev-app.js +109 -6
- package/dist/app/proxy-dev-app.js.map +1 -1
- package/dist/binary-cache.d.ts +1 -1
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +6 -1
- package/dist/binary-cache.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +3 -0
- package/dist/commands/adopt.d.ts.map +1 -0
- package/dist/commands/adopt.js +58 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/cloud.d.ts +4 -9
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +49 -91
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +25 -47
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +117 -74
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +21 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +37 -37
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +77 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +80 -33
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/introspect.d.ts +3 -0
- package/dist/commands/introspect.d.ts.map +1 -0
- package/dist/commands/introspect.js +34 -0
- package/dist/commands/introspect.js.map +1 -0
- package/dist/commands/link-helpers.d.ts +15 -0
- package/dist/commands/link-helpers.d.ts.map +1 -0
- package/dist/commands/link-helpers.js +187 -0
- package/dist/commands/link-helpers.js.map +1 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +116 -14
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +32 -5
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +102 -129
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +93 -29
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +6 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/dev-compose.d.ts +23 -0
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +183 -6
- package/dist/dev-compose.js.map +1 -1
- package/dist/diff-output.d.ts +5 -1
- package/dist/diff-output.d.ts.map +1 -1
- package/dist/diff-output.js +69 -0
- package/dist/diff-output.js.map +1 -1
- package/dist/engine-client.d.ts +10 -1
- package/dist/engine-client.d.ts.map +1 -1
- package/dist/engine-client.js +64 -13
- package/dist/engine-client.js.map +1 -1
- package/dist/engine-push-output.d.ts +1 -0
- package/dist/engine-push-output.d.ts.map +1 -1
- package/dist/engine-push-output.js +4 -1
- package/dist/engine-push-output.js.map +1 -1
- package/dist/gitignore.d.ts +8 -0
- package/dist/gitignore.d.ts.map +1 -0
- package/dist/gitignore.js +41 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/link.d.ts +66 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +159 -0
- package/dist/link.js.map +1 -0
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.d.ts.map +1 -1
- package/dist/process-manager.js +2 -0
- package/dist/process-manager.js.map +1 -1
- package/dist/project-config.d.ts +8 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js.map +1 -1
- package/dist/pull-utils.d.ts +50 -14
- package/dist/pull-utils.d.ts.map +1 -1
- package/dist/pull-utils.js +152 -12
- package/dist/pull-utils.js.map +1 -1
- package/dist/resolve-target.d.ts +86 -0
- package/dist/resolve-target.d.ts.map +1 -0
- package/dist/resolve-target.js +291 -0
- package/dist/resolve-target.js.map +1 -0
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +7 -0
- package/dist/runtime-routes.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +1 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +2 -2
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/schema-sources.d.ts +40 -0
- package/dist/schema-sources.d.ts.map +1 -0
- package/dist/schema-sources.js +183 -0
- package/dist/schema-sources.js.map +1 -0
- package/dist/self-host-compose.d.ts +10 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +85 -3
- package/dist/self-host-compose.js.map +1 -1
- package/dist/storage-provision.d.ts +4 -0
- package/dist/storage-provision.d.ts.map +1 -1
- package/dist/storage-provision.js +24 -2
- package/dist/storage-provision.js.map +1 -1
- package/dist/target-client.d.ts +10 -0
- package/dist/target-client.d.ts.map +1 -0
- package/dist/target-client.js +22 -0
- package/dist/target-client.js.map +1 -0
- package/dist/type-extractor.d.ts +11 -0
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +95 -8
- package/dist/type-extractor.js.map +1 -1
- package/package.json +1 -1
- package/src/app/framework.ts +1 -3
- package/src/app/proxy-dev-app.ts +113 -6
- package/src/binary-cache.ts +6 -1
- package/src/cli.ts +6 -0
- package/src/commands/adopt.ts +83 -0
- package/src/commands/cloud.ts +66 -108
- package/src/commands/db.ts +28 -52
- package/src/commands/deploy.ts +162 -104
- package/src/commands/dev.ts +24 -10
- package/src/commands/diff.ts +40 -41
- package/src/commands/doctor.ts +102 -0
- package/src/commands/functions.ts +95 -37
- package/src/commands/init.ts +25 -4
- package/src/commands/introspect.ts +47 -0
- package/src/commands/link-helpers.ts +228 -0
- package/src/commands/migrate.ts +163 -15
- package/src/commands/pull.ts +37 -9
- package/src/commands/push.ts +132 -166
- package/src/commands/status.ts +100 -33
- package/src/commands/update.ts +6 -2
- package/src/config.ts +2 -1
- package/src/dev-compose.ts +240 -6
- package/src/diff-output.ts +79 -1
- package/src/engine-client.ts +70 -13
- package/src/engine-push-output.ts +7 -3
- package/src/gitignore.ts +48 -0
- package/src/link.ts +242 -0
- package/src/process-manager.ts +4 -0
- package/src/project-config.ts +8 -0
- package/src/pull-utils.ts +217 -23
- package/src/resolve-target.ts +419 -0
- package/src/runtime-routes.ts +7 -0
- package/src/schema-ast-v2.ts +2 -1
- package/src/schema-sources.ts +248 -0
- package/src/self-host-compose.ts +87 -3
- package/src/storage-provision.ts +33 -1
- package/src/target-client.ts +40 -0
- package/src/type-extractor.ts +124 -11
- package/tests/cli-help.test.ts +27 -2
- package/tests/init.test.ts +1 -1
- package/tests/link.test.ts +148 -0
- package/tests/proxy-dev-app.test.ts +45 -1
- package/tests/pull-utils.test.ts +5 -4
- package/tests/runtime-contract.test.ts +44 -1
- package/tests/schema-sources.test.ts +119 -0
- package/tests/storage-provision.test.ts +100 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/pull-utils.ts
CHANGED
|
@@ -1,41 +1,237 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Utilities for
|
|
2
|
+
* Utilities for `pull` and `introspect` — map engine DatabaseState to Model<> scaffold.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface DbColumnState {
|
|
6
6
|
name: string
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
hasDefault: boolean
|
|
37
|
+
comment?: string | null
|
|
12
38
|
}
|
|
13
39
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
49
|
+
pgType: string
|
|
18
50
|
nullable: boolean
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
51
|
+
isPrimary: boolean
|
|
52
|
+
isUnique: boolean
|
|
53
|
+
hasDefault: boolean
|
|
22
54
|
references?: { table: string; column: string }
|
|
23
55
|
}
|
|
24
56
|
|
|
25
|
-
/** Map engine introspection
|
|
26
|
-
export function introspectColumnToColumnInfo(col:
|
|
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.
|
|
61
|
+
pgType: col.udtName || col.dataType,
|
|
31
62
|
nullable: col.nullable,
|
|
32
|
-
isPrimary: col.
|
|
33
|
-
isUnique:
|
|
34
|
-
hasDefault:
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 }
|
package/src/runtime-routes.ts
CHANGED
|
@@ -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",
|