UNPKG

@onurege3467/zerohelper

Version:

ZeroHelper is a versatile JavaScript library offering helper functions, validation, logging, database utilities and migration system for developers. It supports MongoDB, MySQL, SQLite, Redis, and PostgreSQL with increment/decrement operations.

527 lines (442 loc) 17.6 kB
const IDatabase = require('./IDatabase'); // Arayüzü import et const { Pool } = require('pg'); /** * @implements {IDatabase} */ class PostgreSQLDatabase extends IDatabase { constructor(config) { super(); this.config = config; this.pool = null; this._queue = []; this._connected = false; this._connectionPromise = new Promise(async (resolve, reject) => { try { // First, connect to create database if not exists const tempPool = new Pool({ host: config.host, port: config.port || 5432, user: config.user, password: config.password, database: 'postgres' // Connect to default database first }); try { await tempPool.query(`CREATE DATABASE "${config.database}"`); } catch (error) { // Database might already exist, ignore error if (!error.message.includes('already exists')) { console.warn('Database creation warning:', error.message); } } await tempPool.end(); // Now connect to the actual database this.pool = new Pool({ host: config.host, port: config.port || 5432, user: config.user, password: config.password, database: config.database, max: config.connectionLimit || 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); this._connected = true; resolve(this.pool); this._processQueue(); // Process any queued requests } catch (error) { console.error("PostgreSQL connection error:", error); reject(error); } }); } /** * Değerin türüne göre PostgreSQL column türünü otomatik belirler */ _getColumnType(value) { if (value === null || value === undefined) { return 'TEXT'; // Null değerler için varsayılan } if (typeof value === 'boolean') { return 'BOOLEAN'; } if (typeof value === 'number') { if (Number.isInteger(value)) { // Integer range kontrolü if (value >= -32768 && value <= 32767) { return 'SMALLINT'; } else if (value >= -2147483648 && value <= 2147483647) { return 'INTEGER'; } else { return 'BIGINT'; } } else { // Float/Double için return 'DOUBLE PRECISION'; } } if (typeof value === 'string') { const length = value.length; if (length <= 255) { return 'VARCHAR(255)'; } else { return 'TEXT'; } } if (typeof value === 'object') { // Array ve Object'ler JSONB olarak saklanır return 'JSONB'; } if (value instanceof Date) { return 'TIMESTAMP'; } // Varsayılan return 'TEXT'; } /** * Birden fazla değere göre en uygun column türünü belirler */ _getBestColumnType(values) { const types = values.map(val => this._getColumnType(val)); const uniqueTypes = [...new Set(types)]; // Eğer hepsi aynı türse, o türü kullan if (uniqueTypes.length === 1) { return uniqueTypes[0]; } // Mixed türler için öncelik sırası const typePriority = { 'TEXT': 10, 'JSONB': 9, 'VARCHAR(255)': 8, 'TIMESTAMP': 7, 'DOUBLE PRECISION': 6, 'BIGINT': 5, 'INTEGER': 4, 'SMALLINT': 3, 'BOOLEAN': 2 }; // En yüksek öncelikli türü seç let bestType = uniqueTypes[0]; let bestPriority = typePriority[bestType] || 0; for (const type of uniqueTypes) { const priority = typePriority[type] || 0; if (priority > bestPriority) { bestType = type; bestPriority = priority; } } return bestType; } /** * Eksik kolonları kontrol eder ve ekler */ async _ensureMissingColumns(table, data) { const existingColumns = await this.query(` SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' `, [table]); if (!existingColumns || existingColumns.length === 0) { throw new Error(`Table ${table} does not exist.`); } const existingColumnNames = existingColumns.map(col => col.column_name); for (const key of Object.keys(data)) { if (!existingColumnNames.includes(key)) { const columnType = this._getColumnType(data[key]); const alterSQL = `ALTER TABLE "${table}" ADD COLUMN "${key}" ${columnType}`; await this.query(alterSQL); console.log(`Added missing column '${key}' to table '${table}' with type ${columnType}`); } } } async _queueRequest(operation) { if (this._connected) { return operation(); } else { return new Promise((resolve, reject) => { this._queue.push({ operation, resolve, reject }); }); } } async _processQueue() { if (!this._connected) return; while (this._queue.length > 0) { const { operation, resolve, reject } = this._queue.shift(); try { const result = await operation(); resolve(result); } catch (error) { reject(error); } } } async query(sql, params = []) { return this._queueRequest(async () => { const pool = await this._connectionPromise; const result = await pool.query(sql, params); return result.rows; }); } async ensureTable(table, data = {}) { return this._queueRequest(async () => { 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 columnDefinitions = Object.keys(data).map(col => { const columnType = this._getColumnType(data[col]); return `"${col}" ${columnType}`; }); let columnsPart = ''; if (columnDefinitions.length > 0) { columnsPart = ', ' + columnDefinitions.join(", "); } const createTableSQL = ` CREATE TABLE "${table}" ( "_id" SERIAL PRIMARY KEY ${columnsPart} ) `; await this.query(createTableSQL); } }); } async insert(table, data) { return this._queueRequest(async () => { const copy = { ...data }; await this.ensureTable(table, copy); // Eksik kolonları ekle await this._ensureMissingColumns(table, copy); const existingColumns = await this.query(` SELECT column_name, column_default, is_nullable, data_type FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' `, [table]); const primaryKeyColumn = existingColumns.find(col => col.column_default && col.column_default.includes('nextval') ); const insertData = { ...copy }; // Remove the auto-incrementing primary key from insertData if it's present if (primaryKeyColumn && insertData[primaryKeyColumn.column_name] !== undefined) { delete insertData[primaryKeyColumn.column_name]; } const keys = Object.keys(insertData); const placeholders = keys.map((_, index) => `$${index + 1}`).join(","); const values = Object.values(insertData).map(value => this._serializeValue(value)); const sql = `INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(",")}) VALUES (${placeholders}) RETURNING "_id"`; const result = await this.query(sql, values); return result[0]._id; }); } async update(table, data, where) { return this._queueRequest(async () => { await this.ensureTable(table, { ...data, ...where }); // Eksik kolonları ekle await this._ensureMissingColumns(table, { ...data, ...where }); const setString = Object.keys(data).map((k, i) => `"${k}" = $${i + 1}`).join(", "); const whereString = Object.keys(where).map((k, i) => `"${k}" = $${Object.keys(data).length + i + 1}`).join(" AND "); const sql = `UPDATE "${table}" SET ${setString} WHERE ${whereString}`; const result = await this.query(sql, [...Object.values(data).map(v => this._serializeValue(v)), ...Object.values(where).map(v => this._serializeValue(v))]); return result.length; // PostgreSQL doesn't return affectedRows in the same way }); } async delete(table, where) { return this._queueRequest(async () => { if (!where || Object.keys(where).length === 0) return 0; await this.ensureTable(table, { ...where }); // Eksik kolonları ekle (where koşulları için) await this._ensureMissingColumns(table, where); const whereString = Object.keys(where).map((k, i) => `"${k}" = $${i + 1}`).join(" AND "); const sql = `DELETE FROM "${table}" WHERE ${whereString}`; const result = await this.query(sql, Object.values(where).map(v => this._serializeValue(v))); return result.length; }); } async select(table, where = null) { return this._queueRequest(async () => { await this.ensureTable(table, where || {}); // Eğer where koşulu varsa, eksik kolonları ekle if (where && Object.keys(where).length > 0) { await this._ensureMissingColumns(table, where); } let sql = `SELECT * FROM "${table}"`; let params = []; if (where && Object.keys(where).length > 0) { const whereString = Object.keys(where).map((k, i) => `"${k}" = $${i + 1}`).join(" AND "); sql += ` WHERE ${whereString}`; params = Object.values(where).map(v => this._serializeValue(v)); } const rows = await this.query(sql, params); return rows.map(row => { const newRow = {}; for (const key in row) { newRow[key] = this._deserializeValue(row[key]); } return newRow; }); }); } async set(table, data, where) { return this._queueRequest(async () => { await this.ensureTable(table, { ...data, ...where }); // Eksik kolonları ekle await this._ensureMissingColumns(table, { ...data, ...where }); const existing = await this.select(table, where); if (existing.length === 0) { return await this.insert(table, { ...where, ...data }); } else { return await this.update(table, data, where); } }); } async selectOne(table, where = null) { return this._queueRequest(async () => { const results = await this.select(table, where); if (results[0]) { const newResult = {}; for (const key in results[0]) { newResult[key] = this._deserializeValue(results[0][key]); } return newResult; } return null; }); } async deleteOne(table, where) { return this._queueRequest(async () => { await this.ensureTable(table, where); // Eksik kolonları ekle (where koşulları için) await this._ensureMissingColumns(table, where); const row = await this.selectOne(table, where); if (!row) return 0; const whereString = Object.keys(where).map((k, i) => `"${k}" = $${i + 1}`).join(" AND "); const sql = `DELETE FROM "${table}" WHERE ${whereString} AND ctid = (SELECT ctid FROM "${table}" WHERE ${whereString} LIMIT 1)`; const result = await this.query(sql, [...Object.values(where).map(v => this._serializeValue(v)), ...Object.values(where).map(v => this._serializeValue(v))]); return result.length; }); } async updateOne(table, data, where) { return this._queueRequest(async () => { await this.ensureTable(table, { ...data, ...where }); // Eksik kolonları ekle await this._ensureMissingColumns(table, { ...data, ...where }); const setString = Object.keys(data).map((k, i) => `"${k}" = $${i + 1}`).join(", "); const whereString = Object.keys(where).map((k, i) => `"${k}" = $${Object.keys(data).length + i + 1}`).join(" AND "); const sql = `UPDATE "${table}" SET ${setString} WHERE ${whereString} AND ctid = (SELECT ctid FROM "${table}" WHERE ${whereString} LIMIT 1)`; const result = await this.query(sql, [...Object.values(data).map(v => this._serializeValue(v)), ...Object.values(where).map(v => this._serializeValue(v)), ...Object.values(where).map(v => this._serializeValue(v))]); return result.length; }); } async bulkInsert(table, dataArray) { return this._queueRequest(async () => { if (!Array.isArray(dataArray) || dataArray.length === 0) return 0; await this.ensureTable(table, dataArray[0]); const existingColumns = await this.query(` SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' `, [table]); const existingColumnNames = existingColumns.map(col => col.column_name); // Tüm datalardan gelen tüm anahtarları topla const allKeys = new Set(); dataArray.forEach(obj => { Object.keys(obj).forEach(key => allKeys.add(key)); }); // Eksik kolonları kontrol et ve ekle for (const key of allKeys) { if (!existingColumnNames.includes(key)) { // Tüm değerleri kontrol ederek en uygun türü belirle const columnValues = dataArray .map(obj => obj[key]) .filter(val => val !== undefined && val !== null); const columnType = columnValues.length > 0 ? this._getBestColumnType(columnValues) : 'TEXT'; await this.query(`ALTER TABLE "${table}" ADD COLUMN "${key}" ${columnType}`); console.log(`Added missing column '${key}' to table '${table}' with type ${columnType}`); } } const keys = Array.from(allKeys); // PostgreSQL için VALUES clause oluştur const placeholders = dataArray.map((_, rowIndex) => `(${keys.map((_, colIndex) => `$${rowIndex * keys.length + colIndex + 1}`).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.length; }); } async close() { if (this.pool) await this.pool.end(); } // Helper to serialize values for storage _serializeValue(value) { if (value instanceof Date) { return value.toISOString(); // PostgreSQL TIMESTAMP format } if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { return JSON.stringify(value); // PostgreSQL will handle JSONB conversion } return value; } // Helper to deserialize values after retrieval _deserializeValue(value) { // PostgreSQL JSONB columns are automatically parsed by pg driver // No need for manual JSON parsing unlike MySQL return value; } /** * WHERE clause oluşturur (PostgreSQL $n syntax için) * @param {object} where - WHERE koşulları * @param {number} startIndex - Başlangıç parameter indeksi * @returns {object} - whereClause string ve values array */ buildWhereClause(where = {}, startIndex = 0) { const conditions = Object.keys(where); if (conditions.length === 0) { return { whereClause: '', values: [] }; } const whereClause = ' WHERE ' + conditions.map((key, index) => `${key} = $${startIndex + index + 1}` ).join(' AND '); const values = Object.values(where).map(v => this._serializeValue(v)); return { whereClause, values }; } /** * Numerik alanları artırır (increment). * @param {string} table - Verinin güncelleneceği tablo adı. * @param {object} increments - Artırılacak alanlar ve miktarları. * @param {object} where - Güncelleme koşulları. * @returns {Promise<number>} Etkilenen kayıt sayısı. */ async increment(table, increments, where = {}) { const incrementClauses = Object.keys(increments).map((field, index) => `${field} = ${field} + $${index + 1}` ).join(', '); const incrementValues = Object.values(increments); const { whereClause, values: whereValues } = this.buildWhereClause(where, incrementValues.length); const sql = `UPDATE ${table} SET ${incrementClauses}${whereClause}`; const allValues = [...incrementValues, ...whereValues]; const result = await this.pool.query(sql, allValues); return result.rowCount; } /** * Numerik alanları azaltır (decrement). * @param {string} table - Verinin güncelleneceği tablo adı. * @param {object} decrements - Azaltılacak alanlar ve miktarları. * @param {object} where - Güncelleme koşulları. * @returns {Promise<number>} Etkilenen kayıt sayısı. */ async decrement(table, decrements, where = {}) { const decrementClauses = Object.keys(decrements).map((field, index) => `${field} = ${field} - $${index + 1}` ).join(', '); const decrementValues = Object.values(decrements); const { whereClause, values: whereValues } = this.buildWhereClause(where, decrementValues.length); const sql = `UPDATE ${table} SET ${decrementClauses}${whereClause}`; const allValues = [...decrementValues, ...whereValues]; const result = await this.pool.query(sql, allValues); return result.rowCount; } } module.exports = PostgreSQLDatabase;