maintrackutils
v1.0.1
Published
Utilidades MainTrack para microservicios Node.js: common, RabbitMQ, tenant Express/Mongoose, y Morgan+S3+cron (morgan-s3-logger). Un solo paquete npm con subpaths.
Maintainers
Readme
maintrackUtils
Paquete npm único (nombre en registro: maintrackutils) que agrupa módulos internos para microservicios MainTrack, cada uno importable por subpath para mantener el código separado y el mantenimiento claro.
| Subpath | Contenido |
|---------|-----------|
| maintrackutils/common | Tipos compartidos, errores HTTP, helpers, validadores Zod |
| maintrackutils/message-broker | Cliente RabbitMQ (eventos topic, DLX, RPC) |
| maintrackutils/tenant-manager | Multi-tenant Express + Mongoose + caché Redis opcional |
| maintrackutils/morgan-s3-logger | Morgan → archivo local, cron (p. ej. 24h) sube a S3 |
Node.js ≥ 20 (requisito unificado del paquete).
Los submódulos no leen process.env: URL, tokens y credencia los define siempre tu aplicación.
Tabla de contenidos
- Instalación
- Imports por subpath
- Módulo
common - Módulo
message-broker - Módulo
tenant-manager - Módulo
morgan-s3-logger - TypeScript y resolución de tipos
- Desarrollo y publicación
- Licencia
Instalación
npm install maintrackutilsSolo necesitas una dependencia en package.json; elige en el código qué subpath importar según lo que use el microservicio.
Imports por subpath
Usa siempre el subpath del módulo que necesites (evita importar desde la raíz del paquete: no hay entrada "." en exports).
// Tipos y utilidades compartidas
import { AppError, paginationSchema } from 'maintrackutils/common';
// RabbitMQ
import { EventPublisher, consumeEvents } from 'maintrackutils/message-broker';
// Multi-tenant
import { configureTenantManager, tenantMiddleware } from 'maintrackutils/tenant-manager';En CommonJS:
const { getPublisher } = require('maintrackutils/message-broker');Módulo common
Import: maintrackutils/common
Dependencias runtime: zod
Qué incluye
- Eventos / routing keys:
EventType(enum con el catálogo de claves paramaintrack.events; edítalo para alinearlo con todos tus microservicios),EventRoutingKey,EventPayload,EVENT_TYPE_VALUES - Interfaces:
PaginationParams,PaginatedResponse,ApiResponse,TenantContext,UserContext,ServiceConfig - Errores:
AppError,ValidationError,NotFoundError,UnauthorizedError,ForbiddenError,ConflictError,ServiceUnavailableError,BadRequestError - Helpers:
retryWithBackoff,generateCorrelationId,sleep,formatError,isValidObjectId - Validadores Zod:
paginationSchema,idParamSchema,tenantHeaderSchemay tipos inferidosPaginationInput,IdParam,TenantHeader
Ejemplo: validación de query y cabecera
import { paginationSchema, tenantHeaderSchema } from 'maintrackutils/common';
const query = paginationSchema.parse(req.query);
const headers = tenantHeaderSchema.parse(req.headers);Ejemplo: respuesta de API y errores
import { ApiResponse, NotFoundError } from 'maintrackutils/common';
function ok<T>(data: T): ApiResponse<T> {
return { success: true, data };
}
async function loadUser(id: string) {
const user = await findById(id);
if (!user) throw new NotFoundError('User', id);
return user;
}Módulo message-broker
Import: maintrackutils/message-broker
Dependencias runtime: amqplib, uuid
Cliente TypeScript para RabbitMQ: exchange topic maintrack.events, colas con dead-letter hacia maintrack.dlx / routing key failed, y RPC por cola con correlationId y cola de respuesta exclusiva.
Conexión (RabbitMqConnectConfig)
import type { RabbitMqConnectConfig } from 'maintrackutils/message-broker';Es string | import('amqplib').Options.Connect. La URL u opciones las construyes en tu servicio (por ejemplo leyendo process.env allí, no dentro del paquete).
Catálogo de eventos (EventType)
Las routing keys compartidas entre microservicios están centralizadas en maintrackutils/common (src/common/types/events.ts → enum EventType). Importa el enum y úsalo al publicar para evitar typos:
import { EventType } from 'maintrackutils/common';
import { getPublisher } from 'maintrackutils/message-broker';
await getPublisher().publish(EventType.USER_REGISTERED, { tenantId: 't1', email: '[email protected]' });EventPublisher.publish acepta EventType | string: puedes seguir usando strings si un evento aún no está en el enum.
Cómo se crean el exchange y las colas en RabbitMQ
| Recurso | Quién lo crea | Cómo |
|--------|----------------|------|
| Exchange maintrack.events (topic, durable) | EventPublisher.connect y EventConsumer.connect | channel.assertExchange(...) — idempotente: si ya existe igual, no pasa nada. |
| Colas de consumo (p. ej. ms-notifications.email-jobs) | Solo el consumidor, al llamar subscribe / consumeEvents | assertQueue con argumentos de dead-letter + bindQueue a las routing keys que indiques. El publicador no declara colas. |
| Exchange maintrack.dlx | Tu infraestructura (no este paquete) | El código solo pone x-dead-letter-exchange: maintrack.dlx en las colas nuevas. Ese exchange debe existir en el broker antes de consumir en producción. |
| Cola de respuesta RPC | RPCClient.connect | Cola exclusiva anónima (assertQueue('', { exclusive: true })) para correlación. |
En resumen: las colas “de cada microservicio” nacen cuando ese servicio arranca su EventConsumer y se suscribe con un nombre de cola y una lista de routing keys (normalmente valores de EventType).
Publicador
import { EventPublisher } from 'maintrackutils/message-broker';
const publisher = new EventPublisher();
await publisher.connect(rabbitMqConfigFromEnv());
await publisher.publish('user.registered', {
tenantId: 'tenant-1',
userId: 'abc',
});
await publisher.close();Singleton: getPublisher().
Consumidor
import { EventConsumer } from 'maintrackutils/message-broker';
const consumer = new EventConsumer();
await consumer.connect(config);
await consumer.subscribe(
'mi-servicio.cola',
['user.registered'],
async (message) => { /* ACK automático si no lanzas */ }
);Atajo: consumeEvents(config, queueName, routingKeys, handler).
RPC (solo cliente)
import { RPCClient, callRPC } from 'maintrackutils/message-broker';
const rpc = new RPCClient();
await rpc.connect(config);
const result = await rpc.call('users.lookup', { email: '[email protected]' }, 8000);También: getRPCClient(), callRPC(config, queue, data, timeout?). El worker que responde en replyTo con el mismo correlationId lo implementas tú.
Infraestructura esperada
| Recurso | Valor |
|---------|--------|
| Exchange de eventos | maintrack.events (topic, durable) |
| DLX en colas del consumidor | maintrack.dlx |
| DL routing key | failed |
Tabla de exports principales
| Export | Uso |
|--------|-----|
| RabbitMqConnectConfig | Config para connect() |
| MessageEnvelope | Forma del JSON del publicador |
| EventPublisher / getPublisher | Publicación |
| EventConsumer / consumeEvents | Consumo |
| EventHandler | Tipo del callback del consumidor |
| RPCClient / getRPCClient / callRPC | RPC |
La referencia detallada está en JSDoc en el código fuente y en los .d.ts generados.
Módulo tenant-manager
Import: maintrackutils/tenant-manager
Peer dependencies (debes tenerlas en tu microservicio): express (^4.18 o ^5), mongoose (^8), axios (^1.7), ioredis (^5). Instálalas junto a maintrackutils para que el middleware y el cliente Redis resuelvan en runtime y TypeScript use una sola copia de tipos de Express.
Librería para microservicios Express con multi-tenancy Mongoose (una conexión por tenant), AsyncLocalStorage por petición y configuración obtenida vía HTTP desde tu servicio admin. Caché opcional en Redis.
El módulo common del mismo paquete ya no es dependencia externa: si compartes tipos entre tenant-manager y tu app, importa desde maintrackutils/common.
Contrato del servicio admin
GET {adminServiceBaseUrl}/internal/tenants/{tenantId}/configCampos usados por el middleware (entre otros): tenantId, isActive, connectionString, connectionUser, connectionPassword, connectionDatabase, companyId o _id, name. 404 del admin se interpreta como tenant inexistente.
Configuración al arranque
import { configureTenantManager } from 'maintrackutils/tenant-manager';
configureTenantManager({
adminServiceBaseUrl: process.env.ADMIN_SERVICE_URL!,
tenantConfigHeaders: { 'x-service-token': process.env.SERVICE_INTERNAL_TOKEN! },
redis: {
host: process.env.REDIS_HOST!,
port: Number(process.env.REDIS_PORT ?? '6379'),
password: process.env.REDIS_PASSWORD,
},
});Cabeceras dinámicas: tenantConfigHeaders puede ser una función async () => ({ ... }).
Express: orden típico
configureTenantManager(...)una vez al iniciar.app.use(tenantMiddleware)en rutas que puedan llevarx-tenant-id.requireTenanten rutas que exijan tenant.getTenantConnection()/getCurrentTenantId()/tenantContexten controladores.
import express from 'express';
import {
configureTenantManager,
tenantMiddleware,
requireTenant,
getTenantConnection,
} from 'maintrackutils/tenant-manager';
const app = express();
app.use(express.json());
configureTenantManager({
adminServiceBaseUrl: process.env.ADMIN_SERVICE_URL!,
tenantConfigHeaders: { 'x-service-token': process.env.SERVICE_TOKEN! },
});
app.use(tenantMiddleware);
app.get('/health', (_req, res) => res.json({ ok: true }));
app.get('/api/items', requireTenant, async (_req, res) => {
const conn = getTenantConnection();
// conn.model(...) o modelos registrados con registerTenantModel
res.json({ ok: true });
});Cabecera del cliente o API gateway:
x-tenant-id: <id-del-tenant>Redis
- Clave:
tenant:config:{tenantId} - TTL al escribir: 3600 s
Apagado ordenado
import { TenantConnectionManager } from 'maintrackutils/tenant-manager';
await TenantConnectionManager.getInstance().closeAllConnections();Modelos por tenant
registerTenantModel, registerTenantModels, getTenantModel, clearTenantModels evitan doble registro y mantienen un registro interno por conexión.
API pública (resumen)
| Área | Exports |
|------|---------|
| Config | configureTenantManager, TenantManagerConfig, TenantManagerHttpHeaders, TenantConfigHeaders |
| Middleware | tenantMiddleware, requireTenant |
| Request | getTenantConnection, getCurrentTenantId |
| Contexto | tenantContext, TenantInfo |
| Conexiones | TenantConnectionManager |
Módulo morgan-s3-logger
Import: maintrackutils/morgan-s3-logger
Dependencias runtime del submódulo: morgan, node-cron, @aws-sdk/client-s3 (ya vienen con maintrackutils).
Peer: express (el middleware de Morgan se usa con Express).
Escribe los access logs de Morgan en un archivo local; un cron (por defecto 0 0 * * *, una vez al día a las 00:00 en la zona horaria TZ del proceso) sube el archivo a S3 con PutObject. Cada microservicio inyecta region, bucket, accessKeyId, secretAccessKey (y opcional keyPrefix, endpoint para MinIO), por ejemplo leyendo process.env en su server.ts — este módulo no lee env.
| Método | Uso |
|--------|-----|
| getMorganStream() | Writable para morgan('combined', { stream }) manual. |
| createMorganMiddleware(format?) | app.use(logManager.createMorganMiddleware()) — recomendado. |
| startUploadCron() / stopUploadCron() / restartUploadCron() | Control del cron (iniciar, parar, reiniciar; opcional nueva expresión en start/restart). |
| isCronActive() / getCronExpression() | Estado del programador. |
| uploadToS3() | Sube ahora (pruebas o botón “flush”); respeta truncateAfterUpload. |
| getLocalLogTail({ lines, maxBytes }) / getLocalLogStack(...) | Últimas líneas del log en disco (protegido por maxLogReadBytes por defecto). |
Key S3 generada: {keyPrefix o maintrack/logs}/{serviceName}/{isoTimestamp}-{nombreArchivo}.
Ejemplo (Express)
import express from 'express';
import { MorganS3LogManager } from 'maintrackutils/morgan-s3-logger';
const app = express();
const logToS3 = new MorganS3LogManager({
serviceName: 'ms-auth',
logFilePath: './logs/http-access.log',
s3: {
region: process.env.AWS_REGION!,
bucket: process.env.LOGS_S3_BUCKET!,
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
keyPrefix: 'maintrack/prod',
},
uploadScheduleCron: '0 0 * * *',
truncateAfterUpload: true,
startCronOnInit: true,
});
app.use(logToS3.createMorganMiddleware('combined'));Notas
- Tras subir, si
truncateAfterUpload: true, el archivo local se vacía; el mismoWritablese reabre para que no tengas que re-montar Express. - Tráfico muy alto: entre
endyfinishdel stream interno hay una ventana mínima; en la práctica suele ser aceptable en ventanas de cron de baja carga. - Asegúrate de que el rol IAM tenga
s3:PutObjecten el prefijo que uses.
TypeScript y resolución de tipos
- Versión recomendada: TypeScript ≥ 4.7 (soporte sólido de
package.json"exports"). - Cada subpath tiene su propio
typesenexports, así queimport ... from 'maintrackutils/message-broker'resuelve los.d.tscorrectos.
Si usas moduleResolution: "node" (estilo clásico) y TypeScript no resuelve maintrackutils/..., puedes mapear los subpaths en el tsconfig.json del microservicio:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"maintrackutils/common": ["node_modules/maintrackutils/dist/common/index"],
"maintrackutils/message-broker": ["node_modules/maintrackutils/dist/message-broker/index"],
"maintrackutils/tenant-manager": ["node_modules/maintrackutils/dist/tenant-manager/index"],
"maintrackutils/morgan-s3-logger": ["node_modules/maintrackutils/dist/morgan-s3-logger/index"]
}
}
}En runtime, Node sigue resolviendo bien los imports gracias al campo exports del paquete. La alternativa recomendada a largo plazo es usar "moduleResolution": "node16" o "nodenext" junto con "module": "node16" / "nodenext" según la guía de TypeScript.
Desarrollo y publicación
En el clon del monorepo, desde la carpeta packages/maintrackutils:
npm install
npm run buildnpm run dev—tsc --watchnpm run clean— borradist/prepublishOnly— limpia y compila antes denpm publish
Solo se publican los archivos listados en package.json → files (dist, README.md).
Antes de publicar, revisa en package.json si quieres añadir repository, homepage y bugs con URLs reales de tu repositorio.
Licencia
ISC — ver el campo license en package.json.
