@teamnet/ic-orm
Version:
Database Management System for Total.js v4 and standalone
1,275 lines (1,125 loc) • 36.7 kB
JavaScript
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'));