UNPKG

ravel

Version:

Ravel Rapid Application Development Framework

160 lines (139 loc) 5.55 kB
'use strict'; const sRavelInstance = Symbol.for('_ravelInstance'); const sOpenConnections = Symbol.for('_openConnections'); const sCloseConnections = Symbol.for('_closeConnections'); /** * Database transaction support for `ravel` applications provided in two ways: * - Middlware (transaction-per-request): Will open and close connections * automatically and manage rollbacks when errors are thrown. This mechanism * is exposed via the [@transaction](#transaction) decorator. * - Scoped transaction: For use outside of the context of an explicit web route. * Useful for tasks such as database migration. * * @private */ class TransactionFactory { /** * @param {Ravel} ravelInstance - An instance of a Ravel app. * @private */ constructor(ravelInstance) { this[sRavelInstance] = ravelInstance; } /** * Middleware which populates a request object with * an open transaction for all configured database providers. * This transaction will be automatically rolled back or committed * when the response is finished (if the given provider suports * rollbacks). * * @param {...string} providerNames - The names of which providers * to open connections for. Optional. * @private */ middleware(...providerNames) { const self = this; return async function (ctx, next) { const closers = []; ctx.transaction = await self[sOpenConnections](providerNames, closers); // try awaiting on next middleware and committing statements afterwards. Rollback if there was an error try { await next(); await self[sCloseConnections](closers, true); } catch (err) { try { await self[sCloseConnections](closers, false); } catch (e) {} throw err; } }; } /** * For use when middleware transactions can't be used (in other words, * outside of `Routes` and `Resources`). This method is made available * through `$db.scoped`. See [`$db.scoped`](#$db#scoped) for examples. * * @param {Array} args - Arguments beginning with 0-N Strings representing providers to open connections on, * followed by an async function which Will be provided with a context which contains * this.transaction. * @returns {Promise} A Promise which is resolved when inGen is finished running, or rejected if * an error was thrown. * @private */ async scoped(...args) { const scope = args[args.length - 1]; const provs = args.slice(0, args.length - 1); const ctx = Object.create(null); return this.middleware(...provs)(ctx, async () => scope(ctx)); } } /** * Private function for opening all transactional connections * to the registered database providers. * * @param {Array<string>} providerNames - The names of which providers to open connections for. If empty, all * connections will be opened. * @param {Array} closers - A place to put connection closing closures. * @private * */ TransactionFactory.prototype[sOpenConnections] = function (providerNames, closers) { // guarantee provider names are unique let uniqueProviderNames = new Set(); providerNames.forEach(n => uniqueProviderNames.add(n)); uniqueProviderNames = Array.from(uniqueProviderNames); return new Promise((resolve, reject) => { const providers = this[sRavelInstance].databaseProviders(); if (providers.length === 0) { this[sRavelInstance].$log.debug('Middleware transaction attempted, but no database providers are registered.'); // resolve with no connections resolve(Object.create(null)); } else { const sConnName = Symbol.for('name'); const toOpen = providerNames.length === 0 ? providers : providers.filter(p => providerNames.indexOf(p.name) >= 0); // index provider promises in an array and use co to open connections. Promise.all(toOpen.map(p => { return p.getTransactionConnection() // chain an extra then() on the end, which will store connection closing functions // which will allow us to clean up if one of the opens fails. .then(conn => { closers.push(shouldCommit => { return p.exitTransaction(conn, shouldCommit); }); conn[sConnName] = p.name; // store name in promise for later return conn; }); })).then(connections => { // convert array into map with provider names const connObj = Object.create(null); for (const c of connections) { connObj[c[sConnName]] = c; } resolve(connObj); }).catch(err => { this[sCloseConnections](closers, false); reject(err); }); } }); }; /** * Private function for closing all open transactional connections. * * @param {Array} closers - A place to put connection-closing closures. * @param {boolean} shouldCommit - Whether or not to commit the transaction. * @private */ TransactionFactory.prototype[sCloseConnections] = function (closers, shouldCommit) { return Promise.all(closers.map(closer => { return closer(shouldCommit); })).catch(err => { throw err; }); }; /** * Populates a `ravel instance` with a TransactionFactory. * * @private * @param {Ravel} ravelInstance - An instance of a Ravel app. */ module.exports = function (ravelInstance) { ravelInstance.registerParameter('always rollback transactions', false); return new TransactionFactory(ravelInstance); };