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