@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/commands/migrate.ts
CHANGED
|
@@ -1,11 +1,72 @@
|
|
|
1
1
|
import type { Command } from "commander"
|
|
2
2
|
import { createInterface } from "node:readline"
|
|
3
|
+
import { join } from "node:path"
|
|
3
4
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
4
|
-
import { connectionString, schemaPathFromProject } from "../project-config.js"
|
|
5
|
+
import { connectionString, projectRootFromConfig, schemaPathFromProject } from "../project-config.js"
|
|
5
6
|
import { ensureEngine, engineRequest } from "../engine-client.js"
|
|
7
|
+
import { loadProjectLink } from "../link.js"
|
|
8
|
+
import {
|
|
9
|
+
resolveTarget,
|
|
10
|
+
targetSchemaDiff,
|
|
11
|
+
targetSchemaRollback,
|
|
12
|
+
targetListMigrations,
|
|
13
|
+
schemaPgSchema,
|
|
14
|
+
type SchemaRollbackResult,
|
|
15
|
+
} from "../resolve-target.js"
|
|
16
|
+
import {
|
|
17
|
+
restoreSchemaSourcesFromGz,
|
|
18
|
+
findOrphanSchemaFiles,
|
|
19
|
+
type SchemaSourcesManifest,
|
|
20
|
+
} from "../schema-sources.js"
|
|
6
21
|
|
|
7
22
|
export function registerMigrate(program: Command): void {
|
|
8
|
-
|
|
23
|
+
const migrations = program
|
|
24
|
+
.command("migrations")
|
|
25
|
+
.description("Migration history utilities")
|
|
26
|
+
|
|
27
|
+
migrations
|
|
28
|
+
.command("list")
|
|
29
|
+
.description("List applied migrations with schema snapshot metadata")
|
|
30
|
+
.option("--connection <url>", "Database connection URL (overrides config)")
|
|
31
|
+
.option("--env <name>", "Target environment when linked")
|
|
32
|
+
.option("--direct", "Use local engine subprocess (skip control plane)")
|
|
33
|
+
.action(async (opts: { connection?: string; env?: string; direct?: boolean }) => {
|
|
34
|
+
const cwd = process.cwd()
|
|
35
|
+
const link = loadProjectLink(cwd)
|
|
36
|
+
const useDirect = opts.direct || Boolean(opts.connection)
|
|
37
|
+
|
|
38
|
+
let target
|
|
39
|
+
if (link && !useDirect && !opts.connection) {
|
|
40
|
+
target = resolveTarget(cwd, { env: opts.env })
|
|
41
|
+
} else {
|
|
42
|
+
target = resolveTarget(cwd, {
|
|
43
|
+
env: opts.env,
|
|
44
|
+
direct: true,
|
|
45
|
+
connection: opts.connection,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const list = await targetListMigrations(target)
|
|
50
|
+
if (list.length === 0) {
|
|
51
|
+
console.log("No migrations applied.")
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const m of list) {
|
|
56
|
+
const manifest = m.schemaSourcesManifest
|
|
57
|
+
const size =
|
|
58
|
+
manifest?.compressedBytes !== undefined
|
|
59
|
+
? `${(manifest.compressedBytes / 1024).toFixed(1)} KB`
|
|
60
|
+
: "—"
|
|
61
|
+
const files = manifest?.fileCount ?? "—"
|
|
62
|
+
const author = manifest?.pushedBy ?? "—"
|
|
63
|
+
const rolled = m.rolledBack ? " (rolled back)" : ""
|
|
64
|
+
console.log(
|
|
65
|
+
`${m.name}${rolled}\n applied: ${m.appliedAt} status: ${m.status}\n author: ${author} files: ${files} snapshot: ${size}`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
9
70
|
program
|
|
10
71
|
.command("migrate")
|
|
11
72
|
.description("Apply pending migrations from the migration history")
|
|
@@ -23,25 +84,46 @@ export function registerMigrate(program: Command): void {
|
|
|
23
84
|
console.log(result.message ?? "Migrations applied.")
|
|
24
85
|
})
|
|
25
86
|
|
|
26
|
-
// rollback — undo the last applied migration
|
|
27
87
|
program
|
|
28
88
|
.command("rollback")
|
|
29
89
|
.description("Roll back the last applied migration")
|
|
30
90
|
.option("--connection <url>", "Database connection URL (overrides config)")
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
91
|
+
.option("--env <name>", "Target environment when linked")
|
|
92
|
+
.option("--direct", "Use local engine subprocess (skip control plane)")
|
|
93
|
+
.option("--sync-schema", "Restore schema source files from DB snapshot without prompting")
|
|
94
|
+
.option("--no-sync-schema", "Revert database only; do not restore schema files")
|
|
95
|
+
.action(async (opts: {
|
|
96
|
+
connection?: string
|
|
97
|
+
env?: string
|
|
98
|
+
direct?: boolean
|
|
99
|
+
syncSchema?: boolean
|
|
100
|
+
noSyncSchema?: boolean
|
|
101
|
+
}) => {
|
|
102
|
+
const cwd = process.cwd()
|
|
103
|
+
const config = loadConfig(cwd)
|
|
104
|
+
const pgSchema = schemaPgSchema(cwd)
|
|
105
|
+
const link = loadProjectLink(cwd)
|
|
106
|
+
const useDirect = opts.direct || Boolean(opts.connection)
|
|
34
107
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
108
|
+
let target
|
|
109
|
+
if (link && !useDirect && !opts.connection) {
|
|
110
|
+
target = resolveTarget(cwd, { env: opts.env })
|
|
111
|
+
} else {
|
|
112
|
+
target = resolveTarget(cwd, {
|
|
113
|
+
env: opts.env,
|
|
114
|
+
direct: true,
|
|
115
|
+
connection: opts.connection,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await targetSchemaRollback(target, { schema: pgSchema })
|
|
41
120
|
console.log(result.message ?? "Rolled back.")
|
|
121
|
+
|
|
122
|
+
if (!opts.noSyncSchema) {
|
|
123
|
+
await offerSchemaRestore(cwd, config, target, result, pgSchema, opts.syncSchema ?? false)
|
|
124
|
+
}
|
|
42
125
|
})
|
|
43
126
|
|
|
44
|
-
// reset — drop all tables and re-apply from scratch
|
|
45
127
|
program
|
|
46
128
|
.command("reset")
|
|
47
129
|
.description(
|
|
@@ -76,12 +158,78 @@ export function registerMigrate(program: Command): void {
|
|
|
76
158
|
})
|
|
77
159
|
}
|
|
78
160
|
|
|
161
|
+
async function offerSchemaRestore(
|
|
162
|
+
cwd: string,
|
|
163
|
+
config: ReturnType<typeof loadConfig>,
|
|
164
|
+
target: ReturnType<typeof resolveTarget>,
|
|
165
|
+
result: SchemaRollbackResult,
|
|
166
|
+
pgSchema: string,
|
|
167
|
+
autoSync: boolean,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
const manifest = result.schemaSourcesManifest as SchemaSourcesManifest | undefined
|
|
170
|
+
const gzB64 = result.schemaSourcesBase64
|
|
171
|
+
|
|
172
|
+
if (!gzB64 || !manifest) {
|
|
173
|
+
console.warn(
|
|
174
|
+
"No schema source snapshot on the restored migration (legacy push). Run `supatype pull` to draft from DB if needed.",
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
180
|
+
const diff = await targetSchemaDiff(target, ast, { schema: pgSchema })
|
|
181
|
+
const drift = (diff.operations ?? []).length > 0
|
|
182
|
+
|
|
183
|
+
if (!drift && !autoSync) {
|
|
184
|
+
console.log("Schema files match reverted database (no restore needed).")
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const fileList = manifest.files.map((f) => f.path).join(", ")
|
|
189
|
+
const sizeKb = (manifest.compressedBytes / 1024).toFixed(1)
|
|
190
|
+
const label = result.restoredMigrationName ?? result.name
|
|
191
|
+
|
|
192
|
+
let proceed = autoSync
|
|
193
|
+
if (!proceed) {
|
|
194
|
+
proceed = await confirm(
|
|
195
|
+
`\nRolled back migration ${result.name}.\nRestore ${manifest.fileCount} schema files from database snapshot (${sizeKb} KB)?\n ${fileList}\n [Y/n] `,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!proceed) {
|
|
200
|
+
console.log("Skipped schema file restore. Run `supatype diff` to review drift.")
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const root = projectRootFromConfig(config, cwd)
|
|
205
|
+
const backupDir = join(cwd, ".supatype", "schema-backups", `${Date.now()}`)
|
|
206
|
+
const gz = Buffer.from(gzB64, "base64")
|
|
207
|
+
restoreSchemaSourcesFromGz(gz, manifest, root, { backupDir })
|
|
208
|
+
|
|
209
|
+
const manifestPaths = new Set(manifest.files.map((f) => f.path))
|
|
210
|
+
const orphans = findOrphanSchemaFiles(root, manifest.entryPoint, manifestPaths)
|
|
211
|
+
for (const orphan of orphans) {
|
|
212
|
+
console.warn(`Warning: ${orphan} not in snapshot — review manually`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`Restored schema files from migration ${label}.`)
|
|
216
|
+
console.log(`Backup saved to ${backupDir}`)
|
|
217
|
+
|
|
218
|
+
const postDiff = await targetSchemaDiff(target, ast, { schema: pgSchema })
|
|
219
|
+
if ((postDiff.operations ?? []).length === 0) {
|
|
220
|
+
console.log("Schema matches database after restore.")
|
|
221
|
+
} else {
|
|
222
|
+
console.log("Run `supatype diff` — schema may still differ from database.")
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
79
226
|
async function confirm(prompt: string): Promise<boolean> {
|
|
80
227
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
81
|
-
return new Promise((
|
|
228
|
+
return new Promise((resolveConfirm) => {
|
|
82
229
|
rl.question(prompt, (answer) => {
|
|
83
230
|
rl.close()
|
|
84
|
-
|
|
231
|
+
const trimmed = answer.trim().toLowerCase()
|
|
232
|
+
resolveConfirm(trimmed === "" || trimmed === "y" || trimmed === "yes")
|
|
85
233
|
})
|
|
86
234
|
})
|
|
87
235
|
}
|
package/src/commands/pull.ts
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
import type { Command } from "commander"
|
|
2
|
-
import {
|
|
2
|
+
import { writeFileSync } from "node:fs"
|
|
3
|
+
import { loadConfig } from "../config.js"
|
|
4
|
+
import { schemaPathFromProject } from "../project-config.js"
|
|
5
|
+
import { ensureEngine, engineRequest } from "../engine-client.js"
|
|
6
|
+
import { resolveHostEngineDatabaseUrl } from "../dev-compose.js"
|
|
7
|
+
import { databaseStateToSchemaScaffold, type DatabaseStateJson } from "../pull-utils.js"
|
|
3
8
|
|
|
4
9
|
export function registerPull(program: Command): void {
|
|
5
10
|
program
|
|
6
11
|
.command("pull")
|
|
7
|
-
.description(
|
|
8
|
-
|
|
9
|
-
)
|
|
10
|
-
.
|
|
12
|
+
.description("Scaffold schema/index.ts from live database introspection (draft — review before push)")
|
|
13
|
+
.option("--connection <url>", "Database connection URL (overrides config)")
|
|
14
|
+
.option("--out <path>", "Write scaffold to file (default: stdout)")
|
|
15
|
+
.option("--dry-run", "Print scaffold to stdout without writing files")
|
|
16
|
+
.action(async (opts: { connection?: string; out?: string; dryRun?: boolean }) => {
|
|
17
|
+
const cwd = process.cwd()
|
|
18
|
+
const config = loadConfig(cwd)
|
|
19
|
+
const connection = await resolveHostEngineDatabaseUrl(cwd, config, opts.connection)
|
|
20
|
+
const pgSchema = config.schema?.pg_schema ?? "public"
|
|
21
|
+
|
|
11
22
|
await ensureEngine()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
|
|
24
|
+
console.error("Introspecting database...")
|
|
25
|
+
const state = await engineRequest<DatabaseStateJson>("/introspect", {
|
|
26
|
+
database_url: connection,
|
|
27
|
+
schema: pgSchema,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const scaffold = databaseStateToSchemaScaffold(state)
|
|
31
|
+
const defaultOut = schemaPathFromProject(config, cwd)
|
|
32
|
+
|
|
33
|
+
if (opts.dryRun || !opts.out) {
|
|
34
|
+
console.log(scaffold)
|
|
35
|
+
if (!opts.dryRun && !opts.out) {
|
|
36
|
+
console.error("\n(draft printed to stdout — use --out to write a file)")
|
|
37
|
+
}
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
writeFileSync(opts.out ?? defaultOut, scaffold, "utf8")
|
|
42
|
+
console.log(`Wrote draft schema to ${opts.out ?? defaultOut}`)
|
|
43
|
+
console.log("Review access rules and relations, then run `supatype generate`.")
|
|
16
44
|
})
|
|
17
45
|
}
|
package/src/commands/push.ts
CHANGED
|
@@ -3,16 +3,29 @@ import { mkdirSync, writeFileSync } from "node:fs"
|
|
|
3
3
|
import { createInterface } from "node:readline"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { printDiffWarnings } from "../diff-output.js"
|
|
6
|
+
import { resolveRuntimeProvider, schemaPathFromProject, serverBaseUrl } from "../project-config.js"
|
|
7
|
+
import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
|
|
8
|
+
import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
|
|
10
9
|
import { signJwt } from "../jwt.js"
|
|
11
|
-
import {
|
|
10
|
+
import { provisionBucketsFromAst } from "../storage-provision.js"
|
|
11
|
+
import type { ExtractedSchemaAstV2 } from "../schema-ast-v2.js"
|
|
12
12
|
import { promptFirstAdminUser } from "./admin.js"
|
|
13
13
|
import { withAdminRoles } from "../studio-admin-roles.js"
|
|
14
14
|
import { restoreSystemRelationTargets } from "../restore-system-relation-targets.js"
|
|
15
15
|
import type { SupatypeProjectConfig } from "../project-config.js"
|
|
16
|
+
import {
|
|
17
|
+
resolveTarget,
|
|
18
|
+
targetSchemaDiff,
|
|
19
|
+
targetSchemaPush,
|
|
20
|
+
schemaPgSchema,
|
|
21
|
+
type DeployTarget,
|
|
22
|
+
} from "../resolve-target.js"
|
|
23
|
+
import { loadProjectLink } from "../link.js"
|
|
24
|
+
import {
|
|
25
|
+
buildSchemaSourcesPayload,
|
|
26
|
+
cacheSchemaSourcesLocally,
|
|
27
|
+
resolvePushedBy,
|
|
28
|
+
} from "../schema-sources.js"
|
|
16
29
|
|
|
17
30
|
const DEV_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"
|
|
18
31
|
|
|
@@ -24,202 +37,155 @@ export function registerPush(program: Command): void {
|
|
|
24
37
|
)
|
|
25
38
|
.option("--yes", "Skip confirmation prompts for destructive changes")
|
|
26
39
|
.option("--connection <url>", "Database connection URL (overrides config)")
|
|
27
|
-
.
|
|
40
|
+
.option("--env <name>", "Target environment when linked")
|
|
41
|
+
.option("--direct", "Use local engine subprocess (skip control plane)")
|
|
42
|
+
.option("--local", "Alias for --direct")
|
|
43
|
+
.action(async (opts: {
|
|
44
|
+
yes?: boolean
|
|
45
|
+
connection?: string
|
|
46
|
+
env?: string
|
|
47
|
+
direct?: boolean
|
|
48
|
+
local?: boolean
|
|
49
|
+
}) => {
|
|
28
50
|
const cwd = process.cwd()
|
|
51
|
+
const config = loadConfig(cwd)
|
|
52
|
+
const pgSchema = schemaPgSchema(cwd)
|
|
53
|
+
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
29
54
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
await
|
|
55
|
+
const linked = loadProjectLink(cwd)
|
|
56
|
+
const useDirect = opts.direct || opts.local || Boolean(opts.connection)
|
|
57
|
+
|
|
58
|
+
if (linked && !useDirect && !opts.connection) {
|
|
59
|
+
const target = resolveTarget(cwd, { env: opts.env })
|
|
60
|
+
await pushViaTarget(cwd, config, target, ast, pgSchema, opts.yes ?? false)
|
|
36
61
|
return
|
|
37
62
|
}
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
if (!opts.connection && !useDirect && resolveRuntimeProvider(config) === "docker") {
|
|
65
|
+
const localTarget = resolveTarget(cwd, { env: opts.env })
|
|
66
|
+
if (localTarget.mode === "local" && localTarget.token) {
|
|
67
|
+
await pushViaTarget(cwd, config, localTarget, ast, pgSchema, opts.yes ?? false)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
45
70
|
const { pushSchemaDocker } = await import("../dev-compose.js")
|
|
46
71
|
await pushSchemaDocker(cwd, config)
|
|
47
72
|
return
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log("Loading schema...")
|
|
55
|
-
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
56
|
-
|
|
57
|
-
console.log("Diffing against database...")
|
|
58
|
-
const diff = await engineRequest<DiffResult>("/diff", {
|
|
59
|
-
ast,
|
|
60
|
-
database_url: connection,
|
|
61
|
-
schema: "public",
|
|
75
|
+
const target = resolveTarget(cwd, {
|
|
76
|
+
env: opts.env,
|
|
77
|
+
direct: true,
|
|
78
|
+
connection: opts.connection,
|
|
62
79
|
})
|
|
63
|
-
|
|
64
|
-
const ops = diff.operations ?? []
|
|
65
|
-
printDiffWarnings(diff)
|
|
66
|
-
|
|
67
|
-
if (ops.length === 0) {
|
|
68
|
-
console.log(
|
|
69
|
-
"Schema matches the database (no DDL). Syncing Studio metadata...",
|
|
70
|
-
)
|
|
71
|
-
} else {
|
|
72
|
-
printDiff(ops)
|
|
73
|
-
|
|
74
|
-
const risky = ops.filter(
|
|
75
|
-
(o) => o.risk === "cautious" || o.risk === "destructive" || o.risk === "warn" || o.risk === "danger",
|
|
76
|
-
)
|
|
77
|
-
if (risky.length > 0 && !opts.yes) {
|
|
78
|
-
const confirmed = await confirm(
|
|
79
|
-
`\n${risky.length} risky operation(s) above (type changes or data loss). Proceed? [y/N] `,
|
|
80
|
-
)
|
|
81
|
-
if (!confirmed) {
|
|
82
|
-
console.log("Aborted.")
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
console.log(ops.length > 0 ? "\nApplying migration..." : "\nSyncing with engine...")
|
|
89
|
-
const pushResult = await engineRequest<{
|
|
90
|
-
message?: string
|
|
91
|
-
status?: string
|
|
92
|
-
admin_refreshed?: boolean
|
|
93
|
-
}>("/push", {
|
|
94
|
-
ast,
|
|
95
|
-
database_url: connection,
|
|
96
|
-
schema: "public",
|
|
97
|
-
force: true,
|
|
98
|
-
})
|
|
99
|
-
if (pushResult.status === "up_to_date") {
|
|
100
|
-
console.log(
|
|
101
|
-
pushResult.admin_refreshed
|
|
102
|
-
? "Database schema unchanged — Studio metadata synced."
|
|
103
|
-
: "Schema is up to date.",
|
|
104
|
-
)
|
|
105
|
-
} else {
|
|
106
|
-
console.log(pushResult.message ?? "Migration applied.")
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
await writeLocalAdminConfig(ast, config)
|
|
110
|
-
|
|
111
|
-
// After a DDL migration, check if this is the first push and offer to create an
|
|
112
|
-
// admin user if none exist (Gap Appendices task 48).
|
|
113
|
-
if (ops.length > 0) {
|
|
114
|
-
await promptFirstAdminUser(connection)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Provision storage buckets declared in the schema.
|
|
118
|
-
const baseUrl = serverBaseUrl(config)
|
|
119
|
-
const serviceRoleKey =
|
|
120
|
-
process.env["SUPATYPE_SERVICE_ROLE_KEY"] ??
|
|
121
|
-
(config.server.mode === "dev"
|
|
122
|
-
? signJwt({ role: "service_role", iss: "supatype", iat: Math.floor(Date.now() / 1000) }, DEV_JWT_SECRET)
|
|
123
|
-
: undefined)
|
|
124
|
-
|
|
125
|
-
if (baseUrl && serviceRoleKey) {
|
|
126
|
-
const parsedAst = await engineRequest<{
|
|
127
|
-
storageBuckets?: Array<{
|
|
128
|
-
id: string
|
|
129
|
-
public: boolean
|
|
130
|
-
accessMode?: "public" | "private" | "custom"
|
|
131
|
-
allowedMimeTypes?: string[]
|
|
132
|
-
fileSizeLimit?: number
|
|
133
|
-
s3BucketPolicy?: string
|
|
134
|
-
}>
|
|
135
|
-
}>("/parse", { ast })
|
|
136
|
-
const buckets = (parsedAst.storageBuckets ?? []).map((b) => ({
|
|
137
|
-
id: b.id,
|
|
138
|
-
public: b.public,
|
|
139
|
-
...(b.accessMode !== undefined && { access_mode: b.accessMode }),
|
|
140
|
-
...(b.allowedMimeTypes != null && { allowed_mime_types: b.allowedMimeTypes }),
|
|
141
|
-
...(b.fileSizeLimit != null && { file_size_limit: b.fileSizeLimit }),
|
|
142
|
-
...(b.s3BucketPolicy != null &&
|
|
143
|
-
b.s3BucketPolicy !== "" && { s3_bucket_policy: b.s3BucketPolicy }),
|
|
144
|
-
}))
|
|
145
|
-
if (buckets.length > 0) {
|
|
146
|
-
console.log("Provisioning storage buckets...")
|
|
147
|
-
await provisionBuckets(`${baseUrl}/storage/v1`, serviceRoleKey, buckets)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (config.output?.types ?? config.output?.client) {
|
|
152
|
-
console.log("Generating types...")
|
|
153
|
-
const genBody: Record<string, unknown> = { ast, lang: "typescript" }
|
|
154
|
-
if (config.output?.types) genBody["types_path"] = config.output.types
|
|
155
|
-
if (config.output?.client) genBody["client_path"] = config.output.client
|
|
156
|
-
|
|
157
|
-
const genResult = await engineRequest<{ code?: string; message?: string }>("/generate", genBody)
|
|
158
|
-
console.log(genResult.message ?? "Types generated.")
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const studioBase = baseUrl?.replace(/\/$/, "") ?? ""
|
|
162
|
-
if (studioBase) {
|
|
163
|
-
console.log(`\nStudio: ${studioBase}/studio/ — sign in with the admin user you created.`)
|
|
164
|
-
} else {
|
|
165
|
-
console.log("\nDone.")
|
|
166
|
-
}
|
|
80
|
+
await pushViaTarget(cwd, config, target, ast, pgSchema, opts.yes ?? false)
|
|
167
81
|
})
|
|
168
82
|
}
|
|
169
83
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
console.log(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
84
|
+
async function pushViaTarget(
|
|
85
|
+
cwd: string,
|
|
86
|
+
config: SupatypeProjectConfig,
|
|
87
|
+
target: DeployTarget,
|
|
88
|
+
ast: unknown,
|
|
89
|
+
pgSchema: string,
|
|
90
|
+
skipConfirm: boolean,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
console.log("Diffing against database...")
|
|
93
|
+
const diff = await targetSchemaDiff(target, ast, { schema: pgSchema })
|
|
94
|
+
const ops = diff.operations ?? []
|
|
95
|
+
printDiffWarnings(diff)
|
|
96
|
+
|
|
97
|
+
if (ops.length === 0) {
|
|
98
|
+
console.log("Schema matches the database (no DDL). Syncing Studio metadata...")
|
|
99
|
+
} else {
|
|
100
|
+
printDiffOperations({ operations: ops })
|
|
101
|
+
const risky = ops.filter(
|
|
102
|
+
(o) => o.risk === "cautious" || o.risk === "destructive" || o.risk === "warn" || o.risk === "danger",
|
|
103
|
+
)
|
|
104
|
+
if (risky.length > 0 && !skipConfirm) {
|
|
105
|
+
const confirmed = await confirm(
|
|
106
|
+
`\n${risky.length} risky operation(s) above. Proceed? [y/N] `,
|
|
107
|
+
)
|
|
108
|
+
if (!confirmed) {
|
|
109
|
+
console.log("Aborted.")
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
}
|
|
184
113
|
}
|
|
185
|
-
}
|
|
186
114
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
115
|
+
console.log(ops.length > 0 ? "\nApplying migration..." : "\nSyncing with engine...")
|
|
116
|
+
const schemaSources = buildSchemaSourcesPayload(cwd, resolvePushedBy())
|
|
117
|
+
const pushResult = await targetSchemaPush(target, ast, {
|
|
118
|
+
force: true,
|
|
119
|
+
schema: pgSchema,
|
|
120
|
+
schemaSources,
|
|
121
|
+
})
|
|
122
|
+
if ((pushResult as { status?: string }).status === "up_to_date") {
|
|
123
|
+
console.log("Schema is up to date.")
|
|
124
|
+
} else {
|
|
125
|
+
console.log((pushResult as { message?: string }).message ?? "Migration applied.")
|
|
126
|
+
const migrationName = (pushResult as { name?: string }).name
|
|
127
|
+
if (migrationName && schemaSources) {
|
|
128
|
+
cacheSchemaSourcesLocally(cwd, migrationName, schemaSources.gz)
|
|
129
|
+
}
|
|
190
130
|
}
|
|
191
131
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
132
|
+
if (target.mode === "direct" || target.mode === "local") {
|
|
133
|
+
await writeLocalAdminConfig(ast, config)
|
|
134
|
+
if (ops.length > 0 && target.databaseUrl) {
|
|
135
|
+
await promptFirstAdminUser(target.databaseUrl)
|
|
136
|
+
}
|
|
137
|
+
await generateTypesLocal(ast, config)
|
|
138
|
+
await provisionLocalStorage(ast, config)
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`Pushed to ${target.mode} (${target.environment}).`)
|
|
199
141
|
}
|
|
200
|
-
|
|
201
|
-
|
|
142
|
+
|
|
143
|
+
const baseUrl = (serverBaseUrl(config) ?? "").replace(/\/$/, "")
|
|
144
|
+
if (baseUrl) {
|
|
145
|
+
console.log(`\nStudio: ${baseUrl}/studio/`)
|
|
202
146
|
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function generateTypesLocal(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
|
|
150
|
+
if (!config.output?.types && !config.output?.client) return
|
|
151
|
+
console.log("Generating types...")
|
|
152
|
+
await ensureEngine()
|
|
153
|
+
const genBody: Record<string, unknown> = { ast, lang: "typescript" }
|
|
154
|
+
if (config.output?.types) genBody["types_path"] = config.output.types
|
|
155
|
+
if (config.output?.client) genBody["client_path"] = config.output.client
|
|
156
|
+
const genResult = await engineRequest<{ message?: string }>("/generate", genBody)
|
|
157
|
+
console.log(genResult.message ?? "Types generated.")
|
|
158
|
+
}
|
|
203
159
|
|
|
204
|
-
|
|
205
|
-
|
|
160
|
+
async function provisionLocalStorage(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
|
|
161
|
+
const baseUrl = serverBaseUrl(config)
|
|
162
|
+
const serviceRoleKey =
|
|
163
|
+
process.env["SUPATYPE_SERVICE_ROLE_KEY"] ??
|
|
164
|
+
process.env["SERVICE_ROLE_KEY"] ??
|
|
165
|
+
(config.server.mode === "dev"
|
|
166
|
+
? signJwt({ role: "service_role", iss: "supatype", iat: Math.floor(Date.now() / 1000) }, DEV_JWT_SECRET)
|
|
167
|
+
: undefined)
|
|
168
|
+
if (!baseUrl || !serviceRoleKey) return
|
|
169
|
+
await ensureEngine()
|
|
170
|
+
const parsedAst = await engineRequest<Pick<ExtractedSchemaAstV2, "storageBuckets">>("/parse", { ast })
|
|
171
|
+
await provisionBucketsFromAst(parsedAst, `${baseUrl}/storage/v1`, serviceRoleKey)
|
|
206
172
|
}
|
|
207
173
|
|
|
208
174
|
async function confirm(prompt: string): Promise<boolean> {
|
|
209
175
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
210
|
-
return new Promise((
|
|
176
|
+
return new Promise((resolveConfirm) => {
|
|
211
177
|
rl.question(prompt, (answer) => {
|
|
212
178
|
rl.close()
|
|
213
|
-
|
|
179
|
+
resolveConfirm(answer.toLowerCase() === "y")
|
|
214
180
|
})
|
|
215
181
|
})
|
|
216
182
|
}
|
|
217
183
|
|
|
218
|
-
/** Write `.supatype/admin-config.json` for local Studio (same layout as `supatype dev`). */
|
|
219
184
|
async function writeLocalAdminConfig(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
|
|
220
185
|
const cwd = process.cwd()
|
|
221
186
|
const dir = join(cwd, ".supatype")
|
|
222
187
|
mkdirSync(dir, { recursive: true })
|
|
188
|
+
await ensureEngine()
|
|
223
189
|
const admin = withAdminRoles(await engineRequest<unknown>("/admin", { ast }), config)
|
|
224
190
|
restoreSystemRelationTargets(admin, ast)
|
|
225
191
|
writeFileSync(join(dir, "admin-config.json"), `${JSON.stringify(admin, null, 2)}\n`)
|