everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
130 lines (128 loc) • 4.52 kB
JavaScript
import { extractExpectedTables, pluginMigrationSlug } from "../db.mjs";
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { createHash } from "node:crypto";
//#region src/cli/db-doctor.ts
function hashFile(raw) {
return createHash("sha256").update(raw).digest("hex");
}
function readLocalMigrations(workspaceDir) {
const migrationsDir = resolve(workspaceDir, "src/db/migrations");
const journalPath = join(join(migrationsDir, "meta"), "_journal.json");
if (!existsSync(journalPath)) return [];
return JSON.parse(readFileSync(journalPath, "utf8")).entries.map((entry) => {
const sqlPath = join(migrationsDir, `${entry.tag}.sql`);
if (!existsSync(sqlPath)) return {
tag: entry.tag,
hash: "",
sql: []
};
const raw = readFileSync(sqlPath, "utf8");
const sql = raw.split("--> statement-breakpoint").map((s) => s.trim());
return {
tag: entry.tag,
hash: hashFile(raw),
sql
};
});
}
async function diagnosePlugin(info) {
const { Pool } = await import("../node_modules/pg/esm/index.mjs");
const slug = pluginMigrationSlug(info.key);
const journalTable = `__drizzle_migrations_${slug}`;
const journalSchema = "drizzle";
const journalRef = `"${journalSchema}"."${journalTable}"`;
const localMigrations = info.workspaceDir ? readLocalMigrations(info.workspaceDir) : [];
const expectedTables = extractExpectedTables(localMigrations);
const localHashes = localMigrations.map((m) => m.hash).filter(Boolean);
const migrationHashes = localHashes;
const pool = new Pool({
connectionString: info.databaseUrl,
ssl: info.databaseUrl.includes("localhost") || info.databaseUrl.includes("127.0.0.1") ? false : { rejectUnauthorized: false },
max: 1,
connectionTimeoutMillis: 1e4
});
try {
const journalExists = await pool.query(`
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = '${journalSchema}' AND table_name = '${journalTable}'
) AS exists
`);
let appliedHashCount = 0;
if (journalExists.rows[0]?.exists) appliedHashCount = (await pool.query(`SELECT hash FROM ${journalRef}`)).rows.length;
let missingTables = [];
if (expectedTables.length > 0) {
const tableResult = await pool.query(`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ANY($1)
`, [expectedTables]);
const existing = new Set(tableResult.rows.map((r) => r.table_name));
missingTables = expectedTables.filter((t) => !existing.has(t));
}
const legacyCount = await countLegacyHashes(pool, localHashes);
let diagnosis;
if (localMigrations.length === 0) diagnosis = "no-local-migrations";
else if (appliedHashCount === 0 && localHashes.length > 0) diagnosis = legacyCount > 0 ? "legacy-importable" : "unapplied";
else if (missingTables.length === 0) diagnosis = "healthy";
else if (missingTables.length === expectedTables.length) diagnosis = "drift-safe-repair";
else diagnosis = "drift-manual";
const masked = info.databaseUrl.replace(/\/\/[^:]+:[^@]+@/, "//***:***@");
return {
plugin: info.key,
slug,
journalTable,
journalSchema,
dbSecret: info.databaseSecret,
dbUrl: masked,
workspaceDir: info.workspaceDir,
localMigrationCount: localMigrations.length,
appliedHashCount,
expectedTables,
missingTables,
migrationHashes,
diagnosis,
legacyCount
};
} catch (error) {
return {
plugin: info.key,
slug,
journalTable,
journalSchema,
dbSecret: info.databaseSecret,
dbUrl: info.databaseUrl.replace(/\/\/[^:]+:[^@]+@/, "//***:***@"),
workspaceDir: info.workspaceDir,
localMigrationCount: localMigrations.length,
appliedHashCount: 0,
expectedTables,
missingTables: [],
migrationHashes: [],
diagnosis: "error",
legacyCount: 0,
error: error instanceof Error ? error.message : String(error)
};
} finally {
await pool.end().catch(() => {});
}
}
async function countLegacyHashes(pool, localHashes) {
if (localHashes.length === 0) return 0;
const candidates = [{
schema: "drizzle",
table: "__drizzle_migrations"
}, {
schema: "public",
table: "drizzle_migrations"
}];
let total = 0;
for (const c of candidates) try {
const result = await pool.query(`SELECT count(*)::int AS cnt FROM "${c.schema}"."${c.table}" WHERE hash = ANY($1)`, [localHashes]);
total += result.rows[0]?.cnt ?? 0;
} catch {}
return total;
}
//#endregion
export { diagnosePlugin };
//# sourceMappingURL=db-doctor.mjs.map