/** * Generische SQL-CRUD-Bausteine über die Tabellen-Registry (tableRegistry.ts) — von allen * server/db/queries/*.ts-Modulen genutzt, damit Spalten-Codecs (JSON/Bool) nur an einer * Stelle behandelt werden. */ import { db } from '../database' import { TABLE_DEFINITIONS, type TableDef, type ColumnDef, type ColumnCodec } from './tableRegistry' const tablesByName = new Map(TABLE_DEFINITIONS.map(t => [t.name, t])) function requireTable(name: string): TableDef { const t = tablesByName.get(name) if (!t) throw new Error(`Unbekannte Tabelle: ${name}`) return t } type SqlPrimitive = number | string | bigint | null function toBindValue(raw: unknown, codec: ColumnCodec): SqlPrimitive { if (raw === undefined || raw === null) return null if (codec === 'json') return JSON.stringify(raw) if (codec === 'bool') return raw ? 1 : 0 return raw as SqlPrimitive } function fromSqlValue(raw: unknown, codec: ColumnCodec, sqlType?: ColumnDef['sqlType']): unknown { if (raw === null || raw === undefined) return null if (codec === 'json') return JSON.parse(raw as string) if (codec === 'bool') return raw === 1 // SQLite/Affinity: INTEGER-Spalten können als string/bigint ankommen (v.a. nach TEXT→INTEGER-Schemawechsel) if (sqlType === 'INTEGER' || sqlType === 'REAL') { if (typeof raw === 'bigint') return Number(raw) if (typeof raw === 'string' && raw.trim() !== '') { const n = Number(raw) return Number.isFinite(n) ? n : raw } } return raw } export function decodeRow(table: TableDef, row: Record): T { const result: Record = { id: row.id } for (const c of table.columns) result[c.name] = fromSqlValue(row[c.name], c.codec, c.sqlType) return result as T } export function insertRow(tableName: string, obj: Record): number { const table = requireTable(tableName) const cols = table.columns.map(c => `"${c.name}"`).join(', ') const placeholders = table.columns.map(() => '?').join(', ') const values = table.columns.map(c => toBindValue(obj[c.name], c.codec)) const info = db.prepare(`INSERT INTO "${tableName}" (${cols}) VALUES (${placeholders})`).run(...values) return Number(info.lastInsertRowid) } /** Für Backup-Restore: fügt eine Zeile mit expliziter id ein, damit Fremdschlüssel-Referenzen im importierten Datensatz stabil bleiben. */ export function insertRowWithId(tableName: string, id: number, obj: Record): void { const table = requireTable(tableName) const cols = ['"id"', ...table.columns.map(c => `"${c.name}"`)].join(', ') const placeholders = ['?', ...table.columns.map(() => '?')].join(', ') const values: SqlPrimitive[] = [id, ...table.columns.map(c => toBindValue(obj[c.name], c.codec))] db.prepare(`INSERT INTO "${tableName}" (${cols}) VALUES (${placeholders})`).run(...values) } export function updateRow(tableName: string, id: number, patch: Record): number { const table = requireTable(tableName) const entries = Object.entries(patch).filter(([k]) => k !== 'id') if (entries.length === 0) return 0 const setSql = entries.map(([k]) => `"${k}" = ?`).join(', ') const values = entries.map(([k, v]) => { const colDef = table.columns.find(c => c.name === k) return toBindValue(v, colDef?.codec ?? 'plain') }) const info = db.prepare(`UPDATE "${tableName}" SET ${setSql} WHERE id = ?`).run(...values, id) return Number(info.changes) } export function getRowById(tableName: string, id: number): T | undefined { const table = requireTable(tableName) const row = db.prepare(`SELECT * FROM "${tableName}" WHERE id = ?`).get(id) as Record | undefined return row ? decodeRow(table, row) : undefined } export function getAllRows(tableName: string): T[] { const table = requireTable(tableName) const rows = db.prepare(`SELECT * FROM "${tableName}"`).all() as Record[] return rows.map(r => decodeRow(table, r)) } export function getAllRowsOrderedBy(tableName: string, column: string): T[] { const table = requireTable(tableName) const rows = db.prepare(`SELECT * FROM "${tableName}" ORDER BY "${column}" ASC`).all() as Record[] return rows.map(r => decodeRow(table, r)) } export function getRowsWhereEquals(tableName: string, column: string, value: SqlPrimitive): T[] { const table = requireTable(tableName) const rows = db.prepare(`SELECT * FROM "${tableName}" WHERE "${column}" = ?`).all(value) as Record[] return rows.map(r => decodeRow(table, r)) } export function getRowsWhereIn(tableName: string, column: string, values: number[]): T[] { if (values.length === 0) return [] const table = requireTable(tableName) const placeholders = values.map(() => '?').join(', ') const rows = db.prepare(`SELECT * FROM "${tableName}" WHERE "${column}" IN (${placeholders})`).all(...values) as Record[] return rows.map(r => decodeRow(table, r)) } export function deleteRowById(tableName: string, id: number): void { requireTable(tableName) db.prepare(`DELETE FROM "${tableName}" WHERE id = ?`).run(id) } export function deleteRowsWhereEquals(tableName: string, column: string, value: SqlPrimitive): number { requireTable(tableName) const info = db.prepare(`DELETE FROM "${tableName}" WHERE "${column}" = ?`).run(value) return Number(info.changes) } export function deleteRowsWhereIn(tableName: string, column: string, values: number[]): number { if (values.length === 0) return 0 requireTable(tableName) const placeholders = values.map(() => '?').join(', ') const info = db.prepare(`DELETE FROM "${tableName}" WHERE "${column}" IN (${placeholders})`).run(...values) return Number(info.changes) } export function countWhereEquals(tableName: string, column: string, value: SqlPrimitive): number { requireTable(tableName) const row = db.prepare(`SELECT COUNT(*) as c FROM "${tableName}" WHERE "${column}" = ?`).get(value) as { c: number } return row.c }