@knorm/knorm
Version:
A JavaScript ORM written using ES6 classes
406 lines (405 loc) • 15.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const KnormError_1 = require("./KnormError");
const Connection_1 = require("./Connection");
class TransactionError extends KnormError_1.KnormError {
}
/**
* Creates and executes transactions, allowing multiple queries to be run within
* a transaction.
*/
class Transaction {
/**
* Creates a {@link Transaction} instance.
*
* @param {function} [callback] The transaction callback, when [running
* transactions with a callback
* function](/guides/transactions.md#transactions-with-a-callback).
*/
constructor(callback) {
if (typeof callback === 'function') {
this.callback = callback;
}
const transaction = this;
this.models = Object.entries(this.models).reduce((models, [name, model]) => {
class TransactionModel extends model {
static get name() {
return model.name;
}
}
class TransactionQuery extends model.Query {
static get name() {
return model.Query.name;
}
}
models[name] = TransactionModel;
models[name].Query = TransactionQuery;
[models[name], models[name].Query].forEach((scopedClass) => {
scopedClass.prototype.models = scopedClass.models = models;
scopedClass.prototype.transaction = scopedClass.transaction = transaction;
});
return models;
}, {});
}
/**
* Connects to the database, via {@link Connection#create}. This method is
* called by {@link Transaction#begin} or by {@link Query#connect} when
* queries are executed within transactions.
*
* @returns {Promise} The `Promise` from {@link Connection#create}, that is
* resolved when a connection is established or rejected with a
* {@link TransactionError} on error.
*/
connect() {
return __awaiter(this, void 0, void 0, function* () {
try {
this.connection = new this.constructor.Connection();
return yield this.connection.create();
}
catch (e) {
throw new this.constructor.TransactionError(e);
}
});
}
/**
* Closes the database connection (via {@link Connection#close}) after
* committing (via {@link Transaction#commit}) or rolling back (via
* {@link Transaction#rollback}) a transaction.
*
* @param {QueryError} [error] The error from {@link Transaction#rollback}, if
* it was called with one. This error is then passed to
* {@link Connection#close}.
*
* @returns {Promise} The `Promise` from {@link Connection#close}, that is
* resolved when the connection is closed or rejected with a
* {@link QueryError} on error.
*/
disconnect(error) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield (error
? this.connection.close(error)
: this.connection.close());
}
catch (e) {
throw new this.constructor.TransactionError(e);
}
});
}
/**
* Begins a transaction, via {@link Transaction#_begin}. If no database
* connection exists, one is created via {@link Transaction#connect}.
*
* @returns {Promise} A `Promise` that is resolved when the transaction is begun
* or rejected with a {@link TransactionError} on error.
*/
begin() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.connection) {
yield this.connect();
}
try {
yield this._begin();
/**
* Shows the state of a transaction. This is initially `undefined`, set to
* `true` after {@link Transaction#begin} and finally set to `false` after
* {@link Transaction#commit} or {@link Transaction#rollback}.
*
* @type {boolean}
*/
this.active = true;
/**
* Shows the state of a transaction. This is initially `undefined` and set
* to `true` after {@link Transaction#begin}. Note that it stays `true`
* even after {@link Transaction#commit} or {@link Transaction#rollback}.
*
* @type {boolean}
*/
this.started = true;
}
catch (e) {
const beginError = new this.constructor.TransactionError(e);
yield this.disconnect(beginError);
throw beginError;
}
});
}
/**
* Sends a `BEGIN` query (via {@link Connection#query}) to start a
* transaction. This can be overloaded to send a different query e.g. `START
* TRANSACTION`.
*
* @returns {Promise} The `Promise` from {@link Connection#query}, that is
* resolved with the query result.
*/
_begin() {
return __awaiter(this, void 0, void 0, function* () {
// TODO: reject with a QueryError instead
return this.connection.query('BEGIN');
});
}
/**
* Commits a transaction, via {@link Transaction#_commit} and afterwards
* closes the database connection, via {@link Transaction#disconnect}. If the
* commit fails, the transaction is automaticaly rolled back via
* {@link Transaction#rollback}.
*
* @returns {Promise} A `Promise` that is resolved when the transaction is
* committed and the connection closed or rejected with a
* {@link TransactionError} on error.
*/
commit() {
return __awaiter(this, void 0, void 0, function* () {
if (this.ended) {
// when running transactions with a callback, allow users to call
// Transaction#commit within the callback. in that case, let the second
// call made by us will be a no-op.
return;
}
try {
yield this._commit();
this.active = false;
/**
* Shows the state of a transaction. This is initially `undefined` and
* only set to `true` after {@link Transaction#commit} or
* {@link Transaction#rollback}.
*
* @type {boolean}
*/
this.ended = true;
}
catch (e) {
const commitError = new this.constructor.TransactionError(e);
yield this.rollback(commitError);
throw commitError;
}
// do not disconnect within the try..catch so that errors closing the
// connection don't lead to Transaction#rollback being called (yet `COMMIT`
// already ran successfully)
yield this.disconnect();
});
}
/**
* Sends a `COMMIT` query (via {@link Connection#query}) to commit a
* transaction. This can be overloaded to send a different query.
*
* @returns {Promise} The `Promise` from {@link Connection#query}, that is
* resolved with the query result.
*/
_commit() {
return __awaiter(this, void 0, void 0, function* () {
return this.connection.query('COMMIT');
});
}
/**
* Rolls back a transaction, via {@link Transaction#_rollback} and afterwards
* closes the database connection, via {@link Transaction#disconnect}.
*
* @param {QueryError} [error] A {@link QueryError} from {@link Query#query},
* or a {@link TransactionError} from {@link Transaction#commit} or
* {@link Transaction#rollback}, if one occurs when those methods are run.
* This error is then passed to {@link Transaction#disconnect} and finally to
* {@link Connection#close}.
*
* @returns {Promise} A `Promise` that is resolved when the transaction is
* rolled back and the connection closed or rejected with a
* {@link TransactionError} on error.
*/
rollback(error) {
return __awaiter(this, void 0, void 0, function* () {
if (this.ended) {
// Transaction#rollback is automatically called when a query error occurs.
// when a transaction is wrapped in a try..catch block (e.g. in
// transactions with a callback) and Transaction#rollback is called in the
// catch block, then Transaction#rollback would end up being called twice.
return;
}
try {
yield this._rollback();
}
catch (e) {
const rollbackError = new this.constructor.TransactionError(e);
if (!error) {
error = rollbackError;
}
throw rollbackError;
}
finally {
// update state and disconnect whether or not the ROLLBACK call fails
this.active = false;
this.ended = true;
yield (error ? this.disconnect(error) : this.disconnect());
}
});
}
/**
* Sends a `ROLLBACK` query (via {@link Connection#query}) to roll back a
* transaction. This can be overloaded to send a different query.
*
* @returns {Promise} The `Promise` from {@link Connection#query}, that is
* resolved with the query result.
*/
_rollback() {
return __awaiter(this, void 0, void 0, function* () {
return this.connection.query('ROLLBACK');
});
}
/**
* Executes a transaction when [running a transaction with a callback
* function](/guides/transactions.md#transactions-with-a-callback). This
* method begins a transaction (via {@link Transaction#begin}), executes the
* callback function and commits the transaction (via
* {@link Transaction#commit}).
*
* @returns {Promise} A `Promise` that is resolved with the resolution value
* of the callback function.
*/
execute() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.callback) {
throw new this.constructor.TransactionError('no transaction callback provided');
}
yield this.begin();
let result;
try {
result = yield this.callback(this);
}
catch (e) {
// NOTE: this error should not be passed to Transaction#rollback. query
// errors will have already been passed by Query#execute. so any error
// happening here is not a query error and should not end up being passed
// to Transaction#disconnect.
yield this.rollback();
throw e;
}
// do not commit within the try..catch, since Transaction#commit itself
// calls Transaction#rollback on failure
yield this.commit();
return result;
});
}
/**
* Simulates a `Promise` interface. This method calls
* {@link Transaction#execute} and resolves with it's resolution value.
*
* @example ```js
* (async function() {
* const value = await new Transaction(async transaction => {
* return 'foo';
* });
*
* console.log(value); // => 'foo'
* })();
* ```
*
* @example ```js
* new Transaction(async transaction => {
* return 'foo';
* }).then(value => {
* console.log(value); // => 'foo'
* });
* ```
*
* @returns {Promise}
*/
then(...args) {
return __awaiter(this, void 0, void 0, function* () {
const promise = this.execute();
return promise.then(...args);
});
}
/**
* Simulates a `Promise` interface. This method calls
* {@link Transaction#execute} and calls `catch` on the promise returned.
*
* @example ```js
* (async function() {
* try {
* const value = await new Transaction(async transaction => {
* throw new Error('foo');
* });
* } catch (e) {
* console.log(e.message); // => 'foo'
* }
* })();
* ```
*
* @example ```js
* new Transaction(async transaction => {
* throw new Error('foo');
* }).catch(e => {
* console.log(e.message); // => 'foo'
* });
* ```
*
* @returns {Promise}
*/
catch(...args) {
return __awaiter(this, void 0, void 0, function* () {
const promise = this.execute();
return promise.catch(...args);
});
}
}
exports.Transaction = Transaction;
/**
* The base error that all errors thrown by {@link Transaction} inherit from.
*/
Transaction.TransactionError = TransactionError;
/**
* A reference to the {@link Knorm} instance.
*
* ::: tip
* This is the same instance assigned to the {@link Transaction.knorm} static
* property, just added as a convenience for use in instance methods.
* :::
*/
Transaction.prototype.knorm = null;
/**
* A reference to the {@link Knorm} instance.
*
* ::: tip
* This is the same instance assigned to the {@link Transaction#knorm} instance
* property, just added as a convenience for use in static methods.
* :::
*/
Transaction.knorm = null;
/**
* The model registry. This is an object containing all the models added to the
* ORM, keyed by name. See [model registry](/guides/models.md#model-registry)
* for more info.
*
* ::: tip
* This is the same object assigned to the {@link Transaction.models} static
* property, just added as a convenience for use in instance methods.
* :::
*
* @type {object}
*/
Transaction.prototype.models = {};
/**
* The model registry. This is an object containing all the models added to the
* ORM, keyed by name. See [model registry](/guides/models.md#model-registry)
* for more info.
*
* ::: tip
* This is the same object assigned to the {@link Transaction#models} instance
* property, just added as a convenience for use in static methods.
* :::
*
* @type {object}
*/
Transaction.models = {};
/**
* A reference to {@link Connection}, for use within {@link Transaction}.
*/
Transaction.Connection = Connection_1.Connection;