/* ---------------------------------------------------------------------------- Question Server GitLab: This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ------------------------------------------------------------------------- */ // https://www.sqlitetutorial.net/sqlite-nodejs/ // https://github.com/JoshuaWise/better-sqlite3/blob/HEAD/docs/api.md export default { GetDB: GetDB, AddColumn: AddColumn, TableInfo: TableInfo, Update: Update, Delete: Delete, CreateTable: CreateTable, SelectAll: SelectAll, Select: Select, Insert: Insert, CloseDB: CloseDB, runStatement: runStatement, sanitizeQuery: sanitizeQuery, } import Sqlite, { Database, RunResult } from 'better-sqlite3' import logger from '../utils/logger' import utils from '../utils/utils' const debugLog = process.env.NS_SQL_DEBUG_LOG function sanitizeQuery(val: string | number): string | number { if (typeof val === 'string') { return val.replace(/'/g, '').replace(/;/g, '') } return val } // { asd: 'asd', basd: 4 } => asd = 'asd', basd = 4 function GetSqlQuerry( conditions: { [key: string]: string | number }, type?: string, joiner?: string ) { const res = Object.keys(conditions).reduce((acc, key) => { const item = conditions[key] const conditionKey = sanitizeQuery(key) const condition = sanitizeQuery(conditions[key]) if (typeof item === 'string') { acc.push(`${conditionKey} = '${condition}'`) } else { acc.push(`${conditionKey} = ${condition}`) } return acc }, []) if (type === 'where') { if (joiner) { return res.join(` ${joiner} `) } else { return res.join(' AND ') } } else { return res.join(', ') } } // ------------------------------------------------------------------------- function GetDB(path: string): Database { utils.CreatePath(path) const res = new Sqlite(path) res.pragma('synchronous = OFF') return res } function DebugLog(msg: string) { if (debugLog) { logger.DebugLog(msg, 'sql', 0) } } // FIXME: this might not work: what is col exactly, and how we use AddColumn? function AddColumn( db: Database, table: string, col: { [key: string]: string | number } ): RunResult { try { const colName = Object.keys(col)[0] const colType = col.type const command = `ALTER TABLE ${table} ADD COLUMN ${colName} ${colType}` const stmt = PrepareStatement(db, command) return stmt.run() } catch (err) { console.error(err) return null } } function TableInfo( db: Database, table: string ): { columns: any[] dataCount: number } { try { const command = `PRAGMA table_info(${table})` const stmt = PrepareStatement(db, command) const infoRes = stmt.all() const s2 = `SELECT COUNT(*) FROM ${table}` const stmt2 = PrepareStatement(db, s2) const countRes = stmt2.get() return { columns: infoRes, dataCount: countRes[Object.keys(countRes)[0]], } } catch (err) { console.error(err) return null } } function Update( db: Database, table: string, newData: { [key: string]: string | number }, conditions: { [key: string]: string | number } ): RunResult { try { const command = `UPDATE ${table} SET ${GetSqlQuerry( newData, 'set' )} WHERE ${GetSqlQuerry(conditions, 'where')}` const stmt = PrepareStatement(db, command) return stmt.run() } catch (err) { console.error(err) return null } } function Delete( db: Database, table: string, conditions: { [key: string]: string | number } ): RunResult { try { const command = `DELETE FROM ${table} WHERE ${GetSqlQuerry( conditions, 'where' )}` const stmt = PrepareStatement(db, command) return stmt.run() } catch (err) { console.error(err) return null } } interface DbColumnDescription { [key: string]: { type: string primary?: boolean autoIncrement?: boolean notNull?: boolean defaultZero?: boolean [key: string]: any } } function CreateTable( db: Database, name: string, columns: DbColumnDescription, foreignKeys: { keysFrom: string[] table: string keysTo: string[] }[] ): RunResult { // CREATE TABLE users(pw text PRIMARY KEY NOT NULL, id number, lastIP text, notes text, loginCount // number, lastLogin text, lastAccess text // // FOREIGN KEY(songartist, songalbum) REFERENCES album(albumartist, albumname) ) try { const cols = Object.keys(columns) .reduce((acc, key) => { const item = columns[key] const flags: string[] = [] const toCheck = { primary: 'PRIMARY KEY', notNull: 'NOT NULL', unique: 'UNIQUE', autoIncrement: 'AUTOINCREMENT', defaultZero: 'DEFAULT 0', } Object.keys(toCheck).forEach((key) => { if (item[key]) { flags.push(toCheck[key]) } }) acc.push(`${key} ${item.type} ${flags.join(' ')}`) return acc }, []) .join(', ') const fKeys: string[] = [] if (foreignKeys) { foreignKeys.forEach((foreignKey) => { const { keysFrom, table, keysTo } = foreignKey fKeys.push( `, FOREIGN KEY(${keysFrom.join( ', ' )}) REFERENCES ${table}(${keysTo.join(', ')})` ) }) } // IF NOT EXISTS const command = `CREATE TABLE ${name}(${cols}${fKeys.join(' ')})` const stmt = PrepareStatement(db, command) return stmt.run() } catch (err) { console.error(err) return null } } function SelectAll(db: Database, from: string): any[] { try { const command = `SELECT * from ${from}` const stmt = PrepareStatement(db, command) return stmt.all() } catch (err) { console.error(err) return null } } // SELECT * FROM MyTable WHERE SomeColumn > LastValue ORDER BY SomeColumn LIMIT 100; function Select( db: Database, from: string, conditions: { [key: string]: string | number }, options: { joiner?: string; limit?: number } = {} ): any[] { const { joiner, limit } = options try { let command = `SELECT * from ${from} WHERE ${GetSqlQuerry( conditions, 'where', joiner )}` if (!isNaN(limit)) { command += ` LIMIT ${limit}` } const stmt = PrepareStatement(db, command) return stmt.all() } catch (err) { console.error(err) return null } } function Insert( db: Database, table: string, data: { [key: string]: number | string } ): RunResult { try { const cols = Object.keys(data) .reduce((acc, key) => { acc.push(`${key}`) return acc }, []) .join(', ') const values = Object.keys(data) .map((item) => { if (typeof item === 'string') { return `'${item}'` } else { return `${item}` } }) .join(', ') const command = `INSERT INTO ${table} (${cols}) VALUES (${values})` const stmt = PrepareStatement(db, command) return stmt.run() } catch (err) { console.error(err) return null } } function runStatement(db: Database, command: string, runType?: string): any { const stmt = PrepareStatement(db, command) if (!runType) { return stmt.all() } else if (runType === 'run') { return stmt.run() } return null } function CloseDB(db: Database): void { db.close() } // ------------------------------------------------------------------------- function PrepareStatement(db: Database, command: string) { if (!db) { throw new Error( 'DB is undefined in prepare statement! DB action called with undefined db' ) } DebugLog(command) return db.prepare(command) }