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;
}