neo4j-driver-core
Version:
Internals of neo4j-driver
529 lines (528 loc) • 26.9 kB
JavaScript
"use strict";
/**
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://neo4j.com]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable @typescript-eslint/promise-function-async */
var util_1 = require("./internal/util");
var connection_holder_1 = require("./internal/connection-holder");
var bookmarks_1 = require("./internal/bookmarks");
var tx_config_1 = require("./internal/tx-config");
var observers_1 = require("./internal/observers");
var error_1 = require("./error");
var result_1 = __importDefault(require("./result"));
/**
* Represents a transaction in the Neo4j database.
*
* @access public
*/
var Transaction = /** @class */ (function () {
/**
* @constructor
* @param {object} args
* @param {ConnectionHolder} args.connectionHolder - the connection holder to get connection from.
* @param {function()} args.onClose - Function to be called when transaction is committed or rolled back.
* @param {function(bookmarks: Bookmarks)} args.onBookmarks callback invoked when new bookmark is produced.
* @param {function()} args.onConnection - Function to be called when a connection is obtained to ensure the conneciton
* is not yet released.
* @param {boolean} args.reactive whether this transaction generates reactive streams
* @param {number} args.fetchSize - the record fetch size in each pulling batch.
* @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session.
* @param {number} args.highRecordWatermark - The high watermark for the record buffer.
* @param {number} args.lowRecordWatermark - The low watermark for the record buffer.
* @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction.
* @param {NonAutoCommitApiTelemetryConfig} args.apiTelemetryConfig - The api telemetry configuration. Empty/Null for disabling telemetry
*/
function Transaction(_a) {
var connectionHolder = _a.connectionHolder, onClose = _a.onClose, onBookmarks = _a.onBookmarks, onConnection = _a.onConnection, reactive = _a.reactive, fetchSize = _a.fetchSize, impersonatedUser = _a.impersonatedUser, highRecordWatermark = _a.highRecordWatermark, lowRecordWatermark = _a.lowRecordWatermark, notificationFilter = _a.notificationFilter, apiTelemetryConfig = _a.apiTelemetryConfig;
var _this = this;
this._connectionHolder = connectionHolder;
this._reactive = reactive;
this._state = _states.ACTIVE;
this._onClose = onClose;
this._onBookmarks = onBookmarks;
this._onConnection = onConnection;
this._onError = this._onErrorCallback.bind(this);
this._fetchSize = fetchSize;
this._onComplete = this._onCompleteCallback.bind(this);
this._results = [];
this._impersonatedUser = impersonatedUser;
this._lowRecordWatermak = lowRecordWatermark;
this._highRecordWatermark = highRecordWatermark;
this._bookmarks = bookmarks_1.Bookmarks.empty();
this._notificationFilter = notificationFilter;
this._apiTelemetryConfig = apiTelemetryConfig;
this._acceptActive = function () { }; // satisfy DenoJS
this._activePromise = new Promise(function (resolve, reject) {
_this._acceptActive = resolve;
});
}
/**
* @private
* @param {Bookmarks | string | string []} bookmarks
* @param {TxConfig} txConfig
* @param {Object} events List of observers to events
* @returns {void}
*/
Transaction.prototype._begin = function (getBookmarks, txConfig, events) {
var _this = this;
this._connectionHolder
.getConnection()
.then(function (connection) { return __awaiter(_this, void 0, void 0, function () {
var _a;
var _this = this;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
this._onConnection();
if (!(connection != null)) return [3 /*break*/, 2];
_a = this;
return [4 /*yield*/, getBookmarks()];
case 1:
_a._bookmarks = _b.sent();
return [2 /*return*/, connection.beginTransaction({
bookmarks: this._bookmarks,
txConfig: txConfig,
mode: this._connectionHolder.mode(),
database: this._connectionHolder.database(),
impersonatedUser: this._impersonatedUser,
notificationFilter: this._notificationFilter,
apiTelemetryConfig: this._apiTelemetryConfig,
beforeError: function (error) {
if (events != null) {
events.onError(error);
}
_this._onError(error).catch(function () { });
},
afterComplete: function (metadata) {
if (events != null) {
events.onComplete(metadata);
}
_this._onComplete(metadata);
}
})];
case 2: throw (0, error_1.newError)('No connection available');
}
});
}); })
.catch(function (error) {
if (events != null) {
events.onError(error);
}
_this._onError(error).catch(function () { });
})
// It should make the transaction active anyway
// further errors will be treated by the existing
// observers
.finally(function () { return _this._acceptActive(); });
};
/**
* Run Cypher query
* Could be called with a query object i.e.: `{text: "MATCH ...", parameters: {param: 1}}`
* or with the query and parameters as separate arguments.
* @param {mixed} query - Cypher query to execute
* @param {Object} parameters - Map with parameters to use in query
* @return {Result} New Result
*/
Transaction.prototype.run = function (query, parameters) {
var _a = (0, util_1.validateQueryAndParameters)(query, parameters), validatedQuery = _a.validatedQuery, params = _a.params;
var result = this._state.run(validatedQuery, params, {
connectionHolder: this._connectionHolder,
onError: this._onError,
onComplete: this._onComplete,
onConnection: this._onConnection,
reactive: this._reactive,
fetchSize: this._fetchSize,
highRecordWatermark: this._highRecordWatermark,
lowRecordWatermark: this._lowRecordWatermak,
preparationJob: this._activePromise
});
this._results.push(result);
return result;
};
/**
* Commits the transaction and returns the result.
*
* After committing the transaction can no longer be used.
*
* @returns {Promise<void>} An empty promise if committed successfully or error if any error happened during commit.
*/
Transaction.prototype.commit = function () {
var _this = this;
var committed = this._state.commit({
connectionHolder: this._connectionHolder,
onError: this._onError,
onComplete: function (meta) { return _this._onCompleteCallback(meta, _this._bookmarks); },
onConnection: this._onConnection,
pendingResults: this._results,
preparationJob: this._activePromise
});
this._state = committed.state;
// clean up
this._onClose();
return new Promise(function (resolve, reject) {
committed.result.subscribe({
onCompleted: function () { return resolve(); },
onError: function (error) { return reject(error); }
});
});
};
/**
* Rollbacks the transaction.
*
* After rolling back, the transaction can no longer be used.
*
* @returns {Promise<void>} An empty promise if rolled back successfully or error if any error happened during
* rollback.
*/
Transaction.prototype.rollback = function () {
var rolledback = this._state.rollback({
connectionHolder: this._connectionHolder,
onError: this._onError,
onComplete: this._onComplete,
onConnection: this._onConnection,
pendingResults: this._results,
preparationJob: this._activePromise
});
this._state = rolledback.state;
// clean up
this._onClose();
return new Promise(function (resolve, reject) {
rolledback.result.subscribe({
onCompleted: function () { return resolve(); },
onError: function (error) { return reject(error); }
});
});
};
/**
* Check if this transaction is active, which means commit and rollback did not happen.
* @return {boolean} `true` when not committed and not rolled back, `false` otherwise.
*/
Transaction.prototype.isOpen = function () {
return this._state === _states.ACTIVE;
};
/**
* Closes the transaction
*
* This method will roll back the transaction if it is not already committed or rolled back.
*
* @returns {Promise<void>} An empty promise if closed successfully or error if any error happened during
*/
Transaction.prototype.close = function () {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!this.isOpen()) return [3 /*break*/, 2];
return [4 /*yield*/, this.rollback()];
case 1:
_a.sent();
_a.label = 2;
case 2: return [2 /*return*/];
}
});
});
};
// eslint-disable-next-line
// @ts-ignore
Transaction.prototype[Symbol.asyncDispose] = function () {
return this.close();
};
Transaction.prototype._onErrorCallback = function (error) {
// error will be "acknowledged" by sending a RESET message
// database will then forget about this transaction and cleanup all corresponding resources
// it is thus safe to move this transaction to a FAILED state and disallow any further interactions with it
this._state = _states.FAILED;
this._onClose();
this._results.forEach(function (result) {
if (result.isOpen()) {
// @ts-expect-error
result._streamObserverPromise
.then(function (resultStreamObserver) { return resultStreamObserver.onError(error); })
// Nothing to do since we don't have a observer to notify the error
// the result will be already broke in other ways.
.catch(function (_) { });
}
});
// release connection back to the pool
return this._connectionHolder.releaseConnection();
};
/**
* @private
* @param {object} meta The meta with bookmarks
* @returns {void}
*/
Transaction.prototype._onCompleteCallback = function (meta, previousBookmarks) {
this._onBookmarks(new bookmarks_1.Bookmarks(meta === null || meta === void 0 ? void 0 : meta.bookmark), previousBookmarks !== null && previousBookmarks !== void 0 ? previousBookmarks : bookmarks_1.Bookmarks.empty(), meta === null || meta === void 0 ? void 0 : meta.db);
};
return Transaction;
}());
var _states = {
// The transaction is running with no explicit success or failure marked
ACTIVE: {
commit: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete, onConnection = _a.onConnection, pendingResults = _a.pendingResults, preparationJob = _a.preparationJob;
return {
result: finishTransaction(true, connectionHolder, onError, onComplete, onConnection, pendingResults, preparationJob),
state: _states.SUCCEEDED
};
},
rollback: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete, onConnection = _a.onConnection, pendingResults = _a.pendingResults, preparationJob = _a.preparationJob;
return {
result: finishTransaction(false, connectionHolder, onError, onComplete, onConnection, pendingResults, preparationJob),
state: _states.ROLLED_BACK
};
},
run: function (query, parameters, _a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete, onConnection = _a.onConnection, reactive = _a.reactive, fetchSize = _a.fetchSize, highRecordWatermark = _a.highRecordWatermark, lowRecordWatermark = _a.lowRecordWatermark, preparationJob = _a.preparationJob;
// RUN in explicit transaction can't contain bookmarks and transaction configuration
// No need to include mode and database name as it shall be included in begin
var requirements = preparationJob !== null && preparationJob !== void 0 ? preparationJob : Promise.resolve();
var observerPromise = connectionHolder.getConnection()
.then(function (conn) { return requirements.then(function () { return conn; }); })
.then(function (conn) {
onConnection();
if (conn != null) {
return conn.run(query, parameters, {
bookmarks: bookmarks_1.Bookmarks.empty(),
txConfig: tx_config_1.TxConfig.empty(),
beforeError: onError,
afterComplete: onComplete,
reactive: reactive,
fetchSize: fetchSize,
highRecordWatermark: highRecordWatermark,
lowRecordWatermark: lowRecordWatermark
});
}
else {
throw (0, error_1.newError)('No connection available');
}
})
.catch(function (error) { return new observers_1.FailedObserver({ error: error, onError: onError }); });
return newCompletedResult(observerPromise, query, parameters, connectionHolder, highRecordWatermark, lowRecordWatermark);
}
},
// An error has occurred, transaction can no longer be used and no more messages will
// be sent for this transaction.
FAILED: {
commit: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return {
result: newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot commit this transaction, because it has been rolled back either because of an error or explicit termination.'),
onError: onError
}), 'COMMIT', {}, connectionHolder, 0, // high watermark
0 // low watermark
),
state: _states.FAILED
};
},
rollback: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return {
result: newCompletedResult(new observers_1.CompletedObserver(), 'ROLLBACK', {}, connectionHolder, 0, // high watermark
0 // low watermark
),
state: _states.FAILED
};
},
run: function (query, parameters, _a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot run query in this transaction, because it has been rolled back either because of an error or explicit termination.'),
onError: onError
}), query, parameters, connectionHolder, 0, // high watermark
0 // low watermark
);
}
},
// This transaction has successfully committed
SUCCEEDED: {
commit: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return {
result: newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot commit this transaction, because it has already been committed.'),
onError: onError
}), 'COMMIT', {}, connection_holder_1.EMPTY_CONNECTION_HOLDER, 0, // high watermark
0 // low watermark
),
state: _states.SUCCEEDED,
connectionHolder: connectionHolder
};
},
rollback: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return {
result: newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot rollback this transaction, because it has already been committed.'),
onError: onError
}), 'ROLLBACK', {}, connection_holder_1.EMPTY_CONNECTION_HOLDER, 0, // high watermark
0 // low watermark
),
state: _states.SUCCEEDED,
connectionHolder: connectionHolder
};
},
run: function (query, parameters, _a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot run query in this transaction, because it has already been committed.'),
onError: onError
}), query, parameters, connectionHolder, 0, // high watermark
0 // low watermark
);
}
},
// This transaction has been rolled back
ROLLED_BACK: {
commit: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return {
result: newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot commit this transaction, because it has already been rolled back.'),
onError: onError
}), 'COMMIT', {}, connectionHolder, 0, // high watermark
0 // low watermark
),
state: _states.ROLLED_BACK
};
},
rollback: function (_a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return {
result: newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot rollback this transaction, because it has already been rolled back.')
}), 'ROLLBACK', {}, connectionHolder, 0, // high watermark
0 // low watermark
),
state: _states.ROLLED_BACK
};
},
run: function (query, parameters, _a) {
var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
return newCompletedResult(new observers_1.FailedObserver({
error: (0, error_1.newError)('Cannot run query in this transaction, because it has already been rolled back.'),
onError: onError
}), query, parameters, connectionHolder, 0, // high watermark
0 // low watermark
);
}
}
};
/**
*
* @param {boolean} commit
* @param {ConnectionHolder} connectionHolder
* @param {function(err:Error): any} onError
* @param {function(metadata:object): any} onComplete
* @param {function() : any} onConnection
* @param {list<Result>>}pendingResults all run results in this transaction
*/
function finishTransaction(commit, connectionHolder, onError, onComplete, onConnection, pendingResults, preparationJob) {
var requirements = preparationJob !== null && preparationJob !== void 0 ? preparationJob : Promise.resolve();
var observerPromise = connectionHolder.getConnection()
.then(function (conn) { return requirements.then(function () { return conn; }); })
.then(function (connection) {
onConnection();
pendingResults.forEach(function (r) { return r._cancel(); });
return Promise.all(pendingResults.map(function (result) { return result.summary(); })).then(function (results) {
if (connection != null) {
if (commit) {
return connection.commitTransaction({
beforeError: onError,
afterComplete: onComplete
});
}
else {
return connection.rollbackTransaction({
beforeError: onError,
afterComplete: onComplete
});
}
}
else {
throw (0, error_1.newError)('No connection available');
}
});
})
.catch(function (error) { return new observers_1.FailedObserver({ error: error, onError: onError }); });
// for commit & rollback we need result that uses real connection holder and notifies it when
// connection is not needed and can be safely released to the pool
return new result_1.default(observerPromise, commit ? 'COMMIT' : 'ROLLBACK', {}, connectionHolder, {
high: Number.MAX_VALUE,
low: Number.MAX_VALUE
});
}
/**
* Creates a {@link Result} with empty connection holder.
* For cases when result represents an intermediate or failed action, does not require any metadata and does not
* need to influence real connection holder to release connections.
* @param {ResultStreamObserver} observer - an observer for the created result.
* @param {string} query - the cypher query that produced the result.
* @param {Object} parameters - the parameters for cypher query that produced the result.
* @param {ConnectionHolder} connectionHolder - the connection holder used to get the result
* @return {Result} new result.
* @private
*/
function newCompletedResult(observerPromise, query, parameters, connectionHolder, highRecordWatermark, lowRecordWatermark) {
if (connectionHolder === void 0) { connectionHolder = connection_holder_1.EMPTY_CONNECTION_HOLDER; }
return new result_1.default(Promise.resolve(observerPromise), query, parameters, new connection_holder_1.ReadOnlyConnectionHolder(connectionHolder !== null && connectionHolder !== void 0 ? connectionHolder : connection_holder_1.EMPTY_CONNECTION_HOLDER), {
low: lowRecordWatermark,
high: highRecordWatermark
});
}
exports.default = Transaction;