npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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

  1. Instalación
  2. Imports por subpath
  3. Módulo common
  4. Módulo message-broker
  5. Módulo tenant-manager
  6. Módulo morgan-s3-logger
  7. TypeScript y resolución de tipos
  8. Desarrollo y publicación
  9. Licencia

Instalación

npm install maintrackutils

Solo 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 para maintrack.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, tenantHeaderSchema y tipos inferidos PaginationInput, 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}/config

Campos 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

  1. configureTenantManager(...) una vez al iniciar.
  2. app.use(tenantMiddleware) en rutas que puedan llevar x-tenant-id.
  3. requireTenant en rutas que exijan tenant.
  4. getTenantConnection() / getCurrentTenantId() / tenantContext en 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.tseste 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 mismo Writable se reabre para que no tengas que re-montar Express.
  • Tráfico muy alto: entre end y finish del 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:PutObject en 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 types en exports, así que import ... from 'maintrackutils/message-broker' resuelve los .d.ts correctos.

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 build
  • npm run devtsc --watch
  • npm run clean — borra dist/
  • prepublishOnly — limpia y compila antes de npm publish

Solo se publican los archivos listados en package.jsonfiles (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.