Plataforma técnica · Orchestrator
SQL Builders
Orchestrator Common Sql
sqlBuilders.ts provee tres funciones puras para armar SQL dinámico parametrizado:
| Función | Para |
|---|---|
buildWhere | Cláusula WHERE desde un objeto de filtros con mapping declarativo. |
buildUpdate | Statement UPDATE con SET dinámico + audit fields automáticos. |
buildInsert | Statement INSERT con created_by/updated_by injectados. |
Todas devuelven { sql, values } listo para pool.query(sql, values). Cero string concat de valores — siempre $1, $2, ....
buildWhere(filters, mapping)
Section titled “buildWhere(filters, mapping)”Recorre filters y, según mapping[key], agrega una cláusula. Skip de valores undefined | null | '' y de arrays vacíos para IN.
Tipos de mapping
Section titled “Tipos de mapping”| Config | SQL generado |
|---|---|
{ column: 'estado' } (default = equality) | estado = $N |
{ column: 'codigo', op: 'IN' } | codigo = ANY($N) |
{ column: 'codigo', op: 'IN', arrayType: 'text' } | codigo = ANY($N::text[]) |
{ column: 'nombre', op: 'LIKE' } | nombre ILIKE $N con valor %X% |
{ custom: (value, idx) => ({ sql, values }) } | Lo que devuelva la función (full control). |
Ejemplo
Section titled “Ejemplo”import { buildWhere } from '@/domain/common/sqlBuilders';
const filters = { estado: 'ACTIVO', codigos: ['1101001', '1101002'] };const mapping = {estado: { column: 'estado' },codigos: { column: 'codigo', op: 'IN', arrayType: 'text' },nombre: { column: 'nombre', op: 'LIKE' },};
const { whereSql, values } = buildWhere(filters, mapping);// whereSql: "estado = $1 AND codigo = ANY($2::text[])"// values: ['ACTIVO', ['1101001', '1101002']]
const sql = `SELECT * FROM administracion.plan_contable WHERE ${whereSql} ORDER BY codigo`;await pool.query(sql, values);Custom mapping
Section titled “Custom mapping”Para condiciones que no encajan en eq/IN/LIKE (rangos de fecha, JSON ops, subqueries), usar custom:
const mapping = {fecha_desde: { custom: (value, index) => ({ sql: `fecha_movimiento >= $${index}`, values: [value], }),},fecha_hasta: { custom: (value, index) => ({ sql: `fecha_movimiento <= $${index}`, values: [value], }),},};El index es el próximo $N disponible. Si tu custom necesita varios placeholders, los empuja secuencialmente y el builder respeta la cuenta.
Skip semantics
Section titled “Skip semantics”shouldSkipFilter salta un filtro cuando:
value === undefinedonullo''.op === 'IN'con array vacío.
Esto permite pasar el objeto de filtros del request directo sin pre-limpiar — los nulls no contaminan el WHERE.
buildUpdate(table, id, data, userId, opts?)
Section titled “buildUpdate(table, id, data, userId, opts?)”Construye:
UPDATE <table>SET <campo> = $N, ..., updated_by = $K, updated_at = CURRENT_TIMESTAMPWHERE id = $1RETURNING *$1 siempre es el id. El resto son los valores de data.
Opciones
Section titled “Opciones”| Opción | Default | Notas |
|---|---|---|
idColumn | 'id' | Para PK con otro nombre (e.g. 'codigo'). |
jsonFields | [] | Estos campos se serializan con JSON.stringify. |
includeUpdatedBy | true | Añade updated_by = $K. |
includeUpdatedAt | true | Añade updated_at = <updatedAtSql>. |
updatedAtSql | 'CURRENT_TIMESTAMP' | Expresión SQL literal (sin parametrizar). |
Comportamiento defensivo
Section titled “Comportamiento defensivo”- Filtra automáticamente
id/idColumn,created_by,created_at,updated_by,updated_atdeldata— si los pasan, se ignoran. Evita que el caller sobreescriba audit fields a mano. - Si no quedan campos para actualizar, lanza
ValidationError("No fields to update")— no genera un UPDATE vacío.
Ejemplo
Section titled “Ejemplo”import { buildUpdate } from '@/domain/common/sqlBuilders';
const { sql, values } = buildUpdate('administracion.capital',capitalId,{ monto: 1500000, descripcion: 'Aporte revisado' },ctx.userId,);// sql:// UPDATE administracion.capital// SET monto = $2, descripcion = $3, updated_by = $4, updated_at = CURRENT_TIMESTAMP// WHERE id = $1// RETURNING *// values: [capitalId, 1500000, 'Aporte revisado', ctx.userId]const { rows } = await pool.query<CapitalEntry>(sql, values);Campos JSON
Section titled “Campos JSON”buildUpdate( 'tabla', id, { metadata: { firma: 'X', version: 2 } }, userId, { jsonFields: ['metadata'] },);// → metadata = $2 con valor JSON.stringify({firma:'X',version:2})Sin jsonFields, el objeto se pasa a node-postgres tal cual y suele fallar con invalid input syntax for type json. Útil cuando la columna es jsonb.
buildInsert(table, data, userId, opts?)
Section titled “buildInsert(table, data, userId, opts?)”Construye:
INSERT INTO <table> (<columns>, created_by, updated_by)VALUES ($1, $2, ..., $N, $N)RETURNING *created_by y updated_by se rellenan ambos con userId. Cualquier created_by/updated_by que venga en data se ignora.
Opciones
Section titled “Opciones”| Opción | Default | Notas |
|---|---|---|
jsonFields | [] | Igual que buildUpdate. |
Ejemplo
Section titled “Ejemplo”const { sql, values } = buildInsert('administracion.capital',{ tipo_capital: 'AUMENTO_CAPITAL', monto: 5000000, fecha_movimiento: '2026-05-24',},ctx.userId,);// sql:// INSERT INTO administracion.capital (tipo_capital, monto, fecha_movimiento, created_by, updated_by)// VALUES ($1, $2, $3, $4, $5)// RETURNING *// values: ['AUMENTO_CAPITAL', 5000000, '2026-05-24', ctx.userId, ctx.userId]Cuándo NO usar estos helpers
Section titled “Cuándo NO usar estos helpers”Estas son funciones de conveniencia para SQL plano y predecible. Si necesitas:
- JOINs o subqueries — escribir SQL manual.
ON CONFLICTupsert — escribir SQL manual conINSERT ... ON CONFLICT (col) DO UPDATE SET ....- Múltiples returning o ctes — SQL manual.
- Bulk insert (>50 filas) — usar
pg-formato construir un soloINSERT ... VALUES (...), (...), ...manual.
Los repositories del proyecto mezclan ambos: helpers para CRUD trivial, SQL manual para queries que pesan (CapitalRepository.list con filtros complejos, LegalRepRepository.findBancoByCuentaCodigo con agregación).
Consumidores en el código
Section titled “Consumidores en el código”| Repository | Función usada |
|---|---|
CapitalRepository.update | buildUpdate con updatedAtSql: 'CURRENT_TIMESTAMP'. |
LegalRepRepository.update | buildUpdate. |
ManualCuentasRepository | No usa — Mongo. |
| Resto de repositories del proyecto | Mayoría buildUpdate; algunos buildWhere para listados con filtros opcionales. |