import Database from "better-sqlite3"; import { Context, Effect, Layer } from "effect"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class DatabaseError { readonly _tag = "DatabaseError"; constructor(readonly message: string, readonly cause?: unknown) {} } export interface DatabaseConfig { readonly path: string; readonly readOnly?: boolean; readonly fileMustExist?: boolean; readonly timeout?: number; readonly verbose?: boolean; } export class DatabaseService extends Context.Tag("DatabaseService")< DatabaseService, { readonly db: Database.Database; readonly run: ( sql: string, params?: unknown[] ) => Effect.Effect; readonly get: ( sql: string, params?: unknown[] ) => Effect.Effect; readonly all: ( sql: string, params?: unknown[] ) => Effect.Effect; readonly exec: (sql: string) => Effect.Effect; readonly close: () => Effect.Effect; } >() { static Live = (config: DatabaseConfig) => Layer.effect( this, Effect.gen(function* (_) { const db = yield* _( Effect.try({ try: () => { const options: { readonly?: boolean; fileMustExist?: boolean; timeout?: number; verbose?: (message?: unknown) => void; } = {}; if (config.readOnly !== undefined) { options.readonly = config.readOnly; } if (config.fileMustExist !== undefined) { options.fileMustExist = config.fileMustExist; } if (config.timeout !== undefined) { options.timeout = config.timeout; } if (config.verbose) { options.verbose = (message?: unknown) => console.log(message); } return new Database(config.path, options); }, catch: (error) => new DatabaseError("Failed to open database", error), }) ); yield* _( Effect.try({ try: () => { db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); db.pragma("synchronous = NORMAL"); }, catch: (error) => new DatabaseError("Failed to set pragmas", error), }) ); const schemaPath = join(__dirname, "schema.sql"); const schema = yield* _( Effect.try({ try: () => readFileSync(schemaPath, "utf-8"), catch: (error) => new DatabaseError("Failed to read schema file", error), }) ); yield* _( Effect.try({ try: () => db.exec(schema), catch: (error) => new DatabaseError("Failed to initialize schema", error), }) ); const run = ( sql: string, params?: unknown[] ): Effect.Effect => Effect.try({ try: () => { const stmt = db.prepare(sql); return stmt.run(...(params ?? [])) as T; }, catch: (error) => new DatabaseError("Query execution failed", error), }); const get = ( sql: string, params?: unknown[] ): Effect.Effect => Effect.try({ try: () => { const stmt = db.prepare(sql); return stmt.get(...(params ?? [])) as T | undefined; }, catch: (error) => new DatabaseError("Query execution failed", error), }); const all = ( sql: string, params?: unknown[] ): Effect.Effect => Effect.try({ try: () => { const stmt = db.prepare(sql); return stmt.all(...(params ?? [])) as T[]; }, catch: (error) => new DatabaseError("Query execution failed", error), }); const exec = (sql: string): Effect.Effect => Effect.try({ try: () => { db.exec(sql); }, catch: (error) => new DatabaseError("Exec failed", error), }); const close = (): Effect.Effect => Effect.try({ try: () => { db.close(); }, catch: (error) => new DatabaseError("Close failed", error), }); return { db, run, get, all, exec, close }; }) ); }