openreader/src/lib/server/db-adapter.ts
Richard R 4689f3676f refactor(auth): consolidate authentication contexts and enhance rate limit UI integration
Streamline auth client factory and remove redundant session manager
Introduce unified auto rate limit context for seamless status updates
Implement problem details in TTS responses for improved error clarity
Add rate limit pause controls and banners across document viewers
Refine PDF scaling logic with height-aware resize handling
Update privacy popup triggers and first-visit modal behavior

BREAKING CHANGE: Daily character limits adjusted (anonymous: 250K → 50K, authenticated: 1M → 500K)
2026-01-25 11:12:19 -07:00

143 lines
4.4 KiB
TypeScript

import { Pool, PoolClient } from "pg";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
/**
* Common interface for database operations to support both Postgres and SQLite
*/
export interface DBAdapter {
query(text: string, params?: unknown[]): Promise<{ rows: unknown[], rowCount?: number }>;
transaction<T>(callback: (client: DBAdapter) => Promise<T>): Promise<T>;
}
export class PostgresAdapter implements DBAdapter {
constructor(private pool: Pool) { }
async query(text: string, params?: unknown[]) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await this.pool.query(text, params as any[]);
return {
rows: result.rows,
rowCount: result.rowCount ?? undefined
};
} catch (error) {
console.error("Postgres Query Error:", error);
throw error;
}
}
async transaction<T>(callback: (client: DBAdapter) => Promise<T>): Promise<T> {
const client = await this.pool.connect();
try {
await client.query("BEGIN");
const connectedAdapter = new PostgresConnectedAdapter(client);
const result = await callback(connectedAdapter);
await client.query("COMMIT");
return result;
} catch (e) {
await client.query("ROLLBACK");
throw e;
} finally {
client.release();
}
}
}
class PostgresConnectedAdapter implements DBAdapter {
constructor(private client: PoolClient) { }
async query(text: string, params?: unknown[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await this.client.query(text, params as any[]);
return {
rows: result.rows,
rowCount: result.rowCount ?? undefined
};
}
async transaction<T>(callback: (client: DBAdapter) => Promise<T>): Promise<T> {
// Nested transactions not explicitly supported, just pass through
return callback(this);
}
}
export class SqliteAdapter implements DBAdapter {
private db: Database.Database;
constructor(filePath: string) {
// Ensure directory exists
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
try {
fs.mkdirSync(dir, { recursive: true });
console.log(`Created directory for SQLite: ${dir}`);
} catch (e) {
console.error(`Failed to create directory ${dir}:`, e);
}
}
this.db = new Database(filePath);
// Enable WAL mode for better concurrency
this.db.pragma('journal_mode = WAL');
}
async query(text: string, params?: unknown[]) {
// Convert Postgres $n params to SQLite placeholders.
// We separately reorder params to match placeholder order.
const convertedSql = text.replace(/\$(\d+)/g, "?");
// Reorder params array to match the order they appear in the SQL
// SQLite expects params in the order they appear, not by their $n number
const orderedParams: unknown[] = [];
if (params && params.length > 0) {
// Extract parameter numbers in order of appearance
const paramNumbers: number[] = [];
const regex = /\$(\d+)/g;
let match;
while ((match = regex.exec(text)) !== null) {
paramNumbers.push(parseInt(match[1], 10));
}
// Map to actual values
for (const num of paramNumbers) {
orderedParams.push(params[num - 1]); // $1 maps to params[0]
}
}
try {
const stmt = this.db.prepare(convertedSql);
const safeParams = orderedParams.length > 0 ? orderedParams : [];
const lowerSql = convertedSql.trim().toLowerCase();
// If it's a SELECT or RETURNING, we use .all() or .get()
if (lowerSql.startsWith("select") || /returning\s+/.test(lowerSql)) {
const rows = stmt.all(...safeParams);
return { rows, rowCount: rows.length };
} else {
// INSERT/UPDATE/DELETE with no RETURNING
const info = stmt.run(...safeParams);
return { rows: [], rowCount: info.changes };
}
} catch (error) {
console.error("SQLite Query Error:", error);
throw error;
}
}
async transaction<T>(callback: (client: DBAdapter) => Promise<T>): Promise<T> {
// Manual transaction management
// better-sqlite3 is synchronous, but we simulate async interface
this.db.exec("BEGIN");
try {
const result = await callback(this);
this.db.exec("COMMIT");
return result;
} catch (e) {
this.db.exec("ROLLBACK");
throw e;
}
}
}