import { PGlite } from '@electric-sql/pglite' import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres' import { migrate as migratePg } from 'drizzle-orm/node-postgres/migrator' import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core' import { drizzle as drizzlePglite } from 'drizzle-orm/pglite' import { migrate as migratePglite } from 'drizzle-orm/pglite/migrator' import { mkdirSync } from 'node:fs' import { join } from 'node:path' import pg from 'pg' import * as schema from './schema.ts' export type Db = PgDatabase export interface Database { db: Db close(): Promise } const migrationsFolder = join(import.meta.dirname, 'migrations') /** * Opens the database and applies pending migrations. * * - `postgres://…` connects to PostgreSQL (production). * - `pglite://` uses embedded PostgreSQL stored in `` (local development, very small installs). * - `memory://` uses a throwaway in-memory database (tests). */ export async function openDatabase(url: string): Promise { if (/^postgres(ql)?:\/\//.test(url)) { const pool = new pg.Pool({ connectionString: url }) const db = drizzlePg({ client: pool, schema, casing: 'snake_case' }) await migratePg(db, { migrationsFolder }) return { db: db as unknown as Db, close: () => pool.end() } } if (url !== 'memory://' && !url.startsWith('pglite://')) { throw new Error(`Unsupported DATABASE_URL: expected postgres://, pglite:// or memory://`) } const dataDir = url === 'memory://' ? undefined : url.slice('pglite://'.length) if (dataDir) mkdirSync(dataDir, { recursive: true }) const client = new PGlite(dataDir) const db = drizzlePglite({ client, schema, casing: 'snake_case' }) await migratePglite(db, { migrationsFolder }) return { db: db as unknown as Db, close: () => client.close() } } /** True for a unique-constraint violation from either driver (Drizzle wraps driver errors in `cause`). */ export function isUniqueViolation(error: unknown): boolean { let current: unknown = error for (let depth = 0; current && typeof current === 'object' && depth < 5; depth++) { if ((current as { code?: unknown }).code === '23505') return true current = (current as { cause?: unknown }).cause } return false }