smartport-lib
Version:
Librería de caché inteligente y gestión de endpoints API para SmartPort
810 lines (676 loc) • 36.3 kB
JavaScript
// SmartPort.js
const fs = require('fs');
const path = require('path');
const express = require('express');
class SmartPort {
constructor({ ev, apiUrl, endpoints, cacheDir = './cache', cacheDuration = 600000 }) {
this.apiUrl = apiUrl || process.env['SmartPort-ApiURL'] || '';
this.endpoints = endpoints;
this.cacheDir = cacheDir;
this.cacheDuration = cacheDuration;
this.cache = {};
this.defaultEv = ev || process.env['SmartPort-ev'] || 'undefined';
this.initCache(this.defaultEv);
}
async initCache(ev = 'undefined') {
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true });
}
for (const endpoint of this.endpoints) {
if (!this.cache[ev]) this.cache[ev] = {};
this.cache[ev][endpoint] = { data: [], timestamp: 0 };
await this.loadFromStorage(endpoint, ev);
}
}
async fetchFromAPI(endpoint, params = {}) {
try {
if (!params.ev) params.ev = this.defaultEv;
const fetch = (await import('node-fetch')).default;
const url = new URL(`${this.apiUrl}/${endpoint}`);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
console.log("URL: " + url);
const response = await fetch(url.toString());
if (!response.ok) throw new Error(`Error en ${endpoint}`);
return await response.json();
} catch (error) {
console.error(`Error obteniendo datos de ${endpoint}:`, error);
return null;
}
}
saveToStorage(endpoint, ev = 'undefined') {
const dir = path.join(this.cacheDir, ev);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filePath = path.join(dir, `${endpoint.replace(/\W/g, '_')}.json`);
let toTransform = this.cache[ev]?.[endpoint]?.data?.answer ?? this.cache[ev]?.[endpoint]?.data ?? this.cache[ev]?.[endpoint];
fs.writeFileSync(filePath, JSON.stringify(toTransform, null, 2));
}
loadFromStorage(endpoint, ev = 'undefined') {
const filePath = path.join(this.cacheDir, ev, `${endpoint.replace(/\W/g, '_')}.json`);
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
if (!this.cache[ev]) this.cache[ev] = {};
this.cache[ev][endpoint] = {
data, // permite objeto o array
timestamp: Date.now()
};
}
}
async updateCache(endpoint, params = {}) {
const ev = params.ev || this.defaultEv || 'undefined';
const data = await this.fetchFromAPI(endpoint, params);
if (data) {
if (!this.cache[ev]) this.cache[ev] = {};
this.cache[ev][endpoint] = { data: data.answer ?? data, timestamp: Date.now() };
this.saveToStorage(endpoint, ev);
}
}
async refresh(endpoint, params = {}) {
await this.updateCache(endpoint, params);
}
async getData(endpoint, { ev = "undefined", limit, skip = 0, sort, filter, search } = {}) {
if (!this.cache[ev] || !this.cache[ev][endpoint]) {
// this.loadFromStorage(endpoint, ev);
this.loadFromStorage(endpoint, ev);
// 🔁 Si aún así no existe, forzar actualización desde API
if (!this.cache[ev]?.[endpoint]) {
try {
// await this.initCache(ev);
await this.refresh(endpoint, {
ev,
// 'smartport-key': process.env['SmartPort-Key']
});
// Volver a intentar cargar desde disco
this.loadFromStorage(endpoint, ev);
} catch (err) {
console.error(`⚠️ Error forzando refresh de ${endpoint}:`, err.message);
}
}
}
const cached = this.cache[ev]?.[endpoint];
if (!cached || !cached.data) return [];
// let result = Array.isArray(cached.data)
// ? [...cached.data]
// : Object.values(cached.data);
let result = cached.data; // puede ser array o objeto
if (Array.isArray(result)) {
if (filter) {
if (filter.$or && Array.isArray(filter.$or)) {
result = result.filter(item =>
filter.$or.some(cond =>
Object.entries(cond).every(([key, val]) => {
const fieldVal = item[key];
// 🔍 Si no existe ese campo, no pasa
if (typeof fieldVal === 'undefined') return false;
// 🎯 Soporte para condiciones tipo { key: { $op: val } }
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
return Object.entries(val).every(([op, v]) => {
let compareVal = fieldVal;
if (compareVal?.$date?.$numberLong) compareVal = parseInt(compareVal.$date.$numberLong);
if (compareVal?.$oid) compareVal = compareVal.$oid;
switch (op) {
case '$in': return Array.isArray(v) && v.map(String).includes(String(compareVal));
case '$ne': return compareVal != v;
case '$regex': return new RegExp(v, 'i').test(compareVal);
default: return compareVal == v;
}
});
}
// 👇 Comparación directa
if (fieldVal?.$oid) return fieldVal.$oid == val;
if (typeof fieldVal === 'object' && '$date' in fieldVal)
return new Date(parseInt(fieldVal.$date.$numberLong)).getTime() == new Date(val).getTime();
return String(fieldVal) == String(val);
})
)
);
delete filter.$or;
}
result = result.filter(item =>
Object.entries(filter).every(([key, condition]) => {
// if(key == 'smartport-key') return;
const fieldVal = item[key];
if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
return Object.entries(condition).every(([op, val]) => {
let compareVal = fieldVal;
if (fieldVal?.$date?.$numberLong) {
compareVal = parseInt(fieldVal.$date.$numberLong);
}
if (fieldVal?.$oid) {
compareVal = fieldVal.$oid;
}
if ((op === '$gt' || op === '$lt') && typeof compareVal === 'number' && typeof val === 'string') {
if (val.includes('/')) {
const [day, month, year] = val.split('/');
val = new Date(`${year}-${month}-${day}`).getTime();
} else {
val = new Date(val).getTime();
}
}
switch (op) {
case '$in':
return Array.isArray(val) && val.map(v => String(v)).includes(String(compareVal));
case '$gt':
return compareVal > val;
case '$lt':
return compareVal < val;
case '$regex':
return new RegExp(val, 'i').test(compareVal);
case '$ne':
return compareVal !== val && compareVal != null;
case '$nin':
return Array.isArray(val) && !val.map(v => String(v)).includes(String(compareVal));
}
});
}
if (typeof condition === 'string') {
if (typeof fieldVal === 'string') return fieldVal.startsWith(condition);
if (Array.isArray(fieldVal)) return fieldVal.some(v => v.startsWith(condition));
if (typeof fieldVal === 'object') {
if ('$oid' in fieldVal) return fieldVal.$oid === condition;
if (fieldVal?.$date?.$numberLong) {
const itemDate = new Date(parseInt(fieldVal.$date.$numberLong));
let inputDate;
if (condition.includes('/')) {
const [day, month, year] = condition.split('/');
inputDate = new Date(`${year}-${month}-${day}`);
} else {
inputDate = new Date(condition);
}
return (
itemDate.getUTCFullYear() === inputDate.getUTCFullYear() &&
itemDate.getUTCMonth() === inputDate.getUTCMonth() &&
itemDate.getUTCDate() === inputDate.getUTCDate()
);
}
}
}
// return fieldVal === condition;
return fieldVal == condition;
})
);
}
if (sort) {
const [key, dir] = sort.split(':');
const order = dir?.toLowerCase();
if (order === 'random') {
result.sort(() => Math.random() - 0.5);
} else {
const sortOrder = order === 'desc' ? 'desc' : 'asc';
result.sort((a, b) => {
let aVal = a[key];
let bVal = b[key];
if (aVal?.$date?.$numberLong) aVal = parseInt(aVal.$date.$numberLong);
if (bVal?.$date?.$numberLong) bVal = parseInt(bVal.$date.$numberLong);
// Si son strings, quitar espacios al inicio y final
if (typeof aVal === 'string') aVal = aVal.trim();
if (typeof bVal === 'string') bVal = bVal.trim();
if (typeof aVal === 'string' && typeof bVal === 'string') {
return sortOrder === 'desc' ? bVal.localeCompare(aVal) : aVal.localeCompare(bVal);
}
return sortOrder === 'desc' ? (bVal > aVal ? 1 : -1) : (aVal > bVal ? 1 : -1);
});
}
}
if (search) {
const terms = search.toLowerCase().split(/\s+/);
result = result.filter(item => {
const searchable = Object.values(item).flatMap(val => {
if (typeof val === 'string') return val.toLowerCase();
if (typeof val === 'number') return val.toString();
if (val?.$oid) return val.$oid;
if (val?.$date?.$numberLong) {
const d = new Date(parseInt(val.$date.$numberLong));
return [d.toISOString().slice(0, 10), `${d.getUTCDate().toString().padStart(2, '0')}/${(d.getUTCMonth() + 1).toString().padStart(2, '0')}/${d.getUTCFullYear()}`];
}
try {
return JSON.stringify(val).toLowerCase();
} catch {
return '';
}
});
return terms.every(term => searchable.some(field => field.includes(term)));
});
}
if (skip || limit) {
const start = skip || 0;
const end = limit ? start + limit : undefined;
result = result.slice(start, end);
}
}
return result;
}
async getCount(endpoint, { ev = "undefined", filter, search } = {}) {
if (!this.cache[ev] || !this.cache[ev][endpoint]) {
this.loadFromStorage(endpoint, ev);
if (!this.cache[ev]?.[endpoint]) {
try {
await this.refresh(endpoint, { ev });
this.loadFromStorage(endpoint, ev);
} catch (err) {
console.error(`⚠️ Error forzando refresh de ${endpoint}:`, err.message);
return 0;
}
}
}
const cached = this.cache[ev]?.[endpoint];
if (!cached || !cached.data || !Array.isArray(cached.data)) return 0;
let result = cached.data;
// 🔎 aplicar filtros igual que en getData()
if (filter) {
if (filter.$or && Array.isArray(filter.$or)) {
result = result.filter(item =>
filter.$or.some(cond =>
Object.entries(cond).every(([key, val]) => {
const fieldVal = item[key];
// 🔍 Si no existe ese campo, no pasa
if (typeof fieldVal === 'undefined') return false;
// 🎯 Soporte para condiciones tipo { key: { $op: val } }
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
return Object.entries(val).every(([op, v]) => {
let compareVal = fieldVal;
if (compareVal?.$date?.$numberLong) compareVal = parseInt(compareVal.$date.$numberLong);
if (compareVal?.$oid) compareVal = compareVal.$oid;
switch (op) {
case '$in': return Array.isArray(v) && v.map(String).includes(String(compareVal));
case '$ne': return compareVal != v;
case '$regex': return new RegExp(v, 'i').test(compareVal);
default: return compareVal == v;
}
});
}
// 👇 Comparación directa
if (fieldVal?.$oid) return fieldVal.$oid == val;
if (typeof fieldVal === 'object' && '$date' in fieldVal)
return new Date(parseInt(fieldVal.$date.$numberLong)).getTime() == new Date(val).getTime();
return String(fieldVal) == String(val);
})
)
);
delete filter.$or;
}
result = result.filter(item =>
Object.entries(filter).every(([key, condition]) => {
const fieldVal = item[key];
if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
return Object.entries(condition).every(([op, val]) => {
let compareVal = fieldVal;
if (fieldVal?.$date?.$numberLong) compareVal = parseInt(fieldVal.$date.$numberLong);
if (fieldVal?.$oid) compareVal = fieldVal.$oid;
switch (op) {
case '$in':
return Array.isArray(val) && val.map(v => String(v)).includes(String(compareVal));
case '$gt':
return compareVal > val;
case '$lt':
return compareVal < val;
case '$regex':
return new RegExp(val, 'i').test(compareVal);
case '$ne':
return compareVal !== val && compareVal != null;
case '$nin':
return Array.isArray(val) && !val.map(v => String(v)).includes(String(compareVal));
}
});
}
if (typeof condition === 'string') {
if (typeof fieldVal === 'string') return fieldVal.startsWith(condition);
if (Array.isArray(fieldVal)) return fieldVal.some(v => v.startsWith(condition));
if (typeof fieldVal === 'object') {
if ('$oid' in fieldVal) return fieldVal.$oid === condition;
if (fieldVal?.$date?.$numberLong) {
const itemDate = new Date(parseInt(fieldVal.$date.$numberLong));
const [day, month, year] = condition.includes('/') ? condition.split('/') : [null];
const inputDate = condition.includes('/') ? new Date(`${year}-${month}-${day}`) : new Date(condition);
return (
itemDate.getUTCFullYear() === inputDate.getUTCFullYear() &&
itemDate.getUTCMonth() === inputDate.getUTCMonth() &&
itemDate.getUTCDate() === inputDate.getUTCDate()
);
}
}
}
return fieldVal == condition;
})
);
}
if (search) {
const terms = search.toLowerCase().split(/\s+/);
result = result.filter(item => {
const searchable = Object.values(item).flatMap(val => {
if (typeof val === 'string') return val.toLowerCase();
if (typeof val === 'number') return val.toString();
if (val?.$oid) return val.$oid;
if (val?.$date?.$numberLong) {
const d = new Date(parseInt(val.$date.$numberLong));
return [d.toISOString().slice(0, 10), `${d.getUTCDate().toString().padStart(2, '0')}/${(d.getUTCMonth() + 1).toString().padStart(2, '0')}/${d.getUTCFullYear()}`];
}
try {
return JSON.stringify(val).toLowerCase();
} catch {
return '';
}
});
return terms.every(term => searchable.some(field => field.includes(term)));
});
}
return result.length;
}
parseQueryToFilter(query) {
console.log("query", query);
const filter = {};
for (const key in query) {
const val = query[key];
console.log(key, val);
// 🔍 Detectar si ya viene parseado como objeto con operadores
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
filter[key] = {};
for (const op in val) {
const opVal = val[op];
if (op === '$in') {
if (Array.isArray(opVal)) {
filter[key][op] = opVal;
} else if (typeof opVal === 'string' && opVal.includes(',')) {
filter[key][op] = opVal.split(',');
} else {
filter[key][op] = [opVal];
}
} else {
filter[key][op] = opVal;
}
}
continue;
}
// 🔁 Compatibilidad con alias[]=TRU&alias[]=SUC → $in implícito
if (Array.isArray(val)) {
filter[key] = { $in: val };
continue;
}
// 🔚 Filtro directo
filter[key] = val;
}
return filter;
}
getRouter() {
// const express = require('express');
const router = express.Router();
// 🚫 Middleware para evitar caché desde el navegador
router.use((req, res, next) => {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.set('Pragma', 'no-cache');
res.set('Expires', '0');
next();
});
const deleteAndLog = (alias, ev) => {
const filePath = path.join(this.cacheDir, ev, `${alias.replace(/\W/g, '_')}.json`);
const existed = fs.existsSync(filePath);
try {
if (existed) fs.unlinkSync(filePath);
if (this.cache[ev] && this.cache[ev][alias]) {
delete this.cache[ev][alias];
}
return { alias, existed, success: true };
} catch (err) {
return { alias, existed, success: false, error: err.message };
}
};
const handleDelete = (req, res, alias) => {
const queryParams = req.query;
const ev = queryParams.ev || this.defaultEv;
const clientKey = queryParams['smartport-key'];
const expectedKey = process.env['SmartPort-Key'];
delete queryParams['smartport-key'];
if (!expectedKey || clientKey !== expectedKey) {
return res.status(403).json({ success: false, error: 'Clave no válida para eliminación de caché.' });
}
const result = deleteAndLog(alias, ev);
if (result.success) {
res.json({ success: true, message: `Cache eliminado: ${alias}`, existed: result.existed });
} else {
res.status(500).json({ success: false, error: result.error });
}
};
const handleDeleteMultiple = (req, res) => {
const queryParams = req.query;
const ev = queryParams.ev || this.defaultEv;
console.log('query', req.query);
// return;
const clientKey = queryParams['smartport-key'];
const expectedKey = process.env['SmartPort-Key'];
delete queryParams['smartport-key'];
const endpoints = queryParams.endpoint;
if (!expectedKey || clientKey !== expectedKey) {
return res.status(403).json({ success: false, error: 'Clave no válida para eliminación múltiple.' });
}
if (!endpoints || !Array.isArray(endpoints)) {
return res.status(400).json({ success: false, error: 'Se esperaba endpoint[]=alias1&endpoint[]=alias2' });
}
// ✅ Eliminar de memoria
if (!this.cache[ev]) this.cache[ev] = {};
for (const alias of endpoints) {
const clean = alias.trim();
delete this.cache[ev][clean];
}
const resultados = endpoints.map((aliasRaw) => {
const alias = aliasRaw.trim();
return deleteAndLog(alias, ev);
});
res.json({ success: true, deleted: resultados });
};
// 🟥 Rutas segmentadas para /delete
router.get('/delete/:alias', (req, res) =>
handleDelete(req, res, req.params.alias)
);
router.get('/delete/:alias/:extra', (req, res) =>
handleDelete(req, res, `${req.params.alias}/${req.params.extra}`)
);
router.get('/delete/:alias/:extra/:id', (req, res) =>
handleDelete(req, res, `${req.params.alias}/${req.params.extra}/${req.params.id}`)
);
// 🟥 Ruta para eliminación múltiple por GET
router.get('/delete-multiple', handleDeleteMultiple);
// ⏫ Handler compartido para actualizar caché
// const handleUpdate = async (req, res, alias) => {
// const queryParams = req.query;
// const ev = queryParams.ev || this.defaultEv;
// console.log('🔁 Actualizando cache');
// console.log('🧩 Alias:', alias);
// console.log('📦 Evento:', ev);
// // 🔐 Validación de clave privada
// const clientKey = queryParams['smartport-key'];
// const expectedKey = process.env['SmartPort-Key'];
// delete queryParams['smartport-key'];
// if (!expectedKey || clientKey !== expectedKey) {
// return res.status(403).json({ success: false, error: 'Clave no válida para actualización de caché.' });
// }
// try {
// await this.initCache(ev);
// await this.refresh(alias, queryParams);
// res.json({ success: true, message: `Cache de '${alias}' actualizado.` });
// } catch (err) {
// console.error(err);
// res.status(500).json({ success: false, error: 'Error actualizando cache.' });
// }
// };
const updateCacheEntry = async (alias, ev, queryParams = {}) => {
try {
await this.initCache(ev);
await this.refresh(alias, queryParams);
return { alias, success: true };
} catch (err) {
return { alias, success: false, error: err.message };
}
};
// ⏫ Handler compartido para actualizar caché
const handleUpdate = async (req, res, alias) => {
const queryParams = req.query;
const ev = queryParams.ev || this.defaultEv;
const clientKey = queryParams['smartport-key'];
const expectedKey = process.env['SmartPort-Key'];
delete queryParams['smartport-key'];
if (!expectedKey || clientKey !== expectedKey) {
return res.status(403).json({ success: false, error: 'Clave no válida para actualización de caché.' });
}
const result = await updateCacheEntry(alias, ev, queryParams);
if (result.success) {
res.json({ success: true, message: `Cache de '${alias}' actualizado.` });
} else {
res.status(500).json({ success: false, error: result.error });
}
};
// const handleUpdateMultiple = async (req, res) => {
// const queryParams = req.query;
// const ev = queryParams.ev || this.defaultEv;
// const clientKey = queryParams['smartport-key'];
// const expectedKey = process.env['SmartPort-Key'];
// delete queryParams['smartport-key'];
// const endpoints = queryParams.endpoint;
// if (!expectedKey || clientKey !== expectedKey) {
// return res.status(403).json({ success: false, error: 'Clave no válida para actualización múltiple.' });
// }
// if (!endpoints || !Array.isArray(endpoints)) {
// return res.status(400).json({ success: false, error: 'Se esperaba endpoint[]=alias1&endpoint[]=alias2' });
// }
// // ✅ Eliminar de memoria
// if (!this.cache[ev]) this.cache[ev] = {};
// for (const alias of endpoints) {
// const clean = alias.trim();
// delete this.cache[ev][clean];
// }
// const results = await Promise.all(
// endpoints.map(alias => updateCacheEntry(alias.trim(), ev, queryParams))
// );
// res.json({ success: true, updated: results });
// };
const handleUpdateMultiple = async (req, res) => {
const originalParams = req.query;
const ev = originalParams.ev || this.defaultEv;
const clientKey = originalParams['smartport-key'];
const expectedKey = process.env['SmartPort-Key'];
if (!expectedKey || clientKey !== expectedKey) {
return res.status(403).json({ success: false, error: 'Clave no válida para actualización múltiple.' });
}
const endpoints = originalParams.endpoint;
if (!endpoints || !Array.isArray(endpoints)) {
return res.status(400).json({ success: false, error: 'Se esperaba endpoint[]=alias1&endpoint[]=alias2' });
}
// ✅ Crear copia limpia sin endpoint ni smartport-key
const queryParams = { ...originalParams };
delete queryParams['smartport-key'];
delete queryParams['endpoint'];
// 🧹 Limpia de memoria antes de actualizar
if (!this.cache[ev]) this.cache[ev] = {};
for (const alias of endpoints) {
const clean = alias.trim();
delete this.cache[ev][clean];
}
// 🔁 Actualiza cada endpoint con los parámetros limpios
const results = await Promise.all(
endpoints.map(alias => updateCacheEntry(alias.trim(), ev, queryParams))
);
res.json({ success: true, updated: results });
};
// 🟨 Rutas segmentadas para /update
router.get('/update/:alias', (req, res) =>
handleUpdate(req, res, req.params.alias)
);
router.get('/update/:alias/:extra', (req, res) =>
handleUpdate(req, res, `${req.params.alias}/${req.params.extra}`)
);
router.get('/update/:alias/:extra/:id', (req, res) =>
handleUpdate(req, res, `${req.params.alias}/${req.params.extra}/${req.params.id}`)
);
router.get('/update-multiple', handleUpdateMultiple);
// 🟩 Handler compartido para /data
const handleData = async (req, res, alias) => {
let { ev, limit, skip, sort, search, ...rest } = req.query;
ev = ev || this.defaultEv;
// 🚫 Eliminar claves que no deben entrar al filtro
const excludedKeys = ['smartport-key', 'token'];
excludedKeys.forEach(k => delete rest[k]);
const filter = this.parseQueryToFilter(rest);
try {
const data = await this.getData(alias, {
ev,
limit: limit ? parseInt(limit) : undefined,
skip: skip ? parseInt(skip) : 0,
sort,
search,
filter
});
if (!data || data.length === 0) {
return res.status(404).json({ success: false, data: [], message: 'No hay datos en caché.' });
}
res.json({ success: true, data, count: data.length });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, error: 'Error obteniendo datos.' });
}
};
// 🟨 Rutas segmentadas para /data
router.get('/data/:alias', (req, res) =>
handleData(req, res, req.params.alias)
);
router.get('/data/:alias/:extra', (req, res) =>
handleData(req, res, `${req.params.alias}/${req.params.extra}`)
);
router.get('/data/:alias/:extra/:id', (req, res) =>
handleData(req, res, `${req.params.alias}/${req.params.extra}/${req.params.id}`)
);
// 🧹 Función reutilizable para borrar del cache y del disco
// const deleteCacheEntry = (alias, ev) => {
// const filePath = path.join(this.cacheDir, ev, `${alias.replace(/\W/g, '_')}.json`);
// if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
// if (this.cache[ev] && this.cache[ev][alias]) {
// delete this.cache[ev][alias];
// }
// };
// 🔴 Handler para eliminar un solo alias
// const handleDelete = (req, res, alias) => {
// const queryParams = req.query;
// const ev = queryParams.ev || this.defaultEv;
// const clientKey = queryParams['smartport-key'];
// const expectedKey = process.env['SmartPort-Key'];
// delete queryParams['smartport-key'];
// if (!expectedKey || clientKey !== expectedKey) {
// return res.status(403).json({ success: false, error: 'Clave no válida para eliminación de caché.' });
// }
// try {
// deleteCacheEntry(alias, ev);
// res.json({ success: true, message: `Cache eliminado: ${alias}` });
// } catch (err) {
// res.status(500).json({ success: false, error: 'Error eliminando cache: ' + err.message });
// }
// };
// 🧹 Handler para eliminar múltiples endpoints con endpoint[]=...
// const handleDeleteMultiple = (req, res) => {
// const queryParams = req.query;
// const ev = queryParams.ev || this.defaultEv;
// const clientKey = queryParams['smartport-key'];
// const expectedKey = process.env['SmartPort-Key'];
// delete queryParams['smartport-key'];
// const endpoints = queryParams.endpoint;
// if (!expectedKey || clientKey !== expectedKey) {
// return res.status(403).json({ success: false, error: 'Clave no válida para eliminación múltiple.' });
// }
// if (!endpoints || !Array.isArray(endpoints)) {
// return res.status(400).json({ success: false, error: 'Se esperaba endpoint[]=alias1&endpoint[]=alias2' });
// }
// const resultados = endpoints.map((aliasRaw) => {
// const alias = aliasRaw.trim();
// try {
// deleteCacheEntry(alias, ev);
// return { alias, success: true };
// } catch (err) {
// return { alias, success: false, error: err.message };
// }
// });
// res.json({ success: true, deleted: resultados });
// };
return router;
}
}
module.exports = SmartPort;