UNPKG

mongoose-readwrite

Version:

Remove unauthorized properties from inputs and outputs

217 lines (177 loc) 6.07 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = readwritePlugin; var _flattenObj = _interopRequireDefault(require("flatten-obj")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const flatten = (0, _flattenObj.default)(); const Debug = require('debug'); const debug = Debug('mongoose-readwrite'); const VALID_RULES = 'readable writable'.split(' '); const DEFAULT_OPTIONS = { defaultUnwritable: '_id'.split(' '), strictSchema: false, defaults: { writable: true, readable: true } }; const isObject = item => typeof item === 'object' && item !== null && !Array.isArray(item); // Return schema object for subdocument path const deepSchema = (schema, path) => path.split('.').reduce((schemaCol, prop) => schemaCol.path(prop).schema, schema); // Returns a function that takes a dot path like 'path.subpath.subsubpath' and // checks if the schema.path(dotpath) has instance === 'Mixed'. // If it can't find the path info or the pathInfo.instance !== 'Mixed', removes the last // component of the path and tries again. // If at some point it identifies a pathInfo.instance === 'Mixed', returns the identified path // otherwise returns false const getMixedChecker = schema => dotPath => { const components = dotPath.split('.'); while (components.length) { const path = components.join('.'); const pathInfo = schema.path(path); if (pathInfo && pathInfo.instance === 'Mixed') { return path; } components.pop(); } // Never found pathInfo or never found a pathInfo with instance = 'Mixed' return false; }; // Returns a function that takes a path and a persona and returns true if the // path received is writable by said persona acording to the writable rule. // A path is writable by default. Returns false if: // - rules[path].writable === false // - rules[path].writable is an array and does not contain persona // - options.strictSchema === true const getWritableChecker = (schema, options) => (path, persona) => { if (options.defaultUnwritable.includes(path)) { return false; } const pathInfo = schema.path(path); if (!pathInfo) { return !options.strictSchema; } const { defaults: { writable: defaultWritable } } = options; const { readwriteOptions: { writable = defaultWritable } = { writable: defaultWritable } } = pathInfo.options; if (Array.isArray(writable)) { return writable.includes(persona); } return writable; }; // Returns a function that works on a mongoose document toObject() transform option. // It hides any non-readable property for the persona passed. const obscure = (persona, options) => (doc, ret) => { const obscureRecursive = (obj, prefix) => Object.keys(obj).reduce((col, path) => { const value = obj[path]; const dotPath = prefix ? `${prefix}.${path}` : path; const pathInfo = doc.schema.path(dotPath); if (!pathInfo) { if (value !== null && !Array.isArray(value) && typeof value === 'object') { col[path] = obscureRecursive(value, dotPath); } else if (doc.schema.virtual(dotPath)) { col[path] = value; } } else { const { defaults: { readable: defaultReadable } } = options; const { readwriteOptions: { readable = defaultReadable } = { readable: defaultReadable } } = pathInfo.options; debug(persona); debug(readable); debug(Array.isArray(readable) ? readable.includes(persona) : 'not array'); const pathReadable = Array.isArray(readable) && readable.includes(persona) || readable === true; if (pathReadable) { debug(`path ${dotPath} is readable: ${pathReadable}... adding`); col[path] = value; } } return col; }, {}); return obscureRecursive(ret); }; const applyRules = (...params) => { const applyRulesRecursive = (rules, schema, prefix) => { if (!isObject(rules)) { return; } Object.keys(rules).forEach(property => { const path = prefix ? `${prefix}.${property}` : property; const pathInfo = schema.path(path); const propertyRules = rules[property]; if (!pathInfo) { applyRulesRecursive(propertyRules, schema, path); return; } Object.keys(propertyRules).forEach(rule => { if (!VALID_RULES.includes(rule)) { delete propertyRules[rule]; } }); pathInfo.options.readwriteOptions = propertyRules; }); }; return applyRulesRecursive(...params); }; function readwritePlugin(modelSchema, { rules, options: opts }) { if (rules) { applyRules(rules, modelSchema); } const pluginOptions = Object.assign({}, DEFAULT_OPTIONS, opts); /** * Return function to * @param {object} input - The object that will become an instance of Model * @param {object} options - tbd {strict, subdocument} */ modelSchema.statics.getInputFilter = function getInputFilter(options = {}) { const { subdocument } = options; const { schema } = this; const thisSchema = subdocument ? deepSchema(schema, subdocument) : schema; const isMixed = getMixedChecker(thisSchema); const isWritable = getWritableChecker(thisSchema, pluginOptions); return (body, persona) => { const flatBody = flatten(body); return Object.keys(flatBody).reduce((col, dotPath) => { const mixedPath = isMixed(dotPath); if (mixedPath === false) { if (isWritable(dotPath, persona)) { col[dotPath] = flatBody[dotPath]; } } else if (isWritable(mixedPath, persona)) { col[dotPath] = flatBody[dotPath]; } return col; }, {}); }; }; modelSchema.methods.redact = function redact(options = {}) { const { persona } = options; return this.toObject({ transform: obscure(persona, pluginOptions), virtuals: true }); }; }