@onurege3467/zerohelper
Version:
ZeroHelper is a versatile high-performance utility library and database framework for Node.js, fully written in TypeScript.
283 lines (282 loc) • 10.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MySQLDatabase = void 0;
const IDatabase_1 = require("./IDatabase");
const promise_1 = __importDefault(require("mysql2/promise"));
class MySQLDatabase 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 connection = await promise_1.default.createConnection({
host: config.host || 'localhost',
port: config.port || 3306,
user: config.username,
password: config.password,
});
await connection.query(`CREATE DATABASE IF NOT EXISTS
${config.database}
`);
await connection.end();
this.pool = promise_1.default.createPool({
host: config.host || 'localhost',
port: config.port || 3306,
user: config.username,
password: config.password,
database: config.database,
waitForConnections: true,
connectionLimit: config.poolSize || 15, // Balanced limit
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 10000
});
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 execute = async () => {
const res = await fn();
this.recordMetric(op, table, Date.now() - start);
return res;
};
if (this._connected)
return execute();
return new Promise((resolve, reject) => this._queue.push({ operation: execute, resolve, reject }));
}
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;
let retries = 3;
while (retries > 0) {
try {
const [rows] = await pool.execute(sql, params);
return rows;
}
catch (error) {
if ((error.code === 'ER_CON_COUNT_ERROR' || error.message.includes('Too many connections')) && retries > 1) {
retries--;
await new Promise(r => setTimeout(r, 1000)); // Wait 1s and retry
continue;
}
throw error;
}
}
}
async _ensureMissingColumns(table, data) {
const existingColumns = await this.query(`DESCRIBE
${table}
`).catch(() => []);
const columnNames = existingColumns.map(col => col.Field);
for (const key of Object.keys(data)) {
if (key !== '_id' && !columnNames.includes(key)) {
await this.query(`ALTER TABLE
${table}
ADD COLUMN
${key}
TEXT`);
}
}
}
async ensureTable(table, data = {}) {
const escapedTable = promise_1.default.escape(table);
const tables = await this.query(`SHOW TABLES LIKE ${escapedTable}`);
if (tables.length === 0) {
const defs = Object.keys(data).map(k => `
${k}
TEXT`);
const columnsPart = defs.length > 0 ? ', ' + defs.join(", ") : '';
await this.query(`CREATE TABLE
${table}
(_id INT PRIMARY KEY AUTO_INCREMENT ${columnsPart}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
}
else {
await this._ensureMissingColumns(table, data);
}
}
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(() => '?').join(",")})`;
const result = await this.query(sql, Object.values(data).map(v => typeof v === 'object' ? JSON.stringify(v) : v));
const finalData = { _id: result.insertId, ...data };
await this.runHooks('afterInsert', table, finalData);
return result.insertId;
});
}
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 set = Object.keys(data).map(k => `
${k}
= ?`).join(", ");
const wKeys = Object.keys(where);
const sql = `UPDATE
${table}
SET ${set} WHERE ${wKeys.map(k => `
${k}
= ?`).join(" AND ")}`;
const result = await this.query(sql, [...Object.values(data).map(v => typeof v === 'object' ? JSON.stringify(v) : v), ...Object.values(where).map(v => typeof v === 'object' ? JSON.stringify(v) : v)]);
return result.affectedRows;
});
}
async delete(table, where) {
await this.runHooks('beforeDelete', table, where);
return this._execute('delete', table, async () => {
await this.ensureTable(table, where);
const keys = Object.keys(where);
const whereClause = keys.length > 0 ? `WHERE ${keys.map(k => `
${k}
= ?`).join(" AND ")}` : '';
const sql = `DELETE FROM
${table}
${whereClause}`;
const result = await this.query(sql, Object.values(where).map(v => typeof v === 'object' ? JSON.stringify(v) : v));
return result.affectedRows;
});
}
async select(table, where = null) {
return this._execute('select', table, async () => {
await this.ensureTable(table, where || {});
const keys = where ? Object.keys(where) : [];
const sql = `SELECT * FROM
${table}
` + (keys.length ? ' WHERE ' + keys.map(k => `
${k}
= ?`).join(" AND ") : '');
const rows = await this.query(sql, keys.map(k => typeof where[k] === 'object' ? JSON.stringify(where[k]) : where[k]));
return rows.map((row) => {
const nr = {};
for (const k in row) {
try {
nr[k] = JSON.parse(row[k]);
}
catch {
nr[k] = row[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 existing = await this.selectOne(table, where);
return existing ? this.update(table, data, where) : this.insert(table, { ...where, ...data });
}
async bulkInsert(table, dataArray) {
if (!dataArray.length)
return 0;
return this._execute('bulkInsert', table, async () => {
await this.ensureTable(table, dataArray[0]);
const keys = Object.keys(dataArray[0]);
const placeholders = dataArray.map(() => `(${keys.map(() => '?').join(',')})`).join(',');
const values = dataArray.flatMap(obj => keys.map(k => this._serializeValue(obj[k])));
const sql = `INSERT INTO
${table}
(${keys.map(k => `
${k}
`).join(",")}) VALUES ${placeholders}`;
const result = await this.query(sql, values);
return result.affectedRows;
});
}
async increment(table, incs, where) {
return this._execute('increment', table, async () => {
await this.ensureTable(table, where);
const set = Object.keys(incs).map(f => `
${f}
=
${f}
+ ?`).join(', ');
const wKeys = Object.keys(where);
const sql = `UPDATE
${table}
SET ${set} WHERE ${wKeys.map(k => `
${k}
= ?`).join(" AND ")}`;
const result = await this.query(sql, [...Object.values(incs), ...Object.values(where).map(v => typeof v === 'object' ? JSON.stringify(v) : v)]);
return result.affectedRows;
});
}
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) ? 'INT' : 'DOUBLE';
if (v instanceof Date)
return 'DATETIME';
if (typeof v === 'object')
return 'JSON';
return 'TEXT';
}
_serializeValue(v) {
if (v instanceof Date)
return v.toISOString().slice(0, 19).replace('T', ' ');
return (typeof v === 'object' && v !== null) ? JSON.stringify(v) : v;
}
_buildWhereClause(where) {
if (!where)
return { whereClause: '', values: [] };
const keys = Object.keys(where);
if (!keys.length)
return { whereClause: '', values: [] };
return {
whereClause: 'WHERE ' + keys.map(k => `
${k}
= ?`).join(' AND '),
values: Object.values(where).map(v => this._serializeValue(v))
};
}
}
exports.MySQLDatabase = MySQLDatabase;
exports.default = MySQLDatabase;