// 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 };