Skip to content

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.

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: updateByKey invalida 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.

SystemConfig:

CampoTipoNotas
idUUID
clavestringIdentificador (e.g. feature.x.enabled).
valorstring?Siempre TEXT en DB — los accessors hacen el parsing.
descripcionstring?Documentación operacional.
categoriastring?Agrupador para UI.
tipo_datoenum'string' | 'text' | 'number' | 'boolean' | 'json'.
activaboolean
created_at / updated_attimestamp
created_by / updated_byUUID?

valor se guarda siempre como string. El tipo_dato indica cómo debe interpretarse y dispara la validación en updates.

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.

Devuelve el array completo del tenant (desde cache si vigente). Ordenado por la DB como ORDER BY categoria, clave.

Busca en el array cacheado con .find(c => c.clave === clave). Lanza Error("Configuration with key 'X' not found") si no existe.

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.

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.

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.

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_datoValidación
number!isNaN(Number(value)).
booleanvalue.toLowerCase() ∈ { 'true', 'false', '0', '1' }.
jsonJSON.parse(value) no lanza.
stringCualquier cosa.
textCualquier cosa.

null siempre pasa — implica “desconfigurar” (la DB acepta NULL si la columna lo permite).

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.

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").

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 }).

Documentados en Administración API. Resumen:

MétodoRutaService call
GET/api/admin/system-configgetAll(ctx)
GET/api/admin/system-config/:clavegetByKey(ctx, clave)
PUT/api/admin/system-config/:claveupdateByKey(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.

Desde otro service que necesita una config:

Lectura desde otro service
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.