mongoose-hidden
Version:
Hides certain model properties when invoking toJSON or toObject.
365 lines (321 loc) • 9.47 kB
JavaScript
/* eslint no-use-before-define: 0 operator-linebreak: 0 */
const mpath = require('./mpath.js')
const applyRecursively = 'applyRecursively'
const hide = 'hide'
const hideObject = 'hideObject'
const hideJSON = 'hideJSON'
const hidden = 'hidden'
const defaultHidden = 'defaultHidden'
const virtuals = 'virtuals'
/**
* Plugin constructor
*
* @param {Schema} schema a mongoose schema
* @param {Object} options a set of options
*
* @return {void}
*/
function plugin(schema, options) {
const paths = getPathnames(options, schema)
const transformer = transformerFactory(schema, options, paths)
applyTransformers(schema, transformer)
applyToChildren(schema, options)
}
/**
* Returns an array of pathnames based on the schema and the default settings
*
* @access private
*
* @param {Object} options a set of options
* @param {Schema} schema a mongoose schema
* @returns {array} an array of paths
*/
function getPathnames(options, schema) {
let paths = pathsFromTree(schema.tree)
Object.keys(options['defaultHidden']).forEach(path => {
if (paths.indexOf(path) === -1) {
paths.push(path)
}
})
return paths
}
/**
* Constructs a transformer function that can be applied to toJSON and toObject
* on the schema
*
* @param {Schema} schema a mongoose schema
* @param {Object} options a set of options
* @param {array} paths an array of paths
* @return {Function} a transformer function
*/
function transformerFactory(schema, options, paths) {
return (target, prevTransform) => (doc, transformed, opt) => {
const transformation = {
finalTransform: {},
options,
schema,
target,
transformed,
}
// Apply existing transformer
applyExisting(transformation, prevTransform, opt)
// Copy real values
paths.forEach(copyRealValues(transformation, doc))
// Copy virtual values
Object.keys(transformed).forEach(copyVirtualValues(transformation))
return transformation.finalTransform
}
}
/**
* Applies existing transformation if any
*
* @param {Object} transformation a transformation value object
* @param {Function} prevTransform existing transformer function
* @param {Object} opt transformation options
*
* @return {void}
*/
function applyExisting(transformation, prevTransform, opt) {
if (typeof prevTransform === 'function') {
const { doc, transformed } = transformation
transformation.finalTransform = prevTransform(doc, transformed, opt)
}
}
/**
* Copy 'real' values (non-virtuals) onto the finalTransform
*
* @param {Object} transformation a transformation value object
* @param {Object} doc the document being transformed
*
* @return {Function} transformer function for a given path
*/
function copyRealValues(transformation, doc) {
return pathname => {
const { finalTransform, transformed } = transformation
if (shouldHide(transformation, doc, pathname)) {
mpath.unset(pathname, finalTransform)
return
}
const value = mpath.get(pathname, transformed)
if (typeof value !== 'undefined') {
mpath.set(pathname, value, finalTransform)
}
}
}
/**
* Copy virtual values onto the finalTransform
*
* @param {Object} transformation a transformation value object
* @return {Function} transformer function for a given key
*/
function copyVirtualValues(transformation) {
return key => {
const { finalTransform, transformed } = transformation
if (shouldCopyVirtual(transformation, key)) {
mpath.set(key, mpath.get(key, transformed), finalTransform)
}
}
}
/**
* Tests to see if the pathname for target is hidden by an option
*
* @access private
*
* @param {Object} options a set of options
* @param {string} pathname property path
* @returns {Boolen} true of pathname should be hidden
*/
function testOptions(options, pathname) {
return options.defaultHidden[pathname] || options.virtuals[pathname]
}
/**
* Tests to see if the hide property is set on the schema
*
* @access private
*
* @param {Schema} schema a mongoose schema
* @param {string} key the key to test
* @param {Object} doc original document
* @param {Object} transformed transformed document
* @returns {Boolen} true of pathname should be hidden
*/
function testSchema(schema, key, doc, transformed) {
if (typeof schema === 'undefined') {
return false
}
return (
schema.options[key] === true ||
(typeof schema.options[key] === 'function' && schema.options[key](doc, transformed))
)
}
/**
* Should a property be hidden er not
*
* @access private
*
* @param {Object} transformation a transformation value object
* @param {Object} doc original document
* @param {string} pathname property path
* @returns {Boolen} true of pathname should be hidden
*/
function shouldHide({ options, schema, target, transformed }, doc, pathname) {
const schemaType = schema.path(pathname)
const hideTarget = hide + target
// Is hiding turned off?
if (options[hideTarget] === false) {
return false
}
// Test hide by option or schema
return (
testOptions(options, pathname) ||
testSchema(schemaType, hide, doc, transformed) ||
testSchema(schemaType, hideTarget, doc, transformed)
)
}
/**
* Should a virtual property by be hidden er not
*
* @access private
*
* @param {Schema} schema a mongoose schema
* @param {string} key object key name
* @returns {Boolen} true of pathname should be hidden
*/
function shouldCopyVirtual({ options, schema, target }, key) {
return (
schema.pathType(key) === 'virtual' &&
[hide, `hide${target}`].indexOf(options.virtuals[key]) === -1
)
}
/**
* Join key paths
*
* @access private
*
* @param {string} parent first part
* @param {string} child second part
* @returns {string} combined path
*/
function joinKey(parent, child) {
return parent ? parent + '.' + child : child
}
/**
* Builds a list of paths based on the schema tree. This includes virtuals and nested objects as well
*
* @access private
*
* @param {Object} obj the root object
* @param {string} parentPath the path taken to get to here
* @returns {array} an array of paths from all children of the root object
*/
function pathsFromTree(obj, parentPath) {
if (Array.isArray(obj)) {
return parentPath
}
if (typeof obj === 'object' && obj.constructor.name === 'VirtualType') {
return obj.path
}
if (obj.constructor.name === 'Schema') {
obj = obj.tree
}
return Object.keys(obj).reduce((paths, key) => {
if (typeof obj[key] !== 'object' || typeof obj[key].type !== 'undefined') {
paths.push(joinKey(parentPath, key))
return paths
}
return [].concat(paths, pathsFromTree(obj[key], joinKey(parentPath, key)))
}, [])
}
/**
* Returns a safe options lookup function
*
* @private
*
* @param {Object} options plugin options
* @return {Function} ensure function
*/
function ensureOption(options) {
return (option, fallback) => (option in options ? options[option] : fallback)
}
/**
* Merges interal defaults with plugin defaults
*
* @access private
*
* @param {Object} defaults the default set of options
* @returns {Object} a combined options set
*/
function prepDefaults(defaults) {
return Object.assign(
{},
{
applyRecursively: false,
autoHide: true,
autoHideJSON: true,
autoHideObject: true,
defaultHidden: { _id: true, __v: true },
virtuals: {},
},
defaults || {}
)
}
/**
* Merges options from defaults
*
* @access private
*
* @param {Object} options an optional set of options
* @param {Object} defaults the default set of options
* @returns {Object} a combined options set
*/
function prepOptions(options, defaults) {
let _options = (options = options || {})
// Set defaults from options and default
let ensure = ensureOption(options)
options = {
applyRecursively: ensure(applyRecursively, defaults.applyRecursively),
hide: ensure(hide, defaults.autoHide),
hideJSON: ensure(hideJSON, defaults.autoHideJSON),
hideObject: ensure(hideObject, defaults.autoHideObject),
defaultHidden: Object.assign({}, ensure(defaultHidden, defaults.defaultHidden)),
virtuals: ensure(virtuals, defaults.virtuals),
}
// Add to list of default hidden
if (typeof _options[hidden] === 'object') {
options[defaultHidden] = Object.assign(options[defaultHidden], _options[hidden])
}
if (options[hide] === false) {
options[hideJSON] = false
options[hideObject] = false
}
return options
}
function applyTransformers(schema, transformer) {
let toJSONOptions = schema.get('toJSON') || {}
schema.set('toJSON', {
getters: toJSONOptions['getters'] || false,
virtuals: toJSONOptions[virtuals] || false,
transform: transformer('JSON', toJSONOptions['transform'] || null),
})
let toObjectOptions = schema.get('toObject') || {}
schema.set('toObject', {
getters: toObjectOptions['getters'] || false,
virtuals: toObjectOptions[virtuals] || false,
transform: transformer('Object', toObjectOptions['transform'] || null),
})
}
function applyToChildren(schema, options) {
if (options.applyRecursively) {
schema.childSchemas.forEach(child => {
plugin(child.schema, options)
})
}
}
module.exports = function (defaults) {
let _defaults = prepDefaults(defaults)
return function (schema, options) {
options = prepOptions(options, _defaults)
return plugin(schema, options)
}
}