UNPKG

mvom

Version:

Multivalue Object Mapper

343 lines (309 loc) 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _dataTransformers = require("./dataTransformers"); var _errors = require("./errors"); var _schemaType = require("./schemaType"); // #region Types // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style -- Record cannot circularly reference itself so index signature must be used. refer here: https://github.com/microsoft/TypeScript/pull/33050#issuecomment-714348057 for additional information /** Infer whether a schema type definition is required and union the result with null if it is not */ /** * Infer the output of a string type definition with specific handling for enumerations * If an enumeration is a readonly array, the return type of the definition will be a union * of the array elements. Otherwise, the return type will be a string. */ /** * Infer the output type of an array schema definition * * Allows a constraint to be specified to filter the output to only include scalar arrays of a specific type */ /** Infer the output type of a schema type definition */ /** * Infer the shape of a `Document` instance based upon the Schema it was instantiated with * * Allows a constraint to be specified to filter the output to only include properties of a specific type */ /** Infer the shape of a `Model` instance based upon the Schema it was instantiated with */ /** * Flatten a document to string keyPath (i.e. { "foo.bar.baz": number }) * * Allows a constraint to be specified to filter the output to only include properties of a specific type */ /** Infer the string keyPaths of a schema */ // #endregion /** Schema constructor */ class Schema { /** Key/value pairs of schema object path structure and associated multivalue dictionary ids */ /** The compiled schema object path structure */ /** Foreign key definition for the record id */ /** Regular expression to validate the record id against */ /** The schema definition passed to the constructor */ /** Map of the compiled schema object position paths */ /** Map of all subdocument schemas represented in this Schema with parentPath as key */ /** Optional function to use for encryption of sensitive data */ /** Optional function to use for decryption of sensitive data */ constructor(definition, { dictionaries, idForeignKey, idMatch, encrypt, decrypt } = {}) { this.idForeignKey = idForeignKey; this.idMatch = idMatch; this.definition = definition; this.positionPaths = new Map(); this.subdocumentSchemas = new Map(); this.encrypt = encrypt; this.decrypt = decrypt; this.dictPaths = this.buildDictionaryPaths(dictionaries); this.paths = this.buildPaths(this.definition); } /** Get all multivalue data paths in this schema and its subdocument schemas */ getMvPaths() { return Array.from(this.subdocumentSchemas.values()).reduce((mvPaths, schema) => mvPaths.concat(schema.getMvPaths()), Array.from(this.positionPaths.values()).slice()); } /** Transform the paths to positions */ transformPathsToDbPositions(paths) { if (paths.length === 0) { return []; } const positionPaths = this.getPositionPaths(); const positionKeys = Array.from(positionPaths.keys()); const positions = paths.reduce((acc, positionPath) => { if (positionPaths.has(positionPath)) { // find the key in position paths // add position // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const [dbPosition] = positionPaths.get(positionPath); acc.add(dbPosition + 1); } else if (!positionPath.includes('.')) { // if the property is a parent key, we will add positions for all children // e.g we only pass property "name" to return all data for name.first, name.last, etc. const matchedPositionPaths = positionKeys.filter(key => key.split('.')[0] === positionPath); matchedPositionPaths.forEach(key => { // add child position // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const [dbPosition] = positionPaths.get(key); acc.add(dbPosition + 1); }); } return acc; }, new Set()); return [...positions]; } /** * Transform the path to its ordinal position. Returning a '.' delimited string. Ex. "1.2.3" * @throws {Error} Invalid path provided */ transformPathToOrdinalPosition(path) { const positionPaths = this.getPositionPaths(); const ordinalPath = positionPaths.get(path); if (ordinalPath == null) { throw new Error('Invalid path provided'); } const [attributePosition, valuePosition = 0, subvaluePosition = 0] = ordinalPath; return `${attributePosition + 1}.${valuePosition + 1}.${subvaluePosition + 1}`; } /** Build the dictionary path map for additional dictionaries provided as schema options */ buildDictionaryPaths(dictionaries = {}) { // Add reference for _id --> @ID by default const dictPaths = new Map([['_id', { dictionary: '@ID', dataTransformer: new _dataTransformers.StringDataTransformer() }]]); return Object.entries(dictionaries).reduce((acc, [queryProperty, dictionaryDefinition]) => { if (typeof dictionaryDefinition === 'string') { return acc.set(queryProperty, { dictionary: dictionaryDefinition, dataTransformer: new _dataTransformers.StringDataTransformer() }); } const { type, dictionary } = dictionaryDefinition; switch (type) { case 'string': return acc.set(queryProperty, { dictionary, dataTransformer: new _dataTransformers.StringDataTransformer() }); case 'number': return acc.set(queryProperty, { dictionary, dataTransformer: new _dataTransformers.NumberDataTransformer() }); case 'boolean': return acc.set(queryProperty, { dictionary, dataTransformer: new _dataTransformers.BooleanDataTransformer() }); case 'ISOCalendarDate': return acc.set(queryProperty, { dictionary, dataTransformer: new _dataTransformers.ISOCalendarDateDataTransformer() }); case 'ISOCalendarDateTime': return acc.set(queryProperty, { dictionary, dataTransformer: new _dataTransformers.ISOCalendarDateTimeDataTransformer(dictionaryDefinition.dbFormat) }); case 'ISOTime': return acc.set(queryProperty, { dictionary, dataTransformer: new _dataTransformers.ISOTimeDataTransformer(dictionaryDefinition.dbFormat) }); default: return acc; } }, dictPaths); } /** * Get all positionPaths with path as key and position array as value including children schemas * e.g. parent field 'foo' has a child schema which has ["bar",[2]], the returned positionPath will be ["foo.bar",[2]]. */ getPositionPaths() { // merge the positionPaths from subdocumentSchemas with parentPath appended by the childPath recursively return Array.from(this.subdocumentSchemas.entries()).reduce((mvPaths, [parentKey, schema]) => { const childrenPositions = schema.getPositionPaths(); childrenPositions.forEach((subPath, subKey) => { mvPaths.set(`${parentKey}.${subKey}`, subPath); }); return mvPaths; }, new Map(this.positionPaths)); } /** Construct instance member paths */ buildPaths(definition, prev) { return Object.entries(definition).reduce((acc, [key, value]) => { // construct flattened keypath const newKey = prev != null ? `${prev}.${key}` : key; if (Array.isArray(value)) { return acc.set(newKey, this.castArray(value, newKey)); } if (this.isScalarDefinition(value)) { // cast this value as a schemaType return acc.set(newKey, this.castScalar(value, newKey)); } if (value instanceof Schema) { // value is an already compiled schema - cast as embedded document this.handleSubDocumentSchemas(value, newKey); return acc.set(newKey, new _schemaType.EmbeddedType(value)); } const nestedPaths = this.buildPaths(value, newKey); return new Map([...acc, ...nestedPaths]); }, new Map()); } /** * Cast an array to a schemaType * @throws {@link InvalidParameterError} An invalid parameter was passed to the function */ castArray(castee, keyPath) { if (castee.length !== 1) { // a schema array definition must contain exactly one value of language-type object (which includes arrays) throw new _errors.InvalidParameterError({ message: 'castee parameter must be an array containing a single object', parameterName: 'castee' }); } const [arrayValue] = castee; if (Array.isArray(arrayValue)) { if (arrayValue.length !== 1) { // a schema nested array definition must contain exactly one schema definition throw new _errors.InvalidParameterError({ message: 'castee parameter must be an array containing a single object', parameterName: 'castee' }); } const [nestedArrayValue] = arrayValue; if (!this.isScalarDefinition(nestedArrayValue)) { throw new _errors.InvalidParameterError({ message: 'nested array value of schema type definition must be a scalar', parameterName: 'castee' }); } return new _schemaType.NestedArrayType(this.castScalar(nestedArrayValue, keyPath)); } if (arrayValue instanceof Schema) { this.handleSubDocumentSchemas(arrayValue, keyPath); return new _schemaType.DocumentArrayType(arrayValue); } if (this.isScalarDefinition(arrayValue)) { return new _schemaType.ArrayType(this.castScalar(arrayValue, keyPath)); } const subdocumentSchema = new Schema(arrayValue, { encrypt: this.encrypt, decrypt: this.decrypt }); this.handleSubDocumentSchemas(subdocumentSchema, keyPath); return new _schemaType.DocumentArrayType(subdocumentSchema); } /** * Cast a scalar definition to a scalar schemaType * @throws {@link InvalidParameterError} An invalid parameter was passed to the function */ castScalar(castee, keyPath) { const options = { encrypt: this.encrypt, decrypt: this.decrypt }; let schemaTypeValue; switch (castee.type) { case 'boolean': schemaTypeValue = new _schemaType.BooleanType(castee, options); break; case 'ISOCalendarDateTime': schemaTypeValue = new _schemaType.ISOCalendarDateTimeType(castee, options); break; case 'ISOCalendarDate': schemaTypeValue = new _schemaType.ISOCalendarDateType(castee, options); break; case 'ISOTime': schemaTypeValue = new _schemaType.ISOTimeType(castee, options); break; case 'number': schemaTypeValue = new _schemaType.NumberType(castee, options); break; case 'string': schemaTypeValue = new _schemaType.StringType(castee, options); break; default: { const exhaustiveCheck = castee; throw new _errors.InvalidParameterError({ message: `Data definition does not contain a supported type value: ${exhaustiveCheck}`, parameterName: 'castee' }); } } // add to mvPath array this.positionPaths.set(keyPath, schemaTypeValue.path); // update dictPaths if (schemaTypeValue.dictionary != null) { this.dictPaths.set(keyPath, { dictionary: schemaTypeValue.dictionary, dataTransformer: schemaTypeValue }); } return schemaTypeValue; } /** Perform ancillary updates needed when a subdocument is in the Schema definition */ handleSubDocumentSchemas(schema, keyPath) { this.subdocumentSchemas.set(keyPath, schema); this.mergeSchemaDictionaries(schema, keyPath); } /** Determine if an object matches the structure of a scalar definition */ isScalarDefinition(schemaTypeDefinition) { return Object.prototype.hasOwnProperty.call(schemaTypeDefinition, 'type') && Object.prototype.hasOwnProperty.call(schemaTypeDefinition, 'path'); } /** Merge subdocument schema dictionaries with the parent schema's dictionaries */ mergeSchemaDictionaries(schema, keyPath) { this.dictPaths = Array.from(schema.dictPaths).reduce((acc, [subDictPath, subDictTypeDetail]) => { const dictKey = `${keyPath}.${subDictPath}`; return acc.set(dictKey, subDictTypeDetail); }, this.dictPaths); } } var _default = exports.default = Schema;