sails-hook-orm
Version:
The ORM hook from Sails core.
214 lines (183 loc) • 12.1 kB
JavaScript
/**
* Module dependencies
*/
var assert = require('assert');
var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
var helpLeaseConnection = require('./help-lease-connection');
var STRIP_COMMENTS_RX = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg;
/**
* helpRunTransaction()
*
* Lease a transactional database connection, run the provided `during` function,
* then either commit the transaction or (in the event of an error) roll it back.
* Finally, release the connection back to the manager from whence it came.
*
* > This utility is for a datastore (RDI) method. Before attempting to use this,
* > the datastore method should guarantee that the adapter (via its driver) actually
* > supports all the necessary pieces.
*
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* @param {Dictionary} options
* @required {Ref} manager
* @required {Ref} driver
* @required {Function} during
* @param {Ref} db [The leased (transactional) database connection.]
* @param {Function} proceed
* @param {Error?} err
* @param {Ref?} resultMaybe
* @optional {Dictionary} meta
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* @param {Function} done
* @param {Error?} err
* @param {Ref?} resultMaybe
* If defined, this is the result sent back from the provided
* `during` function.
*/
module.exports = function helpRunTransaction(options, done){
assert(!options.connection, 'A pre-existing `connection` should never be passed in to the helpRunTransaction() utility. (Transaction-ifying an existing connection is not supported.)');
assert(options.driver);
assert(options.manager);
assert(_.isFunction(options.during));
assert(!options.meta || options.meta && _.isObject(options.meta));
helpLeaseConnection({
manager: options.manager,
driver: options.driver,
meta: options.meta,
during: function (db, proceed){
// ╔╗ ╔═╗╔═╗╦╔╗╔ ┌┬┐┬─┐┌─┐┌┐┌┌─┐┌─┐┌─┐┌┬┐┬┌─┐┌┐┌
// ╠╩╗║╣ ║ ╦║║║║ │ ├┬┘├─┤│││└─┐├─┤│ │ ││ ││││
// ╚═╝╚═╝╚═╝╩╝╚╝ ┴ ┴└─┴ ┴┘└┘└─┘┴ ┴└─┘ ┴ ┴└─┘┘└┘
options.driver.beginTransaction({
connection: db,
meta: options.meta
}, function (err /*, report */){
if (err) { return proceed(err); }
// ╦═╗╦ ╦╔╗╔ ┌┬┐┬ ┬┌─┐ \│/┌┬┐┬ ┬┬─┐┬┌┐┌┌─┐\│/ ┌─┐┬ ┬┌┐┌┌─┐┌┬┐┬┌─┐┌┐┌
// ╠╦╝║ ║║║║ │ ├─┤├┤ ─ ─ │││ │├┬┘│││││ ┬─ ─ ├┤ │ │││││ │ ││ ││││
// ╩╚═╚═╝╝╚╝ ┴ ┴ ┴└─┘ /│\─┴┘└─┘┴└─┴┘└┘└─┘/│\ └ └─┘┘└┘└─┘ ┴ ┴└─┘┘└┘
// Invoke a self-calling function that calls the provided `during` function.
(function _makeCallToDuringFn(proceed){
// Check if the iteratee declares a callback parameter
var seemsToExpectCallback = (function(){
var fnStr = options.during.toString().replace(STRIP_COMMENTS_RX, '');
var parametersAsString = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')'));
return !! parametersAsString.match(/\,\s*([^,\{\}\[\]\s]+)\s*$/);
})();//†
// TODO: use an approach that works better for arrow functions that omit
// the `()` around arguments!
// TODO: if a non-async function is detected, and it doesn't seem to expect
// a callback, finish with an error
// Note that, if you try to call the callback more than once in the iteratee,
// this method logs a warning explaining what's up, ignoring any subsequent calls
// to the callback that occur after the first one.
var didDuringFnAlreadyHalt;
try {
var promiseOrResultMaybe = options.during(db, function (err, resultMaybe) {
if (!seemsToExpectCallback) { return proceed(new Error('Unexpected attempt to invoke callback: the "during" function provided to `.transaction()` does not appear to expect a callback parameter. Please either explicitly list the callback parameter among the arguments or change this code to no longer use a callback.')); }//•
if (err) { return proceed(err); }//•
if (didDuringFnAlreadyHalt) {
console.warn(
'Warning: The `during` function provided to `.transaction()` triggered its callback \n'+
'again-- after already triggering it once! Please carefully check your `during` function\'s \n'+
'code to figure out why this is happening. (Ignoring this subsequent invocation...)'
);
return;
}//-•
didDuringFnAlreadyHalt = true;
return proceed(undefined, resultMaybe);
});//_∏_ </ invoked `during` >
// Take care of unhandled promise rejections from `await` (if appropriate)
if (options.during.constructor.name === 'AsyncFunction') {
if (!seemsToExpectCallback) {
promiseOrResultMaybe = promiseOrResultMaybe.then(function(resultMaybe){
didDuringFnAlreadyHalt = true;
proceed(undefined, resultMaybe);
});//_∏_
}//fi
promiseOrResultMaybe.catch(function(e){ proceed(e); });//_∏_
} else {
if (!seemsToExpectCallback) {
didDuringFnAlreadyHalt = true;
return proceed(undefined, promiseOrResultMaybe);
}
}
} catch (e) { return proceed(e); }
})(function _afterCallingDuringFn(duringErr, resultMaybe){
// ╦ ╦╔═╗╔╗╔╔╦╗╦ ╔═╗ ┌─┐┬─┐┬─┐┌─┐┬─┐ ┌─┐┬─┐┌─┐┌┬┐ \│/┌┬┐┬ ┬┬─┐┬┌┐┌┌─┐\│/
// ╠═╣╠═╣║║║ ║║║ ║╣ ├┤ ├┬┘├┬┘│ │├┬┘ ├┤ ├┬┘│ ││││ ─ ─ │││ │├┬┘│││││ ┬─ ─
// ╩ ╩╩ ╩╝╚╝═╩╝╩═╝╚═╝ └─┘┴└─┴└─└─┘┴└─ └ ┴└─└─┘┴ ┴ /│\─┴┘└─┘┴└─┴┘└┘└─┘/│\
// ┬ ╦═╗╔═╗╦ ╦ ╔╗ ╔═╗╔═╗╦╔═
// ┌┼─ ╠╦╝║ ║║ ║ ╠╩╗╠═╣║ ╠╩╗
// └┘ ╩╚═╚═╝╩═╝╩═╝╚═╝╩ ╩╚═╝╩ ╩
// If an error occured while running the duringFn, automatically rollback
// the transaction.
if (duringErr) {
options.driver.rollbackTransaction({ connection: db, meta: options.meta }, function(secondaryErr) {
if (secondaryErr) {
return proceed(flaverr({
raw: duringErr
}, new Error(
'First, encountered error:\n'+
'```\n'+
duringErr.message +'\n'+
'```\n'+
'...AND THEN when attempting to roll back the database transaction, there was a secondary error:\n'+
'```\n'+
secondaryErr.stack+'\n'+
'```'
)));
}//•
// Otherwise, the rollback was successful-- so proceed with the
// original error (this will release the connection).
return proceed(duringErr);
});//_∏_ </driver.rollbackTransaction()>
return;
}//--•
// ┌─┐┌┬┐┬ ┬┌─┐┬─┐┬ ┬┬┌─┐┌─┐ ╦ ╦╔═╗╔╗╔╔╦╗╦ ╔═╗ ┌─┐┬ ┬┌─┐┌─┐┌─┐┌─┐┌─┐
// │ │ │ ├─┤├┤ ├┬┘││││└─┐├┤ ╠═╣╠═╣║║║ ║║║ ║╣ └─┐│ ││ │ ├┤ └─┐└─┐
// └─┘ ┴ ┴ ┴└─┘┴└─└┴┘┴└─┘└─┘ ╩ ╩╩ ╩╝╚╝═╩╝╩═╝╚═╝ └─┘└─┘└─┘└─┘└─┘└─┘└─┘
// ┬ ╔═╗╔═╗╔╦╗╔╦╗╦╔╦╗ ┌┬┐┬─┐┌─┐┌┐┌┌─┐┌─┐┌─┐┌┬┐┬┌─┐┌┐┌
// ┌┼─ ║ ║ ║║║║║║║║ ║ │ ├┬┘├─┤│││└─┐├─┤│ │ ││ ││││
// └┘ ╚═╝╚═╝╩ ╩╩ ╩╩ ╩ ┴ ┴└─┴ ┴┘└┘└─┘┴ ┴└─┘ ┴ ┴└─┘┘└┘
// IWMIH, then the `during` function ran successfully.
options.driver.commitTransaction({ connection: db, meta: options.meta }, function(secondaryErr) {
if (secondaryErr) {
// Proceed to release the connection, and send back an error.
// (Since the transaction could not be committed, this effectively failed.)
return proceed(new Error(
'The `during` function ran successfully, but there was an issue '+
'commiting the db transaction:\n'+
'```\n' +
secondaryErr.stack+'\n'+
'```'
));
}//•
// Proceed to release the connection, sending back the result
// (only relevant if the provided `during` function sent one back).
return proceed(undefined, resultMaybe);
});//</callback from driver.commitTransaction()>
});//</callback from self-calling function that ran the provided `during` fn>
});//</callback from driver.beginTransaction()>
}//</argins for helpLeaseConnection()>
}, function (err, resultMaybe) {
if (err) { return done(err); }
return done(undefined, resultMaybe);
});//</callback from helpLeaseConnection()>
};
// To test:
// ```
// User.getDatastore().transaction(function(db, proceed){ async.map(require('lodash').range(10), function (i, next){ var rand = Math.floor(Math.random()*10); User.findOrCreate({luckyNumber: rand}, {luckyNumber: rand }).usingConnection(db).meta({fetch: true}).exec(next); }, proceed); }).exec(function _afterwards(){if (arguments[0]) { console.log('ERROR:', arguments[0]); return; } console.log('Ok. Result:',arguments[1]); });
// ```
//
// -OR-
//
// ```
// Product.getDatastore().transaction(function(db, proceed){ async.map(require('lodash').range(10), function (i, next){ var rand = Math.floor(Math.random()*10); Product.getDatastore().sendNativeQuery('SELECT luckyNumber FROM product WHERE luckyNumber=$1;', [rand]).usingConnection(db).exec(function(err, rawResult) { if(err) { return next(err); } if (rawResult.rows.length > 0) { return next(); } Product.getDatastore().sendNativeQuery('INSERT INTO product (luckyNumber) VALUES ($1);', [rand]).usingConnection(db).exec(next); }); }, proceed); }).exec(function _afterwards(){if (arguments[0]) { console.log('ERROR:', arguments[0]); return; } console.log('Ok. Result:',arguments[1]); });
// ```
//
//^^^ IN EITHER CASE:
//^^^ should result in an error-- and when examining the state of the database afterwards,
//^^^ nothing should have been created. (Contrast this behavior with the same code, but
//^^^ replacing "transaction" with "leaseConnection". Without the transaction, some records
//^^^ would be created, but the transaction prevents this from happening)