@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
JavaScript
"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;