Dataforth UI push + dedup + refactor, GuruRMM roadmap evolution, Azure signing setup
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>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user