UNPKG

denormalize-mongoose

Version:

Bidirectional denormalize for Mongoose with own type and plugin

191 lines (149 loc) 4.58 kB
const { SchemaTypes } = require('mongoose'); const defaults = require('lodash/defaults'); const isArray = require('lodash/isArray'); const isObject = require('lodash/isObject'); const pick = require('lodash/pick'); const flatten = require('lodash/flatten'); const entries = require('lodash/entries'); const Graph = require('graph-data-structure'); const dSetPaths = Symbol('mongoose#denormalizeOptions'); const dOptions = Symbol('mongoose#setPaths'); const connections = Graph(); const connectionsMap = new Map(); const $defaults = { paths: [], suffix: 'Data', }; class DenormalizeError extends Error { /** * @param {string} key * @param {string|string[]} option * @param {string} message */ constructor(key, option, message) { const options = isArray(option) ? option : [option]; const optionsString = `'${options.join('|')}'`; super(`Denormalize: ${optionsString}' option in '${key}' field ${message}`); this.option = option; this.key = key; } } const primitives = { String, Number, Symbol, Array, }; const processOf = (key, of) => { if (typeof of === 'string') { if (!primitives[of] && !SchemaTypes[of]) { throw new DenormalizeError(key, 'of', `There is no '${of}' type`); } return primitives[of] || SchemaTypes[of]; } return primitives[of.name] || SchemaTypes[of.name] || ((v) => new of(v)); }; /** * @param {Object|Array} paths */ function normalisePaths(paths = {}) { if (isArray(paths)) { return paths.map((v) => { if (typeof v === 'object') { return normalisePaths(v); } return v; }); } return flatten(entries(paths).map(([key, values]) => { if (isArray(values) || typeof values === 'object') { return flatten(normalisePaths(values)).map((v) => `${key}.${v}`); } if (['number', 'boolean'].includes(typeof values)) { return key; } return `${key}:${values}`; })); } const groupPaths = (paths) => paths.map((path) => { const [get, set] = path.split(/[:]+/g); let $set = get; if (set) { const [, ...rest] = get.split(/[.]+/).reverse(); $set = `${rest.reverse().join('.')}.${set}` .replace(/^[.]|[.]$/g, ''); } return { get, set: $set }; }); const processPaths = (paths) => groupPaths(normalisePaths(paths) .sort((a, b) => a.split('.').length - b.split('.').length)); const transformOptions = (key, _options) => { const options = defaults(_options, $defaults); if (!isArray(options.paths) && !isObject(options.paths)) { throw new DenormalizeError(key, 'paths', 'have to be an Array<string> or Object<string.string>'); } let isInternal = false; if (!options.from) { if (!options.to && !options.suffix) { throw new DenormalizeError(key, 'suffix', 'has to be not empty String'); } if (!options.ref) { throw new DenormalizeError(key, 'ref', 'has to be not empty String'); } isInternal = true; } const base = pick(options, ['ref', 'validate', 'set', 'get']); const patched = pick(options, ['suffix', 'paths', 'of', 'from', 'to']); patched.isInternal = isInternal; patched.key = key; patched.of = patched.of || SchemaTypes.ObjectId; patched.caster = function caster(v) { if (patched.isInternal) { const process = processOf(key, patched.of); if (v && v.constructor.name === process.name) { return v; } return process(v); } return v; }; patched.patched = { paths: processPaths(patched.paths), to: (isInternal && (patched.to || `${key}${patched.suffix}`)) || key, from: (isInternal && key) || patched.from, }; return { base, patched }; }; const getSchemaType = (type) => (schema) => Object.entries(schema.paths) .filter(([, schemaType]) => { if (['SchemaArray', 'SchemaMap'].includes(schemaType.constructor.name)) { return schemaType.caster instanceof type; } return schemaType.constructor === type; }); const getDenormalizeOptions = (schemaType) => { if (schemaType.constructor.name === 'SchemaArray') { return [schemaType.caster, Array]; } if (schemaType.constructor.name === 'SchemaMap') { return [schemaType.caster, Map]; } return [schemaType, false]; }; function getRef(schemaType) { return schemaType.options.ref || getRef(schemaType.caster); } function getModel(doc) { return doc.constructor.modelName || getModel(doc.$parent()); } module.exports = { dOptions, dSetPaths, transformOptions, getSchemaType, getDenormalizeOptions, getRef, getModel, connections, connectionsMap, };