/** * In-memory `DatabaseAdapter` test double — NOT a mock. * * Satisfies the `@suaveplan/db` `DatabaseAdapter` interface with a real * implementation backed by `Map>`. Used by unit * tests for fast feedback. Integration tests exercise the same * repository / service code against a real SurrealDB testcontainer. * * The SurQL subset supported: * * - `SELECT * FROM ` / with `WHERE = $p0` / `LIMIT n START m` / `ORDER BY created_at DESC` * - `SELECT count() AS count FROM
[WHERE ...] GROUP ALL` * - `UPDATE
: SET k = $pN, ..., updated_at = time::now()` returning the updated row * - `DELETE
WHERE ... RETURN BEFORE` * * This is enough surface to cover every code path in * `BankRepository`, `BankAccountRepository`, and the * `BankingService` policy layer. Routes are tested by mounting them * against this adapter via the real `BankingService`. */ import type { DatabaseAdapter, DbOperationOptions, MigrationConfig, MigrationResult, } from "@suaveplan/db"; import type { DatabaseLike } from "@suaveplan/types"; type Row = Record & { id: string }; interface SelectFilter { field?: string; paramIndex?: number; extraField?: string; extraParamIndex?: number; extraOp?: "=" | "!="; limit?: number; offset?: number; orderByCreatedDesc?: boolean; containsParamIndex?: number; countOnly?: boolean; } const NOW = () => new Date().toISOString(); export class FakeDatabaseAdapter implements DatabaseAdapter { public readonly tables = new Map>(); public readonly callLog: string[] = []; private seq = 0; public failNextQuery = false; public get isConnected(): boolean { return true; } public async connect(): Promise {} public async disconnect(): Promise {} public async close(): Promise {} public async migrate( _config?: MigrationConfig, _opts?: DbOperationOptions ): Promise { return { applied: 0, migrations: [] }; } public async transaction( fn: (tx: DatabaseLike) => Promise, _opts?: DbOperationOptions ): Promise { return fn(this as unknown as DatabaseLike); } public async execute( sql: string, _params?: readonly unknown[], _opts?: DbOperationOptions ): Promise<{ rowsAffected: number }> { this.callLog.push(`execute:${sql.trim().split(/\s+/, 2).join(" ")}`); return { rowsAffected: 0 }; } public async query>( sql: string, params?: readonly unknown[], _opts?: DbOperationOptions ): Promise { this.callLog.push(`query:${sql.trim().slice(0, 80)}`); if (this.failNextQuery) { this.failNextQuery = false; throw new Error("Forced query failure"); } return this.parseAndRun(sql, params ?? []); } public async create>( table: string, data: T, _opts?: DbOperationOptions ): Promise { this.callLog.push(`create:${table}`); const map = this.ensureTable(table); const id = `${table}:${String(++this.seq)}`; const row: Row = { ...data, id, created_at: NOW(), updated_at: NOW(), }; map.set(id, row); return row as T & { id: string }; } public async select>( table: string, id: string, _opts?: DbOperationOptions ): Promise { this.callLog.push(`select:${table}:${id}`); const map = this.tables.get(table); if (map === undefined) return null; const fullId = id.includes(":") ? id : `${table}:${id}`; const row = map.get(fullId); return row === undefined ? null : (row as unknown as T); } public async update>( table: string, id: string, data: Partial, _opts?: DbOperationOptions ): Promise { const fullId = id.includes(":") ? id : `${table}:${id}`; const map = this.tables.get(table); if (map === undefined) return null; const existing = map.get(fullId); if (existing === undefined) return null; const updated = { ...existing, ...data, updated_at: NOW() }; map.set(fullId, updated as Row); return updated as unknown as T; } public async delete( table: string, id: string, _opts?: DbOperationOptions ): Promise { const fullId = id.includes(":") ? id : `${table}:${id}`; const map = this.tables.get(table); if (map !== undefined) map.delete(fullId); } public async selectAll>( table: string, _opts?: DbOperationOptions ): Promise { const map = this.tables.get(table); if (map === undefined) return []; return Array.from(map.values()) as unknown as T[]; } private ensureTable(table: string): Map { let map = this.tables.get(table); if (map === undefined) { map = new Map(); this.tables.set(table, map); } return map; } private parseAndRun(sql: string, params: readonly unknown[]): T[] { const trimmed = sql.replaceAll(/\s+/g, " ").trim(); // UPDATE table:id SET ... — returns updated row const upd = trimmed.match( /^UPDATE\s+([a-z_]+):([^\s]+)\s+SET\s+(.+)$/i ); if (upd !== null) { const table = upd[1]; const id = `${table}:${upd[2]}`; const setClause = upd[3]; const map = this.tables.get(table); if (map === undefined) return []; const row = map.get(id); if (row === undefined) return []; const updated: Row = { ...row }; for (const part of setClause.split(",")) { const m = part .trim() .match( /^([a-z_]+)\s*=\s*(?:\$p(\d+)|([a-z_]+)\s*([+-])\s*\$p(\d+)|time::now\(\))/i ); if (m === null) continue; const field = m[1]; if (m[2] !== undefined) { updated[field] = params[Number(m[2])]; } else if (m[3] !== undefined) { const cur = Number(updated[m[3]] ?? 0); const delta = Number(params[Number(m[5])]); updated[field] = m[4] === "+" ? cur + delta : cur - delta; } else { updated[field] = NOW(); } } updated.updated_at = NOW(); map.set(id, updated); return [updated as unknown as T]; } // DELETE table WHERE ... RETURN BEFORE const del = trimmed.match( /^DELETE\s+([a-z_]+)\s+WHERE\s+(.+?)\s+RETURN\s+BEFORE$/i ); if (del !== null) { const table = del[1]; const map = this.tables.get(table); if (map === undefined) return []; const removed: Row[] = []; const filter = this.parseWhereExpression(del[2], params); for (const [id, row] of Array.from(map.entries())) { if (filter(row)) { removed.push(row); map.delete(id); } } return removed as unknown as T[]; } // SELECT count() AS count FROM table [WHERE ...] GROUP ALL const cnt = trimmed.match( /^SELECT\s+count\(\)\s+AS\s+count\s+FROM\s+([a-z_]+)(?:\s+WHERE\s+(.+?))?\s+GROUP\s+ALL$/i ); if (cnt !== null) { const table = cnt[1]; const map = this.tables.get(table); if (map === undefined) return [{ count: 0 } as unknown as T]; const where = cnt[2]; if (where === undefined) { return [{ count: map.size } as unknown as T]; } const filter = this.parseWhereExpression(where, params); let n = 0; for (const row of map.values()) if (filter(row)) n++; return [{ count: n } as unknown as T]; } // SELECT * FROM table [WHERE ...] [ORDER BY created_at DESC] [LIMIT n START m] const sel = trimmed.match( /^SELECT\s+(?:id|\*)\s+FROM\s+([a-z_]+)(?:\s+WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+created_at\s+DESC)?(?:\s+LIMIT\s+(\d+)\s+START\s+(\d+))?(?:\s+LIMIT\s+(\d+))?$/i ); if (sel !== null) { const table = sel[1]; const where = sel[2]; const limit = sel[3] ?? sel[5]; const start = sel[4]; const map = this.tables.get(table); if (map === undefined) return []; let rows = Array.from(map.values()); if (where !== undefined) { const filter = this.parseWhereExpression(where, params); rows = rows.filter(filter); } if (/ORDER\s+BY\s+created_at\s+DESC/i.test(trimmed)) { rows = rows.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)) ); } if (start !== undefined) { rows = rows.slice(Number(start)); } if (limit !== undefined) { rows = rows.slice(0, Number(limit)); } return rows as unknown as T[]; } return []; } private parseWhereExpression( expr: string, params: readonly unknown[] ): (row: Row) => boolean { const clauses = expr.split(/\s+AND\s+/i).map((c) => c.trim()); const predicates: ((row: Row) => boolean)[] = []; for (const c of clauses) { const eq = c.match(/^([a-z_]+)\s*=\s*\$p(\d+)$/i); if (eq !== null) { const f = eq[1]; const v = params[Number(eq[2])]; predicates.push((r) => r[f] === v); continue; } const neq = c.match(/^([a-z_]+)\s*!=\s*\$p(\d+)$/i); if (neq !== null) { const f = neq[1]; const v = params[Number(neq[2])]; predicates.push((r) => r[f] !== v); continue; } const cont = c.match( /^string::lowercase\(\s*([a-z_]+)\s*\)\s+CONTAINS\s+string::lowercase\(\s*\$p(\d+)\s*\)$/i ); if (cont !== null) { const f = cont[1]; const v = String(params[Number(cont[2])] ?? "").toLowerCase(); predicates.push((r) => String(r[f] ?? "") .toLowerCase() .includes(v) ); } } return (row) => predicates.every((p) => p(row)); } }