sing-ui/lib/atlas.ts

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 });
}
);
});
}