import * as fs from 'fs'; import Database from 'better-sqlite3'; import { DyFM_Error } from '@futdevpro/fsm-dynamo'; import { DyNTS_SqliteColumnInfo } from '../_models/interfaces/dynts-sqlite-reader.interface'; /** * `DyNTS_Sqlite_Reader_Util` (BFR-AM-009) — **read-only** SQLite reader statikus util. Egy IZOLÁLT, * opcionális natív függőséggel (`better-sqlite3`, read-only mode) ad általános, domain-agnosztikus * SQLite-olvasást: `listTables` / `getSchema` / `readTable` / `query` (SELECT-only). * * **Read-only garancia (kétréteg):** * 1. a DB MINDIG `{ readonly: true }`-vel nyílik (a SQLite-engine elutasít minden írást), * 2. a `query()` ráadásul forrás-szinten egy **SELECT-only guard**-ot tesz (csak `SELECT`/`WITH` * kezdetű, single-statement SQL fut) — így a destruktív SQL még a DB-rétegig sem jut el. * * **Fail-soft (no-silent-failure):** hiányzó `path` / nem-létező fájl / megnyitás-hiba / tiltott SQL * → tiszta, strukturált `DyFM_Error` (errorCode + deskriptív message), NEM hang/crash/néma-[]. A DB * MINDIG zárul (try/finally). * * **Opcionális natív dep:** a `better-sqlite3` peer-dep `optional:true` — a consumer CSAK akkor adja * hozzá, ha SQLite-ot olvas. A `/mcp`-vel diszjunkt; ez a util egy import-ban él, így a require * tényleges hívásig (statikus metódus) nem terheli a nem-SQLite consumert. */ export class DyNTS_Sqlite_Reader_Util { /** A hibák issuer-e (a strukturált DyFM_Error-okhoz). */ private static readonly issuer: string = 'DyNTS_Sqlite_Reader_Util'; /** A `query()` SELECT-only guard regex-e: a (trimmelt, lowercase) SQL `select`/`with`-tel kezdődik. */ private static readonly SELECT_ONLY: RegExp = /^\s*(select|with)\b/i; /** * Egy tábla ÖSSZES sorának read-only olvasása (`SELECT * FROM `). A `table` nevet * `sqlite_master` ellen validáljuk (csak létező táblanév fut, így nincs SQL-injekciós felület a * tábla-néven). Nem-létező tábla → strukturált hiba (NEM néma-[]). */ public static readTable(dbPath: string, table: string): unknown[] { return DyNTS_Sqlite_Reader_Util.withDb(dbPath, (db) => { if (!DyNTS_Sqlite_Reader_Util.tableExists(db, table)) { throw new DyFM_Error({ errorCode: 'DyNTS-SQLITE-READ-002', message: `Table '${table}' does not exist in the SQLite DB: '${dbPath}'.`, issuer: DyNTS_Sqlite_Reader_Util.issuer, }); } // A `table` itt már egy igazolt, létező táblanév (sqlite_master), nem nyers user-input. return db.prepare(`SELECT * FROM "${table}"`).all(); }); } /** * Egy tetszőleges **SELECT-only** lekérdezés read-only futtatása. A guard elutasít minden nem-SELECT * (insert/update/delete/drop/attach/pragma-write/…) SQL-t → `DyNTS-SQLITE-READONLY-001`. A * `params` opcionális prepared-statement paraméterek (pozícionális `?` vagy named `@x`). */ public static query(dbPath: string, sql: string, params?: unknown[] | { [key: string]: unknown }): unknown[] { DyNTS_Sqlite_Reader_Util.assertSelectOnly(sql); return DyNTS_Sqlite_Reader_Util.withDb(dbPath, (db) => { const statement: Database.Statement = db.prepare(sql); // A `better-sqlite3` `.all()` csak read-statement-en ad sort; write-statement-en dobna — // a SELECT-only guard ezt már a forrás-rétegben kizárja (defenzív kettősség). return params === undefined ? statement.all() : statement.all(params); }); } /** A DB ÖSSZES (nem-belső) táblanevének read-only listája (`sqlite_master`, ABC-sorrendben). */ public static listTables(dbPath: string): string[] { return DyNTS_Sqlite_Reader_Util.withDb(dbPath, (db) => { const rows: { name: string }[] = db.prepare( 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\' ORDER BY name', ).all() as { name: string }[]; return rows.map((row) => row.name); }); } /** * Egy tábla séma-leírása (`PRAGMA table_info(
)` — a SQLite read-only introspekciója). A * `table` nevet `sqlite_master` ellen validáljuk (nem-létező → strukturált hiba). Az oszlop-info * (`cid`/`name`/`type`/`notNull`/`defaultValue`/`primaryKey`) normalizált shape-ben tér vissza. */ public static getSchema(dbPath: string, table: string): DyNTS_SqliteColumnInfo[] { return DyNTS_Sqlite_Reader_Util.withDb(dbPath, (db) => { if (!DyNTS_Sqlite_Reader_Util.tableExists(db, table)) { throw new DyFM_Error({ errorCode: 'DyNTS-SQLITE-READ-002', message: `Table '${table}' does not exist in the SQLite DB: '${dbPath}'.`, issuer: DyNTS_Sqlite_Reader_Util.issuer, }); } const rows: PragmaColumnRow[] = db.pragma(`table_info("${table}")`) as PragmaColumnRow[]; return rows.map((row) => ({ cid: row.cid, name: row.name, type: row.type, notNull: row.notnull === 1, defaultValue: row.dflt_value, primaryKey: row.pk > 0, })); }); } // ========================================================================= // Belső segédek (read-only megnyitás + guard-ok) // ========================================================================= /** * A DB read-only megnyitása + a callback futtatása + GARANTÁLT zárás (try/finally). A `path` * validálva (üres/nem-létező → strukturált hiba); a megnyitás-/olvasás-hibát strukturált * `DyFM_Error`-rá fordítjuk (a már-DyFM_Error-t NEM csomagoljuk újra). A `readonly:true` az * első védvonal (a SQLite-engine elutasít minden írást). */ private static withDb(dbPath: string, work: (db: Database.Database) => T): T { if (!dbPath?.trim().length) { throw new DyFM_Error({ errorCode: 'DyNTS-SQLITE-READ-001', message: 'A SQLite read requires a non-empty `dbPath` (path to the `.db` file).', issuer: DyNTS_Sqlite_Reader_Util.issuer, }); } if (!fs.existsSync(dbPath)) { throw new DyFM_Error({ errorCode: 'DyNTS-SQLITE-READ-001', message: `The SQLite DB file was not found: '${dbPath}'.`, issuer: DyNTS_Sqlite_Reader_Util.issuer, }); } let db: Database.Database | undefined = undefined; try { db = new Database(dbPath, { readonly: true, fileMustExist: true }); return work(db); } catch (error) { if (error instanceof DyFM_Error) { throw error; } throw new DyFM_Error({ errorCode: 'DyNTS-SQLITE-READ-003', message: `Opening/reading the SQLite DB failed: '${dbPath}'.\n error: ` + `${DyFM_Error.getErrorMessage(error)}`, issuer: DyNTS_Sqlite_Reader_Util.issuer, error: error, }); } finally { if (db) { db.close(); } } } /** A SELECT-only guard: nem-SELECT/WITH kezdetű VAGY multi-statement SQL → strukturált hiba. */ private static assertSelectOnly(sql: string): void { const trimmed: string = (sql ?? '').trim(); if (!trimmed.length || !DyNTS_Sqlite_Reader_Util.SELECT_ONLY.test(trimmed)) { throw new DyFM_Error({ errorCode: 'DyNTS-SQLITE-READONLY-001', message: 'Only read-only SELECT/WITH queries are allowed (the reader is read-only).', issuer: DyNTS_Sqlite_Reader_Util.issuer, }); } // Multi-statement (`;` után további nem-üres tartalom) tiltott — egy SELECT mögé nem rejthető írás. const withoutTrailingSemicolon: string = trimmed.replace(/;\s*$/, ''); if (withoutTrailingSemicolon.includes(';')) { throw new DyFM_Error({ errorCode: 'DyNTS-SQLITE-READONLY-001', message: 'Multi-statement SQL is not allowed (only a single read-only SELECT/WITH).', issuer: DyNTS_Sqlite_Reader_Util.issuer, }); } } /** Egy tábla létezésének read-only ellenőrzése (`sqlite_master`). */ private static tableExists(db: Database.Database, table: string): boolean { const found: unknown = db.prepare('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=?').get(table); return Boolean(found); } } /** A `PRAGMA table_info` nyers sor-shape-je (a `better-sqlite3` ezt adja vissza). */ interface PragmaColumnRow { cid: number; name: string; type: string; notnull: number; dflt_value: string | null; pk: number; }