UNPKG

mvom

Version:

Multivalue Object Mapper

216 lines (199 loc) 7.5 kB
"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;