From d7e4649e476f1c36849e28b1f47b8540297fc388 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Fri, 30 Jan 2026 14:23:42 +0100 Subject: [PATCH] feat(02-01): create database connection factory - src/db/config.ts: getDbPath() returns ~/.cw/data/cw.db with CW_DB_PATH override - src/db/index.ts: createDatabase() factory with WAL mode and foreign keys - drizzle.config.ts: Drizzle Kit configuration for migrations Pattern: Factory function (not singleton) allows isolated test instances --- drizzle.config.ts | 12 ++++++++++++ src/db/config.ts | 33 +++++++++++++++++++++++++++++++++ src/db/index.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ src/db/schema.ts | 15 +++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 drizzle.config.ts create mode 100644 src/db/config.ts create mode 100644 src/db/index.ts create mode 100644 src/db/schema.ts diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..a78b15c --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'drizzle-kit'; +import { join } from 'node:path'; +import { homedir } from 'node:os'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'sqlite', + dbCredentials: { + url: process.env.CW_DB_PATH ?? join(homedir(), '.cw', 'data', 'cw.db'), + }, +}); diff --git a/src/db/config.ts b/src/db/config.ts new file mode 100644 index 0000000..4a20673 --- /dev/null +++ b/src/db/config.ts @@ -0,0 +1,33 @@ +import { mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { homedir } from 'node:os'; + +/** + * Get the database path. + * + * - Default: ~/.cw/data/cw.db + * - Override via CW_DB_PATH environment variable + * - For testing, pass ':memory:' as CW_DB_PATH + */ +export function getDbPath(): string { + const envPath = process.env.CW_DB_PATH; + if (envPath) { + return envPath; + } + + return join(homedir(), '.cw', 'data', 'cw.db'); +} + +/** + * Ensure the parent directory for the database file exists. + * No-op for in-memory databases. + */ +export function ensureDbDirectory(dbPath: string): void { + // Skip for in-memory database + if (dbPath === ':memory:') { + return; + } + + const dir = dirname(dbPath); + mkdirSync(dir, { recursive: true }); +} diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..01c7833 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,43 @@ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; + +import { getDbPath, ensureDbDirectory } from './config.js'; +import * as schema from './schema.js'; + +export type DrizzleDatabase = BetterSQLite3Database; + +/** + * Create a new database connection. + * + * This is a factory function (not a singleton) to allow multiple instances + * for testing with isolated databases. + * + * @param path - Optional path override. Defaults to getDbPath(). + * Use ':memory:' for in-memory testing database. + * @returns Drizzle database instance with schema + */ +export function createDatabase(path?: string): DrizzleDatabase { + const dbPath = path ?? getDbPath(); + + // Ensure directory exists for file-based databases + ensureDbDirectory(dbPath); + + // Create SQLite connection + const sqlite = new Database(dbPath); + + // Enable WAL mode for better concurrent read performance + sqlite.pragma('journal_mode = WAL'); + + // Enable foreign keys (SQLite has them disabled by default) + sqlite.pragma('foreign_keys = ON'); + + // Create Drizzle instance with schema + return drizzle(sqlite, { schema }); +} + +// Re-export config utilities +export { getDbPath, ensureDbDirectory } from './config.js'; + +// Re-export schema and types +export * from './schema.js'; diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..c94ee98 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,15 @@ +/** + * Database schema for Codewalk District. + * + * Defines the four-level task hierarchy: + * - Initiative: Top-level project + * - Phase: Major milestone within initiative + * - Plan: Group of related tasks within phase + * - Task: Individual work item + * + * Schema will be defined in Task 3. + */ + +// Placeholder export to allow index.ts to compile +// Full schema defined in Task 3 +export {};