Dataforth (projects/dataforth-dos/): - UI feature: row coloring + PUSH/RE-PUSH buttons + Website Status filter - Database dedup to one row per SN (2.89M -> 469K rows, UNIQUE constraint added) - Import logic handles FAIL -> PASS retest transition - Refactored upload-to-api.js to render datasheets in-memory (dropped For_Web filesystem dep) - Bulk pushed 170,984 records to Hoffman API - Statistical sanity check: 100/100 stamped SNs verified on Hoffman GuruRMM (projects/msp-tools/guru-rmm/): - ROADMAP.md: added Terminology (5-tier hierarchy), Tunnel Channels Phase 2, Logging/Audit/Observability, Multi-tenancy, Modular Architecture, Protocol Versioning, Certificates sections + Decisions Log - CONTEXT.md: hierarchy table, new anti-patterns (bootstrap sacred, no cross-module imports), revised next-steps priorities Session logs for both projects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
4.0 KiB
JavaScript
140 lines
4.0 KiB
JavaScript
/**
|
|
* PostgreSQL Database Abstraction Layer
|
|
*
|
|
* Provides a connection pool and helper methods for the TestDataDB app.
|
|
* Replaces better-sqlite3 singleton with pg.Pool.
|
|
*
|
|
* Environment variables (all optional, defaults connect to local PG):
|
|
* PGHOST (default: localhost)
|
|
* PGPORT (default: 5432)
|
|
* PGUSER (default: testdatadb_app)
|
|
* PGPASSWORD (default: DfTestDB2026!)
|
|
* PGDATABASE (default: testdatadb)
|
|
*/
|
|
|
|
const { Pool } = require('pg');
|
|
|
|
const pool = new Pool({
|
|
host: process.env.PGHOST || 'localhost',
|
|
port: parseInt(process.env.PGPORT || '5432', 10),
|
|
user: process.env.PGUSER || 'testdatadb_app',
|
|
password: process.env.PGPASSWORD || 'DfTestDB2026!',
|
|
database: process.env.PGDATABASE || 'testdatadb',
|
|
max: 20,
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 5000,
|
|
});
|
|
|
|
pool.on('error', (err) => {
|
|
console.error(`[${new Date().toISOString()}] [PG POOL ERROR] ${err.message}`);
|
|
});
|
|
|
|
/**
|
|
* Convert SQLite-style ? placeholders to PostgreSQL $1, $2, ... placeholders.
|
|
* Skips ? inside single-quoted strings.
|
|
*/
|
|
function convertPlaceholders(sql) {
|
|
let idx = 0;
|
|
let inString = false;
|
|
let result = '';
|
|
for (let i = 0; i < sql.length; i++) {
|
|
const ch = sql[i];
|
|
if (ch === "'" && (i === 0 || sql[i - 1] !== '\\')) {
|
|
inString = !inString;
|
|
result += ch;
|
|
} else if (ch === '?' && !inString) {
|
|
idx++;
|
|
result += '$' + idx;
|
|
} else {
|
|
result += ch;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Execute a query, return all rows.
|
|
* @param {string} sql - SQL with ? or $N placeholders
|
|
* @param {Array} params - Parameter values
|
|
* @returns {Promise<Array>} rows
|
|
*/
|
|
async function query(sql, params = []) {
|
|
const pgSql = sql.includes('?') ? convertPlaceholders(sql) : sql;
|
|
const result = await pool.query(pgSql, params);
|
|
return result.rows;
|
|
}
|
|
|
|
/**
|
|
* Execute a query, return the first row or null.
|
|
*/
|
|
async function queryOne(sql, params = []) {
|
|
const rows = await query(sql, params);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Execute a statement (INSERT/UPDATE/DELETE), return { rowCount }.
|
|
*/
|
|
async function execute(sql, params = []) {
|
|
const pgSql = sql.includes('?') ? convertPlaceholders(sql) : sql;
|
|
const result = await pool.query(pgSql, params);
|
|
return { rowCount: result.rowCount, rows: result.rows };
|
|
}
|
|
|
|
/**
|
|
* Run a function inside a transaction.
|
|
* The callback receives a client with query/execute helpers.
|
|
* @param {Function} fn - async (client) => result
|
|
* @returns {Promise<*>} result of fn
|
|
*/
|
|
async function transaction(fn) {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const txClient = {
|
|
async query(sql, params = []) {
|
|
const pgSql = sql.includes('?') ? convertPlaceholders(sql) : sql;
|
|
const result = await client.query(pgSql, params);
|
|
return result.rows;
|
|
},
|
|
async queryOne(sql, params = []) {
|
|
const rows = await txClient.query(sql, params);
|
|
return rows[0] || null;
|
|
},
|
|
async execute(sql, params = []) {
|
|
const pgSql = sql.includes('?') ? convertPlaceholders(sql) : sql;
|
|
const result = await client.query(pgSql, params);
|
|
return { rowCount: result.rowCount, rows: result.rows };
|
|
},
|
|
// Direct pg client access for COPY or other advanced operations
|
|
raw: client,
|
|
};
|
|
|
|
const result = await fn(txClient);
|
|
await client.query('COMMIT');
|
|
return result;
|
|
} catch (err) {
|
|
await client.query('ROLLBACK');
|
|
throw err;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the pool (for graceful shutdown).
|
|
*/
|
|
async function close() {
|
|
await pool.end();
|
|
}
|
|
|
|
/**
|
|
* Get the raw pool (for advanced use like COPY).
|
|
*/
|
|
function getPool() {
|
|
return pool;
|
|
}
|
|
|
|
module.exports = { query, queryOne, execute, transaction, close, getPool, convertPlaceholders };
|