UNPKG

@syntropylog/adapters

Version:
1,333 lines (1,322 loc) 78.1 kB
import * as amqplib from 'amqplib'; import { JSONCodec, connect, headers } from 'nats'; import axios, { isAxiosError } from 'axios'; /** * Utility class for handling payload serialization/deserialization * across different broker adapters. */ class PayloadSerializer { /** * Serializes a BrokerMessage payload for sending to a broker. * Handles Buffer objects by extracting their JSON content. */ static serializeForBroker(message) { let payloadToSend; if (Buffer.isBuffer(message.payload)) { // If it's already a Buffer, decode it as JSON and re-encode try { const jsonString = message.payload.toString(); payloadToSend = JSON.parse(jsonString); } catch { // If it's not valid JSON, send as string payloadToSend = message.payload.toString(); } } else { payloadToSend = message.payload; } return JSON.stringify(payloadToSend); } /** * Deserializes a payload received from a broker into a Buffer * that the SyntropyLog framework expects. */ static deserializeFromBroker(brokerPayload) { if (!brokerPayload) { return Buffer.alloc(0); } try { // Convert to string if it's a Buffer const jsonString = Buffer.isBuffer(brokerPayload) ? brokerPayload.toString() : brokerPayload; // Parse the JSON const parsedPayload = JSON.parse(jsonString); // Return as Buffer with JSON stringified content return Buffer.from(JSON.stringify(parsedPayload)); } catch { // If it's not valid JSON, return as Buffer return Buffer.isBuffer(brokerPayload) ? brokerPayload : Buffer.from(brokerPayload); } } /** * Creates a BrokerMessage with properly deserialized payload */ static createBrokerMessage(brokerPayload, headers) { return { payload: this.deserializeFromBroker(brokerPayload), headers: headers || {}, }; } } /** * Helper function to normalize Kafka's complex IHeaders object into * the simple Record<string, string | Buffer> that our framework expects. * @param headers The headers object from a Kafka message. * @returns A normalized headers object. */ function normalizeKafkaHeaders(headers) { if (!headers) { return undefined; } const normalized = {}; for (const key in headers) { if (Object.prototype.hasOwnProperty.call(headers, key)) { const value = headers[key]; // We only accept string or Buffer, and we discard undefined or arrays for simplicity. if (typeof value === 'string' || Buffer.isBuffer(value)) { normalized[key] = value; } } } return normalized; } class KafkaAdapter { // The constructor now receives the Kafka instance already created. // This makes it more flexible and easier to test. constructor(kafkaInstance, groupId) { this.producer = kafkaInstance.producer(); this.consumer = kafkaInstance.consumer({ groupId }); } async connect() { await this.producer.connect(); await this.consumer.connect(); } async disconnect() { await this.producer.disconnect(); await this.consumer.disconnect(); } async publish(topic, message) { const serializedPayload = PayloadSerializer.serializeForBroker(message); await this.producer.send({ topic, messages: [{ value: serializedPayload, headers: message.headers }], }); } async subscribe(topic, handler) { await this.consumer.subscribe({ topic, fromBeginning: true }); await this.consumer.run({ eachMessage: async ({ topic, partition, message }) => { try { const brokerMessage = PayloadSerializer.createBrokerMessage(message.value, normalizeKafkaHeaders(message.headers)); const controls = { ack: async () => { await this.consumer.commitOffsets([ { topic, partition, offset: (Number(message.offset) + 1).toString(), }, ]); }, nack: async () => { // Nacking in Kafka is complex. For now, we just log. // A real implementation might move the message to a dead-letter queue. console.log(`NACK received for message on topic ${topic}.`); }, }; await handler(brokerMessage, controls); } catch (err) { // If there's an error (e.g., JSON parsing), we can't process the message, // but we don't want to crash the whole service. We'll log it. // A more robust implementation might publish to a dead-letter queue. console.error(`Failed to process message from topic ${topic}`, err); } }, }); } } class RabbitMQAdapter { constructor(connectionString, exchangeName = 'topic_logs') { this.connection = null; this.channel = null; this.consumerTags = new Map(); this.connectionString = connectionString; this.exchangeName = exchangeName; } async connect() { this.connection = await amqplib.connect(this.connectionString); if (!this.connection) { throw new Error('Failed to connect to RabbitMQ'); } this.channel = await this.connection.createChannel(); if (!this.channel) { throw new Error('Failed to create RabbitMQ channel'); } await this.channel.assertExchange(this.exchangeName, 'topic', { durable: true }); } async disconnect() { try { // Cancel all active consumers first if (this.channel && this.consumerTags.size > 0) { for (const [topic, consumerTag] of this.consumerTags) { try { await this.channel.cancel(consumerTag); console.log(`✅ Cancelled consumer for topic: ${topic}`); } catch (error) { console.warn(`⚠️ Error cancelling consumer for topic ${topic}:`, error); } } this.consumerTags.clear(); } // Close channel if (this.channel) { await this.channel.close(); } // Close connection if (this.connection) { await this.connection.close(); } } catch (error) { console.error('Error during RabbitMQ disconnection:', error); } finally { this.channel = null; this.connection = null; } } async publish(topic, message) { if (!this.channel) { throw new Error('RabbitMQ channel is not available. Please connect first.'); } const routingKey = topic; const serializedPayload = PayloadSerializer.serializeForBroker(message); const content = Buffer.from(serializedPayload); const options = { headers: message.headers || {}, persistent: true, }; this.channel.publish(this.exchangeName, routingKey, content, options); } async subscribe(topic, handler) { if (!this.channel) { throw new Error('RabbitMQ channel is not available. Please connect first.'); } const q = await this.channel.assertQueue('', { exclusive: true }); await this.channel.bindQueue(q.queue, this.exchangeName, topic); const { consumerTag } = await this.channel.consume(q.queue, async (msg) => { if (msg && this.channel) { try { const brokerMessage = PayloadSerializer.createBrokerMessage(msg.content, msg.properties.headers); const ack = async () => this.channel.ack(msg); const nack = async (requeue = false) => this.channel.nack(msg, false, requeue); await handler(brokerMessage, { ack, nack }); } catch (err) { // If there's an error (e.g., JSON parsing), we can't process the message, // but we don't want to crash the whole service. We'll log it. // A more robust implementation might publish to a dead-letter queue. console.error(`Failed to process message from topic ${topic}`, err); // Nack the message to prevent infinite retries this.channel.nack(msg, false, false); } } }, { noAck: false }); this.consumerTags.set(topic, consumerTag); } async unsubscribe(topic) { if (!this.channel) { throw new Error('RabbitMQ channel is not available.'); } const consumerTag = this.consumerTags.get(topic); if (consumerTag) { await this.channel.cancel(consumerTag); this.consumerTags.delete(topic); } else { console.warn(`No active subscription found for topic: ${topic}`); } } } class NatsAdapter { constructor(natsServers = ['nats://localhost:4222']) { this.natsConnection = null; this.codec = JSONCodec(); this.subscriptions = new Map(); this.natsServers = natsServers; } async connect() { this.natsConnection = await connect({ servers: this.natsServers, }); } async disconnect() { if (this.natsConnection) { // Unsubscribe from all topics first if (this.subscriptions.size > 0) { for (const [topic, subscription] of this.subscriptions) { try { subscription.unsubscribe(); console.log(`✅ Cancelled NATS subscription for topic: ${topic}`); } catch (error) { console.warn(`⚠️ Error cancelling subscription for topic ${topic}:`, error); } } this.subscriptions.clear(); } await this.natsConnection.drain(); this.natsConnection.close(); this.natsConnection = null; } } async publish(topic, message) { if (!this.natsConnection) { throw new Error('NATS connection is not available. Please connect first.'); } const serializedPayload = PayloadSerializer.serializeForBroker(message); const natsHeaders = this.recordToNatsHeaders(message.headers); await this.natsConnection.publish(topic, this.codec.encode(JSON.parse(serializedPayload)), { headers: natsHeaders }); } async subscribe(topic, handler) { if (!this.natsConnection) { throw new Error('NATS connection is not available. Please connect first.'); } const subscription = this.natsConnection.subscribe(topic); (async () => { for await (const msg of subscription) { try { // Decode the JSON payload from NATS const decodedPayload = this.codec.decode(msg.data); const headers = this.natsHeadersToRecord(msg.headers); const brokerMessage = PayloadSerializer.createBrokerMessage(Buffer.from(JSON.stringify(decodedPayload)), headers); const controls = { ack: async () => { // NATS doesn't require explicit ack for most use cases // but we can implement it if needed }, nack: async () => { // NATS doesn't have a built-in nack mechanism // but we can implement custom logic if needed console.log(`NACK received for message on topic ${topic}.`); }, }; await handler(brokerMessage, controls); } catch (err) { // If there's an error (e.g., JSON parsing), we can't process the message, // but we don't want to crash the whole service. We'll log it. // A more robust implementation might publish to a dead-letter queue. console.error(`Failed to process message from topic ${topic}`, err); } } })().catch(console.error); this.subscriptions.set(topic, subscription); } async unsubscribe(topic) { if (!this.natsConnection) { throw new Error('NATS connection is not available.'); } const subscription = this.subscriptions.get(topic); if (subscription) { subscription.unsubscribe(); this.subscriptions.delete(topic); console.log(`✅ Unsubscribed from NATS topic: ${topic}`); } else { console.warn(`No active subscription found for topic: ${topic}`); } } natsHeadersToRecord(natsHeaders) { if (!natsHeaders) { return undefined; } const record = {}; // NATS headers are iterable but don't have .entries() method for (const [key, value] of natsHeaders) { record[key] = value; } return record; } recordToNatsHeaders(record) { if (!record) { return undefined; } const natsHeaders = headers(); for (const [key, value] of Object.entries(record)) { natsHeaders.set(key, String(value)); } return natsHeaders; } } /** * @file src/http/adapters/AxiosAdapter.ts * @description An implementation of the IHttpClientAdapter for the Axios library. * This class acts as a "translator," converting requests and responses * between the framework's generic format and the Axios-specific format. */ /** * A helper function to normalize the Axios headers object. * The Axios header type is complex (`AxiosResponseHeaders` | `RawAxiosResponseHeaders`), * while our adapter interface expects a simple `Record<string, ...>`. * This function performs the conversion safely. * @param {RawAxiosResponseHeaders | AxiosResponseHeaders} headers - The Axios headers object. * @returns {Record<string, string | number | string[]>} A simple, normalized headers object. */ function normalizeHeaders(headers) { const normalized = {}; for (const key in headers) { if (Object.prototype.hasOwnProperty.call(headers, key)) { // Axios headers can be undefined, so we ensure they are not included. const value = headers[key]; if (value !== undefined && value !== null) { normalized[key] = value; } } } return normalized; } /** * @class AxiosAdapter * @description An adapter that allows SyntropyLog to instrument HTTP requests * made with the Axios library. It implements the `IHttpClientAdapter` interface. * @implements {IHttpClientAdapter} */ class AxiosAdapter { /** * @constructor * @param {AxiosRequestConfig | AxiosInstance} config - Either a pre-configured * Axios instance or a configuration object to create a new instance. */ constructor(config) { if ('request' in config && typeof config.request === 'function') { this.axiosInstance = config; } else { this.axiosInstance = axios.create(config); } } /** * Executes an HTTP request using the configured Axios instance. * It translates the generic `AdapterHttpRequest` into an `AxiosRequestConfig`, * sends the request, and then normalizes the Axios response or error back * into the framework's generic format (`AdapterHttpResponse` or `AdapterHttpError`). * @template T The expected type of the response data. * @param {AdapterHttpRequest} request The generic request object. * @returns {Promise<AdapterHttpResponse<T>>} A promise that resolves with the normalized response. * @throws {AdapterHttpError} Throws a normalized error if the request fails. */ async request(request) { try { // Sanitize headers before passing them to Axios. // The `request.headers` object from the instrumenter contains the full context, // which might include non-string values or keys that are not valid HTTP headers. // This ensures we only pass valid, string-based headers to the underlying client. const sanitizedHeaders = {}; const excludedHeaders = ['host', 'connection', 'content-length']; // Headers to exclude for (const key in request.headers) { if (Object.prototype.hasOwnProperty.call(request.headers, key) && typeof request.headers[key] === 'string' && !excludedHeaders.includes(key.toLowerCase()) // Exclude problematic headers ) { sanitizedHeaders[key] = request.headers[key]; } } const axiosConfig = { url: request.url, method: request.method, headers: sanitizedHeaders, params: request.queryParams, data: request.body, }; const response = await this.axiosInstance.request(axiosConfig); return { statusCode: response.status, data: response.data, headers: normalizeHeaders(response.headers), }; } catch (error) { if (isAxiosError(error)) { const normalizedError = { name: 'AdapterHttpError', message: error.message, stack: error.stack, isAdapterError: true, request: request, response: error.response ? { statusCode: error.response.status, data: error.response.data, headers: normalizeHeaders(error.response.headers), } : undefined, }; throw normalizedError; } throw error; } } } class FetchAdapter { async request(request) { const response = await fetch(request.url, { method: request.method, headers: request.headers, body: request.body ? JSON.stringify(request.body) : undefined, }); // Handle cases where the response body might be empty const text = await response.text(); const data = (text ? JSON.parse(text) : {}); return { statusCode: response.status, data: data, headers: Object.fromEntries(response.headers.entries()), }; } } class PrismaSerializer { constructor() { this.name = 'prisma'; this.priority = 75; } canSerialize(data) { return (this.isPrismaQuery(data) || this.isPrismaError(data) || this.isPrismaClient(data)); } getComplexity(data) { if (this.isPrismaQuery(data)) { return this.assessQueryComplexity(data); } if (this.isPrismaError(data)) { return 'low'; } if (this.isPrismaClient(data)) { return 'low'; } return 'low'; } async serialize(data, context) { const startTime = Date.now(); try { let result; if (this.isPrismaQuery(data)) { result = this.serializeQuery(data); } else if (this.isPrismaError(data)) { result = this.serializeError(data); } else if (this.isPrismaClient(data)) { result = this.serializeClient(data); } else { throw new Error('Tipo de dato Prisma no reconocido'); } const duration = Date.now() - startTime; // ✅ Verificar que la serialización respeta el timeout del contexto const timeout = context.timeout || 50; if (duration > timeout) { throw new Error(`Serialización lenta: ${duration}ms (máximo ${timeout}ms)`); } return { success: true, data: result, metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } catch (error) { const duration = Date.now() - startTime; return { success: false, error: error instanceof Error ? error.message : 'Error desconocido en serialización Prisma', metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } } isPrismaQuery(data) { return (data && typeof data === 'object' && typeof data.model === 'string' && typeof data.action === 'string'); } isPrismaError(data) { return (data && typeof data === 'object' && typeof data.code === 'string' && typeof data.message === 'string'); } isPrismaClient(data) { return (data && typeof data === 'object' && typeof data.$connect === 'function' && typeof data.$disconnect === 'function' && typeof data.$queryRaw === 'function'); } assessQueryComplexity(query) { let complexity = 0; // Basado en la acción if (query.action === 'findMany') complexity += 1; else if (query.action === 'findFirst') complexity += 1; else if (query.action === 'findUnique') complexity += 1; else if (query.action === 'create') complexity += 2; else if (query.action === 'update') complexity += 2; else if (query.action === 'updateMany') complexity += 3; else if (query.action === 'delete') complexity += 2; else if (query.action === 'deleteMany') complexity += 3; else if (query.action === 'upsert') complexity += 3; else if (query.action === 'aggregate') complexity += 4; else if (query.action === 'groupBy') complexity += 4; else if (query.action === 'count') complexity += 1; // Basado en argumentos complejos if (query.args) { if (query.args.include) complexity += 1; if (query.args.select) complexity += 1; if (query.args.where && typeof query.args.where === 'object') { const whereKeys = Object.keys(query.args.where); complexity += Math.min(whereKeys.length, 2); } if (query.args.orderBy) complexity += 1; if (query.args.take) complexity += 1; if (query.args.skip) complexity += 1; if (query.args.distinct) complexity += 1; } if (complexity >= 7) return 'high'; if (complexity >= 4) return 'medium'; return 'low'; } serializeQuery(query) { return { type: 'PrismaQuery', model: query.model, action: query.action, args: query.args, // Datos originales, sin sanitizar duration: query.duration, timestamp: query.timestamp, complexity: this.assessQueryComplexity(query) }; } serializeError(error) { return { type: 'PrismaError', code: error.code, message: error.message, meta: error.meta, // Datos originales, sin sanitizar clientVersion: error.clientVersion, stack: error.stack }; } serializeClient(client) { return { type: 'PrismaClient', hasConnect: typeof client.$connect === 'function', hasDisconnect: typeof client.$disconnect === 'function', hasQueryRaw: typeof client.$queryRaw === 'function', hasExecuteRaw: typeof client.$executeRaw === 'function', hasTransaction: typeof client.$transaction === 'function', hasUse: typeof client.$use === 'function', hasOn: typeof client.$on === 'function' }; } } class TypeORMSerializer { constructor() { this.name = 'typeorm'; this.priority = 80; } canSerialize(data) { return (this.isTypeORMQuery(data) || this.isTypeORMError(data) || this.isTypeORMEntity(data) || this.isTypeORMRepository(data) || this.isTypeORMConnection(data)); } getComplexity(data) { if (this.isTypeORMQuery(data)) { return this.assessQueryComplexity(data); } if (this.isTypeORMError(data)) { return 'low'; } if (this.isTypeORMEntity(data)) { return this.assessEntityComplexity(data); } if (this.isTypeORMRepository(data)) { return 'medium'; } if (this.isTypeORMConnection(data)) { return 'low'; } return 'low'; } async serialize(data, context) { const startTime = Date.now(); try { let result; if (this.isTypeORMQuery(data)) { result = this.serializeQuery(data); } else if (this.isTypeORMError(data)) { result = this.serializeError(data); } else if (this.isTypeORMEntity(data)) { result = this.serializeEntity(data); } else if (this.isTypeORMRepository(data)) { result = this.serializeRepository(data); } else if (this.isTypeORMConnection(data)) { result = this.serializeConnection(data); } else { throw new Error('Tipo de dato TypeORM no reconocido'); } const duration = Date.now() - startTime; // ✅ Verificar que la serialización respeta el timeout del contexto const timeout = context.timeout || 50; if (duration > timeout) { throw new Error(`Serialización lenta: ${duration}ms (máximo ${timeout}ms)`); } return { success: true, data: result, metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } catch (error) { const duration = Date.now() - startTime; return { success: false, error: error instanceof Error ? error.message : 'Error desconocido en serialización TypeORM', metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } } isTypeORMQuery(data) { return (data && typeof data === 'object' && typeof data.sql === 'string' && (data.parameters === undefined || Array.isArray(data.parameters))); } isTypeORMError(data) { return (data && typeof data === 'object' && typeof data.message === 'string' && (data.code === undefined || typeof data.code === 'string')); } isTypeORMEntity(data) { return (data && typeof data === 'object' && data.constructor && data.constructor.name && (data.constructor.name.includes('Entity') || data.constructor.name.includes('Model') || data.id !== undefined)); } isTypeORMRepository(data) { return (data && typeof data === 'object' && typeof data.find === 'function' && typeof data.findOne === 'function' && typeof data.save === 'function'); } isTypeORMConnection(data) { return (data && typeof data === 'object' && typeof data.isConnected === 'function' && typeof data.close === 'function'); } assessQueryComplexity(query) { let complexity = 0; // Basado en el tipo de query if (query.queryType === 'SELECT') complexity += 1; else if (query.queryType === 'INSERT') complexity += 2; else if (query.queryType === 'UPDATE') complexity += 3; else if (query.queryType === 'DELETE') complexity += 3; // Basado en joins if (query.joins && query.joins.length > 0) { complexity += query.joins.length * 2; } // Basado en la longitud del SQL if (query.sql.length > 500) complexity += 2; else if (query.sql.length > 200) complexity += 1; // Basado en parámetros if (query.parameters && query.parameters.length > 10) complexity += 2; else if (query.parameters && query.parameters.length > 5) complexity += 1; if (complexity >= 6) return 'high'; if (complexity >= 3) return 'medium'; return 'low'; } assessEntityComplexity(entity) { const keys = Object.keys(entity); if (keys.length > 20) return 'high'; if (keys.length > 10) return 'medium'; return 'low'; } serializeQuery(query) { return { type: 'TypeORMQuery', queryType: query.queryType || 'UNKNOWN', sql: query.sql, // SQL original, sin sanitizar parameters: query.parameters, // Parámetros originales, sin sanitizar table: query.table, alias: query.alias, joins: query.joins, // Datos originales, sin sanitizar where: query.where, // Datos originales, sin sanitizar orderBy: query.orderBy, limit: query.limit, offset: query.offset, complexity: this.assessQueryComplexity(query) }; } serializeError(error) { return { type: 'TypeORMError', code: error.code, message: error.message, query: error.query, // SQL original, sin sanitizar parameters: error.parameters, // Parámetros originales, sin sanitizar table: error.table, constraint: error.constraint, detail: error.detail, hint: error.hint, position: error.position, internalPosition: error.internalPosition, internalQuery: error.internalQuery, // SQL original, sin sanitizar where: error.where, // SQL original, sin sanitizar schema: error.schema, column: error.column, dataType: error.dataType }; } serializeEntity(entity) { const serialized = { type: 'TypeORMEntity', entityName: entity.constructor?.name || 'UnknownEntity', id: entity.id, fields: {} }; // Serializar campos del entity (datos originales, sin sanitizar) for (const [key, value] of Object.entries(entity)) { if (key !== 'constructor' && typeof value !== 'function') { serialized.fields[key] = value; // Valor original, sin sanitizar } } return serialized; } serializeRepository(repo) { return { type: 'TypeORMRepository', repositoryName: repo.constructor?.name || 'UnknownRepository', target: repo.target?.name || 'UnknownTarget', metadata: repo.metadata ? { tableName: repo.metadata.tableName, columns: repo.metadata.columns?.map((col) => col.propertyName) || [], relations: repo.metadata.relations?.map((rel) => rel.propertyName) || [] } : undefined }; } serializeConnection(connection) { return { type: 'TypeORMConnection', name: connection.name || 'default', isConnected: connection.isConnected ? connection.isConnected() : undefined, driver: connection.driver?.constructor?.name || 'UnknownDriver', options: connection.options ? { type: connection.options.type, host: connection.options.host, port: connection.options.port, database: connection.options.database, username: connection.options.username // Usuario original, sin sanitizar } : undefined }; } } class MySQLSerializer { constructor() { this.name = 'mysql'; this.priority = 85; } canSerialize(data) { return (this.isMySQLQuery(data) || this.isMySQLError(data) || this.isMySQLConnection(data) || this.isMySQLPool(data)); } getComplexity(data) { if (this.isMySQLQuery(data)) { return this.assessQueryComplexity(data); } if (this.isMySQLError(data)) { return 'low'; } if (this.isMySQLConnection(data)) { return 'low'; } if (this.isMySQLPool(data)) { return 'medium'; } return 'low'; } async serialize(data, context) { const startTime = Date.now(); try { let result; if (this.isMySQLQuery(data)) { result = this.serializeQuery(data); } else if (this.isMySQLError(data)) { result = this.serializeError(data); } else if (this.isMySQLConnection(data)) { result = this.serializeConnection(data); } else if (this.isMySQLPool(data)) { result = this.serializePool(data); } else { throw new Error('Tipo de dato MySQL no reconocido'); } const duration = Date.now() - startTime; // ✅ Verificar que la serialización respeta el timeout del contexto const timeout = context.timeout || 50; if (duration > timeout) { throw new Error(`Serialización lenta: ${duration}ms (máximo ${timeout}ms)`); } return { success: true, data: result, metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } catch (error) { const duration = Date.now() - startTime; return { success: false, error: error instanceof Error ? error.message : 'Error desconocido en serialización MySQL', metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } } isMySQLQuery(data) { return (data && typeof data === 'object' && typeof data.sql === 'string' && (data.values === undefined || Array.isArray(data.values))); } isMySQLError(data) { return (data && typeof data === 'object' && typeof data.code === 'string' && typeof data.errno === 'number' && typeof data.sqlMessage === 'string'); } isMySQLConnection(data) { return (data && typeof data === 'object' && typeof data.query === 'function' && typeof data.connect === 'function' && typeof data.end === 'function'); } isMySQLPool(data) { return (data && typeof data === 'object' && typeof data.getConnection === 'function' && typeof data.query === 'function' && typeof data.end === 'function'); } assessQueryComplexity(query) { let complexity = 0; const sql = query.sql.toLowerCase(); // Basado en el tipo de operación if (sql.includes('select') && !sql.includes('join')) complexity += 1; else if (sql.includes('insert')) complexity += 2; else if (sql.includes('update')) complexity += 3; else if (sql.includes('delete')) complexity += 3; else if (sql.includes('create') || sql.includes('alter') || sql.includes('drop')) { complexity += 4; // DDL operations } // Basado en joins if (sql.includes('join')) { const joinCount = (sql.match(/join/g) || []).length; complexity += joinCount * 2; } // Basado en subqueries if (sql.includes('(select') || sql.includes('( select')) { const subqueryCount = (sql.match(/\(select/g) || []).length; complexity += subqueryCount * 3; } // Basado en funciones complejas if (sql.includes('group_concat') || sql.includes('json_')) complexity += 2; if (sql.includes('window') || sql.includes('over(')) complexity += 3; // Basado en la longitud del SQL if (sql.length > 1000) complexity += 3; else if (sql.length > 500) complexity += 2; else if (sql.length > 200) complexity += 1; // Basado en parámetros if (query.values && query.values.length > 20) complexity += 2; else if (query.values && query.values.length > 10) complexity += 1; if (complexity >= 8) return 'high'; if (complexity >= 4) return 'medium'; return 'low'; } serializeQuery(query) { return { type: 'MySQLQuery', sql: query.sql, // SQL original, sin sanitizar values: query.values, // Valores originales, sin sanitizar timeout: query.timeout, connectionConfig: query.connectionConfig ? { host: query.connectionConfig.host, port: query.connectionConfig.port, database: query.connectionConfig.database, user: query.connectionConfig.user, password: query.connectionConfig.password // Contraseña original, sin sanitizar } : undefined, complexity: this.assessQueryComplexity(query) }; } serializeError(error) { return { type: 'MySQLError', code: error.code, errno: error.errno, sqlMessage: error.sqlMessage, sqlState: error.sqlState, index: error.index, sql: error.sql, // SQL original, sin sanitizar fatal: error.fatal }; } serializeConnection(connection) { return { type: 'MySQLConnection', threadId: connection.threadId, state: connection.state, config: connection.config ? { host: connection.config.host, port: connection.config.port, database: connection.config.database, user: connection.config.user, password: connection.config.password // Contraseña original, sin sanitizar } : undefined, hasQuery: typeof connection.query === 'function', hasConnect: typeof connection.connect === 'function', hasEnd: typeof connection.end === 'function' }; } serializePool(pool) { return { type: 'MySQLPool', config: pool.config ? { host: pool.config.host, port: pool.config.port, database: pool.config.database, user: pool.config.user, password: pool.config.password, // Contraseña original, sin sanitizar connectionLimit: pool.config.connectionLimit, acquireTimeout: pool.config.acquireTimeout, timeout: pool.config.timeout } : undefined, hasGetConnection: typeof pool.getConnection === 'function', hasQuery: typeof pool.query === 'function', hasEnd: typeof pool.end === 'function' }; } } class PostgreSQLSerializer { constructor() { this.name = 'postgresql'; this.priority = 90; } canSerialize(data) { return (this.isPostgreSQLQuery(data) || this.isPostgreSQLError(data) || this.isPostgreSQLClient(data) || this.isPostgreSQLPool(data)); } getComplexity(data) { if (this.isPostgreSQLQuery(data)) { return this.assessQueryComplexity(data); } if (this.isPostgreSQLError(data)) { return 'low'; } if (this.isPostgreSQLClient(data)) { return 'low'; } if (this.isPostgreSQLPool(data)) { return 'medium'; } return 'low'; } async serialize(data, context) { const startTime = Date.now(); try { let result; if (this.isPostgreSQLQuery(data)) { result = this.serializeQuery(data); } else if (this.isPostgreSQLError(data)) { result = this.serializeError(data); } else if (this.isPostgreSQLClient(data)) { result = this.serializeClient(data); } else if (this.isPostgreSQLPool(data)) { result = this.serializePool(data); } else { throw new Error('Tipo de dato PostgreSQL no reconocido'); } const duration = Date.now() - startTime; // ✅ Verificar que la serialización respeta el timeout del contexto const timeout = context.timeout || 50; if (duration > timeout) { throw new Error(`Serialización lenta: ${duration}ms (máximo ${timeout}ms)`); } return { success: true, data: result, metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } catch (error) { const duration = Date.now() - startTime; return { success: false, error: error instanceof Error ? error.message : 'Error desconocido en serialización PostgreSQL', metadata: { serializer: this.name, complexity: this.getComplexity(data), duration, timestamp: new Date().toISOString() } }; } } isPostgreSQLQuery(data) { return (data && typeof data === 'object' && typeof data.text === 'string' && (data.values === undefined || Array.isArray(data.values))); } isPostgreSQLError(data) { return (data && typeof data === 'object' && typeof data.code === 'string' && typeof data.message === 'string'); } isPostgreSQLClient(data) { return (data && typeof data === 'object' && typeof data.query === 'function' && typeof data.connect === 'function' && typeof data.end === 'function'); } isPostgreSQLPool(data) { return (data && typeof data === 'object' && typeof data.connect === 'function' && typeof data.query === 'function' && typeof data.end === 'function'); } assessQueryComplexity(query) { let complexity = 0; const sql = query.text.toLowerCase(); // Basado en el tipo de operación if (sql.includes('select') && !sql.includes('join')) complexity += 1; else if (sql.includes('insert')) complexity += 2; else if (sql.includes('update')) complexity += 3; else if (sql.includes('delete')) complexity += 3; else if (sql.includes('create') || sql.includes('alter') || sql.includes('drop')) { complexity += 4; // DDL operations } // Basado en CTEs (Common Table Expressions) if (sql.includes('with')) { const cteCount = (sql.match(/with\s+\w+\s+as/gi) || []).length; complexity += cteCount * 3; } // Basado en window functions if (sql.includes('over(')) { const windowCount = (sql.match(/over\s*\(/gi) || []).length; complexity += windowCount * 2; } // Basado en joins if (sql.includes('join')) { const joinCount = (sql.match(/join/g) || []).length; complexity += joinCount * 2; } // Basado en subqueries if (sql.includes('(select') || sql.includes('( select')) { const subqueryCount = (sql.match(/\(select/g) || []).length; complexity += subqueryCount * 3; } // Basado en funciones complejas de PostgreSQL if (sql.includes('json_') || sql.includes('array_')) complexity += 2; if (sql.includes('regexp_') || sql.includes('similar to')) complexity += 2; if (sql.includes('full text') || sql.includes('ts_')) complexity += 3; // Basado en la longitud del SQL if (sql.length > 1000) complexity += 3; else if (sql.length > 500) complexity += 2; else if (sql.length > 200) complexity += 1; // Basado en parámetros if (query.values && query.values.length > 20) complexity += 2; else if (query.values && query.values.length > 10) complexity += 1; if (complexity >= 8) return 'high'; if (complexity >= 4) return 'medium'; return 'low'; } serializeQuery(query) { return { type: 'PostgreSQLQuery', text: query.text, // SQL original, sin sanitizar values: query.values, // Valores originales, sin sanitizar name: query.name, rowMode: query.rowMode, types: query.types, config: query.config ? { host: query.config.host, port: query.config.port, database: query.config.database, user: query.config.user, password: query.config.password // Contraseña original, sin sanitizar } : undefined, complexity: this.assessQueryComplexity(query) }; } serializeError(error) { return { type: 'PostgreSQLError', code: error.code, message: error.message, detail: error.detail, hint: error.hint, position: error.position, internalPosition: error.internalPosition, internalQuery: error.internalQuery, // SQL original, sin sanitizar where: error.where, // SQL original, sin sanitizar schema: error.schema, table: error.table, column: error.column, dataType: error.dataType, constraint: error.constraint, file: error.file, line: error.line, routine: error.routine }; } serializeClient(client) { return { type: 'PostgreSQLClient', processID: client.processID, secretKey: client.secretKey, config: client.connectionParameters ? { host: client.connectionParameters.host, port: client.connectionParameters.port, database: client.connectionParameters.database, user: client.connectionParameters.user, password: client.connectionParameters.password // Contraseña original, sin sanitizar } : undefined, hasQuery: typeof client.query === 'function',