Plataforma técnica · Orchestrator
System Config Service
Orchestrator Administración Configuracion
SystemConfigService administra configuraciones dinámicas key/value del tenant — feature flags, umbrales, parámetros que no quieres hard-codear y que el operador puede cambiar sin redeploy. Su característica clave es el cache en memoria con TTL 5 minutos por tenant y un set de accessors tipados que evitan repetir parsing en cada consumidor.
Por qué cache en memoria
Section titled “Por qué cache en memoria”Las configs se leen mucho y mutan poco: cualquier handler que necesite saber “¿está activa la feature X?” no debería pegar a la DB. La estrategia:
- Cache:
Map<tenantDb, { data: SystemConfig[], timestamp: number }>— singleton estático en la clase. - TTL: 5 minutos. Pasado ese tiempo, el primer request del tenant refresca.
- Invalidación:
updateByKeyinvalida el cache del tenant tras COMMIT.
No usa apiCache ni configCache (los caches del proyecto) — son por-key, no por-tenant. Aquí queremos invalidar todo el bundle de configs del tenant cuando una cambia.
Modelo
Section titled “Modelo”SystemConfig:
| Campo | Tipo | Notas |
|---|---|---|
id | UUID | |
clave | string | Identificador (e.g. feature.x.enabled). |
valor | string? | Siempre TEXT en DB — los accessors hacen el parsing. |
descripcion | string? | Documentación operacional. |
categoria | string? | Agrupador para UI. |
tipo_dato | enum | 'string' | 'text' | 'number' | 'boolean' | 'json'. |
activa | boolean | |
created_at / updated_at | timestamp | |
created_by / updated_by | UUID? |
valor se guarda siempre como string. El tipo_dato indica cómo debe interpretarse y dispara la validación en updates.
Cache hit / miss
Section titled “Cache hit / miss”flowchart TB
IN["getCachedConfigs(tenantDb)"]
HIT{"entry existe<br/>y now - timestamp < TTL?"}
CACHE["return entry.data"]
FETCH["SystemConfigRepository.getAll(pool)"]
STORE["cache.set(tenantDb,<br/>{ data, timestamp: now })"]
RET["return data"]
IN --> HIT
HIT -- sí --> CACHE
HIT -- no --> FETCH --> STORE --> RET Todos los accessors públicos (getAll, getByKey, getBoolean, getInt, getJson) llaman a getCachedConfigs — un solo round-trip de DB por tenant cada 5 minutos en el caso típico.
Accessors
Section titled “Accessors”getAll(ctx)
Section titled “getAll(ctx)”Devuelve el array completo del tenant (desde cache si vigente). Ordenado por la DB como ORDER BY categoria, clave.
getByKey(ctx, clave)
Section titled “getByKey(ctx, clave)”Busca en el array cacheado con .find(c => c.clave === clave). Lanza Error("Configuration with key 'X' not found") si no existe.
getBoolean(ctx, clave)
Section titled “getBoolean(ctx, clave)”const v = config.valor?.toLowerCase().trim();return ['true', '1', 'on', 'yes'].includes(v);Acepta 4 representaciones de true: true, 1, on, yes (case insensitive). Cualquier otra cosa — incluyendo null — devuelve false.
getInt(ctx, clave)
Section titled “getInt(ctx, clave)”const val = parseInt(config.valor || '0', 10);if (isNaN(val)) throw new Error("not a valid integer");return val;Null/undefined se trata como '0'. Solo lanza si el string no es parseable a int.
getJson<T>(ctx, clave)
Section titled “getJson<T>(ctx, clave)”JSON.parse(config.valor) con type parameter para el caller. Devuelve null si valor es null. Lanza Error("not valid JSON") si el string no parsea.
Update y Validación
Section titled “Update y Validación”updateByKey(ctx, clave, updates)
Section titled “updateByKey(ctx, clave, updates)”sequenceDiagram
autonumber
participant H as Handler
participant S as SystemConfigService
participant C as Cache (memoria)
participant R as Repository
participant DB as administracion.configuracion_sistema
H->>S: updateByKey(ctx, clave, updates)
S->>S: getByKey (cache o DB) → current
alt updates.valor presente
S->>S: validateValue(current.tipo_dato, updates.valor)
end
S->>R: updateByKey(pool, clave, updates, userId)
R->>DB: UPDATE ... RETURNING *
alt rowCount 0
R-->>S: null
S--xH: Error 'not found'
else 1
R-->>S: SystemConfig actualizada
S->>C: invalidateCache(tenantDb)
S-->>H: ServiceResult { data }
end Validación de valor según el tipo_dato actual (no el del patch — el tipo_dato también puede actualizarse pero la validación usa el preexistente):
tipo_dato | Validación |
|---|---|
number | !isNaN(Number(value)). |
boolean | value.toLowerCase() ∈ { 'true', 'false', '0', '1' }. |
json | JSON.parse(value) no lanza. |
string | Cualquier cosa. |
text | Cualquier cosa. |
null siempre pasa — implica “desconfigurar” (la DB acepta NULL si la columna lo permite).
Invalidación de Cache
Section titled “Invalidación de Cache”SystemConfigService.invalidateCache(tenantDb) es estático público — cualquier otro componente puede invalidar manualmente si modifica la tabla por fuera del service (e.g. una migración o un script). En la práctica, solo updateByKey lo llama.
Repository
Section titled “Repository”Capa fina: SystemConfigRepository.getAll, getByKey, updateByKey. El updateByKey construye SET dinámico filtrando los campos no actualizables (id, clave, created_at, updated_at).
Si updates no trae ningún campo válido, lanza Error("No fields to update").
Por qué no hay create/delete
Section titled “Por qué no hay create/delete”Las claves de config son declarativas — viven en el seed inicial del tenant o se agregan en migraciones cuando se introduce un feature flag nuevo. No es una operación de runtime: si el operador necesita una clave que no existe, la migración correspondiente la crea con un default razonable.
delete tampoco se expone porque borrar una clave que algún servicio lee causaría Error('Configuration with key X not found') en runtime. Para “desactivar” una config: updateByKey(clave, { activa: false }).
Endpoints
Section titled “Endpoints”Documentados en Administración API. Resumen:
| Método | Ruta | Service call |
|---|---|---|
GET | /api/admin/system-config | getAll(ctx) |
GET | /api/admin/system-config/:clave | getByKey(ctx, clave) |
PUT | /api/admin/system-config/:clave | updateByKey(ctx, clave, updates) |
Los accessors tipados (getBoolean, getInt, getJson) son uso interno — no se exponen como endpoints. Los consumen otros services (PayrollService, OperacionesSiiService, etc.) en runtime.
Patrón de Uso Típico
Section titled “Patrón de Uso Típico”Desde otro service que necesita una config:
import { SystemConfigService } from '@/domain/system-config/SystemConfigService';
const systemConfig = new SystemConfigService();const enabled = await systemConfig.getBoolean(ctx, 'feature.depreciacion_automatica');if (!enabled) return;const umbralUf = await systemConfig.getInt(ctx, 'depreciacion.umbral_uf');El cache hace que estas llamadas sean prácticamente gratis después del primer hit.