WelingtonMonteiro/mongoose-history-trace

View on GitHub
lib/plugin.js

Summary

Maintainability
A
25 mins
Test Coverage
'use strict'
const Promise = require('bluebird')
const { HistoryLogModel } = require('./model')
const DeepDiff = require('./diffHelper')
const { isEmpty, get, assign, pick, set, result: execute } = require('lodash')
let mongooseConnection = null

/**
 *
 * @param { mongoose } schema - schema mongoose
 * @param { Object } options
 * @example {
 *   userPaths: []                              //name paths user to saved on logs
 *   isAuthenticate: true                       //default is false
 *   customCollectionName: String               //default is collection name
 *   mongooseConnection: String                 //default is connection of collection
 *   moduleName: String                         //default is collection name
 *   indexes: [
 *        {'path': 1},                          //paths create index on history log collection
 *        {'path': -1},
 *        {'path': 'text'}
 *   ]
 *   omitPaths: ['_id', '__v']                  //paths omit on created history log
 *   addCollectionPaths: [                      //add paths in collection history log
 *      {key: 'path1', value: 'String' },       //    key - name path
 *      {key: 'path2', value: 'String' },       //    value - mongoose types .('ObjectId', 'Mixed', 'String', 'Number', etc)
 *      {key: 'path3.path2',                    //    sudoc : {key: 'field.subdoc', value: 'String'}
 *          value: 'ObjectId' },
 *   ],
 *   changeTransform: {
 *     to: 'newPathToView'                      //new value to path 'to' transform to view
 *     from: 'newPathToView'                    //new value to path 'from' transform to view
 *     label: 'newPathToView'                   //new value to path 'label' transform to view
 *     ops: 'newPathToView'                     //new value to path 'ops' transform to view
 *     path: 'newPathToView'                    //new value to path 'path' transform to view
 *   }
 *
 * }
 */
function HistoryPlugin (schema, options = {}) {
  /**
   * @author Welington Monteiro
   * @param { Object } loggedUser - Logged user from request
   * Model.addLoggedUser(user)
   */
  schema.statics.addLoggedUser = function (loggedUser) {
    if (!loggedUser) {
      console.error('MONGOOSE-HISTORY-TRACE::addLoggedUser - loggedUser params is required. \nExample: Model.addLoggedUser(currentUser)')
      return
    }
    set(schema, '_loggedUser', loggedUser)
  }
  /**
   * @author welington Monteiro
   * @param { Object } [old] - old state json object
   * @param { Object } [current] - current state json object
   * @return {Array} changes -  return array changes diffs
   */
  schema.statics.getChangeDiffs = async function (old, current) {
    return _processGetDiffs.call(this, { old, current }, options)
  }

  /**
   * @author Welington Monteiro
   * @param { Object } params
   */
  schema.statics.createHistory = async function (params) {
    set(this, 'method', get(params, 'method'))
    await _processCreateHistory.call(getCurrentUser.call(this, schema, params), params, options)
  }

  schema.pre('save', async function () {
    await initializeProcess.call(getCurrentUser.call(this, schema), _processPreSave, options)
  })

  schema.pre(/update|updateOne|findOneAndUpdate|findByIdAndUpdate|updateMany|findOneAndReplace|replaceOne/, async function () {
    await initializeProcess.call(getCurrentUser.call(this, schema), _processPreUpdate, options)
  })

  schema.pre(/deleteOne|remove/, async function () {
    await initializeProcess.call(getCurrentUser.call(this, schema), _processPreRemove, options)
  })

  schema.pre(/deleteMany/, async function () {
    await initializeProcess.call(getCurrentUser.call(this, schema), _processRemoveMany, options)
  })

  schema.pre(/remove/, { document: false, query: true }, async function () {
    await initializeProcess.call(getCurrentUser.call(this, schema), _processRemoveMany, options)
  })

  schema.post(/findOneAndRemove|findByIdAndRemove|findByIdAndDelete|findOneAndDelete/, async function (doc) {
    await initializeProcess.call(getCurrentUser.call(this, schema), _processPosRemove, options, doc)
  })

  schema.post('insertMany', async function (docs) {
    await initializeProcess.call(getCurrentUser.call(this, schema), _processPosInsertMany, options, docs)
  })
}

/**
 * @author Welington Monteiro
 * @param { Schema } schema - mongoose Schema
 * @param params
 * @return {getCurrentUser} - return this context with logged user
 */
function getCurrentUser (schema, params) {
  const _loggedUser = get(schema, '_loggedUser', {})
  const loggedUser = get(params, 'loggedUser', {})
  this._loggedUser = _toJSON(!isEmpty(_loggedUser) ? _loggedUser : loggedUser)
  delete schema._loggedUser
  return this
}

/**
 * @author Welington Monteiro
 * @param { function } method - method call to process
 * @param { Object } options - params configuration before saved log
 * @param {Array | Object } docs - data document
 * @return {Promise<void>}
 */
async function initializeProcess (method, options, docs) {
  await _setConnection(options)
  await method.call(this, options, docs)
}

/**
 * @description set mongoose connection
 * @author Welington Monteiro
 * @param { Object } options - params configuration before saved log
 * @private
 */
async function _setConnection (options) {
  const conn = get(options, 'mongooseConnection')
  if ((!mongooseConnection || isEmpty(mongooseConnection)) && (conn || !isEmpty(conn))) {
    mongooseConnection = get(conn, 'connection', conn)
  }
  if (!mongooseConnection || isEmpty(mongooseConnection)) {
    console.error('MONGOOSE-HISTORY-TRACE: mongooseConnection options is required. \nExample: Model.plugin(mongooseHistoryTrace, {mongooseConnection: conn}')
  }
}

/**
 * @author Welington Monteiro
 * @param { Object } json - object transform
 * @param { Boolean } [isArray] - is Array return
 * @return {Object} return object to json
 * @private
 */
function _toJSON (json, isArray) {
  if (isEmpty(json)) { return isArray === true ? [] : {} }
  return JSON.parse(JSON.stringify(json))
}

/**
 * @author Welington Monteiro
 * @param {Object} doc - doc
 * @return {{method: *, documentNumber: *, module: *, changes: *, loggedUser: *, action: *}}
 */
function _mappedFieldsLog (doc = {}) {
  const actions = {
    updated: 'Edited Document',
    deleted: 'Removed Document',
    created: 'Created Document',
    undefined: 'No Defined',
    creatingMany: 'Created Document',
    deleteMany: 'Removed Document'
  }
  const { changes, module, documentNumber, method, user } = doc
  const action = get(actions, `${method}`, method)
  return { changes, action, module, documentNumber, method, user }
}

/**
 *@author Welington Monteiro
 * @param { Object } object - object mapped to saved
 * @param { Object } options - params configuration before saved log
 */
async function _saveHistoryLog (object, options) {
  const hasUsers = !isEmpty(get(object, 'user'))
  const isAuthenticated = get(options, 'isAuthenticated')
  const hasChanges = !isEmpty(get(object, 'changes'))

  if (!hasChanges) {
    console.warn('MONGOOSE-HISTORY-TRACE: path: {changes} no diffs documents.')
    return
  }
  if (!hasUsers && isAuthenticated) {
    console.warn('MONGOOSE-HISTORY-TRACE: path: {user} is required. Example: Model.addLoggedUser(req.user)')
    return
  }
  const HistoryLog = await HistoryLogModel(mongooseConnection, options)
  const history = new HistoryLog(object)
  return await history.save()
}
//
// async function _saveHistoryLogx (object, options, collectionName) {
//   const NewOptions = {
//     ...options,
//     customCollectionName: collectionName
//       ? `${options.customCollectionName}_${collectionName}`
//       : `${options.customCollectionName}_${object.module}`,
//   }
//
//   // This is changes collection is multiple for saving
//   if (object && Array.isArray(object)) {
//     /**
//      * We can loop through object array to check for document number ,
//      * either remove that object from array or complete stop the ongoing process
//      */
//       // if(get(object,"documentNumber") === undefined) return console.warn(`Not found documentNumber `)
//     const HistoryLog = HistoryLogModel(NewOptions)
//     return await HistoryLog.insertMany(object)
//   }
//   if (get(object, 'documentNumber') === undefined) {
//     console.warn(`Not found documentNumber `)
//     return
//   }
//
//   if (isEmpty(get(object, 'changes'))) {
//     console.warn('MONGOOSE-HISTORY-TRACE: path: {changes} no diffs documents.')
//     return
//   }
//   if (isEmpty(get(object, 'user')) && get(NewOptions, 'isAuthenticated', true)) {
//     console.warn('MONGOOSE-HISTORY-TRACE: path: {user} is required. Example: Model.addLoggedUser(req.user)')
//     return
//   }
//   const HistoryLog = HistoryLogModel(NewOptions)
//   const history = new HistoryLog(object)
//   return await history.save()
//
// }

/**
 * @author Welington Monteiro
 * @param { object } params
 * @param { Object } options - params configuration options plugin
 * @return {Array}
 * @private
 */
async function _processGetDiffs (params, options) {
  const old = get(params, 'old', {})
  const current = get(params, 'current', {})

  return DeepDiff.getDiff({ old, current }, {}, options)
}

/**
 * @author Welington Monteiro
 * @param { Object } options - params configuration options plugin
 * @param { Object } [params] - params paths to save history
 * @param { Object } [doc] - object to save
 * @param { String } method - name of method to save
 * @return { Object } - return object mounted to save
 */
async function mountDocToSave ({ options, params, doc }, method) {
  const self = this
  let result = {}
  const userPaths = get(options, 'userPaths', [])

  set(result, 'old', _toJSON(get(params, 'old', {})))
  set(result, 'current', _toJSON(get(params, 'current', {})))
  set(result, 'schema', get(self, 'schema'))
  set(result, 'user', get(self, '_loggedUser'))
  set(result, 'module', get(options, 'moduleName', get(self, 'mongooseCollection.name', get(self, 'collection.name'))))
  set(result, 'method', (method || get(params, 'method', 'undefined')))

  if (!isEmpty(userPaths)) set(result, 'user', pick(get(result, 'user'), userPaths))

  if ({}.hasOwnProperty.call(self, 'isNew')) {
    result = await _helperPreSave.call(this, result)
  }
  if (!isEmpty(doc)) {
    result = _helperPosSaveWithDoc.call(this, result, doc)
  }
  if (method === 'updated') {
    result = await _helperPreUpdate.call(this, result)
  }
  set(result, 'documentNumber', get(result, 'old._id', get(result, 'current._id')))
  return result
}

/**
 * @author Welington Monteiro
 * @param { Object } result - result Object mount paths to save
 * @return { Object } - return object mount
 * @private
 */
async function _helperPreSave (result) {
  const self = this
  set(result, 'method', (get(self, 'isNew') ? 'created' : 'updated'))
  set(result, 'current', _toJSON(execute(self, 'toObject', self)))
  const _id = get(result, 'current._id')

  if (!self.isNew) {
    set(result, 'old', _toJSON(await self.constructor.findOne({ _id })))
  }
  return result
}

/**
 * @author Welington Monteiro
 * @param { Object } result - result Object mount paths to save
 * @return { Object } - return object mount
 * @private
 */
async function _helperPreUpdate (result) {
  const self = this

  const query = get(self, '_conditions')
  if (isEmpty(query)) { return console.warn(`Not found documents to ${result.method} history logs.`) }
  const old = _toJSON(await self.findOne(query))
  if (isEmpty(old)) { return console.warn(`Not found documents to ${result.method} history logs.`) }
  const mod = _toJSON(get(self, '_update.$set', get(self, '_update')))

  set(result, 'old', old)
  set(result, 'current', assign({}, old, DeepDiff.unFlattenJson(mod)))

  return result
}

/**
 *
 * @param { Object } result - result Object mount paths to save
 * @param { Object } doc - object to save
 * @return { Object } - return object mount
 * @private
 */
function _helperPosSaveWithDoc (result, doc) {
  if (result.method === 'created') {
    set(result, 'current', _toJSON(execute(doc, 'toObject', doc)))
  }
  if (result.method === 'deleted') {
    set(result, 'old', _toJSON((execute(doc, 'toObject', doc))))
  }
  return result
}

/**
 * @author Welington Monteiro
 * @param { Object } params - params to save history manually
 * @param { Object } options - params configuration before saved log
 * @return {Promise<void>}
 * @private
 */
async function _processCreateHistory (params, options) {
  await _setConnection(options)
  const {
    schema, user, module, method,
    old, current, documentNumber
  } = await mountDocToSave.call(this, { options, params })

  const changes = DeepDiff.getDiff({ old, current }, schema, options)
  const docMapped = _mappedFieldsLog({ changes, method, module, documentNumber, user })

  await _saveHistoryLog(docMapped, options)
}

/**
 * @author Welington Monteiro
 * @param { Object } options - params configuration before saved log
 * @private
 */
async function _processRemoveMany (options) {
  const query = get(this, '_conditions')
  const olds = _toJSON(await this.find(query), true)

  Promise.map(olds, async (eachOld) => await _processPosRemove.call(this, options, eachOld))
}

/**
 * @author Welington Monteiro
 * @param { Array } docs - List of document inserted
 * @param { Object } options - params configuration before saved log
 * @private
 */
async function _processPosInsertMany (options, docs) {
  return Promise.map(docs, async (eachDoc) => await _processPosSave.call(this, options, eachDoc))
}

/**
 * @author Welington Monteiro
 * @param { Object } options - params configuration before saved log
 * @private
 */
async function _processPreRemove (options) {
  const query = get(this, '_conditions')
  const old = _toJSON(await this.findOne(query))

  await _processPosRemove.call(this, options, old)
}

/**
 * @author Welington Monteiro
 * @param { Object } options - params configuration before saved log
 * @return {Promise<*>}
 */
async function _processPreUpdate (options) {
  const {
    schema, user, module, method,
    old, current, documentNumber
  } = await mountDocToSave.call(this, { options }, 'updated')

  const changes = DeepDiff.getDiff({ old, current }, schema, options)
  const docMapped = _mappedFieldsLog({ changes, method, module, documentNumber, user })

  await _saveHistoryLog(docMapped, options)
}

/**
 * @author Welington Monteiro
 * @param { Object } options - params configuration before saved log
 * @private
 */
async function _processPreSave (options) {
  const {
    schema, user, module, method,
    old, current, documentNumber
  } = await mountDocToSave.call(this, { options })

  const changes = DeepDiff.getDiff({ old, current }, schema, options)
  const docMapped = _mappedFieldsLog({ changes, method, module, documentNumber, user })

  await _saveHistoryLog(docMapped, options)
}

/**
 * @author Welington Monteiro
 * @param doc
 * @param { Object } options - params configuration before saved log
 * @private
 */
async function _processPosSave (options, doc) {
  const {
    schema, user, module, method,
    current, documentNumber
  } = await mountDocToSave.call(this, { options, doc }, 'created')

  const changes = DeepDiff.getDiff({ current }, schema, options)
  const docMapped = _mappedFieldsLog({ changes, method, module, documentNumber, user })

  await _saveHistoryLog(docMapped, options)
}

/**
 * @author Welington Monteiro
 * @param { Object } doc - object mapped
 * @param { Object } options - params configuration before saved log
 * @return {Promise<void>}
 */
async function _processPosRemove (options, doc) {
  const {
    schema, user, module,
    method, old, documentNumber
  } = await mountDocToSave.call(this, { options, doc }, 'deleted')

  const changes = DeepDiff.getDiff({ old }, schema, options)
  const docMapped = _mappedFieldsLog({ changes, method, module, documentNumber, user })

  await _saveHistoryLog(docMapped, options)
}

module.exports = HistoryPlugin