UNPKG

@teamnet/ic-orm

Version:

Database Management System for Total.js v4 and standalone

1,275 lines (1,125 loc) 36.7 kB
const MongoDB = require('mongodb'); const MongoClient = MongoDB.MongoClient; const EMPTYARRAY = []; const EMPTYOBJECT = {}; const INSERTPROJECTION = { projection: { _id: 1 } }; // BUCKETNAME ya no se usa como constante global mutable // Se crea localmente en cada función para evitar race conditions const BLACKLIST = { dbms: 1 }; // Soporte para ObjectId con compatibilidad hacia atrás global.ObjectID = MongoDB.ObjectId || MongoDB.ObjectID; // Instancia compartida de MongoClient let sharedClient = null; // Map para rastrear intentos de reconexión por conexión (thread-safe) const reconnectAttemptsMap = new Map(); const MAX_RECONNECT_ATTEMPTS = 5; const RECONNECT_INTERVAL = 2000; // 2 segundos /** * Función para cerrar la conexión a MongoDB * @param {Function} [callback] - Función opcional que se ejecutará después de cerrar la conexión * @returns {Promise<void>} - Promesa que se resuelve cuando la conexión se ha cerrado */ exports.disconnect = function (callback) { return new Promise((resolve, reject) => { if (!sharedClient) { if (callback) callback(); return resolve(); } try { if (sharedClient.topology && sharedClient.topology.isConnected()) { sharedClient.close(false, () => { console.log('Conexión a MongoDB cerrada explícitamente'); sharedClient = null; if (callback) callback(); resolve(); }); } else { sharedClient = null; if (callback) callback(); resolve(); } } catch (err) { console.error('Error al cerrar la conexión con MongoDB:', err); sharedClient = null; if (callback) callback(err); reject(err); } }); }; /** * Intenta reconectar con MongoDB después de un error * @param {Object} options - Opciones de conexión * @param {Function} callback - Callback a ejecutar después de la reconexión */ function attemptReconnect(options, callback) { // Crear clave única para esta conexión (thread-safe) var connectionKey = (options && options.options) ? options.options : JSON.stringify(options); var attempts = (reconnectAttemptsMap.get(connectionKey) || 0) + 1; if (attempts > MAX_RECONNECT_ATTEMPTS) { console.error( `Máximo número de intentos de reconexión (${MAX_RECONNECT_ATTEMPTS}) alcanzado para esta conexión` ); reconnectAttemptsMap.delete(connectionKey); return callback( new Error('No se pudo reconectar a MongoDB después de múltiples intentos') ); } reconnectAttemptsMap.set(connectionKey, attempts); console.log( `Intentando reconectar a MongoDB (intento ${attempts}/${MAX_RECONNECT_ATTEMPTS})...` ); setTimeout(() => { // Extraer configuración de conexión var maxPoolSize = 10; var connectOptions = options; if (options && typeof options === 'object' && options.maxPoolSize) { maxPoolSize = options.maxPoolSize; connectOptions = options.options; } // Opciones para la reconexión (MongoDB Driver 4.x+) var reconnectOptions = {}; if (maxPoolSize) { reconnectOptions.maxPoolSize = maxPoolSize; } // MongoDB driver retorna Promise MongoClient.connect(connectOptions, reconnectOptions) .then(function(client) { console.log('Reconexión a MongoDB exitosa'); // Limpiar contador de intentos después de éxito reconnectAttemptsMap.delete(connectionKey); sharedClient = client; // Configurar manejadores de eventos setupEventHandlers(client, options); callback(null, sharedClient); }) .catch(function(err) { console.error('Error al reconectar a MongoDB:', err); return attemptReconnect(options, callback); }); }, RECONNECT_INTERVAL); } /** * Configura los manejadores de eventos para el cliente de MongoDB * @param {Object} client - Cliente de MongoDB * @param {Object} options - Opciones originales de conexión (para reconexión) */ function setupEventHandlers(client, originalOptions) { // Remover listeners previos para evitar duplicación client.removeAllListeners('close'); client.removeAllListeners('error'); client.removeAllListeners('timeout'); client.on('close', () => { console.log('Conexión a MongoDB cerrada'); if (sharedClient === client) { sharedClient = null; } }); client.on('error', (err) => { console.error('Error en la conexión a MongoDB:', err); // No intentamos reconectar automáticamente aquí porque el driver ya lo hace }); client.on('timeout', () => { console.warn('Timeout en la conexión a MongoDB'); }); } /** * Función para inicializar y obtener el cliente compartido de MongoDB * @param {Object|string} options - Opciones de conexión u objeto con configuración * @param {Function} callback - Callback a ejecutar con el cliente */ function getClient(options, callback) { if (sharedClient) { return callback(null, sharedClient); } // Configuraciones para el cliente MongoDB var mongoOptions = {}; // Extraer la URI de conexión y las opciones de configuración var connectURI; var maxPoolSize = 10; if (options && typeof options === 'object') { // Si recibimos un objeto de configuración desde CONN if (options.maxPoolSize) { maxPoolSize = parseInt(options.maxPoolSize, 10) || 10; } // La URI está directamente en options (no en options.options) connectURI = options.options || options; } else { // Si solo recibimos la URI como string connectURI = options; } // Si maxPoolSize no está en la URL, agregarlo if (connectURI && connectURI.indexOf('maxPoolSize=') === -1) { var separator = connectURI.indexOf('?') === -1 ? '?' : '&'; connectURI = connectURI + separator + 'maxPoolSize=' + maxPoolSize; } // IMPORTANTE: La URL de conexión puede contener timeouts y otras opciones // que tienen prioridad sobre mongoOptions. El driver de MongoDB parsea // la URL y aplica esos parámetros automáticamente. // Conectar usando la URI y las opciones del cliente // MongoDB driver retorna una Promise, usar then/catch MongoClient.connect(connectURI, mongoOptions) .then(function(client) { sharedClient = client; // Configurar manejadores de eventos setupEventHandlers(client, options); callback(null, sharedClient); }) .catch(function(err) { callback(err); }); } function select(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); var options = {}; var fields = FIELDS(builder); if (fields) options.projection = fields; if (opt.take) options.limit = opt.take; if (opt.skip) options.skip = opt.skip; if (filter.sort) options.sort = filter.sort; builder.db.$debug && builder.db.$debug({ collection: client.$database + '.' + opt.table, condition: filter.where, options: options, }); // toArray() retorna Promise en MongoDB driver 4.x+ client .db(client.$database) .collection(opt.table) .find(filter.where, options) .toArray() .then(function(response) { var rows = response ? response : EMPTYARRAY; if (opt.first) rows = rows.length ? rows[0] : null; // Execute data callback if defined if (opt.callbackok) { opt.callbackok(rows, opt.callbackokparam); } // checks joins if (builder.$joins) { client.$dbms._join(rows, builder); setImmediate(builder.db.$next); } else builder.$callback(null, rows); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); } function bigselect(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); var options = {}; var fields = FIELDS(builder); if (fields) options.projection = fields; if (opt.take) options.limit = opt.take; if (opt.skip) options.skip = opt.skip; if (!filter.sort) filter.sort = { _id: 1 }; builder.db.$debug && builder.db.$debug({ collection: client.$database + '.' + opt.table, condition: filter.where, options: options, }); client .db(client.$database) .collection(opt.table) .find(filter.where, options) .sort(filter.sort) .allowDiskUse() .toArray() .then(function(response) { var rows = response ? response : EMPTYARRAY; if (opt.first) rows = rows.length ? rows[0] : null; // checks joins if (builder.$joins) { client.$dbms._join(rows, builder); setImmediate(builder.db.$next); } else builder.$callback(null, rows); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); } function query(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); var options = {}; var fields = FIELDS(builder); if (fields) options.projection = fields; if (opt.take) options.limit = opt.take; if (opt.skip) options.skip = opt.skip; if (filter.sort) options.sort = filter.sort; var col = client.db(client.$database).collection(cmd.query); cmd.value( col, function (err, response) { if (opt.first && Array.isArray(response)) response = response[0]; err && client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, response); }, filter.where, options ); } function listCollections(client, cmd) { var builder = cmd.builder; var opt = builder.options; builder.db.$debug && builder.db.$debug({ collection: client.$database, }); client .db(client.$database) .listCollections() .toArray() .then(function(response) { builder.$callback(null, response); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); } async function listDatabases(client, cmd) { var builder = cmd.builder; var opt = builder.options; builder.db.$debug && builder.db.$debug({ collection: client.$database, }); try { var result = await client.db(client.$database).admin().listDatabases(); builder.$callback(null, result); } catch (err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); } } function pipeLines(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); builder.db.$debug && builder.db.$debug({ collection: client.$database + '.' + opt.table, condition: filter.where, }); client .db(client.$database) .collection(opt.table) .aggregate([filter, cmd.improved]) .toArray() .then(function(response) { builder.$callback(null, response); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); } function list(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); var options = {}; var fields = FIELDS(builder); if (fields) options.projection = fields; if (opt.take) options.limit = opt.take; if (opt.skip) options.skip = opt.skip; if (filter.sort) options.sort = filter.sort; builder.db.$debug && builder.db.$debug({ collection: client.$database + '.' + opt.table, condition: filter.where, options: options, }); var db = client.db(client.$database).collection(opt.table); // Optimización: usar estimatedDocumentCount cuando no hay filtros (100x más rápido) var hasFilter = filter.where && Object.keys(filter.where).length > 0; var countPromise = hasFilter ? db.countDocuments(filter.where) : db.estimatedDocumentCount(); countPromise .then(function(count) { return db.find(filter.where, options).toArray() .then(function(response) { if (builder.$joins) { client.$dbms._joins(response, builder, count || 0); setImmediate(builder.db.$next); } else { builder.$callback(null, response, count); } }); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null, 0); }); } function scalar(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder, true); var cmdgroup = '$' + cmd.name; // builder.db.$debug && builder.db.$debug(q); switch (cmd.scalar) { case 'count': client .db(client.$database) .collection(opt.table) .aggregate([filter, { $count: 'count' }]) .toArray() .then(function(response) { var rescount = 0; if (response.length) rescount = response[0].count; builder.$callback(null, rescount); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); break; case 'avg': client .db(client.$database) .collection(opt.table) .aggregate([ filter, { $group: { _id: '', average: { $avg: cmdgroup } } }, ]) .toArray() .then(function(response) { var result = (response && response.length > 0 && response[0]) ? response[0].average : null; builder.$callback(null, result); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); break; case 'min': client .db(client.$database) .collection(opt.table) .aggregate([filter, { $group: { _id: '', min: { $min: cmdgroup } } }]) .toArray() .then(function(response) { var result = (response && response.length > 0 && response[0]) ? response[0].min : null; builder.$callback(null, result); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); break; case 'max': client .db(client.$database) .collection(opt.table) .aggregate([filter, { $group: { _id: '', max: { $max: cmdgroup } } }]) .toArray() .then(function(response) { var result = (response && response.length > 0 && response[0]) ? response[0].max : null; builder.$callback(null, result); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); break; case 'sum': client .db(client.$database) .collection(opt.table) .aggregate([filter, { $group: { _id: '', sum: { $sum: cmdgroup } } }]) .toArray() .then(function(response) { var result = (response && response.length > 0 && response[0]) ? response[0].sum : null; builder.$callback(null, result); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); break; case 'group': client .db(client.$database) .collection(opt.table) .aggregate([filter, { $count: 'count' }]) .toArray() .then(function(count) { return client .db(client.$database) .collection(opt.table) .aggregate([ filter, { $group: { _id: cmdgroup, count: { $sum: 1 } } }, { $sort: { count: -1 } }, ]) .toArray() .then(function(response) { var obj = {}; if (count && count[0] && count[0].count) obj.total = count[0].count; else obj.total = 0; obj.items = response; builder.$callback(null, obj); }); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); break; } } async function insert(client, cmd) { var builder = cmd.builder; var keys = Object.keys(cmd.builder.value); var opt = builder.options; var params = {}; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var val = cmd.builder.value[key]; if (val === undefined || BLACKLIST[key]) continue; if (cmd.builder.options.fields && cmd.builder.options.fields.length) { var skip = true; for (var j = 0; j < cmd.builder.options.fields.length; j++) { if ( cmd.builder.options.fields[j] == key || cmd.builder.options.fields[j] == key.substring(1) ) { skip = false; break; } } if (skip) continue; } switch (key[0]) { case '&': key = key.substring(1); var gencode = val; gencode.key = key; break; case '%': key = key.substring(1); var gensearch = val; gensearch.key = key; break; case '-': case '+': case '*': case '/': key = key.substring(1); break; case '<': case '>': key = key.substring(1); break; } params[key] = val == null ? null : typeof val === 'function' ? val(cmd.builder.value) : val; } if (gencode) { var db = DBMS(); var codegen = await db .one(gencode.indexes) .where('code', gencode.precode) .promise(); db.modify(gencode.indexes, { '+index': +1 }).where('code', gencode.precode); var indexcode = codegen.index + 1; indexcode = indexcode.toString(); while (indexcode.length < gencode.output) { indexcode = '0' + indexcode; } params[gencode.key] = gencode.precode + indexcode; if (gensearch) { var one = gensearch.one; var two = gensearch.two; params[gensearch.key] = params[one] + gensearch.join + params[two]; } } builder.db.$debug && builder.db.$debug(); client .db(client.$database) .collection(opt.table) .insertOne(params) .then(function(response) { // En MongoDB driver 4.x+: response.acknowledged, response.insertedId // En driver 3.x: response.result.n var count = response && (response.acknowledged || (response.result && response.result.n)) ? 1 : 0; builder.$callback(null, params, count); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null, 0); }); } async function insertexists(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); // builder.db.$debug && builder.db.$debug(q); try { var response = await client .db(client.$database) .collection(opt.table) .findOne(filter.where, INSERTPROJECTION); if (response) { builder.$callback(null, 0); } else { // insert es async, debemos esperarlo await insert(client, cmd); } } catch(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, 0); } } function modify(client, cmd) { cmd.builder.options.transform && cmd.builder.options.transform( cmd.builder.value, cmd.builder.db.$output, cmd.builder.db.$lastoutput ); var keys = Object.keys(cmd.builder.value); var params = null; var increment = null; var multiply = null; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var val = cmd.builder.value[key]; if (val === undefined || BLACKLIST[key]) continue; if (cmd.builder.options.fields && cmd.builder.options.fields.length) { var skip = true; for (var j = 0; j < cmd.builder.options.fields.length; j++) { if ( cmd.builder.options.fields[j] == key || cmd.builder.options.fields[j] == key.substring(1) ) { skip = false; break; } } if (skip) continue; } var c = key[0]; if (typeof val === 'function') val = val(cmd.builder.value); switch (c) { case '+': !increment && (increment = {}); key = key.substring(1); increment[key] = val ? val : 0; break; case '-': !increment && (increment = {}); key = key.substring(1); // Usar valor negativo para decrementar increment[key] = val ? -Math.abs(val) : 0; break; case '*': !multiply && (multiply = {}); key = key.substring(1); multiply[key] = val ? val : 1; break; case '/': !multiply && (multiply = {}); key = key.substring(1); // División se implementa como multiplicación por el inverso multiply[key] = val ? (1 / val) : 1; break; case '<': case '>': key = key.substring(1); break; default: !params && (params = {}); params[key] = val; break; } } var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); // builder.db.$debug && builder.db.$debug(q); var upd = {}; increment && (upd.$inc = increment); multiply && (upd.$mul = multiply); params && (upd.$set = params); var col = client.db(client.$database).collection(opt.table); var updatePromise = opt.first ? col.updateOne(filter.where, upd) : col.updateMany(filter.where, upd); updatePromise .then(function(response) { // En MongoDB driver 4.x+: response.matchedCount (documentos encontrados) o response.modifiedCount (documentos modificados) // Usar matchedCount porque si el documento existe pero el valor no cambió, modifiedCount será 0 var count = response && (response.matchedCount !== undefined ? response.matchedCount : (response.modifiedCount || 0)); if (!count && cmd.insert) { if (cmd.insert !== true) cmd.builder.value = cmd.insert; cmd.builder.options.insert && cmd.builder.options.insert( cmd.builder.value, cmd.builder.options.insertparams ); return insert(client, cmd); } else { builder.$callback(null, count); } }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); } function remove(client, cmd) { var builder = cmd.builder; var opt = builder.options; var filter = WHERE(builder); // builder.db.$debug && builder.db.$debug(q); var col = client.db(client.$database).collection(opt.table); var deletePromise = opt.first ? col.deleteOne(filter.where) : col.deleteMany(filter.where); deletePromise .then(function(response) { // En MongoDB driver 4.x+: response.deletedCount // En driver 3.x: response.result.n var count = response && (response.deletedCount !== undefined ? response.deletedCount : (response.result ? response.result.n : 0)); builder.$callback(null, count); }) .catch(function(err) { client.$opt.onerror && client.$opt.onerror(err, opt, builder); builder.$callback(err, null); }); } exports.run = function(opt, self, cmd) { // Pasar la configuración completa a getClient, incluyendo maxPoolSize si existe var clientOptions = { options: opt.options, // URI de conexión maxPoolSize: opt.maxPoolSize || 10 // Usar maxPoolSize de la configuración o valor por defecto }; // A partir de este punto getClient se encarga de extraer maxPoolSize correctamente getClient(clientOptions, function(err, client) { if (err) { opt.onerror && opt.onerror(err); if (cmd.builder) cmd.builder.$callback(err); else cmd.db.$next(err); return; } // Configurar propiedades necesarias client.$opt = opt; client.$dbms = self; client.$database = opt.database; // Ejecutar la operación correspondiente switch (cmd.type) { case 'transaction': case 'commit': case 'rollback': cmd.$next(new Error('"' + cmd.type + '" no está implementado.')); break; case 'bigFind': bigselect(client, cmd); break; case 'find': case 'read': select(client, cmd); break; case 'diff': var cb = cmd.builder.$callback; cmd.builder.$callback = function (err, response) { cb.call( cmd.builder, err, err ? EMPTYOBJECT : compareArrayDiff(cmd.key, response, [cmd.form]) ); }; select(client, cmd); break; case 'list': list(client, cmd); break; case 'listCollections': listCollections(client, cmd); break; case 'listDatabases': listDatabases(client, cmd); break; case 'pipeLines': pipeLines(client, cmd); break; case 'scalar': scalar(client, cmd); break; case 'insert': if (cmd.unique) insertexists(client, cmd); else insert(client, cmd); break; case 'update': case 'modify': modify(client, cmd); break; case 'remove': remove(client, cmd); break; case 'query': query(client, cmd); break; default: if (cmd.builder && cmd.builder.$callback) { cmd.builder.$callback( new Error('Operación "' + cmd.type + '" no encontrada') ); } else if (cmd.$callback) { cmd.$callback( new Error('Operación "' + cmd.type + '" no encontrada') ); } break; } }); }; exports.blob_read = function (opt, id, callback, conn) { // Pasar maxPoolSize correctamente var clientOptions = { options: opt.options, maxPoolSize: opt.maxPoolSize || 10 }; getClient(clientOptions, function (err, client) { if (err) { callback(err); return; } // Crear bucketName local para evitar race condition var bucketName = (conn.table && conn.table !== 'default') ? conn.table : 'db'; var bucket = new MongoDB.GridFSBucket(client.db(opt.database), { bucketName: bucketName }); var stream = bucket.openDownloadStream(id); stream.on('error', function(streamErr) { callback(streamErr); stream.destroy(); }); stream.on('end', function() { // Stream terminado correctamente }); callback(null, stream); }); }; exports.blob_write = function (opt, stream, name, callback, conn) { // Pasar maxPoolSize correctamente var clientOptions = { options: opt.options, maxPoolSize: opt.maxPoolSize || 10 }; getClient(clientOptions, function (err, client) { if (err) { callback(err); return; } // Crear bucketName local para evitar race condition var bucketName = (conn.table && conn.table !== 'default') ? conn.table : 'db'; var bucketOptions = { bucketName: bucketName }; // Añadir contentType si está disponible if (global.U) { bucketOptions.contentType = U.getContentType(U.getExtension(name)); } var bucket = new MongoDB.GridFSBucket(client.db(opt.database), { bucketName: bucketName }); var writer = bucket.openUploadStream(name, bucketOptions.contentType ? { contentType: bucketOptions.contentType } : null); stream .pipe(writer) .on('error', function (err) { writer.destroy(); callback(err); }) .on('finish', function () { callback(null, writer.id); }); }); }; exports.blob_remove = function (opt, id, callback, conn) { // Pasar maxPoolSize correctamente var clientOptions = { options: opt.options, maxPoolSize: opt.maxPoolSize || 10 }; getClient(clientOptions, function (err, client) { if (err) { callback && callback(err); return; } // Crear bucketName local para evitar race condition var bucketName = (conn.table && conn.table !== 'default') ? conn.table : 'db'; var bucket = new MongoDB.GridFSBucket(client.db(opt.database), { bucketName: bucketName }); bucket.delete(id, function (err) { callback && callback(err); }); }); }; function eqgtlt(cmd) { var val; if (cmd.compare === '=' || cmd.compare === '==') { val = { '$eq': cmd.value }; } else if (cmd.compare === '>' || cmd.compare === 'gt') { val = { '$gt': cmd.value }; } else if (cmd.compare === '>=' || cmd.compare === 'gte') { val = { '$gte': cmd.value }; } else if (cmd.compare === '<' || cmd.compare === 'lt') { val = { '$lt': cmd.value }; } else if (cmd.compare === '<=' || cmd.compare === 'lte') { val = { '$lte': cmd.value }; } return val; } /** * Función para generar un array de diferencias entre objeto y formulario * @param {string} key - Clave primaria * @param {Object} data - Datos de la base de datos * @param {Array} form - Formulario con datos a comparar * @returns {Array} - Array con diferencias */ function compareArrayDiff(key, data, form) { var arr = []; var tmp; if (!(data instanceof Array)) { if (!data) return EMPTYARRAY; tmp = []; tmp.push(data); data = tmp; } if (!(form instanceof Array)) { tmp = []; tmp.push(form); form = tmp; } for (var i = 0; i < form.length; i++) { var keys = Object.keys(form[i]); for (var j = 0; j < data.length; j++) { if (data[j][key] == form[i][key]) { var json = {}; json[key] = form[i][key]; var b = false; for (var k = 0; k < keys.length; k++) { var m = keys[k]; if (form[i][m] != data[j][m]) { json[m] = form[i][m]; json['old_' + m] = data[j][m]; b = true; } } if (b) arr.push(json); } } } return arr; } function WHERE(builder, scalar) { // , group var condition = {}; var sort = null; var tmp = null; var filter; var value; for (var i = 0; i < builder.$commands.length; i++) { var cmd = builder.$commands[i]; switch (cmd.type) { case 'where': value = cmd.compare === '=' ? cmd.value : eqgtlt(cmd); // Solo convertir a ObjectId si el campo ES _id (no convertir 'id' a '_id') if (cmd.name === '_id' && typeof value === 'string') { try { value = new ObjectID(value); } catch(e) { // Si falla la conversión, dejar como string } } if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'in': if (typeof cmd.value === 'function') cmd.value = cmd.value(); value = cmd.value instanceof Array ? { $in: cmd.value } : cmd.value; // Solo convertir a ObjectId si el campo ES _id (no convertir 'id' a '_id') if (cmd.name === '_id' && value.$in && Array.isArray(value.$in)) { value.$in = value.$in.map(function(v) { if (typeof v === 'string') { try { return new ObjectID(v); } catch(e) { return v; } } return v; }); } if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'notin': if (typeof cmd.value === 'function') cmd.value = cmd.value(); value = cmd.value instanceof Array ? { $nin: cmd.value } : { $ne: cmd.value }; if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'between': value = { $gte: cmd.a, $lte: cmd.b, $exists: true, $nin: [null, ''] }; if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'search': value = (cmd.compare === '*' ? '' : cmd.compare === 'beg' ? '^' : '') + // Escapar caracteres especiales de RegEx en cmd.value String(cmd.value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + (cmd.compare === 'end' ? '$' : ''); var v = ['[aáä]', '[eéë]', '[iíï]', '[oóö]', '[uúü]']; for (let i = 0; i < v.length; i++) value = value.replace(new RegExp(v[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), v[i]); value = { $regex: value, $options: 'i' }; if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'fulltext': value = new RegExp( (cmd.compare === '*' ? '' : cmd.compare === 'beg' ? '^' : '') + // Escapar caracteres especiales de RegEx en cmd.value String(cmd.value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + (cmd.compare === 'end' ? '$' : ''), 'i' ); if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'contains': value = { $exists: true, $nin: [null, ''] }; if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'empty': value = { $exists: false, $in: [null, ''] }; if (tmp) { filter = {}; filter[cmd.name] = value; tmp.push(filter); } else condition[cmd.name] = value; break; case 'month': case 'year': case 'day': case 'hour': case 'minute': throw new Error( "MongoDB doesn't support date function '" + cmd.type + "' for querying. Use $expr or aggregation pipeline instead." ); case 'query': if (tmp) { filter = {}; tmp.push(cmd.query); } else { var arr = Object.keys(cmd.query); for (var j = 0; j < arr.length; j++) condition[arr[j]] = cmd.query[arr[j]]; } break; case 'or': tmp = []; continue; case 'end': condition.$or = tmp; tmp = null; break; case 'and': break; case 'sort': !sort && (sort = {}); sort[cmd.name] = cmd.desc ? -1 : 1; break; case 'regexp': if (tmp) { filter = {}; filter[cmd.name] = cmd.value; tmp.push(filter); } else condition[cmd.name] = cmd.value; break; } } if (scalar) return { $match: condition }; else return { where: condition, sort: sort }; } function FIELDS(builder) { var output = null; var fields = builder.options.fields; if (fields && fields.length) { output = {}; for (var i = 0; i < fields.length; i++) { var name = fields[i]; var is = name[0] === '-'; if (is) name = name.substring(1); output[name] = is ? 0 : 1; } if (builder.$joinmeta) output[builder.$joinmeta.a] = 1; } return output; } /** * Funcion para manejar el cierre de conexiones y terminar el proceso * @param {string} signal - Señal recibida (SIGINT, SIGTERM) */ function handleProcessTermination(signal) { console.log(`Recibida señal ${signal}, cerrando conexiones...`); exports .disconnect() .then(() => { console.log(`Conexiones cerradas correctamente debido a ${signal}`); // Dar un breve tiempo para que los logs se escriban antes de salir setTimeout(() => process.exit(0), 100); }) .catch((err) => { console.error(`Error al cerrar conexiones: ${err.message || err}`); process.exit(1); }); // Si el proceso no termina en 5 segundos, forzar la salida setTimeout(() => { console.error( `Tiempo de espera agotado para cerrar conexiones, forzando salida...` ); process.exit(1); }, 5000); } // Manejar señales de terminación del proceso process.on('SIGINT', () => handleProcessTermination('SIGINT')); process.on('SIGTERM', () => handleProcessTermination('SIGTERM'));