UNPKG

express-slonik

Version:
206 lines 8.61 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RequestTransactionContext = exports.IsolationLevels = exports.sql = void 0; const crypto_1 = require("crypto"); const events_1 = __importDefault(require("events")); const slonik_1 = require("slonik"); const zod_1 = require("zod"); const errors_1 = require("./errors"); exports.sql = (0, slonik_1.createSqlTag)({ typeAliases: { void: zod_1.z.object({}).strict(), }, }); /** * PostgreSQL transaction isolation levels. * @see {@link https://www.postgresql.org/docs/current/transaction-iso.html | PostgreSQL Documentation} for detailed * information. */ exports.IsolationLevels = { /** * Default isolation level in PostgreSQL. Guarantees dirty reads never happen. * * @see {@link https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED | READ COMMITTED} */ READ_COMMITTED: exports.sql.fragment `READ COMMITTED`, /** * Higher isolation level than READ COMMITTED. Guarantees repeatable reads _in addition to_ * never allowing dirty reads. * * @see {@link https://www.postgresql.org/docs/current/transaction-iso.html#XACT-REPEATABLE-READ | REPEATABLE READ} */ REPEATABLE_READ: exports.sql.fragment `REPEATABLE READ`, /** * Highest isolation level. Guarantees phantom reads and serialization anomalies never happen * _in addition to_ never allowing non-repeatable reads and dirty reads. * * @see {@link https://www.postgresql.org/docs/current/transaction-iso.html#XACT-SERIALIZABLE | SERIALIZABLE} */ SERIALIZABLE: exports.sql.fragment `SERIALIZABLE`, }; /** * Transaction context that wraps the Slonik transaction as an EventEmitter. * * @param transaction DatabaseTransactionConnection instance */ class TransactionContext extends events_1.default { constructor(transactionId, transaction, options) { super(options); this.transactionId = transactionId; this.transaction = transaction; this.options = options; } commit() { this.emit("commit"); } rollback(error) { this.error = error; this.emit("rollback", error); } } class RequestTransactionContext { constructor(pool) { this.pool = pool; this.transactionContext = {}; } static getOrCreateContext(pool) { if (!RequestTransactionContext.instance) { RequestTransactionContext.instance = new RequestTransactionContext(pool); } return RequestTransactionContext.instance; } /** * Middleware function for starting a transaction context. While the transaction context is open, * Slonik DatabaseTransactionConnection object is available under req.transaction. * * @param isolationLevel - PostgreSQL [transaction isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Defaults to read committed isolation level. * @param retryLimit - Number of times to retry transaction. Defaults to `5`. */ begin(isolationLevel = exports.IsolationLevels.READ_COMMITTED, retryLimit = 5) { return async (req, res, next) => { if (!this.pool) { return next(new errors_1.UndefinedPoolError("Pool is not instantiated")); } const transactionId = (0, crypto_1.randomUUID)(); try { await this.pool.transaction(async (transaction) => { await transaction.query(exports.sql.typeAlias("void") `SET TRANSACTION ISOLATION LEVEL ${isolationLevel};`); this.transactionContext[transactionId] = new TransactionContext(transactionId, transaction); const transactionContext = this.transactionContext[transactionId]; req.transactionId = transactionId; req.transaction = transaction; const autoCommit = transactionContext.commit.bind(transactionContext); const autoRollback = transactionContext.commit.bind(transactionContext); // Allow transaction to be automatically committed or rolled back when response is sent. // These event handlers must be removed when the promise below is either resolved or // rejected. res.on("finish", autoCommit).on("error", autoRollback); // Hold the transaction open until committed or on error. await new Promise((resolve, reject) => { // We use .once (as opposed to .on) because we want to commit or rollback at most // once. transactionContext .once("commit", () => { // Prevent the response events from being registered multiple times if // transaction.begin() is called again down the middleware chain. res.off("finish", autoCommit).off("error", autoRollback); resolve(); }) .once("rollback", (error) => { // Prevent the response events from being registered multiple times if // transaction.begin() is called again down the middleware chain. res.off("finish", autoCommit).off("error", autoRollback); reject(error); }); // While the transaction is held open, hand off control to next middleware. next(); }); }, retryLimit); // Hand control over to the next middleware in the pipeline when transaction is completed. next(); } catch (error) { next(error); } finally { // Outside of transaction context, the req.transaction is undefined. delete req.transaction; delete req.transactionId; delete this.transactionContext[transactionId]; } }; } /** * Commit the current transaction. */ commit() { return (req, res, next) => { if (!req.transactionId) { return next(new errors_1.TransactionOutOfBoundsError("Cannot commit outside of transaction")); } // No need to call next() here since this.begin handler will call next() // when the promise resolves. this.transactionContext[req.transactionId].commit(); }; } /** * Catch any errors in the request handler stack to rollback transaction. */ catchError() { return (error, req, res, next) => { if (req.transactionId) { // No need to call next(error) here since this.begin handler will call next(err) when // promise rejects. this.transactionContext[req.transactionId].rollback(error); } else { // Hand off control to next error handler if outside of transaction context. next(error); } }; } /** * Ends the transaction. If there are no errors in the request handler chain, the transaction is * automatically committed. Otherwise, it is rolled back. */ end() { return [this.commit(), this.catchError()]; } } exports.RequestTransactionContext = RequestTransactionContext; function isDatabasePool(poolLike) { const keys = [ "any", "anyFirst", "configuration", "connect", "end", "exists", "getPoolState", "many", "manyFirst", "maybeOne", "maybeOneFirst", "one", "oneFirst", "query", "stream", "transaction", ]; return keys.every(Object.prototype.hasOwnProperty.bind(poolLike)); } /** * Request handler wrapped in express-slonik context. * @param pool - Slonik {@link DatabasePool} instance */ function createMiddleware(pool) { if (!isDatabasePool(pool)) { throw new TypeError("Argument must be an instance of Slonik pool instance"); } return RequestTransactionContext.getOrCreateContext(pool); } exports.default = createMiddleware; //# sourceMappingURL=middleware.js.map