UNPKG

@nivinjoseph/n-data

Version:

Data access library for Postgres based on Knex

194 lines (162 loc) 6.81 kB
import { UnitOfWork } from "./unit-of-work.js"; import { DbConnectionFactory } from "../db-connection-factory/db-connection-factory.js"; import { given } from "@nivinjoseph/n-defensive"; import { Knex } from "knex"; import { InvalidOperationException } from "@nivinjoseph/n-exception"; import { inject } from "@nivinjoseph/n-ject"; // public @inject("DbConnectionFactory") export class KnexPgUnitOfWork implements UnitOfWork { private readonly _dbConnectionFactory: DbConnectionFactory; private readonly _onCommits = new Array<PostTransactionExec>(); private readonly _onRollbacks = new Array<PostTransactionExec>(); private _transactionScope: TransactionScope | null = null; public constructor(dbConnectionFactory: DbConnectionFactory) { given(dbConnectionFactory, "dbConnectionFactory").ensureHasValue().ensureIsObject(); this._dbConnectionFactory = dbConnectionFactory; } public getTransactionScope(): Promise<object> { if (this._transactionScope) { if (this._transactionScope.isCommitted || this._transactionScope.isRolledBack) return Promise.reject(new InvalidOperationException("using completed UnitOfWork")); return Promise.resolve(this._transactionScope.trx); } const promise = new Promise<object>((resolve, reject) => { this._dbConnectionFactory.create() .then((knex: any) => { (<Knex>knex) .transaction((trx: Knex.Transaction) => { if (this._transactionScope) { // eslint-disable-next-line @typescript-eslint/no-floating-promises trx.rollback(); if (this._transactionScope.isCommitted || this._transactionScope.isRolledBack) reject(new InvalidOperationException("using completed UnitOfWork")); else resolve(this._transactionScope.trx); } else { this._transactionScope = { trx: trx, isCommitting: false, isCommitted: false, isRollingBack: false, isRolledBack: false }; resolve(this._transactionScope.trx); } }) .catch(() => { /** */ }); }) .catch(err => reject(err)); }); return promise; } public onCommit(callback: () => Promise<void>, priority?: number): void { given(callback, "callback").ensureHasValue().ensureIsFunction(); given(priority, "priority").ensureIsNumber().ensure(t => t >= 0); priority ??= 0; this._onCommits.push({ callback, priority }); } public async commit(): Promise<void> { if (!this._transactionScope) { if (this._onCommits.isNotEmpty) await this._onCommits .groupBy(t => t.priority.toString()) .orderBy(t => Number.parseInt(t.key)) .forEachAsync(t => Promise.all(t.values.map(v => v.callback())) as unknown as Promise<void>, 1); return; } if (this._transactionScope.isCommitted || this._transactionScope.isRolledBack) throw new InvalidOperationException("committing completed UnitOfWork"); if (this._transactionScope.isCommitting) throw new InvalidOperationException("double committing UnitOfWork"); this._transactionScope.isCommitting = true; const promise = new Promise<void>((resolve, reject) => { this._transactionScope!.trx.commit() .then(() => { this._transactionScope!.isCommitted = true; resolve(); }) .catch((err) => reject(err)); }); await promise; if (this._onCommits.isNotEmpty) await this._onCommits .groupBy(t => t.priority.toString()) .orderBy(t => Number.parseInt(t.key)) .forEachAsync(t => Promise.all(t.values.map(v => v.callback())) as unknown as Promise<void>, 1); } public onRollback(callback: () => Promise<void>, priority?: number): void { given(callback, "callback").ensureHasValue().ensureIsFunction(); given(priority, "priority").ensureIsNumber().ensure(t => t >= 0); priority ??= 0; this._onRollbacks.push({ callback, priority }); } public async rollback(): Promise<void> { if (!this._transactionScope) { if (this._onRollbacks.isNotEmpty) await this._onRollbacks .groupBy(t => t.priority.toString()) .orderBy(t => Number.parseInt(t.key)) .forEachAsync(t => Promise.all(t.values.map(v => v.callback())) as unknown as Promise<void>, 1); return; } if (this._transactionScope.isCommitted || this._transactionScope.isRolledBack) throw new InvalidOperationException("rolling back completed UnitOfWork"); if (this._transactionScope.isRollingBack) throw new InvalidOperationException("double rolling back UnitOfWork"); this._transactionScope.isRollingBack = true; const promise = new Promise<void>((resolve, reject) => { this._transactionScope!.trx.rollback("[DELIBERATE]") .then(() => { this._transactionScope!.isRolledBack = true; resolve(); }) .catch((err) => reject(err)); }); await promise; if (this._onRollbacks.isNotEmpty) await this._onRollbacks .groupBy(t => t.priority.toString()) .orderBy(t => Number.parseInt(t.key)) .forEachAsync(t => Promise.all(t.values.map(v => v.callback())) as unknown as Promise<void>, 1); } } interface TransactionScope { trx: Knex.Transaction; isCommitting: boolean; isCommitted: boolean; isRollingBack: boolean; isRolledBack: boolean; } interface PostTransactionExec { callback(): Promise<void>; priority: number; }