2026-05-14 15:33:45 +07:00
|
|
|
import Database from "better-sqlite3";
|
2026-05-14 15:47:03 +07:00
|
|
|
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
|
|
|
|
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
|
2026-05-14 15:33:45 +07:00
|
|
|
import { Pool } from "pg";
|
2026-05-21 12:03:31 +00:00
|
|
|
import { config } from "../config.js";
|
|
|
|
|
import { createChildLogger } from "../logger.js";
|
|
|
|
|
import * as schema from "./schema.js";
|
2026-05-14 15:33:45 +07:00
|
|
|
|
|
|
|
|
const logger = createChildLogger("drizzle");
|
|
|
|
|
|
2026-05-14 15:47:03 +07:00
|
|
|
let db:
|
|
|
|
|
| ReturnType<typeof drizzlePostgres>
|
|
|
|
|
| ReturnType<typeof drizzleSqlite>
|
|
|
|
|
| null = null;
|
2026-05-30 20:40:33 +07:00
|
|
|
let rawSqlite: ReturnType<typeof Database> | null = null;
|
|
|
|
|
let rawPool: Pool | null = null;
|
2026-05-14 15:33:45 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize the database connection based on DATABASE_TYPE config
|
|
|
|
|
* Supports both PostgreSQL and SQLite
|
|
|
|
|
*/
|
|
|
|
|
export async function initializeDatabase() {
|
|
|
|
|
if (db !== null) {
|
|
|
|
|
return db;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 18:24:10 +07:00
|
|
|
// During tests prefer an isolated SQLite instance to avoid using shared
|
|
|
|
|
// external Postgres instances which can lead to flaky test interference.
|
|
|
|
|
const usePostgres =
|
|
|
|
|
config.DATABASE_TYPE === "postgres" && process.env.NODE_ENV !== "test";
|
|
|
|
|
|
|
|
|
|
if (usePostgres) {
|
2026-05-14 16:25:39 +07:00
|
|
|
let pool: Pool;
|
|
|
|
|
|
|
|
|
|
// Use DATABASE_URL if available, otherwise build from individual variables
|
|
|
|
|
if (config.DATABASE_URL) {
|
|
|
|
|
pool = new Pool({
|
|
|
|
|
connectionString: config.DATABASE_URL,
|
|
|
|
|
min: config.POSTGRES_POOL_MIN,
|
|
|
|
|
max: config.POSTGRES_POOL_MAX,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
pool = new Pool({
|
|
|
|
|
host: config.POSTGRES_HOST,
|
|
|
|
|
port: config.POSTGRES_PORT,
|
|
|
|
|
user: config.POSTGRES_USER,
|
|
|
|
|
password: config.POSTGRES_PASSWORD,
|
|
|
|
|
database: config.POSTGRES_DB,
|
|
|
|
|
min: config.POSTGRES_POOL_MIN,
|
|
|
|
|
max: config.POSTGRES_POOL_MAX,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-14 15:33:45 +07:00
|
|
|
|
2026-05-30 20:40:33 +07:00
|
|
|
rawPool = pool;
|
2026-05-14 15:33:45 +07:00
|
|
|
db = drizzlePostgres(pool, { schema });
|
2026-05-17 18:24:10 +07:00
|
|
|
// Provide a simple `run` helper for tests that expect it.
|
|
|
|
|
try {
|
|
|
|
|
(db as any).run = (sql: string) => pool.query(sql);
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
2026-05-14 15:33:45 +07:00
|
|
|
logger.info("PostgreSQL database initialized");
|
|
|
|
|
} else {
|
|
|
|
|
const sqlite = new Database(".muxer-queue.db");
|
|
|
|
|
sqlite.pragma("journal_mode = WAL");
|
|
|
|
|
|
2026-05-30 20:40:33 +07:00
|
|
|
rawSqlite = sqlite;
|
2026-05-14 15:33:45 +07:00
|
|
|
db = drizzleSqlite(sqlite, { schema });
|
2026-05-17 18:24:10 +07:00
|
|
|
// Expose a convenience `run` method used by tests that expect a simple API.
|
|
|
|
|
// `sqlite` is the underlying better-sqlite3 Database instance.
|
|
|
|
|
try {
|
|
|
|
|
(db as any).run = (sql: string) => sqlite.exec(sql);
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
2026-05-14 15:33:45 +07:00
|
|
|
logger.info("SQLite database initialized");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the initialized database instance
|
|
|
|
|
* Throws if database has not been initialized
|
|
|
|
|
*/
|
|
|
|
|
export function getDatabase() {
|
|
|
|
|
if (db === null) {
|
|
|
|
|
throw new Error(
|
2026-05-14 15:47:03 +07:00
|
|
|
"Database not initialized. Call initializeDatabase() first.",
|
2026-05-14 15:33:45 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return db;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 20:40:33 +07:00
|
|
|
function convertPlaceholdersForPostgres(sql: string) {
|
|
|
|
|
let i = 0;
|
|
|
|
|
return sql.replace(/\?/g, () => `$${++i}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function executeAll(sql: string, params?: any[]) {
|
|
|
|
|
if (rawPool) {
|
|
|
|
|
const q = convertPlaceholdersForPostgres(sql);
|
|
|
|
|
const res = await rawPool.query(q, params || []);
|
|
|
|
|
return res.rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rawSqlite) {
|
|
|
|
|
const stmt = rawSqlite.prepare(sql);
|
|
|
|
|
return stmt.all(...(params || []));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error("Database not initialized. Call initializeDatabase() first.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function executeGet(sql: string, params?: any[]) {
|
|
|
|
|
if (rawPool) {
|
|
|
|
|
const q = convertPlaceholdersForPostgres(sql);
|
|
|
|
|
const res = await rawPool.query(q, params || []);
|
|
|
|
|
return res.rows[0] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rawSqlite) {
|
|
|
|
|
const stmt = rawSqlite.prepare(sql);
|
|
|
|
|
return stmt.get(...(params || []));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error("Database not initialized. Call initializeDatabase() first.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 15:33:45 +07:00
|
|
|
/**
|
|
|
|
|
* Close the database connection
|
|
|
|
|
* For PostgreSQL, the pool will close on process exit
|
|
|
|
|
* For SQLite, closes the database connection
|
|
|
|
|
*/
|
|
|
|
|
export async function closeDatabase() {
|
|
|
|
|
if (db === null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.DATABASE_TYPE === "postgres") {
|
|
|
|
|
logger.info("PostgreSQL connection pool will close on process exit");
|
|
|
|
|
} else {
|
|
|
|
|
logger.info("SQLite database closed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db = null;
|
|
|
|
|
}
|