mvom
Version:
Multivalue Object Mapper
216 lines (199 loc) • 7.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _lodash = require("lodash");
var _errors = require("./errors");
/** U2 does not allow pound signs in filenames so we can use it to separate filename/entityName combinations */
const FOREIGN_KEY_SEPARATOR = '#';
// #region Types
/** Type of data property for constructing a document dependent upon the schema */
/**
* An intersection type that combines the `Document` class instance with the
* inferred shape of the document object based on the schema definition.
*/
// #endregion
/** A document object */
class Document {
/** Array of any errors which occurred during transformation from the database */
/** Schema instance which defined this document */
#schema;
/** Record array of multivalue data */
#record;
/** Indicates whether this document is a subdocument of a composing parent */
#isSubdocument;
constructor(schema, options) {
const {
data = {},
record,
isSubdocument = false
} = options;
this.#schema = schema;
this.#record = record ?? [];
this.#isSubdocument = isSubdocument;
this._transformationErrors = [];
Object.defineProperties(this, {
_transformationErrors: {
configurable: false,
enumerable: false,
writable: false
}
});
if (schema == null) {
this._raw = this.#record;
}
this.#transformRecordToDocument();
// load the data passed to constructor into document instance
(0, _lodash.assignIn)(this, data);
}
/** Create a new Subdocument instance from a record array */
static createSubdocumentFromRecord(schema, record) {
return new Document(schema, {
record,
isSubdocument: true
});
}
/** Create a new Subdocument instance from data */
static createSubdocumentFromData(schema, data) {
return new Document(schema, {
data,
isSubdocument: true
});
}
/** Create a new Document instance from a record string */
static createDocumentFromRecordString(schema, recordString, dbServerDelimiters) {
const record = Document.convertMvStringToArray(recordString, dbServerDelimiters);
return new Document(schema, {
record
});
}
/** Convert a multivalue string to an array */
static convertMvStringToArray(recordString, dbServerDelimiters) {
const {
am,
vm,
svm
} = dbServerDelimiters;
const record = recordString === '' ? [] : recordString.split(am).map(attribute => {
if (attribute === '') {
return null;
}
const attributeArray = attribute.split(vm);
if (attributeArray.length === 1 && !attributeArray[0].includes(svm)) {
return attribute;
}
return attributeArray.map(value => {
if (value === '') {
return null;
}
const valueArray = value.split(svm);
if (valueArray.length === 1) {
return value;
}
return valueArray.map(subvalue => subvalue === '' ? null : subvalue);
});
});
return record;
}
/** Transform document structure to multivalue array structure */
transformDocumentToRecord() {
return this.#schema === null ? (0, _lodash.get)(this, '_raw', []) : Array.from(this.#schema.paths).reduce((record, [keyPath, schemaType]) => {
const value = (0, _lodash.get)(this, keyPath, null);
return schemaType.set(record, schemaType.cast(value));
}, this.#isSubdocument ? [] : (0, _lodash.cloneDeep)(this.#record));
}
/** Build a list of foreign key definitions to be used by the database for foreign key validation */
buildForeignKeyDefinitions() {
if (this.#schema === null) {
return [];
}
const definitionMap = Array.from(this.#schema.paths).reduce((foreignKeyDefinitions, [keyPath, schemaType]) => {
const value = (0, _lodash.get)(this, keyPath, null);
const definitions = schemaType.transformForeignKeyDefinitionsToDb(schemaType.cast(value));
// Deduplicate foreign key definitions by using a filename / entity name combination
// We could deduplicate using just the filename but ignoring the entity name could result in confusing error messages
definitions.forEach(({
filename,
entityId,
entityName
}) => {
const key = `${filename}${FOREIGN_KEY_SEPARATOR}${entityName}`;
const accumulatedEntityIds = foreignKeyDefinitions.get(key) ?? new Set();
// For array types we may need to validate multiple foreign keys
accumulatedEntityIds.add(entityId);
foreignKeyDefinitions.set(key, accumulatedEntityIds);
});
return foreignKeyDefinitions;
}, new Map());
return Array.from(definitionMap).reduce((acc, [key, value]) => {
const keyParts = key.split(FOREIGN_KEY_SEPARATOR);
const fileName = keyParts[0];
// If an array of filenames was provided, when we transformed the array into a string above, commas
// would have been inserted between each filename. Split the string back into an array.
const filename = fileName.split(',');
// Just incase the entity name included a pound sign, rejoin
const entityName = keyParts.slice(1).join(FOREIGN_KEY_SEPARATOR);
acc.push({
filename,
entityName,
entityIds: Array.from(value)
});
return acc;
}, []);
}
/** Validate document for errors */
validate() {
const documentErrors = new Map();
if (this.#schema !== null) {
Array.from(this.#schema.paths).forEach(([keyPath, schemaType]) => {
const originalValue = (0, _lodash.get)(this, keyPath, null);
// cast to complex data type if necessary
try {
const castValue = schemaType.cast(originalValue);
(0, _lodash.set)(this, keyPath, castValue);
const validationResult = schemaType.validate(castValue);
if (validationResult instanceof Map) {
validationResult.forEach((errors, key) => {
if (errors.length > 0) {
documentErrors.set(`${keyPath}.${key}`, errors);
}
});
} else if (validationResult.length > 0) {
documentErrors.set(keyPath, validationResult);
}
} catch (err) {
// an error was thrown - return the message from that error in an array in the documentErrors list
documentErrors.set(keyPath, [err.message]);
}
});
}
return documentErrors;
}
/** Apply schema structure using record to document instance */
#transformRecordToDocument() {
if (this.#schema == null) {
// if this is a document without a schema, there is nothing to transform
return;
}
const plainDocument = Array.from(this.#schema.paths).reduce((document, [keyPath, schemaType]) => {
let setValue;
try {
setValue = schemaType.get(this.#record);
} catch (err) {
if (err instanceof _errors.TransformDataError) {
// if this was an error in data transformation, set the value to null and add to transformationErrors list
setValue = null;
this._transformationErrors.push(err);
} else {
// otherwise rethrow any other type of error
throw err;
}
}
(0, _lodash.set)(document, keyPath, setValue);
return document;
}, {});
(0, _lodash.assignIn)(this, plainDocument);
}
}
var _default = exports.default = Document;