mvom
Version:
Multivalue Object Mapper
343 lines (309 loc) • 12.8 kB
JavaScript
"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;