mvom
Version:
Multivalue Object Mapper
496 lines (464 loc) • 14.3 kB
JavaScript
"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;