Skip to content

Plataforma técnica · Orchestrator

SQL Builders

Orchestrator Common Sql

sqlBuilders.ts provee tres funciones puras para armar SQL dinámico parametrizado:

FunciónPara
buildWhereCláusula WHERE desde un objeto de filtros con mapping declarativo.
buildUpdateStatement UPDATE con SET dinámico + audit fields automáticos.
buildInsertStatement 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, ....

Recorre filters y, según mapping[key], agrega una cláusula. Skip de valores undefined | null | '' y de arrays vacíos para IN.

ConfigSQL 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).
buildWhere
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);

Para condiciones que no encajan en eq/IN/LIKE (rangos de fecha, JSON ops, subqueries), usar custom:

Custom mapping
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.

shouldSkipFilter salta un filtro cuando:

  • value === undefined o null o ''.
  • 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_TIMESTAMP
WHERE id = $1
RETURNING *

$1 siempre es el id. El resto son los valores de data.

OpciónDefaultNotas
idColumn'id'Para PK con otro nombre (e.g. 'codigo').
jsonFields[]Estos campos se serializan con JSON.stringify.
includeUpdatedBytrueAñade updated_by = $K.
includeUpdatedAttrueAñade updated_at = <updatedAtSql>.
updatedAtSql'CURRENT_TIMESTAMP'Expresión SQL literal (sin parametrizar).
  • Filtra automáticamente id/idColumn, created_by, created_at, updated_by, updated_at del data — 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.
buildUpdate
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);
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.

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.

OpciónDefaultNotas
jsonFields[]Igual que buildUpdate.
buildInsert
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]

Estas son funciones de conveniencia para SQL plano y predecible. Si necesitas:

  • JOINs o subqueries — escribir SQL manual.
  • ON CONFLICT upsert — escribir SQL manual con INSERT ... ON CONFLICT (col) DO UPDATE SET ....
  • Múltiples returning o ctes — SQL manual.
  • Bulk insert (>50 filas) — usar pg-format o construir un solo INSERT ... 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).

RepositoryFunción usada
CapitalRepository.updatebuildUpdate con updatedAtSql: 'CURRENT_TIMESTAMP'.
LegalRepRepository.updatebuildUpdate.
ManualCuentasRepositoryNo usa — Mongo.
Resto de repositories del proyectoMayoría buildUpdate; algunos buildWhere para listados con filtros opcionales.