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.

526 lines (449 loc) 17.7 kB
const IDatabase = require('./IDatabase'); // Arayüzü import et /** * @implements {IDatabase} */ const mysql = require("mysql2/promise"); class MySQLDatabase extends IDatabase{ constructor(config) { super() this.config = config; this.pool = null; this._queue = []; this._connected = false; this._connectionPromise = new Promise(async (resolve, reject) => { try { const connection = await mysql.createConnection({ host: config.host, port: config.port || 3306, user: config.user, password: config.password, typeCast: function (field, next) { if (field.type === 'TINY' && field.length === 1) { return (field.string() === '1'); // Convert tinyint(1) to boolean } if (field.type === 'INT' || field.type === 'DECIMAL' || field.type === 'NEWDECIMAL' || field.type === 'FLOAT' || field.type === 'DOUBLE') { return Number(field.string()); // Convert numeric types to Number } return next(); } }); await connection.query(`CREATE DATABASE IF NOT EXISTS \`${config.database}\``); await connection.end(); this.pool = mysql.createPool({ host: config.host, port: config.port || 3306, user: config.user, password: config.password, database: config.database, waitForConnections: true, connectionLimit: config.connectionLimit || 10, queueLimit: 0, typeCast: function (field, next) { if (field.type === 'TINY' && field.length === 1) { return (field.string() === '1'); // Convert tinyint(1) to boolean } if (field.type === 'INT' || field.type === 'DECIMAL' || field.type === 'NEWDECIMAL' || field.type === 'FLOAT' || field.type === 'DOUBLE') { return Number(field.string()); // Convert numeric types to Number } return next(); } }); this._connected = true; resolve(this.pool); this._processQueue(); // Process any queued requests } catch (error) { console.error("MySQL connection error:", error); reject(error); } }); } /** * Değerin türüne göre MySQL 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 >= -128 && value <= 127) { return 'TINYINT'; } else if (value >= -32768 && value <= 32767) { return 'SMALLINT'; } else if (value >= -2147483648 && value <= 2147483647) { return 'INT'; } else { return 'BIGINT'; } } else { // Float/Double için return 'DOUBLE'; } } if (typeof value === 'string') { const length = value.length; if (length <= 255) { return 'VARCHAR(255)'; } else if (length <= 65535) { return 'TEXT'; } else if (length <= 16777215) { return 'MEDIUMTEXT'; } else { return 'LONGTEXT'; } } if (typeof value === 'object') { // Array ve Object'ler JSON olarak saklanır const jsonString = JSON.stringify(value); const length = jsonString.length; if (length <= 65535) { return 'JSON'; } else { return 'LONGTEXT'; } } if (value instanceof Date) { return 'DATETIME'; } // 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 = { 'LONGTEXT': 10, 'MEDIUMTEXT': 9, 'TEXT': 8, 'JSON': 7, 'VARCHAR(255)': 6, 'DATETIME': 5, 'DOUBLE': 4, 'BIGINT': 3, 'INT': 2, 'SMALLINT': 1, 'TINYINT': 0, 'BOOLEAN': -1 }; // 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(`DESCRIBE \`${table}\``).catch(() => null); if (!existingColumns) throw new Error(`Table ${table} does not exist.`); const existingColumnNames = existingColumns.map(col => col.Field); 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 [rows] = await pool.execute(sql, params); return rows; }); } async ensureTable(table, data = {}) { return this._queueRequest(async () => { const escapedTable = mysql.escape(table); const tables = await this.query(`SHOW TABLES LIKE ${escapedTable}`); 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 INT PRIMARY KEY AUTO_INCREMENT ${columnsPart} ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `; 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(`DESCRIBE \`${table}\``); const primaryKeyColumn = existingColumns.find(col => col.Key === 'PRI' && col.Extra.includes('auto_increment')); const insertData = { ...copy }; // Remove the auto-incrementing primary key from insertData if it's present if (primaryKeyColumn && insertData[primaryKeyColumn.Field] !== undefined) { delete insertData[primaryKeyColumn.Field]; } const keys = Object.keys(insertData); const placeholders = keys.map(() => "?").join(","); const values = Object.values(insertData).map(value => this._serializeValue(value)); const sql = `INSERT INTO \`${table}\` (${keys.map(k => `\`${k}\``).join(",")}) VALUES (${placeholders})`; const result = await this.query(sql, values); return result.insertId; }); } 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 => `\`${k}\` = ?`).join(", "); const whereString = Object.keys(where).map(k => `\`${k}\` = ?`).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.affectedRows; }); } 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 => `\`${k}\` = ?`).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.affectedRows; }); } 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 => `\`${k}\` = ?`).join(" AND "); sql += ` WHERE ${whereString}`; params = Object.values(where).map(v => this._serializeValue(v)); } return (await this.query(sql, params)).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 => `\`${k}\` = ?`).join(" AND "); const sql = `DELETE FROM \`${table}\` WHERE ${whereString} LIMIT 1`; const result = await this.query(sql, Object.values(where).map(v => this._serializeValue(v))); return result.affectedRows; }); } 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 => `\`${k}\` = ?`).join(", "); const whereString = Object.keys(where).map(k => `\`${k}\` = ?`).join(" AND "); const sql = `UPDATE \`${table}\` SET ${setString} 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))]); return result.affectedRows; }); } 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(`DESCRIBE \`${table}\``); const existingColumnNames = existingColumns.map(col => col.Field); // 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); 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 close() { if (this.pool) await this.pool.end(); } // Helper to serialize values for storage _serializeValue(value) { if (value instanceof Date) { return value.toISOString().slice(0, 19).replace('T', ' '); // MySQL DATETIME format } if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { return JSON.stringify(value); } return value; } // Helper to deserialize values after retrieval _deserializeValue(value) { try { // Attempt to parse only if it looks like a JSON string (e.g., starts with [ or {) if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) { const parsed = JSON.parse(value); // Ensure it was actually an object or array, not just a string that happened to be valid JSON if (typeof parsed === 'object' && parsed !== null) { return parsed; } } } catch (e) { // Not a valid JSON string, return original value } return value; } /** * WHERE clause oluşturur * @param {object} where - WHERE koşulları * @returns {object} - whereClause string ve values array */ buildWhereClause(where = {}) { const conditions = Object.keys(where); if (conditions.length === 0) { return { whereClause: '', values: [] }; } const whereClause = ' WHERE ' + conditions.map(key => `\`${key}\` = ?`).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 => `${field} = ${field} + ?` ).join(', '); const incrementValues = Object.values(increments); const { whereClause, values: whereValues } = this.buildWhereClause(where); const sql = `UPDATE ${table} SET ${incrementClauses}${whereClause}`; const allValues = [...incrementValues, ...whereValues]; const [result] = await this.connection.execute(sql, allValues); return result.affectedRows; } /** * 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 => `${field} = ${field} - ?` ).join(', '); const decrementValues = Object.values(decrements); const { whereClause, values: whereValues } = this.buildWhereClause(where); const sql = `UPDATE ${table} SET ${decrementClauses}${whereClause}`; const allValues = [...decrementValues, ...whereValues]; const [result] = await this.connection.execute(sql, allValues); return result.affectedRows; } } module.exports = MySQLDatabase;