UNPKG

x2node-dbos

Version:
318 lines (283 loc) 12.1 kB
'use strict'; const common = require('x2node-common'); const patches = require('x2node-patches'); const DBDriverDataSource = require('./driver-data-source.js'); const FetchDBO = require('./fetch-dbo.js'); const InsertDBO = require('./insert-dbo.js'); const DeleteDBO = require('./delete-dbo.js'); const UpdateDBO = require('./update-dbo.js'); const Transaction = require('./transaction.js'); const TxFactory = require('./tx-factory.js'); /** * Database operations (DBO) factory. * * @memberof module:x2node-dbos * @inner */ class DBOFactory { /** * <strong>Note:</strong> The constructor is not accessible from the client * code. Instances are created using module's * [createDBOFactory()]{@link module:x2node-dbos.createDBOFactory} * function. * * @protected * @param {module:x2node-dbos.DBDriver} dbDriver Database driver to use. * @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types * library to build DBOs against. */ constructor(dbDriver, recordTypes) { this._dbDriver = dbDriver; this._recordTypes = recordTypes; this._rcMonitor = null; } /** * The database driver. * * @member {module:x2node-dbos.DBDriver} * @readonly */ get dbDriver() { return this._dbDriver; } /** * Record types library associated with the factory. * * @member {module:x2node-records~RecordTypesLibrary} * @readonly */ get recordTypes() { return this._recordTypes; } /** * Create standardized data source object for the database driver associated * with the DBO factory. * * @param {*} source Driver-specific data source object. * @returns {module:x2node-dbos.DataSource} The data source. */ adaptDataSource(source) { return new DBDriverDataSource(this._dbDriver, source); } /** * Assign record collections monitor to the factory. Every DBO created by the * factory will report record collection updates to this monitor. * * @param {module:x2node-dbos.RecordCollectionsMonitor} monitor The monitor. * @returns {module:x2node-dbos~DBOFactory} This factory. */ setRecordCollectionsMonitor(monitor) { this._rcMonitor = monitor; return this; } /** * Record collections monitor assigned to the factory. * * @member {module:x2node-dbos.RecordCollectionsMonitor} * @readonly */ get recordCollectionsMonitor() { return this._rcMonitor; } /** * Build a database <em>fetch</em> operation, which queries the database with * <code>SELECT</code> statements and returns the results. Once built, the * DBO can be executed multiple times with different filter parameters. * * @param {string} recordTypeName Name of the top record type to fetch. * @param {Object} [querySpec] Query specification. If unspecified (same as * an empty object), all records are fetched with all properties that are * fetched by default and the records are returned in no particular order. * @param {Array.<string>} [querySpec.props] Record properties to include. If * unspecified, all properties that are fetched by default are included * (equivalent to <code>['*']</code>). Note, that record id property is * always included. To include super-properties, the super-property name is * included among the patterns starting with a dot. Records are not fetched * if only super-properties are provided. To include records as well, a "*" * can be added. * @param {Array.<Array>} [querySpec.filter] The filter specification. If * unspecified, all records are included. * @param {Array.<string>} [querySpec.order] The records order specification. * If unspecified, the records are returned in no particular order. * @param {Array.<number>} [querySpec.range] The range specification, which * is a two-element array where the first element is the first record index * starting from zero and the second element is the maximum number of records * to return. If the range is not specified, all matching records are * returned. * @param {string} [querySpec.lock] Either "shared" or "exclusive". If * "shared", all involved records will become protected against modification * until the end of the transaction. If "exclusive", the matched records of * the record type being fetched are protected from reading by other * transactions until this transaction completes. Any participating records * of referred record types get locked in "shared" more and are protected * against modification until the end of the transaction. If lock is not * specified, the DBO does not make any effort to lock the records. * @returns {module:x2node-dbos~FetchDBO} The DBO object. * @throws {module:x2node-common.X2UsageError} If the top record type is * unknown. * @throws {module:x2node-common.X2SyntaxError} If the provided query * specification is invalid. */ buildFetch(recordTypeName, querySpec) { // check top record type existense if (!this._recordTypes.hasRecordType(recordTypeName)) throw new common.X2UsageError( `Requested top record type ${recordTypeName} is unknown.`); // parse records query specification let selectedPropPatterns, selectedSuperProps; let filterSpec, orderSpec, rangeSpec; if (querySpec) { if (querySpec.props) { if (!Array.isArray(querySpec.props)) throw new common.X2UsageError( 'Properties list is not an array.'); if (querySpec.props.length === 0) selectedPropPatterns = new Array(); else for (let propPattern of querySpec.props) { if (propPattern.startsWith('.')) { if (propPattern.indexOf('.', 1) > 0) throw new common.X2SyntaxError( 'Super-property name may not contain dots.'); if (!selectedSuperProps) selectedSuperProps = new Array(); selectedSuperProps.push(propPattern.substring(1)); } else { if (!selectedPropPatterns) selectedPropPatterns = new Array(); selectedPropPatterns.push(propPattern); } } if (selectedPropPatterns) { orderSpec = querySpec.order; rangeSpec = querySpec.range; } } else { selectedPropPatterns = [ '*' ]; orderSpec = querySpec.order; rangeSpec = querySpec.range; } filterSpec = querySpec.filter; } else { selectedPropPatterns = [ '*' ]; } // build and return the DBO return new FetchDBO( this._dbDriver, this._recordTypes, recordTypeName, selectedPropPatterns, selectedSuperProps, filterSpec, orderSpec, rangeSpec, (querySpec && querySpec.lock)); } /** * Build a database <em>insert</em> operation, which creates new records with * <code>INSERT</code> statements. * * @param {string} recordTypeName Name of the record type to insert. * @param {Object} record Record template to insert. * @returns {module:x2node-dbos~InsertDBO} The DBO object. * @throws {module:x2node-common.X2UsageError} If the provided record data is * invalid or the record type is unknown. */ buildInsert(recordTypeName, record) { // check top record type existense if (!this._recordTypes.hasRecordType(recordTypeName)) throw new common.X2UsageError( `Specified record type ${recordTypeName} is unknown.`); // build and return the DBO return new InsertDBO( this._dbDriver, this._recordTypes, this._rcMonitor, this._recordTypes.getRecordTypeDesc(recordTypeName), record); } /** * Build a database <em>delete</em> operarion, which deletes records with * <code>DELETE</code> statements. Once built, the DBO can be executed * multiple times with different filter parameters. * * @param {string} recordTypeName Name of the record type to delete. * @param {Array.<Array>} [filter] The filter specification. If unspecified, * all records of the type are deleted. * @returns {module:x2node-dbos~DeleteDBO} The DBO object. * @throws {module:x2node-common.X2UsageError} If record type is unknown or * provided filter specification is invalid. */ buildDelete(recordTypeName, filterSpec) { // check top record type existense if (!this._recordTypes.hasRecordType(recordTypeName)) throw new common.X2UsageError( `Specified record type ${recordTypeName} is unknown.`); // build and return the DBO return new DeleteDBO( this._dbDriver, this._recordTypes, this._rcMonitor, this._recordTypes.getRecordTypeDesc(recordTypeName), filterSpec); } /** * Build a database <em>update</em> operarion, which updates records with * <code>UPDATE</code> (and potentially <code>INSERT</code> and * <code>DELETE</code> for collection properties) statements. Once built, the * DBO can be executed multiple times with different filter parameters. * * <p><strong>Note:</strong> The DBO is not intended for bulk updates that * include large numbers of records. It loads and locks all matching records * (only the properties it needs) into memory and then updates each record * one by one all within the same transaction. The most common use-case for * the DBO is to update a single record matched by its id. * * <p><strong>Also note</strong> how the "test" patch operation is handled: * When a "test" operation fails for a record, no changes for that record are * made in the database. This does not affect other records in the matched * set, neither it affects the transaction. Whether there were any records * that were not patched because of a failed "test" operation is reported * back in the operation result object (see the <code>testFailed</code> * flag and <code>failedRecordIds</code> array). * * @param {string} recordTypeName Name of the record type to update. * @param {(module:x2node-patches~RecordPatch|Array)} patch The patch. If * array, assumed to be a RFC 6902 JSON Patch specification. * @param {(Array.<Array>|module:x2node-dbos~UpdateDBO~recordsFetcher)} [filterOrFetcher] * Either, filter specification or function that provides the DBO with * records, on which to perform the update. If fetcher function is provided, * the DBO will not* perform the initial fetch and lock for the records. If * neither filter nor fetcher function is specified, all records of the type * are fetched, locked and updated (should be exceptionally rare case). * @returns {module:x2node-dbos~UpdateDBO} The DBO object. * @throws {module:x2node-common.X2UsageError} If record type is unknown or * provided filter is invalid. * @throws {module:x2node-common.X2SyntaxError} If the provided patch * specification as an array and it is not valid. */ buildUpdate(recordTypeName, patch, filterOrFetcher) { // check top record type existense if (!this._recordTypes.hasRecordType(recordTypeName)) throw new common.X2UsageError( `Specified record type ${recordTypeName} is unknown.`); // build patch if needed let patchToUse = patch; if (Array.isArray(patch)) patchToUse = patches.build(this._recordTypes, recordTypeName, patch); else if (!patch || ((typeof patch.apply) !== 'function')) throw new common.X2UsageError( 'The provided patch is neither a RecordPatch nor an array.'); // build and return the DBO return new UpdateDBO( this._dbDriver, this._recordTypes, this._rcMonitor, this._recordTypes.getRecordTypeDesc(recordTypeName), patchToUse, filterOrFetcher); } /** * Get new transaction handler. The transaction has not been started. * * @param {*} connection The database connection compatible with the database * driver. * @returns {module:x2node-dbos~Transaction} The transaction handler. * @deprecated Use [createTxFactory()]{module:x2node-dbos~DBOFactory#createTxFactory} * instead. */ newTransaction(connection) { return new Transaction(this._dbDriver, connection); } /** * Create transaction factory compatible with the DBOs built by this DBO * factory and using the provided database connections source. * * @param {module:x2node-dbos.DataSource} ds The data source (see * [adaptDataSource()]{@link module:x2node-dbos~DBOFactory#adaptDataSource}). * @returns {module:x2node-dbos~TxFactory} Transactions factory. */ createTxFactory(ds) { return new TxFactory(this._dbDriver, ds); } } // export the class module.exports = DBOFactory;