2015-05-09 13:58:18 -04:00
|
|
|
|
|
|
|
// Transaction
|
|
|
|
// -------
|
2016-08-09 17:23:07 -04:00
|
|
|
import Promise from 'bluebird';
|
2016-05-17 01:01:34 +10:00
|
|
|
import { EventEmitter } from 'events';
|
2016-05-18 21:30:59 +10:00
|
|
|
import Debug from 'debug'
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
import makeKnex from './util/make-knex';
|
2016-05-18 21:30:59 +10:00
|
|
|
import noop from './util/noop';
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-18 21:30:59 +10:00
|
|
|
const debug = Debug('knex:tx');
|
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
import { uniqueId } from 'lodash';
|
2016-03-02 16:52:32 +01:00
|
|
|
|
2015-05-09 13:58:18 -04:00
|
|
|
// Acts as a facade for a Promise, keeping the internal state
|
|
|
|
// and managing any child transactions.
|
2016-09-12 18:45:35 -04:00
|
|
|
export default class Transaction extends EventEmitter {
|
2016-03-08 08:41:13 +01:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
constructor(client, container, config, outerTx) {
|
|
|
|
super()
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
const txid = this.txid = uniqueId('trx')
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
this.client = client
|
|
|
|
this.outerTx = outerTx
|
|
|
|
this.trxClient = undefined;
|
|
|
|
this._debug = client.config && client.config.debug
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
debug('%s: Starting %s transaction', txid, outerTx ? 'nested' : 'top level')
|
2016-03-08 08:41:13 +01:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
this._promise = Promise.using(this.acquireConnection(client, config, txid), (connection) => {
|
2016-03-08 08:41:13 +01:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
const trxClient = this.trxClient = makeTxClient(this, client, connection)
|
|
|
|
const init = client.transacting ? this.savepoint(connection) : this.begin(connection)
|
|
|
|
|
|
|
|
init.then(() => {
|
|
|
|
return makeTransactor(this, connection, trxClient)
|
|
|
|
})
|
|
|
|
.then((transactor) => {
|
|
|
|
// If we've returned a "thenable" from the transaction container, assume
|
|
|
|
// the rollback and commit are chained to this object's success / failure.
|
|
|
|
// Directly thrown errors are treated as automatic rollbacks.
|
|
|
|
let result
|
|
|
|
try {
|
|
|
|
result = container(transactor)
|
|
|
|
} catch (err) {
|
|
|
|
result = Promise.reject(err)
|
|
|
|
}
|
|
|
|
if (result && result.then && typeof result.then === 'function') {
|
|
|
|
result.then((val) => {
|
|
|
|
return transactor.commit(val)
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
return transactor.rollback(err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
})
|
|
|
|
.catch((e) => this._rejecter(e))
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
return new Promise((resolver, rejecter) => {
|
|
|
|
this._resolver = resolver
|
|
|
|
this._rejecter = rejecter
|
|
|
|
})
|
2015-05-09 13:58:18 -04:00
|
|
|
})
|
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
this._completed = false
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
// If there's a wrapping transaction, we need to wait for any older sibling
|
|
|
|
// transactions to settle (commit or rollback) before we can start, and we
|
|
|
|
// need to register ourselves with the parent transaction so any younger
|
|
|
|
// siblings can wait for us to complete before they can start.
|
|
|
|
this._previousSibling = Promise.resolve(true);
|
|
|
|
if (outerTx) {
|
|
|
|
if (outerTx._lastChild) this._previousSibling = outerTx._lastChild;
|
|
|
|
outerTx._lastChild = this._promise;
|
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
}
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
isCompleted() {
|
2015-05-09 13:58:18 -04:00
|
|
|
return this._completed || this.outerTx && this.outerTx.isCompleted() || false
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
begin(conn) {
|
2015-05-09 13:58:18 -04:00
|
|
|
return this.query(conn, 'BEGIN;')
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
savepoint(conn) {
|
|
|
|
return this.query(conn, `SAVEPOINT ${this.txid};`)
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
commit(conn, value) {
|
2015-05-09 13:58:18 -04:00
|
|
|
return this.query(conn, 'COMMIT;', 1, value)
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
release(conn, value) {
|
|
|
|
return this.query(conn, `RELEASE SAVEPOINT ${this.txid};`, 1, value)
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
rollback(conn, error) {
|
2015-05-09 13:58:18 -04:00
|
|
|
return this.query(conn, 'ROLLBACK;', 2, error)
|
2016-03-21 16:55:18 +01:00
|
|
|
.timeout(5000)
|
2016-03-19 19:14:46 +01:00
|
|
|
.catch(Promise.TimeoutError, () => {
|
|
|
|
this._resolver();
|
|
|
|
});
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
rollbackTo(conn, error) {
|
|
|
|
return this.query(conn, `ROLLBACK TO SAVEPOINT ${this.txid}`, 2, error)
|
2016-03-21 16:55:18 +01:00
|
|
|
.timeout(5000)
|
2016-03-19 19:14:46 +01:00
|
|
|
.catch(Promise.TimeoutError, () => {
|
|
|
|
this._resolver();
|
|
|
|
});
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
query(conn, sql, status, value) {
|
|
|
|
const q = this.trxClient.query(conn, sql)
|
2015-05-09 14:07:58 -04:00
|
|
|
.catch((err) => {
|
2015-05-09 13:58:18 -04:00
|
|
|
status = 2
|
|
|
|
value = err
|
2015-05-09 14:07:58 -04:00
|
|
|
this._completed = true
|
|
|
|
debug('%s error running transaction query', this.txid)
|
2015-05-09 13:58:18 -04:00
|
|
|
})
|
2015-05-09 14:07:58 -04:00
|
|
|
.tap(() => {
|
|
|
|
if (status === 1) this._resolver(value)
|
|
|
|
if (status === 2) this._rejecter(value)
|
2015-05-09 13:58:18 -04:00
|
|
|
})
|
|
|
|
if (status === 1 || status === 2) {
|
2015-05-09 14:07:58 -04:00
|
|
|
this._completed = true
|
2015-05-09 13:58:18 -04:00
|
|
|
}
|
|
|
|
return q;
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
debug(enabled) {
|
2015-05-09 13:58:18 -04:00
|
|
|
this._debug = arguments.length ? enabled : true;
|
|
|
|
return this
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-03-08 08:41:13 +01:00
|
|
|
// Acquire a connection and create a disposer - either using the one passed
|
|
|
|
// via config or getting one off the client. The disposer will be called once
|
2015-05-09 13:58:18 -04:00
|
|
|
// the original promise is marked completed.
|
2016-05-17 01:01:34 +10:00
|
|
|
acquireConnection(client, config, txid) {
|
|
|
|
const configConnection = config && config.connection
|
2016-05-18 21:30:59 +10:00
|
|
|
return Promise.try(() => configConnection || client.acquireConnection().completed)
|
2015-05-09 13:58:18 -04:00
|
|
|
.disposer(function(connection) {
|
|
|
|
if (!configConnection) {
|
|
|
|
debug('%s: releasing connection', txid)
|
|
|
|
client.releaseConnection(connection)
|
|
|
|
} else {
|
|
|
|
debug('%s: not releasing external connection', txid)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-09-12 18:45:35 -04:00
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
// The transactor is a full featured knex object, with a "commit", a "rollback"
|
|
|
|
// and a "savepoint" function. The "savepoint" is just sugar for creating a new
|
|
|
|
// transaction. If the rollback is run inside a savepoint, it rolls back to the
|
|
|
|
// last savepoint - otherwise it rolls back the transaction.
|
2015-05-09 13:58:18 -04:00
|
|
|
function makeTransactor(trx, connection, trxClient) {
|
2016-03-08 08:41:13 +01:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
const transactor = makeKnex(trxClient);
|
2015-05-09 13:58:18 -04:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
transactor.transaction = (container, options) =>
|
|
|
|
new trxClient.Transaction(trxClient, container, options, trx);
|
|
|
|
|
|
|
|
transactor.savepoint = (container, options) =>
|
|
|
|
transactor.transaction(container, options);
|
2015-05-09 13:58:18 -04:00
|
|
|
|
|
|
|
if (trx.client.transacting) {
|
2016-05-17 01:01:34 +10:00
|
|
|
transactor.commit = value => trx.release(connection, value)
|
|
|
|
transactor.rollback = error => trx.rollbackTo(connection, error)
|
2015-05-09 13:58:18 -04:00
|
|
|
} else {
|
2016-05-17 01:01:34 +10:00
|
|
|
transactor.commit = value => trx.commit(connection, value)
|
|
|
|
transactor.rollback = error => trx.rollback(connection, error)
|
2015-05-09 13:58:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return transactor
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-03-08 08:41:13 +01:00
|
|
|
// We need to make a client object which always acquires the same
|
2015-05-09 13:58:18 -04:00
|
|
|
// connection and does not release back into the pool.
|
|
|
|
function makeTxClient(trx, client, connection) {
|
|
|
|
|
2016-05-18 19:59:24 +10:00
|
|
|
const trxClient = Object.create(client.constructor.prototype)
|
2015-05-09 13:58:18 -04:00
|
|
|
trxClient.config = client.config
|
|
|
|
trxClient.driver = client.driver
|
|
|
|
trxClient.connectionSettings = client.connectionSettings
|
|
|
|
trxClient.transacting = true
|
2016-05-19 18:50:18 +03:00
|
|
|
trxClient.valueForUndefined = client.valueForUndefined
|
2016-03-08 08:41:13 +01:00
|
|
|
|
2015-05-09 13:58:18 -04:00
|
|
|
trxClient.on('query', function(arg) {
|
|
|
|
trx.emit('query', arg)
|
2015-09-19 14:45:01 +02:00
|
|
|
client.emit('query', arg)
|
2015-05-09 13:58:18 -04:00
|
|
|
})
|
|
|
|
|
2016-01-30 01:58:23 +01:00
|
|
|
trxClient.on('query-error', function(err, obj) {
|
|
|
|
trx.emit('query-error', err, obj)
|
|
|
|
client.emit('query-error', err, obj)
|
2016-01-30 00:07:04 +01:00
|
|
|
})
|
|
|
|
|
2016-02-26 19:51:35 +01:00
|
|
|
trxClient.on('query-response', function(response, obj, builder) {
|
|
|
|
trx.emit('query-response', response, obj, builder)
|
|
|
|
client.emit('query-response', response, obj, builder)
|
|
|
|
})
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
const _query = trxClient.query;
|
2015-05-09 13:58:18 -04:00
|
|
|
trxClient.query = function(conn, obj) {
|
2016-05-17 01:01:34 +10:00
|
|
|
const completed = trx.isCompleted()
|
2015-05-09 13:58:18 -04:00
|
|
|
return Promise.try(function() {
|
|
|
|
if (conn !== connection) throw new Error('Invalid connection for transaction query.')
|
|
|
|
if (completed) completedError(trx, obj)
|
|
|
|
return _query.call(trxClient, conn, obj)
|
|
|
|
})
|
|
|
|
}
|
2016-05-17 01:01:34 +10:00
|
|
|
const _stream = trxClient.stream
|
2015-05-09 13:58:18 -04:00
|
|
|
trxClient.stream = function(conn, obj, stream, options) {
|
2016-05-17 01:01:34 +10:00
|
|
|
const completed = trx.isCompleted()
|
2015-05-09 13:58:18 -04:00
|
|
|
return Promise.try(function() {
|
|
|
|
if (conn !== connection) throw new Error('Invalid connection for transaction query.')
|
|
|
|
if (completed) completedError(trx, obj)
|
|
|
|
return _stream.call(trxClient, conn, obj, stream, options)
|
|
|
|
})
|
|
|
|
}
|
2016-05-09 17:21:55 -04:00
|
|
|
trxClient.acquireConnection = function () {
|
|
|
|
return {
|
2016-05-18 21:30:59 +10:00
|
|
|
completed: trx._previousSibling.reflect().then(() => connection),
|
2016-05-09 17:21:55 -04:00
|
|
|
abort: noop
|
|
|
|
}
|
2015-05-09 13:58:18 -04:00
|
|
|
}
|
2016-03-08 08:41:13 +01:00
|
|
|
trxClient.releaseConnection = function() {
|
2015-05-09 13:58:18 -04:00
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
|
|
|
return trxClient
|
|
|
|
}
|
|
|
|
|
|
|
|
function completedError(trx, obj) {
|
2016-05-17 01:01:34 +10:00
|
|
|
const sql = typeof obj === 'string' ? obj : obj && obj.sql
|
2015-05-09 13:58:18 -04:00
|
|
|
debug('%s: Transaction completed: %s', trx.id, sql)
|
2016-03-08 08:41:13 +01:00
|
|
|
throw new Error('Transaction query already complete, run with DEBUG=knex:tx for more info')
|
2015-05-09 13:58:18 -04:00
|
|
|
}
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
const promiseInterface = [
|
2015-05-09 13:58:18 -04:00
|
|
|
'then', 'bind', 'catch', 'finally', 'asCallback',
|
|
|
|
'spread', 'map', 'reduce', 'tap', 'thenReturn',
|
2016-03-16 17:52:18 +01:00
|
|
|
'return', 'yield', 'ensure', 'exec', 'reflect'
|
2015-05-09 13:58:18 -04:00
|
|
|
]
|
|
|
|
|
|
|
|
// Creates a method which "coerces" to a promise, by calling a
|
2016-03-07 10:23:46 +01:00
|
|
|
// "then" method on the current `Target`.
|
2015-05-09 13:58:18 -04:00
|
|
|
promiseInterface.forEach(function(method) {
|
|
|
|
Transaction.prototype[method] = function() {
|
|
|
|
return (this._promise = this._promise[method].apply(this._promise, arguments))
|
|
|
|
}
|
|
|
|
})
|