90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
import { execFile } from "child_process";
|
|
import { Connection, getConnection } from "./db";
|
|
import { connectionUrl, execSql } from "./sling";
|
|
import { resolveBinary } from "./binaries";
|
|
|
|
// Atlas needs an empty scratch database ("dev database") to plan diffs.
|
|
// The usual `docker://mysql/8` dev-url spins up a throwaway container, but
|
|
// its dial to host.docker.internal is broken on some Docker runtimes (e.g.
|
|
// OrbStack: connections establish, then the handshake EOFs). Use a dedicated
|
|
// scratch schema on the target server instead — no Docker needed.
|
|
const DEV_SCHEMA = "sling_ui_atlas_dev";
|
|
|
|
function devDatabaseUrl(tgt: Connection): string {
|
|
const user = encodeURIComponent(tgt.user);
|
|
const pass = encodeURIComponent(tgt.password);
|
|
return `mysql://${user}:${pass}@${tgt.host}:${tgt.port}/${DEV_SCHEMA}`;
|
|
}
|
|
|
|
// Create the scratch schema if missing and return its URL. Atlas keeps it
|
|
// clean after each run.
|
|
export function prepareDevDatabase(tgt: Connection): string {
|
|
execSql(tgt, `CREATE DATABASE IF NOT EXISTS \`${DEV_SCHEMA}\``);
|
|
return devDatabaseUrl(tgt);
|
|
}
|
|
|
|
function baseApplyArgs(srcUrl: string, tgtUrl: string, devUrl: string): string[] {
|
|
return [
|
|
"schema",
|
|
"apply",
|
|
"--url",
|
|
tgtUrl,
|
|
"--to",
|
|
srcUrl,
|
|
"--dev-url",
|
|
devUrl,
|
|
];
|
|
}
|
|
|
|
export function buildApplyArgs(
|
|
srcUrl: string,
|
|
tgtUrl: string,
|
|
devUrl: string
|
|
): string[] {
|
|
return [...baseApplyArgs(srcUrl, tgtUrl, devUrl), "--auto-approve"];
|
|
}
|
|
|
|
// atlas rejects --dry-run combined with --auto-approve.
|
|
export function buildDryRunArgs(
|
|
srcUrl: string,
|
|
tgtUrl: string,
|
|
devUrl: string
|
|
): string[] {
|
|
return [...baseApplyArgs(srcUrl, tgtUrl, devUrl), "--dry-run"];
|
|
}
|
|
|
|
function atlasBin(): string {
|
|
return resolveBinary("atlas") ?? "atlas";
|
|
}
|
|
|
|
export function schemaDiff(
|
|
sourceConnId: number,
|
|
targetConnId: number
|
|
): Promise<{ ok: boolean; sql: string }> {
|
|
const src = getConnection(sourceConnId);
|
|
const tgt = getConnection(targetConnId);
|
|
if (!src || !tgt) throw new Error("connection not found");
|
|
let devUrl: string;
|
|
try {
|
|
devUrl = prepareDevDatabase(tgt);
|
|
} catch (e) {
|
|
const err = e as { stdout?: string; stderr?: string; message?: string };
|
|
return Promise.resolve({
|
|
ok: false,
|
|
sql: [err.stdout, err.stderr, err.message].filter(Boolean).join("\n"),
|
|
});
|
|
}
|
|
const args = buildDryRunArgs(connectionUrl(src), connectionUrl(tgt), devUrl);
|
|
return new Promise((resolve) => {
|
|
execFile(
|
|
/* turbopackIgnore: true */ atlasBin(),
|
|
args,
|
|
{ timeout: 120000, maxBuffer: 32 * 1024 * 1024 },
|
|
(error, stdout, stderr) => {
|
|
const sql = [stdout, stderr].filter(Boolean).join("\n");
|
|
resolve({ ok: !error, sql });
|
|
}
|
|
);
|
|
});
|
|
}
|