Helpers de base de datos
Toda interacción con la base de datos en Zentto pasa por tres funciones helper definidas
en web/api/src/db/query.ts. Estas funciones abstraen las diferencias entre
SQL Server y PostgreSQL, permitiendo que el código de los servicios sea idéntico
independientemente del motor activo.
callSp — Consulta simple
Ejecuta un stored procedure y retorna las filas del resultado. Es la función más usada para lecturas simples (GetById, búsquedas, validaciones).
// Firma
async function callSp(
spName: string,
params: Record<string, any>
): Promise<any[]>
Ejemplo de uso
import { callSp } from '../../db/query';
// Obtener un producto por ID
const rows = await callSp('usp_Master_Product_GetById', {
CompanyId: 1,
ProductId: 42
});
const product = rows[0]; // Primera fila o undefined
Comportamiento interno
| Motor | SQL generado |
|---|---|
| SQL Server | EXEC usp_Master_Product_GetById @CompanyId=1, @ProductId=42 |
| PostgreSQL | SELECT * FROM usp_master_product_getbyid($1, $2) -- [1, 42] |
callSpOut — Con parámetros de salida
Ejecuta un SP que tiene parámetros de salida (OUTPUT en SQL Server, columnas en PostgreSQL). Es la función estándar para listas paginadas (con TotalCount) y escrituras (con Resultado/Mensaje).
// Firma
async function callSpOut(
spName: string,
params: Record<string, any>,
outputNames: string[]
): Promise<{ rows: any[]; outputs: Record<string, any> }>
Ejemplo: Lista paginada
import { callSpOut } from '../../db/query';
const { rows, outputs } = await callSpOut(
'usp_Master_Product_List',
{
CompanyId: 1,
Search: 'widget',
PageSize: 20,
PageNumber: 1
},
['TotalCount'] // Parámetros de salida esperados
);
console.log(rows); // Array de productos
console.log(outputs.TotalCount); // Número total para paginación
Ejemplo: Escritura con resultado
const { outputs } = await callSpOut(
'usp_Master_Product_Insert',
{
CompanyId: 1,
Name: 'Producto Nuevo',
Sku: 'PN-001',
Price: 49.99
},
['Resultado', 'Mensaje']
);
if (outputs.Resultado > 0) {
// Éxito — Resultado contiene el nuevo ID
console.log(`Creado con ID: ${outputs.Resultado}`);
} else {
// Error de negocio
console.log(`Error: ${outputs.Mensaje}`);
}
Cómo resuelve las diferencias entre motores
| Concepto | SQL Server | PostgreSQL |
|---|---|---|
| TotalCount | @TotalCount INT OUTPUT | Columna "TotalCount" en cada fila (COUNT OVER) |
| Resultado | @Resultado INT OUTPUT | Columna "ok" en RETURNS TABLE |
| Mensaje | @Mensaje NVARCHAR(500) OUTPUT | Columna "mensaje" en RETURNS TABLE |
En PostgreSQL, callSpOut extrae los valores de las columnas de la primera fila
y los mapea a los nombres de salida. En SQL Server, lee directamente los parámetros OUTPUT.
callSpTx — Transaccional
Ejecuta múltiples stored procedures dentro de una transacción única. Si cualquier operación falla, se hace rollback de todas. Útil para operaciones compuestas como crear una factura con su detalle y asiento contable.
// Firma
async function callSpTx(
operations: Array<{
sp: string;
params: Record<string, any>;
outputs?: string[];
}>
): Promise<Array<{ rows: any[]; outputs?: Record<string, any> }>>
Ejemplo: Factura completa
import { callSpTx } from '../../db/query';
const results = await callSpTx([
{
sp: 'usp_Doc_Invoice_Insert',
params: {
CompanyId: 1,
CustomerId: 42,
Items: JSON.stringify(lineItems),
TaxAmount: 15.30
},
outputs: ['Resultado', 'Mensaje']
},
{
sp: 'usp_Acct_Journal_Post',
params: {
CompanyId: 1,
SourceType: 'Invoice',
SourceId: null, // Se reemplaza con el ID de la factura
Amount: totalAmount
},
outputs: ['Resultado', 'Mensaje']
}
]);
// Si llegamos aquí, ambas operaciones fueron exitosas
const invoiceId = results[0].outputs?.Resultado;
Gestión de pools de conexión
La API mantiene un pool de conexiones persistente al motor activo. La configuración se lee de variables de entorno:
# web/api/.env
DB_TYPE=postgres # o "sqlserver"
# SQL Server
MSSQL_SERVER=DELLXEONE31545
MSSQL_DATABASE=DatqBoxWeb
MSSQL_USER=sa
MSSQL_PASSWORD=****
# PostgreSQL
PG_HOST=172.18.0.1 # Docker gateway en producción
PG_PORT=5432
PG_DATABASE=zentto_prod
PG_USER=zentto_app
PG_PASSWORD=****
Cómo funciona el switch interno
// web/api/src/db/query.ts (simplificado)
const dbType = process.env.DB_TYPE || 'sqlserver';
export async function callSp(spName: string, params: Record<string, any>) {
if (dbType === 'postgres') {
return callSpPostgres(spName, params);
}
return callSpSqlServer(spName, params);
}
// SQL Server: usa EXEC con parámetros nombrados
async function callSpSqlServer(sp: string, params: Record<string, any>) {
const pool = await getPool();
const request = pool.request();
for (const [key, value] of Object.entries(params)) {
request.input(key, value);
}
const result = await request.execute(sp);
return result.recordset;
}
// PostgreSQL: usa SELECT * FROM fn() con parámetros posicionales
async function callSpPostgres(sp: string, params: Record<string, any>) {
const pool = getPool();
const values = Object.values(params);
const placeholders = values.map((_, i) => `${i + 1}`).join(', ');
const sql = `SELECT * FROM ${sp.toLowerCase()}(${placeholders})`;
const result = await pool.query(sql, values);
return result.rows;
}
Nota importante
En PostgreSQL, los nombres de funciones se convierten a lowercase
automáticamente. El SP usp_Master_Product_List se llama como
usp_master_product_list. Los nombres de columnas se preservan con
comillas dobles en la definición de la función PostgreSQL.