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