103 lines
3.8 KiB
JavaScript
103 lines
3.8 KiB
JavaScript
// services/sqlPool.js
|
|
// MSSQL connection pool(s) for all portal stores' direct-SQL access to SAP
|
|
// tables. ONE POOL PER DATABASE NAME — the SQL Server login (SQL_USER/
|
|
// SQL_PASSWORD) is a single server-level credential that can reach every SAP
|
|
// B1 company database on that server (unlike SAP Service Layer, where each
|
|
// company has its own OUSR table with its own per-company password — see
|
|
// [[per-company-sap-login]]), so only the DATABASE a pool points at needs to
|
|
// vary, not the credentials. getPool() with no argument keeps its original
|
|
// behavior (the .env SQL_DATABASE default) for 100% backward compat with
|
|
// every existing caller; pass a companyDB to get/create a pool for that
|
|
// specific database instead.
|
|
//
|
|
// Bug this fixes (2026-08-04): getPool() used to be a SINGLE global pool
|
|
// hardcoded to .env's SQL_DATABASE with no way to target any other company at
|
|
// all — every direct-SQL route (routes/sap.js's item/warehouse/etc. lookups,
|
|
// routes/workOrders.js, reports, dashboards, GST, general ledger…) silently
|
|
// queried the same one database regardless of which company a user selected
|
|
// or was restricted to. Using ConnectionPool directly (not the global
|
|
// sql.connect() singleton) avoids conflicts between concurrently-initialising
|
|
// pools for different databases.
|
|
'use strict';
|
|
const sql = require('mssql');
|
|
|
|
const _pools = new Map(); // dbName → { pool, connecting, waiters }
|
|
const DEFAULT_DB = process.env.SQL_DATABASE;
|
|
|
|
function buildConfig(database) {
|
|
return {
|
|
server: process.env.SQL_SERVER_NAME || process.env.SQL_HOST,
|
|
port: parseInt(process.env.SQL_PORT) || 1433,
|
|
user: process.env.SQL_USER,
|
|
password: process.env.SQL_PASSWORD,
|
|
database,
|
|
connectionTimeout: 30000,
|
|
requestTimeout: 60000,
|
|
pool: { max: 10, min: 0, idleTimeoutMillis: 30000 },
|
|
options: {
|
|
encrypt: true,
|
|
trustServerCertificate: true,
|
|
enableArithAbort: true,
|
|
// SQL Server 2017 on this host requires TLS 1.0.
|
|
// Node.js 17+ disables TLS 1.0 by default — re-enable it for this pool.
|
|
cryptoCredentialsDetails: { minVersion: 'TLSv1' },
|
|
},
|
|
};
|
|
}
|
|
|
|
async function getPool(companyDB) {
|
|
const db = companyDB || DEFAULT_DB;
|
|
let entry = _pools.get(db);
|
|
if (!entry) { entry = { pool: null, connecting: false, waiters: [] }; _pools.set(db, entry); }
|
|
|
|
// Return healthy pool immediately
|
|
if (entry.pool && entry.pool.connected) return entry.pool;
|
|
|
|
// Queue if already connecting
|
|
if (entry.connecting) {
|
|
return new Promise((resolve, reject) => entry.waiters.push({ resolve, reject }));
|
|
}
|
|
|
|
entry.connecting = true;
|
|
try {
|
|
const pool = new sql.ConnectionPool(buildConfig(db));
|
|
|
|
// Reset on pool-level errors so the next caller reconnects
|
|
pool.on('error', err => {
|
|
console.error(`[SQL-POOL] Pool error (${db}) — resetting:`, err.message);
|
|
entry.pool = null;
|
|
});
|
|
|
|
await pool.connect();
|
|
entry.pool = pool;
|
|
console.log(`[SQL-POOL] ✅ Connected ${db} @ ${process.env.SQL_HOST}`);
|
|
entry.waiters.forEach(w => w.resolve(entry.pool));
|
|
return entry.pool;
|
|
} catch (err) {
|
|
entry.pool = null;
|
|
console.error(`[SQL-POOL] ❌ Connection failed (${db}):`, err.message);
|
|
entry.waiters.forEach(w => w.reject(err));
|
|
throw err;
|
|
} finally {
|
|
entry.connecting = false;
|
|
entry.waiters = [];
|
|
}
|
|
}
|
|
|
|
// Parameterised query helper — use ? as placeholder, works like the legacy per-store exec()
|
|
async function query(sqlText, params = [], companyDB) {
|
|
const pool = await getPool(companyDB);
|
|
const req = pool.request();
|
|
let i = 0;
|
|
const text = sqlText.replace(/\?/g, () => {
|
|
const name = `p${i}`;
|
|
req.input(name, params[i]);
|
|
i++;
|
|
return `@${name}`;
|
|
});
|
|
const result = await req.query(text);
|
|
return result.recordset || [];
|
|
}
|
|
|
|
module.exports = { getPool, query };
|