Plataforma técnica · Orchestrator
Base Service
Orchestrator Common Transactions
BaseService es la clase abstracta base que heredan casi todos los servicios de dominio del Orchestrator. Encapsula seis preocupaciones cross-cutting que aparecerían replicadas en cada servicio si no existiera: acceso a pool por tenant, transacciones con outbox para eventos, cache, helpers de validación, logging contextual y result builders consistentes.
Forma del contexto
Section titled “Forma del contexto”Todos los métodos de servicio que heredan BaseService reciben un ServiceContext:
| Campo | Tipo | Uso |
|---|---|---|
tenantDb | string | Nombre de la base del tenant (e.g. nostromo_60004317). |
userId | string | UUID del usuario que originó la request. |
requestId | string? | UUID por petición para correlación de logs y eventos. |
El handler HTTP construye el contexto en routes/.../*.ts desde req.user y req.id:
const ctx: ServiceContext = {tenantDb: await getDatabaseNameForUser(req.user, req),userId: req.user!.userId,requestId: req.id,};const result = await capitalService.create(ctx, dto);Pool Access
Section titled “Pool Access”| Método protected | Devuelve | Cuándo usarlo |
|---|---|---|
this.getPool(tenantDb) | Pool del tenant (cacheado). | Lecturas/escrituras a la base del tenant. |
this.getCommonPool() | Pool de nostromo_common (parámetros). | Leer indicadores, AFP, AFC, etc. |
Ambos delegan en lib/db.ts (getTenantPool, commonPool).
Transactions
Section titled “Transactions”withTransaction(ctx, callback)
Section titled “withTransaction(ctx, callback)”Patrón estándar para operaciones que mutan estado. El callback recibe un PoolClient ya en BEGIN y un outbox transaccional:
return this.withTransaction(ctx, async (client, outbox) => {const entry = await CapitalRepository.create(client, data, ctx.userId);outbox.queue('capital:movimiento', { capitalId: entry.id, tipoMovimiento: mapTipo(entry.tipo_capital), monto: Number(entry.monto),});return this.success(entry);});Comportamiento:
| Fase | Acción |
|---|---|
| Antes del callback | BEGIN. |
| Si callback throw | ROLLBACK. logError(ctx, ...). El throw se propaga. Outbox descartado. |
| Si callback resuelve | COMMIT. Luego flushOutbox(ctx, outbox). Retorna el valor. |
| Siempre | client.release(). |
Outbox Transaccional
Section titled “Outbox Transaccional”Implementación in-memory por TX:
sequenceDiagram
autonumber
participant H as Handler
participant S as Service
participant DB as PostgreSQL
participant O as Outbox (memoria TX)
participant B as DomainEventBus
H->>S: method(ctx, dto)
S->>DB: BEGIN
S->>DB: INSERT/UPDATE...
S->>O: outbox.queue('event:x', payload)
alt success
S->>DB: COMMIT
S->>O: drain()
O-->>S: [PendingEvent...]
loop por cada evento
S->>B: emit(event, payload + ctx)
end
S-->>H: result
else throw
S->>DB: ROLLBACK
Note over O: descartado<br/>(eventos NO emitidos)
S--xH: error
end Por qué outbox y no emit directo: garantiza que si la TX rollback (e.g. unique violation, validación tardía), los listeners no ven un evento “fantasma” que reaccione a un cambio que nunca persistió. El outbox vive solo en memoria de la TX — no hay tabla outbox en disco.
Auto-enriquecimiento del payload: al hacer flushOutbox, el evento se enriquece con tenantDb, userId, requestId del ServiceContext antes de llegar al bus. Los servicios solo pasan el payload de negocio.
Errores en listeners no rompen la TX: los listeners corren post-COMMIT. Si uno lanza, se loguea ([BaseService] flushOutbox: error emitting "X") pero el método retorna ok. La TX ya está cerrada.
Catálogo de eventos disponibles en DomainEventBus.
withSerializableTransaction(ctx, callback)
Section titled “withSerializableTransaction(ctx, callback)”Variante con ISOLATION LEVEL SERIALIZABLE para operaciones críticas (cierres contables, contadores anti-race). Retry automático en serialization failure (40001) — re-llama a sí misma recursivamente. No tiene cap de profundidad — si la contención persiste, puede stack-overflow. En la práctica el conflicto se resuelve en 1-2 retries.
Caching
Section titled “Caching”Tres helpers delgados sobre lib/cache.ts:
| Método | Backing cache | Uso |
|---|---|---|
this.cached(key, fetcher, ttlSeconds) | apiCache | Cacheo general. |
this.invalidateCache(pattern) | apiCache | Borrado por wildcard pattern. |
this.cachedConfig(key, fetcher) | configCache | Parámetros que rara vez cambian (sin TTL fijo). |
Servicios con cache propia (como SystemConfigService) usan estructuras dedicadas — estos helpers son para casos genéricos.
Validation Helpers
Section titled “Validation Helpers”| Helper | Lanza | Para |
|---|---|---|
this.assert(condition, message) | ValidationError(message) | Aserciones inline. |
this.assertExists(entity, name, id?) | NotFoundError(name, id) | Después de findById/findByCode. |
this.validateRequired<T>(data, fields) | ValidationError([{field, message}]) | DTO completo — typo-safe (keyof T). |
validateRequired filtra undefined, null y "". No detecta 0 como faltante — usar assert explícita para campos numéricos requeridos.
Logging Contextual
Section titled “Logging Contextual”Tres niveles con prefijo del nombre del servicio y propagación de ctx:
| Método | Output |
|---|---|
this.log(ctx, message, data?) | [ServiceName] message { requestId, userId, tenant, ...data } |
this.logWarn(ctx, message, data?) | ⚠️ [ServiceName] message { ... } |
this.logError(ctx, message, error) | 🔴 [ServiceName] message { error, stack, ... } |
Salida va a console.log/warn/error — el runtime (Docker, systemd) la captura. No hay sink JSON estructurado.
Result Builders
Section titled “Result Builders”Forma estándar de respuesta para handlers que retornan ServiceResult<T>:
interface ServiceResult<T> { success: boolean; data?: T; message?: string; error?: string; statusCode?: number;}| Builder | Retorna |
|---|---|
this.success(data, message?) | { success: true, data, message, statusCode: 200 } |
this.failure(error) | { success: false, error, statusCode: 500 } |
this.badRequest(message) | { success: false, error: message, statusCode: 400 } |
this.notFound(resource, id?) | { success: false, error: '<resource> with ID <id> not found', statusCode: 404 } |
En la práctica casi todo el código throw-ea NotFoundError/ValidationError y deja que el errorHandler global responda — notFound/badRequest son legacy de pre-error-handler. success sí se usa universalmente.
isPgError helper
Section titled “isPgError helper”isPgError(err): err is PgError — narrowing type guard para errores de node-postgres. Expone code, detail, constraint, table, schema con tipo. Útil para hooks tipo:
catch (e) { if (isPgError(e) && e.code === '23505') { throw new ValidationError(`Duplicado en ${e.constraint}`); } throw e;}Subclases que existen
Section titled “Subclases que existen”Casi todos los services del Orchestrator extienden BaseService directamente:
domain/payroll/PayrollService(y la mayoría de remuneraciones)domain/activo-fijo/*Servicedomain/capital/CapitalServicedomain/chart-of-accounts/ChartOfAccountsServicedomain/cicloContable/CicloContableServicedomain/company/CompanyServicedomain/configContable/ConfigContableServicedomain/system-config/SystemConfigService(con cache propia adicional)domain/legal-representatives/LegalRepService- … y muchos más.
ServiceUtils (alternativa estática)
Section titled “ServiceUtils (alternativa estática)”Para servicios que prefieren métodos estáticos (e.g. PayrollService legacy), ServiceUtils expone funciones equivalentes sin la maquinaria de outbox:
| Función | Equivalente |
|---|---|
ServiceUtils.withTransaction(...) | BaseService.withTransaction sin outbox. |
ServiceUtils.batchProcess(...) | Procesa array con batchSize, onProgress, stopOnError. |
ServiceUtils.assert(...) | Como BaseService.assert. |
ServiceUtils.assertExists(...) | Como BaseService.assertExists. |
Si necesitas outbox/eventos, usa la clase. Si solo necesitas TX y helpers, ServiceUtils evita instanciar.
BaseRepository (bonus)
Section titled “BaseRepository (bonus)”BaseRepository es una clase opcional para repositories con tableName y schema. Ofrece query, queryOne, exists, count. Casi nadie la extiende — el patrón vigente es repositories estáticos envueltos con wrapStaticRepository (ver RepositoryDecorators).
Vive en el mismo archivo BaseService.ts por proximidad histórica, no por dependencia.