v1.0.19
This commit is contained in:
1 parent
4b430340db
commit
9c451aedf9
120 files changed
+7777
-6046
No files matched your search
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* Database Connection Module
|
||||
*
|
||||
* Provides a unified database connection using Bun's SQL API.
|
||||
* Supports both SQLite (default) and PostgreSQL (via DATABASE_URL).
|
||||
*/
|
||||
|
||||
import { SQL } from 'bun';
|
||||
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Database configuration
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const dataDir = process.env.DATA_DIR || './data';
|
||||
|
||||
// Detect database type
|
||||
export const isPostgres = databaseUrl && (databaseUrl.startsWith('postgres://') || databaseUrl.startsWith('postgresql://'));
|
||||
export const isSqlite = !isPostgres;
|
||||
|
||||
/**
|
||||
* Read a SQL file from the appropriate sql directory.
|
||||
*/
|
||||
function readSql(filename: string): string {
|
||||
const sqlDir = isPostgres ? 'postgres' : 'sqlite';
|
||||
return readFileSync(join(__dirname, sqlDir, 'sql', filename), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate PostgreSQL connection URL format.
|
||||
*/
|
||||
function validatePostgresUrl(url: string): void {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
|
||||
if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:') {
|
||||
exitWithError(`Invalid protocol "${parsed.protocol}". Expected "postgres:" or "postgresql:"`, url);
|
||||
}
|
||||
|
||||
if (!parsed.hostname) {
|
||||
exitWithError('Missing hostname in DATABASE_URL', url);
|
||||
}
|
||||
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
exitWithError('Missing database name in DATABASE_URL', url);
|
||||
}
|
||||
} catch {
|
||||
exitWithError('Invalid URL format', url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print connection error and exit.
|
||||
*/
|
||||
function exitWithError(error: string, url?: string): never {
|
||||
console.error('\n' + '='.repeat(70));
|
||||
console.error('DATABASE CONNECTION ERROR');
|
||||
console.error('='.repeat(70));
|
||||
console.error(`\nError: ${error}`);
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.password) parsed.password = '***';
|
||||
console.error(`\nProvided URL: ${parsed.toString()}`);
|
||||
} catch {
|
||||
console.error(`\nProvided URL: ${url.replace(/:[^:@]+@/, ':***@')}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.error('\n' + '-'.repeat(70));
|
||||
console.error('DATABASE_URL format:');
|
||||
console.error('-'.repeat(70));
|
||||
console.error('\n postgres://USER:PASSWORD@HOST:PORT/DATABASE');
|
||||
console.error('\nExamples:');
|
||||
console.error(' postgres://dockhand:secret@localhost:5432/dockhand');
|
||||
console.error(' postgres://admin:p4ssw0rd@192.168.1.100:5432/dockhand');
|
||||
console.error(' postgresql://user:pass@db.example.com/mydb?sslmode=require');
|
||||
console.error('\n' + '-'.repeat(70));
|
||||
console.error('To use SQLite instead, remove the DATABASE_URL environment variable.');
|
||||
console.error('='.repeat(70) + '\n');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the database connection.
|
||||
*/
|
||||
function createConnection(): SQL {
|
||||
if (isPostgres) {
|
||||
// Validate PostgreSQL URL
|
||||
validatePostgresUrl(databaseUrl!);
|
||||
|
||||
console.log('Connecting to PostgreSQL database...');
|
||||
try {
|
||||
const sql = new SQL(databaseUrl!);
|
||||
return sql;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
exitWithError(`Failed to connect to PostgreSQL: ${message}`, databaseUrl);
|
||||
}
|
||||
} else {
|
||||
// SQLite: Ensure db directory exists
|
||||
const dbDir = join(dataDir, 'db');
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const dbPath = join(dbDir, 'dockhand.db');
|
||||
console.log(`Using SQLite database at: ${dbPath}`);
|
||||
|
||||
const sql = new SQL(`sqlite://${dbPath}`);
|
||||
|
||||
// Enable WAL mode for better performance
|
||||
sql.run('PRAGMA journal_mode = WAL');
|
||||
|
||||
return sql;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database schema.
|
||||
*/
|
||||
async function initializeSchema(sql: SQL): Promise<void> {
|
||||
try {
|
||||
// Create schema (tables)
|
||||
await sql.run(readSql('schema.sql'));
|
||||
|
||||
// Create indexes
|
||||
await sql.run(readSql('indexes.sql'));
|
||||
|
||||
// Insert seed data
|
||||
await sql.run(readSql('seed.sql'));
|
||||
|
||||
// Update system roles
|
||||
await sql.run(readSql('system-roles.sql'));
|
||||
|
||||
// Run maintenance
|
||||
await sql.run(readSql('maintenance.sql'));
|
||||
|
||||
console.log(`Database initialized successfully (${isPostgres ? 'PostgreSQL' : 'SQLite'})`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error('Failed to initialize database schema:', message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the database connection
|
||||
export const sql = createConnection();
|
||||
|
||||
// Initialize schema (runs async but we handle it)
|
||||
initializeSchema(sql).catch((error) => {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error('[DB] Database initialization failed:', errorMsg);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to convert SQLite integer booleans to JS booleans.
|
||||
* PostgreSQL returns actual booleans, SQLite returns 0/1.
|
||||
*/
|
||||
export function toBool(value: any): boolean {
|
||||
if (typeof value === 'boolean') return value;
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to convert JS boolean to database value.
|
||||
* PostgreSQL uses boolean, SQLite uses 0/1.
|
||||
*/
|
||||
export function fromBool(value: boolean): boolean | number {
|
||||
return isPostgres ? value : (value ? 1 : 0);
|
||||
}
|
||||
@@ -208,11 +208,11 @@ function readMigrationJournal(migrationsFolder: string): MigrationJournal | null
|
||||
async function getAppliedMigrations(client: any, postgres: boolean): Promise<AppliedMigration[]> {
|
||||
try {
|
||||
if (postgres) {
|
||||
// PostgreSQL using Bun.sql - note the 'drizzle' schema
|
||||
// PostgreSQL using postgres-js - note the 'drizzle' schema
|
||||
const result = await client`SELECT hash, created_at FROM drizzle.__drizzle_migrations ORDER BY id`;
|
||||
return result.map((r: any) => ({ hash: r.hash, createdAt: r.created_at }));
|
||||
} else {
|
||||
// SQLite using bun:sqlite
|
||||
// SQLite using better-sqlite3
|
||||
const stmt = client.prepare('SELECT hash, created_at FROM __drizzle_migrations ORDER BY id');
|
||||
return stmt.all().map((r: any) => ({ hash: r.hash, createdAt: r.created_at }));
|
||||
}
|
||||
@@ -484,10 +484,10 @@ async function runMigrations(
|
||||
// Run migrations
|
||||
try {
|
||||
if (postgres) {
|
||||
const { migrate } = await import('drizzle-orm/bun-sql/migrator');
|
||||
const { migrate } = await import('drizzle-orm/postgres-js/migrator');
|
||||
await migrate(database, { migrationsFolder });
|
||||
} else {
|
||||
const { migrate } = await import('drizzle-orm/bun-sqlite/migrator');
|
||||
const { migrate } = await import('drizzle-orm/better-sqlite3/migrator');
|
||||
await migrate(database, { migrationsFolder });
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ async function initializeDatabase() {
|
||||
logHeader('DATABASE INITIALIZATION');
|
||||
|
||||
if (isPostgres) {
|
||||
// PostgreSQL via postgres-js (more stable than bun:sql for concurrent queries)
|
||||
// PostgreSQL via postgres-js
|
||||
validatePostgresUrl(config.databaseUrl!);
|
||||
|
||||
logInfo(`Database: PostgreSQL`);
|
||||
@@ -634,7 +634,7 @@ async function initializeDatabase() {
|
||||
handleMigrationFailure(result.error, true);
|
||||
}
|
||||
} else {
|
||||
// SQLite via bun:sqlite
|
||||
// SQLite via better-sqlite3
|
||||
const dbDir = join(config.dataDir, 'db');
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
@@ -645,8 +645,8 @@ async function initializeDatabase() {
|
||||
logInfo(`Database: SQLite`);
|
||||
logInfo(`Path: ${dbPath}`);
|
||||
|
||||
const { drizzle } = await import('drizzle-orm/bun-sqlite');
|
||||
const { Database } = await import('bun:sqlite');
|
||||
const { drizzle } = await import('drizzle-orm/better-sqlite3');
|
||||
const Database = (await import('better-sqlite3')).default;
|
||||
|
||||
// Import SQLite schema
|
||||
schema = await import('./schema/index.js');
|
||||
@@ -655,11 +655,11 @@ async function initializeDatabase() {
|
||||
rawClient = new Database(dbPath);
|
||||
|
||||
// Enable WAL mode for better performance and concurrency
|
||||
rawClient.exec('PRAGMA journal_mode = WAL');
|
||||
rawClient.pragma('journal_mode = WAL');
|
||||
// Synchronous NORMAL is a good balance between safety and speed
|
||||
rawClient.exec('PRAGMA synchronous = NORMAL');
|
||||
rawClient.pragma('synchronous = NORMAL');
|
||||
// Increase busy timeout to handle concurrent access (5 seconds)
|
||||
rawClient.exec('PRAGMA busy_timeout = 5000');
|
||||
rawClient.pragma('busy_timeout = 5000');
|
||||
|
||||
db = drizzle({ client: rawClient, schema });
|
||||
logSuccess('SQLite database opened');
|
||||
|
||||
Reference in new issue
Block a user