UNPKG

mvom

Version:

Multivalue Object Mapper

496 lines (464 loc) 14.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _Document = _interopRequireDefault(require("./Document")); var _errors = require("./errors"); var _ForeignKeyDbTransformer = _interopRequireDefault(require("./ForeignKeyDbTransformer")); var _Query = _interopRequireDefault(require("./Query")); var _utils = require("./utils"); // #region Types /** Used as an intersection type to make specific model properties required */ /** * An intersection type that combines the `Model` class instance with the * inferred shape of the model object based on the schema definition. */ // #endregion /** Define a new model */ const compileModel = (connection, schema, file, dbServerDelimiters, logHandler // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types ) => { logHandler.debug(`creating new model for file ${file}`); /** Model constructor */ return class Model extends _Document.default { /** Connection instance which constructed this model definition */ static connection = connection; /** Database file this model acts against */ static file = file; /** Schema that defines this model */ static schema = schema; /** Log handler instance used for diagnostic logging */ static #logHandler = logHandler; /** Database server delimiters */ static #dbServerDelimiters = dbServerDelimiters; /** Document version hash */ /** Id of model instance */ // add definite assignment assertion since property is assigned through defineProperty /** Original record string that model was generated from */ /** Private id tracking property */ #_id; constructor(options) { const { data, record, _id = null, __v = null } = options; const mvRecord = record != null ? _Document.default.convertMvStringToArray(record, Model.#dbServerDelimiters) : []; const documentConstructorOptions = { data, record: mvRecord }; super(Model.schema, documentConstructorOptions); this.#_id = _id; this.__v = __v; this._originalRecordString = record ?? null; Object.defineProperties(this, { _id: { enumerable: true, get: () => this.#_id, set: value => { if (this.#_id != null) { throw new Error('_id value cannot be changed once set'); } this.#_id = value; } }, _originalRecordString: { enumerable: false, writable: false, configurable: false } }); Model.#logHandler.debug(`creating new instance of model for file ${Model.file}`); this._transformationErrors.forEach(error => { // errors occurred while transforming data from multivalue format - log them Model.#logHandler.warn(`error transforming data -- file: ${Model.file}; _id: ${this._id}; class: ${error.transformClass}; value: ${error.transformValue}`); }); } /** * Check to see if a record is locked by another user/process * @returns `true` when record is locked */ static async checkForRecordLockById(id, options = {}) { const { maxReturnPayloadSize, requestId, userDefined } = options; const data = await this.connection.executeDbSubroutine('checkForRecordLockById', { filename: this.file, id }, { ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined != null && { userDefined }) }); return data.result !== 0; } /** Delete a document */ static async deleteById(id, options = {}) { const { maxReturnPayloadSize, requestId, userDefined } = options; const data = await this.connection.executeDbSubroutine('deleteById', { filename: this.file, id }, { ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); if (data.result == null) { return null; } const { _id, __v, record } = data.result; return this.#createModelFromRecordString(record, _id, __v); } /** Find documents via query */ static async find(selectionCriteria = {}, options = {}) { const { maxReturnPayloadSize, requestId, userDefined, ...queryConstructorOptions } = options; const query = new _Query.default(this.connection, this.schema, this.file, this.#logHandler, selectionCriteria, queryConstructorOptions); const { documents } = await query.exec({ ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); return documents.map(document => { const { _id, __v, record } = document; return this.#createModelFromRecordString(record, _id, __v); }); } /** Find documents via query, returning them along with a count */ static async findAndCount(selectionCriteria = {}, options = {}) { const { maxReturnPayloadSize, requestId, userDefined, ...queryConstructorOptions } = options; const query = new _Query.default(this.connection, this.schema, this.file, this.#logHandler, selectionCriteria, queryConstructorOptions); const { count, documents } = await query.exec({ ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); const models = documents.map(document => { const { _id, __v, record } = document; return this.#createModelFromRecordString(record, _id, __v); }); return { count, documents: models }; } /** Find a document by its id */ static async findById(id, options = {}) { const { maxReturnPayloadSize, requestId, projection, userDefined } = options; const data = await this.connection.executeDbSubroutine('findById', { filename: this.file, id, projection: this.#formatProjection(projection) }, { ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); if (data.result == null) { return null; } const { _id, __v, record } = data.result; return this.#createModelFromRecordString(record, _id, __v); } /** Find multiple documents by their ids */ static async findByIds(ids, options = {}) { const { maxReturnPayloadSize, requestId, projection, userDefined } = options; const idsArray = (0, _utils.ensureArray)(ids); const data = await this.connection.executeDbSubroutine('findByIds', { filename: this.file, ids: idsArray, projection: this.#formatProjection(projection) }, { ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); return data.result.map(dbResultItem => { if (dbResultItem == null) { return null; } const { _id, __v, record } = dbResultItem; return this.#createModelFromRecordString(record, _id, __v); }); } /** * Increment fields in a document by values * @throws {Error} if schema is not defined * @throws {Error} if no result is returned from increment operation */ static async increment(id, operations, options = {}) { const { maxReturnPayloadSize, requestId, userDefined, retry = 5, retryDelay = 1 } = options; const transformedOperations = this.#formatIncrementOperations(operations); const data = await this.connection.executeDbSubroutine('increment', { filename: this.file, id, operations: transformedOperations, retry, retryDelay }, { ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); const { originalDocument: { __v: originalVersion, record: originalRecord }, updatedDocument: { __v: updatedVersion, record: updatedRecord } } = data; return { originalDocument: this.#createModelFromRecordString(originalRecord, id, originalVersion), updatedDocument: this.#createModelFromRecordString(updatedRecord, id, updatedVersion) }; } /** Read a DIR file type record directly from file system as Base64 string by its id */ static async readFileContentsById(id, options = {}) { const { maxReturnPayloadSize, requestId, userDefined } = options; const data = await this.connection.executeDbSubroutine('readFileContentsById', { filename: this.file, id }, { ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); return data.result; } /** Create a new Model instance from a record string */ static #createModelFromRecordString(recordString, _id, __v) { return new Model({ _id, __v, record: recordString }); } /** Format projection option */ static #formatProjection(projection) { return projection != null && this.schema != null ? this.schema.transformPathsToDbPositions(projection) : null; } /** * Format increment operations to be sent to the database */ static #formatIncrementOperations(operations) { if (this.schema == null) { throw new Error('Schema must be defined to perform increment operations'); } const incrementSchema = this.schema; return operations.map(({ path, value }) => ({ path: incrementSchema.transformPathToOrdinalPosition(path), value })); } /** Save a document to the database */ async save(options = {}) { const { maxReturnPayloadSize, requestId, userDefined } = options; // validate data prior to saving this.#validate(); try { const data = await Model.connection.executeDbSubroutine('save', { filename: Model.file, id: this._id, __v: this.__v, record: this.#convertToMvString(), foreignKeyDefinitions: this.#buildForeignKeyDefinitions() }, { ...(maxReturnPayloadSize != null && { maxReturnPayloadSize }), ...(requestId != null && { requestId }), ...(userDefined && { userDefined }) }); const { _id, __v, record } = data.result; return Model.#createModelFromRecordString(record, _id, __v); } catch (err) { // enrich caught error object with additional information and rethrow err.other = { ...err.other, // ensure properties are not lost in the event the "other" object existed previously filename: Model.file, _id: this._id }; throw err; } } /** Convert model instance to multivalue string */ #convertToMvString() { const { am, vm, svm } = Model.#dbServerDelimiters; const mvRecord = this.transformDocumentToRecord(); return mvRecord.map(attribute => Array.isArray(attribute) ? attribute.map(value => Array.isArray(value) ? value.join(svm) : value).join(vm) : attribute).join(am); } /** Validate the model instance */ #validate() { if (this._id == null) { throw new TypeError('_id value must be set prior to saving'); } // validate data prior to saving const validationErrors = this.validate(); // validate _id pattern if (typeof this._id === 'string' && schema?.idMatch != null && !schema.idMatch.test(this._id)) { validationErrors.set('_id', ['Model id does not match pattern']); } if (validationErrors.size > 0) { throw new _errors.DataValidationError({ validationErrors, filename: Model.file, recordId: this._id }); } } /** Build a list of foreign key definitions to be used by the database for foreign key validation */ #buildForeignKeyDefinitions() { const foreignKeyDefinitions = this.buildForeignKeyDefinitions(); if (schema?.idForeignKey != null) { const foreignKeyDbTransformer = new _ForeignKeyDbTransformer.default(schema.idForeignKey); const definitions = foreignKeyDbTransformer.transform(this._id).map(({ filename, entityName, entityId }) => ({ filename: (0, _utils.ensureArray)(filename), entityName, entityIds: [entityId] })); foreignKeyDefinitions.push(...definitions); } return foreignKeyDefinitions; } }; }; var _default = exports.default = compileModel;