@syntropylog/adapters
Version:
External adapters for SyntropyLog framework
1,344 lines (1,330 loc) • 79 kB
JavaScript
'use strict';
var amqplib = require('amqplib');
var nats = require('nats');
var axios = require('axios');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var amqplib__namespace = /*#__PURE__*/_interopNamespaceDefault(amqplib);
/**
* 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__namespace.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 = nats.JSONCodec();
this.subscriptions = new Map();
this.natsServers = natsServers;
}
async connect() {
this.natsConnection = await nats.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 = nats.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 (axios.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',