Rhys Sullivanremove v3 workinged7a3488 months agoHistory
206 lines · 6.1 KB · TypeScript
RawDownload
import { PGlite } from "@electric-sql/pglite";import { drizzle as drizzlePGlite } from "drizzle-orm/pglite";import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator";import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";import { existsSync, readdirSync, readFileSync } from "node:fs";import { mkdir, unlink } from "node:fs/promises";import * as path from "node:path";import { fileURLToPath } from "node:url";import postgres from "postgres"; import { drizzleSchema } from "./schema"; export type SqlBackend = "pglite" | "postgres"; export type CreateSqlRuntimeOptions = {  databaseUrl?: string;  localDataDir?: string;  postgresApplicationName?: string;  migrationsFolder?: string;}; const createPGliteDb = (client: PGlite) =>  drizzlePGlite({ client, schema: drizzleSchema }); const createPostgresDb = (client: postgres.Sql) =>  drizzlePostgres({ client, schema: drizzleSchema }); export type PGliteDb = ReturnType<typeof createPGliteDb>;export type PostgresDb = ReturnType<typeof createPostgresDb>;export type DrizzleDb = PGliteDb | PostgresDb; export type SqlRuntime = {  backend: SqlBackend;  db: DrizzleDb;  close: () => Promise<void>;}; const trim = (value: string | undefined): string | undefined => {  const candidate = value?.trim();  return candidate && candidate.length > 0 ? candidate : undefined;}; const isPostgresUrl = (value: string): boolean =>  value.startsWith("postgres://") || value.startsWith("postgresql://"); /** * Remove a stale PGlite `postmaster.pid` lock file if present. * * PGlite writes this file on open and removes it on close. When a process * exits without calling `PGlite.close()` (e.g. the Vite dev server is * killed with SIGKILL, or the terminal is closed), the lock file is left * behind and prevents subsequent PGlite instances from opening the * database. PGlite uses a synthetic PID (e.g. `-42`) that is never a real * OS process, so we cannot use it to detect a live owner. Instead we rely * on the fact that in the local single-user context only one process * should ever own this database at a time. */const cleanupStalePGliteLock = async (dataDir: string): Promise<void> => {  const lockPath = path.join(dataDir, "postmaster.pid");  if (!existsSync(lockPath)) {    return;  }   // PGlite's synthetic PID is always negative (e.g. -42). A real Postgres  // postmaster would write a positive PID. If we ever see a positive PID  // we leave the file alone.  try {    const content = readFileSync(lockPath, "utf-8");    const firstLine = content.split("\n")[0]?.trim();    const pid = Number(firstLine);    if (!Number.isNaN(pid) && pid > 0) {      // Looks like a real Postgres PID — don't touch it.      return;    }  } catch {    // If we can't read the file, try to remove it anyway.  }   try {    await unlink(lockPath);  } catch {    // Best-effort cleanup; PGlite will report the real error if this fails.  }}; const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => {  const normalized = trim(localDataDir) ?? ".executor/control-plane-pgdata";   let client: PGlite;  if (normalized === ":memory:") {    client = new PGlite();  } else {    const resolvedDataDir = path.resolve(normalized);    if (!existsSync(resolvedDataDir)) {      await mkdir(resolvedDataDir, { recursive: true });    }    await cleanupStalePGliteLock(resolvedDataDir);    client = new PGlite(resolvedDataDir);  }   const db = createPGliteDb(client);   return {    backend: "pglite",    db,    close: async () => {      await client.close();    },  };}; const createPostgresRuntime = async (  databaseUrl: string,  applicationName: string | undefined,): Promise<SqlRuntime> => {  const client = postgres(databaseUrl, {    prepare: false,    max: 10,    ...(applicationName      ? { connection: { application_name: applicationName } }      : {}),  });  const db = createPostgresDb(client);   return {    backend: "postgres",    db,    close: async () => {      await client.end({ timeout: 5 });    },  };}; const resolveMigrationsFolder = (explicit: string | undefined): string => {  const explicitTrimmed = trim(explicit);  if (explicitTrimmed) {    return path.resolve(explicitTrimmed);  }   const moduleDir = path.dirname(fileURLToPath(import.meta.url));  const packageRoot = path.resolve(moduleDir, "../..");  const cwd = process.cwd();  const candidates = [    path.resolve(packageRoot, "drizzle"),    path.resolve(cwd, "packages/control-plane/drizzle"),    path.resolve(cwd, "drizzle"),  ];   const hasModernMigrations = (candidate: string): boolean => {    if (!existsSync(candidate)) {      return false;    }     const entries = readdirSync(candidate, { withFileTypes: true });    return entries.some(      (entry) =>        entry.isDirectory()        && existsSync(path.join(candidate, entry.name, "migration.sql")),    );  };   for (const candidate of candidates) {    if (      existsSync(path.join(candidate, "meta", "_journal.json"))      || hasModernMigrations(candidate)    ) {      return candidate;    }  }   throw new Error(    "Unable to resolve Drizzle migrations folder for control-plane persistence",  );}; export const runMigrations = async (  runtime: SqlRuntime,  options?: { migrationsFolder?: string },): Promise<void> => {  const migrationsFolder = resolveMigrationsFolder(options?.migrationsFolder);   if (runtime.backend === "pglite") {    await migratePGlite(runtime.db as PGliteDb, { migrationsFolder });    return;  }   await migratePostgres(runtime.db as PostgresDb, { migrationsFolder });}; export const createSqlRuntime = async (  options: CreateSqlRuntimeOptions,): Promise<SqlRuntime> => {  const databaseUrl = trim(options.databaseUrl);  const runtime =    databaseUrl && isPostgresUrl(databaseUrl)      ? await createPostgresRuntime(          databaseUrl,          trim(options.postgresApplicationName),        )      : await createPGliteRuntime(          options.localDataDir ?? ".executor/control-plane-pgdata",        );   return runtime;};