@lordfokas/magic-orm
Version:
A class-based ORM in TypeScript. Unorthodox and extremely opinionated, made to fit my specific use cases.
226 lines • 7.95 kB
JavaScript
import util from 'node:util';
import Pool from 'pg-pool';
import { LogLevel, PrettyPrinter, Pipe } from '@lordfokas/loggamus';
const DBCONN = new LogLevel("DBCONN", 35);
const DBLOCK = new LogLevel("DBLOCK", 28);
const DBQUERY = new LogLevel("DBQUERY", 22);
let $logger;
let $pool;
/** Define a new logger to send output to */
export function useLogger(logger, options) {
$logger = logger.child('DB', options || {
mintrace: LogLevel.ERROR,
tracedepth: 5,
styles: {
'DBCONN': { color: 'yellow', mods: ['underline'] },
'DBLOCK': { color: 'red', mods: [] },
'DBQUERY': { color: 'white', mods: ['bright'] }
}
});
}
// Picks up PrettyPrinter output and pushes it back into the logging pipeline.
class DBPipe extends Pipe {
usesRaw() { return false; }
usesPretty() { return true; }
write(pretty, raw, meta) {
$logger.log(pretty, DBQUERY);
}
}
const pretty = new PrettyPrinter(new DBPipe());
/** The database connection manager */
export class DB {
/** Initiate the connection pool to the database server. Necessary before acquiring any connections. */
static init(config) {
$logger.log("Initializing DB Connection Pool!", DBCONN);
$pool = new Pool(config);
}
/** Acquire a connection to execute queries on. */
static async acquire() {
$logger.log("Acquiring DB Connection!", DBCONN);
const conn = await $pool.connect();
return new Connection(conn);
}
;
}
/** A database connection to execute queries on. */
export class Connection {
#containers = 0;
#conn = null;
constructor(conn) {
this.#conn = conn;
}
/**
* Execute a PreparedStatement query
* @param sql string or string[] with the query to execute
* @param values parameters to replace in the prepared statement
* @returns query results
*/
async execute(sql, values = []) {
if (Array.isArray(sql))
sql = sql.join('\n');
DBUtil.validate(sql, values, this.#containers);
sql = DBUtil.pgps(sql); // convert ? to $x
const start = Date.now();
const result = await this.#query(sql, DBUtil.patch(values));
const elapsed = Date.now() - start;
if (this.#containers > 0) {
pretty.reset().color("red").write("║ ".repeat(this.#containers)).reset().style('bright');
}
if (result.rowCount)
pretty.write(`>> ${result.rowCount} rows `);
else
pretty.color('black').write(`>> zero rows `);
pretty.color('black').write('in ', elapsed, ' ms').flush(0);
return result;
}
/**
* Executes raw unprepared queries. Should be used solely to run commands unsupported by prepared statements, such as LOCKs
* @param sql the raw SQL query to execute
* @returns query results
* @deprecated
*/
DANGEROUSLY(sql) {
$logger.log(sql, DBLOCK);
return this.#query(sql);
}
/**
* Runs an async function inside a new transaction.
* Automatically commits at the end, and rollbacks on error.
*/
async atomic(fn) {
let success = false;
try {
await this.#open("BEGIN TRANSACTION");
const result = await fn();
await this.#close("COMMIT");
success = true;
return result;
}
finally {
if (!success) {
await this.#close("ROLLBACK");
}
}
}
/**
* Sets the current path to a given list of schemas.
* @param schemas varargs list of schemas to use.
*/
schema(...schemas) {
const query = "SET search_path TO " + schemas.join(', ');
$logger.log(query, DBCONN);
return this.#query(query);
}
/** Opens a new query containment level (table lock, transaction, etc) */
#open = function open(sql) {
$logger.log("║ ".repeat(this.#containers) + "╔═" + sql, DBLOCK);
this.#containers++;
return this.#query(sql);
};
/** Closes top query containment level (table lock, transaction, etc) */
#close = function close(sql) {
this.#containers--;
$logger.log("║ ".repeat(this.#containers) + "╚═" + sql, DBLOCK);
return this.#query(sql);
};
#query = function query(sql, values) {
if (this.#conn) {
return this.#conn.query(sql, values);
}
else {
throw new Error("Query failed because connection is no longer available");
}
};
/** Release this connection back into the Pool. */
release() {
if (this.#conn) {
$logger.log("Releasing DB Connection!", DBCONN);
this.#conn.release();
this.#conn = null;
}
else {
$logger.log("Ignoring release of DB Connection!", DBCONN);
}
}
}
class DBUtil {
/**
* Convert prepared statements from ? to $1 param format.
* @param sql Query string in possibly ? param format
* @returns Query string converted to $i param format
*/
static pgps(sql) {
if (!sql.includes('?'))
return sql;
let segments = sql.split('?');
let result = segments[0];
for (let i = 1; i < segments.length; i++) {
result += '$' + i + segments[i];
}
return result;
}
/** Convert parameters into the correct data types this DB accepts */
static patch(values) {
if (values)
for (const idx in values) {
var v = values[idx];
if (v === undefined)
throw new Error("UNDEFINED value in prepared array index #" + idx);
else if (v === true)
values[idx] = 1;
else if (v === false)
values[idx] = 0;
}
return values;
}
/**
* Validate and pretty print a query and its params.
* @param sql Prepared Statement query to validate
* @param params PS replacement parameters
* @param locked wether or not table locks are currently in effect
*/
static validate(sql, params, containers) {
const regex = /\?/g;
const plen = (sql.match(regex) || []).length;
const glen = params.length;
if (glen !== plen) { // Enforce same number of placeholders and parameters.
$logger.warn(sql);
throw new Error(`Invalid Parameterization: Expected ${plen}, given ${glen} => `
+ util.inspect(params));
}
params = [...params]; // clone array
const query = sql.replace(/\sAS\s"[A-Z]{2}[a-z0-9_]+"/g, '').split(regex);
pretty.style('reset', 'bright');
const cont = 'red';
const sqlc = 'blue';
const strc = 'green';
const prmc = 'yellow';
const depth = "║ ".repeat(containers);
if (containers > 0) {
pretty.style('reset').color(cont).write(depth).style('bright');
}
while (query.length > 0) {
const str = query.shift();
if (containers > 0 && str.includes("\n")) {
const lines = str.split("\n");
pretty.color(sqlc).write(lines.shift());
for (const line of lines) {
pretty.style('reset').color(cont).write('\n', depth).style('bright');
pretty.color(sqlc).write(line);
}
}
else {
pretty.color(sqlc).write(str);
}
if (query.length === 1 && query[0] === '') {
query.shift();
}
if (params.length > 0) {
const param = params.shift();
pretty.color(typeof param === "string" ? strc : prmc).write(param);
}
}
pretty.flush(0);
}
}
//# sourceMappingURL=DB.js.map