UNPKG

@onurege3467/zerohelper

Version:

ZeroHelper is a versatile high-performance utility library and database framework for Node.js, fully written in TypeScript.

201 lines (200 loc) 8.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgreSQLDatabase = void 0; const IDatabase_1 = require("./IDatabase"); const pg_1 = require("pg"); class PostgreSQLDatabase extends IDatabase_1.IDatabase { constructor(config) { super(); this.pool = null; this._queue = []; this._connected = false; this.config = config; this._connectionPromise = new Promise(async (resolve, reject) => { try { const tempPool = new pg_1.Pool({ host: config.host || 'localhost', port: config.port || 5432, user: config.username, password: config.password, database: 'postgres' }); try { await tempPool.query(`CREATE DATABASE "${config.database}"`); } catch (e) { if (!e.message.includes('already exists')) console.warn(e.message); } await tempPool.end(); this.pool = new pg_1.Pool({ host: config.host || 'localhost', port: config.port || 5432, user: config.username, password: config.password, database: config.database, max: config.poolSize || 10 }); this._connected = true; resolve(this.pool); this._processQueue(); } catch (error) { this._queue.forEach(q => q.reject(error)); this._queue = []; reject(error); } }); } async _execute(op, table, fn) { const start = Date.now(); const res = await fn(); this.recordMetric(op, table, Date.now() - start); return res; } ; async _processQueue() { if (!this._connected) return; while (this._queue.length > 0) { const item = this._queue.shift(); if (item) { try { item.resolve(await item.operation()); } catch (error) { item.reject(error); } } } } async query(sql, params = []) { const pool = await this._connectionPromise; const res = await pool.query(sql, params); return res.rows; } async ensureTable(table, data = {}) { const tables = await this.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1`, [table]); if (tables.length === 0) { const defs = Object.keys(data).map(k => `"${k}" ${this._getColumnType(data[k])}`); await this.query(`CREATE TABLE "${table}" ("_id" SERIAL PRIMARY KEY ${defs.length ? ', ' + defs.join(",") : ''})`); } else { const existing = await this.query(`SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public'`, [table]); const names = existing.map((c) => c.column_name); for (const key of Object.keys(data)) { if (key !== '_id' && !names.includes(key)) { await this.query(`ALTER TABLE "${table}" ADD COLUMN "${key}" ${this._getColumnType(data[key])}`); } } } } async insert(table, data) { await this.runHooks('beforeInsert', table, data); return this._execute('insert', table, async () => { await this.ensureTable(table, data); const keys = Object.keys(data); const sql = `INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(",")}) VALUES (${keys.map((_, i) => `$${i + 1}`).join(",")}) RETURNING "_id"`; const res = await this.query(sql, Object.values(data).map(v => this._serializeValue(v))); const finalData = { _id: res[0]._id, ...data }; await this.runHooks('afterInsert', table, finalData); return res[0]._id; }); } async update(table, data, where) { await this.runHooks('beforeUpdate', table, { data, where }); return this._execute('update', table, async () => { await this.ensureTable(table, { ...data, ...where }); const dataKeys = Object.keys(data); const set = dataKeys.map((k, i) => `"${k}" = $${i + 1}`).join(","); const { whereClause, values: whereValues } = this._buildWhereClause(where, dataKeys.length); const sql = `UPDATE "${table}" SET ${set} ${whereClause}`; const pool = await this._connectionPromise; const res = await pool.query(sql, [...Object.values(data).map(v => this._serializeValue(v)), ...whereValues]); return res.rowCount ?? 0; }); } async delete(table, where) { await this.runHooks('beforeDelete', table, where); return this._execute('delete', table, async () => { await this.ensureTable(table, where); const { whereClause, values } = this._buildWhereClause(where); const sql = `DELETE FROM "${table}" ${whereClause}`; const pool = await this._connectionPromise; const res = await pool.query(sql, values); return res.rowCount ?? 0; }); } async select(table, where = null) { return this._execute('select', table, async () => { await this.ensureTable(table, where || {}); const { whereClause, values } = this._buildWhereClause(where); const rows = await this.query(`SELECT * FROM "${table}" ${whereClause}`, values); return rows.map((r) => { const nr = {}; for (const k in r) { // Postgres returns numbers as strings for safety sometimes, // but since we used correct types now, driver might handle it better. // If not, we can cast if it looks like a number. // For now, let's trust the driver + schema. nr[k] = r[k]; } return nr; }); }); } async selectOne(table, where = null) { const res = await this.select(table, where); return res[0] || null; } async set(table, data, where) { const ex = await this.selectOne(table, where); return ex ? this.update(table, data, where) : this.insert(table, { ...where, ...data }); } async bulkInsert(table, dataArray) { if (!dataArray.length) return 0; for (const d of dataArray) await this.insert(table, d); return dataArray.length; } async increment(table, incs, where) { return this._execute('increment', table, async () => { await this.ensureTable(table, where); const incKeys = Object.keys(incs); const set = incKeys.map((f, i) => `"${f}" = "${f}" + $${i + 1}`).join(','); const { whereClause, values } = this._buildWhereClause(where, incKeys.length); const sql = `UPDATE "${table}" SET ${set} ${whereClause}`; const pool = await this._connectionPromise; const res = await pool.query(sql, [...Object.values(incs), ...values]); return res.rowCount ?? 0; }); } async decrement(table, decs, where) { const incs = {}; for (const k in decs) incs[k] = -decs[k]; return this.increment(table, incs, where); } async close() { if (this.pool) await this.pool.end(); } _getColumnType(v) { if (v === null || v === undefined) return 'TEXT'; if (typeof v === 'boolean') return 'BOOLEAN'; if (typeof v === 'number') return Number.isInteger(v) ? 'INTEGER' : 'DOUBLE PRECISION'; if (v instanceof Date) return 'TIMESTAMP'; if (typeof v === 'object') return 'JSONB'; return 'TEXT'; } _serializeValue(v) { if (v instanceof Date) return v.toISOString(); return (typeof v === 'object' && v !== null) ? JSON.stringify(v) : v; } _buildWhereClause(where, offset = 0) { if (!where) return { whereClause: '', values: [] }; const safeWhere = where; const keys = Object.keys(safeWhere); if (!keys.length) return { whereClause: '', values: [] }; return { whereClause: 'WHERE ' + keys.map((k, i) => `"${k}" = $${i + 1 + offset}`).join(' AND '), values: keys.map(k => this._serializeValue(safeWhere[k])) }; } } exports.PostgreSQLDatabase = PostgreSQLDatabase; exports.default = PostgreSQLDatabase;