WelingtonMonteiro/mongoose-history-trace

View on GitHub
lib/diffHelper.js

Summary

Maintainability
B
5 hrs
Test Coverage
'use strict'
const { diff } = require('deep-diff')
const { modelToJSONSchema } = require('./schema-to-json')
const {
  set,
  get,
  map,
  omit,
  startCase,
  camelCase,
  compact,
  concat,
  isEmpty,
  isNumber,
  includes,
  isNil,
  result: execute
} = require('lodash')
const OPERATION = { N: 'Created', D: 'Deleted', E: 'Edited', A: 'Array' }
let changes = []

class DiffHelper {
  /**
   * @author welington Monteiro
   * @param { Object } [diffParams.old] - old json
   * @param { Object } [diffParams.current] - current json
   * @param { Object } [diffParams] - params {old, current}
   * @param {{}} schema - paths schema mapped
   * @param { Object } [options] - options params
   * @param { Array<String> } [options.omitPaths] - options omitPaths params
   * @param { String } [options.changeTransform.label] - options changesToView.label {label: 'otherPathName'}
   * @param { String } [options.changeTransform.to] - options changesToView.to {to: 'otherPathName'}
   * @param { String } [options.changeTransform.from] - options changesToView.from {from: 'otherPathName'}
   * @param { String } [options.changeTransform.ops] - options changesToView.ops {ops: 'otherPathName'}
   * @param { String } [options.changeTransform.path] - options changesToView.path {path: 'otherPathName'}
   * @return { Array } - list of diffs changes
   */
  static getDiff (diffParams = {}, schema = {}, options = {}) {
    let { old = {}, current = {} } = diffParams
    const omitPaths = get(options, 'omitPaths', [])
    const defaultOmitPaths = ['_id']

    old = omit(old, concat(omitPaths, defaultOmitPaths))
    current = omit(current, concat(omitPaths, defaultOmitPaths))
    changes = []

    const differences = diff(old, current)

    DiffHelper._mapItems(differences, schema, options)
    return compact(changes)
  }

  /**
   * @author Welington Monteiro
   * @param { Array } diffs - deep diffs between old and current
   * @param { Schema } schema - paths schema mapped
   * @param { Object } [options] - options params
   * @return {void}
   * @private
   */
  static _mapItems (diffs, schema, options) {
    map(diffs, _addItem)

    function _addItem (item) {
      const to = get(item, 'rhs', get(item, 'item.rhs', ''))
      const path = item.path.join('.')
      const from = get(item, 'lhs', get(item, 'item.lhs', ''))
      let ops = OPERATION[get(item, 'kind', '')]
      const { index, isArray } = DiffHelper._getIndexIsArray({ item, ops })

      if (ops === 'Array') ops = OPERATION[get(item, 'item.kind', '')] || ops

      if (DiffHelper._isObject(to)) {
        return DiffHelper._objectMapped({ path, item: to, ops, schema, index, isArray }, 'to', options)
      }
      if (DiffHelper._isObject(from)) {
        return DiffHelper._objectMapped({ path, item: from, ops, schema, index, isArray }, 'from', options)
      }
      const label = DiffHelper._getLabel(path, schema)
      const jsonSchema = modelToJSONSchema(schema)
      const isOmitted = DiffHelper._omittedFieldsIfNotMapped(path, jsonSchema, options)

      if (isOmitted) return

      changes.push(DiffHelper._changeTransformToView({ to, path, from, ops, label, index, isArray }, options))
    }
  }

  static unFlattenJson (data) {
    if (Object(data) !== data || Array.isArray(data)) {
      return data
    }
    const regex = /\.?([^.[\]]+)|\[(\d+)\]/g
    const resultholder = {}

    for (const p in data) {
      let cur = resultholder
      let prop = ''
      let m
      // eslint-disable-next-line
      while (m = regex.exec(p)) {
        cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}))
        prop = m[2] || m[1]
      }
      cur[prop] = data[p]
    }
    return resultholder[''] || resultholder
  }

  static flatJson (obj, opts, parentProp, $set) {
    $set = $set || {}
    opts = opts || {}
    const json = execute(obj, 'toJSON', obj)

    for (const prop in json) {
      const value = json[prop]
      const newProp = (parentProp ? parentProp + '.' : '') + prop
      const isJsonObject = typeof value === 'object' &&
        value !== null &&
        value !== undefined &&
        !(value instanceof Date) &&
        !(get(value, 'constructor.name') === 'ObjectID') &&
        prop !== '_id'

      if (Array.isArray(value)) {
        $set[newProp] = value
      } else if (isJsonObject) {
        DiffHelper.flatJson(value, opts, newProp, $set)
      } else {
        if (get(opts, 'omitNullAndEmpty')) {
          if (!isNil(value) && value !== '') $set[newProp] = value
        } else {
          $set[newProp] = value
        }
      }
    }
    return $set
  }

  /**
   * @author Welington Monteiro
   * @description Verify is value is object
   * @param { * } value - Value for comparison
   * @return { boolean } - return is object
   */
  static _isObject (value) {
    return Object.prototype.toString.call(value) === '[object Object]'
  }

  /**
   * @author Welington Monteiro
   * @param { Object } change - object params
   * @param { Object } [options] -  options change transform to view
   * @return {Object} newChange - object new transformed
   */
  static _changeTransformToView (change = {}, options = {}) {
    const newChange = { index: null, isArray: false }
    if (isEmpty(change)) return null

    set(newChange, 'isArray', get(change, 'isArray'))
    set(newChange, 'index', get(change, 'index'))
    set(newChange, `${get(options, 'changeTransform.to') || 'to'}`, get(change, 'to'))
    set(newChange, `${get(options, 'changeTransform.from') || 'from'}`, get(change, 'from'))
    set(newChange, `${get(options, 'changeTransform.path') || 'path'}`, get(change, 'path'))
    set(newChange, `${get(options, 'changeTransform.ops') || 'ops'}`, get(change, 'ops'))
    set(newChange, `${get(options, 'changeTransform.label') || 'label'}`, get(change, 'label'))

    return newChange
  }

  /**
   * @author Welington Monteiro
   * @param { Object } item - item to mapped fields
   * @param { String } ops - type of path
   * @return {{index: null, isArray: boolean}} - return object
   * @private
   */
  static _getIndexIsArray ({ item = {}, ops = '' }) {
    let index = get(item, 'index', null)
    let isArray = ops === 'Array'

    map(get(item, 'path'), (field) => {
      if (isNumber(field)) {
        index = field
        isArray = true
      }
    })
    return { index, isArray }
  }

  /**
   * @author Welington Monteiro
   * @param { Object } [params] - item to mapped fields
   * @param { Object } [params.item] - item to mapped fields
   * @param { String } [params.path] - path fields
   * @param { String } [params.ops] - ops mapped
   * @param { Object } [params.schema] - paths schema mapped
   * @param [params] - params mapped
   * @param { String } fieldItem - name field item mapped to object
   * @param { Object } [options] - options change transform to view
   * @private
   */
  static _objectMapped (params = {}, fieldItem, options) {
    const { item = '', path, ops, schema = {}, index = null, isArray = false } = params
    map(item, (value, field) => {
      const newPath = `${path}.${field}`

      if (typeof value === 'object') {
        return DiffHelper._objectMapped({ path: newPath, item: value, ops, schema, index, isArray }, fieldItem, options)
      }
      const change = { from: '', to: '' }
      const label = DiffHelper._getLabel(newPath, schema)

      change[`${fieldItem}`] = value
      changes.push(DiffHelper._changeTransformToView({ ...change, path: newPath, ops, label, index, isArray }, options))
    })
  }

  /**
   * @author Welington Monteiro
   * @param { String } str - string name to capitalize
   * @return {string} name field capitalized
   * @private
   */
  static _capitalizedKey (str = '') {
    return startCase(camelCase(str))
  }

  /**
   * @author Welington Monteiro
   * @param { String } path - path object
   * @param { Object } schema - paths schema mapped
   * @return { string } return result
   * @private
   */
  static _getLabel (path = '', schema = {}) {
    const paths = compact(path.replace(/[0-9]/ig, '').split('.'))
    const lastPath = paths[paths.length - 1]
    const label = DiffHelper._getCustomLabel(paths, schema)
    const nameCapitalized = DiffHelper._capitalizedKey(lastPath)

    return (label || nameCapitalized)
  }

  /**
   * @author Welington Monteiro
   * @param { Array } pathsArray - flatten paths schema
   * @param { Object } schema - Schemas mapped paths
   * @return {string} customLabel - return name custom label
   */
  static _getCustomLabel (pathsArray = [], schema = {}) {
    const flattenPath = pathsArray.join('.')
    const paths = !isEmpty(schema.paths) ? schema.paths[flattenPath] : ''
    const singleNestedPaths = !isEmpty(schema.singleNestedPaths) ? schema.singleNestedPaths[flattenPath] : ''
    const subpaths = !isEmpty(schema.subpaths) ? schema.subpaths[flattenPath] : ''

    return get(paths, 'options._label_', get(singleNestedPaths, 'options._label_', get(subpaths, 'options._label_')))
  }

  /**
   * @author Welington Monteiro
   * @description Omitted field if not mapped on dicionary
   * @param { String } path - Item mapped
   * @param { Object } flatDictionary - Dictionary flatten
   * @return { Boolean } isOmitted - Returns item
   * @param { Object } [options] - options change transform to view
   * @private
   */
  static _omittedFieldsIfNotMapped (path, flatDictionary, options) {
    if (isEmpty(flatDictionary)) return false
    const isStrict = get(options, 'isStrict', true)
    let isOmitted = false
    const isMapped = !!DiffHelper.getPathNameMapped(path, flatDictionary)
    if (!isMapped && isStrict) isOmitted = true

    return isOmitted
  }

  /**
   * @author Welington Monteiro
   * @description Replace index number for place holder
   * @param { String } path - Path item
   * @param flatDictionary
   * @return {*}
   * @private
   */
  static getPathNameMapped (path, flatDictionary = {}) {
    const pathReplaced = DiffHelper.replaceIndexWithPlaceholder(path)
    const pathLabelName = DiffHelper.getLabelOrKeyName(pathReplaced, '_label_')
    const pathKeyName = DiffHelper.getLabelOrKeyName(pathReplaced, '_key_')

    const pathWithLabelName = get(flatDictionary, `${pathLabelName}`)
    const pathWithKeyName = get(flatDictionary, `${pathKeyName}`)
    const pathWithArrayName = get(flatDictionary, `${pathReplaced}`)

    return (pathWithLabelName || pathWithKeyName || pathWithArrayName)
  }

  /**
   * @author Welington Monteiro
   * @param { String } path - Path item
   * @return {*}
   * @private
   */
  static replaceIndexWithPlaceholder (path) {
    path = path.replace(/(\.)([0-9]+)(\.)/g, '._array_.')
    path = path.replace(/(\.)([0-9]+)/g, '._array_')
    return path
  }

  /**
   * @author Welington Monteiro
   * @param { String } pathReplaced - Path with place holder _array_ item
   * @param { String } key - Key name: _key_ , _label_
   * @return {string}
   */
  static getLabelOrKeyName (pathReplaced, key = '_label_') {
    let paths = pathReplaced.split('.')
    const lastPath = paths.pop()

    if (includes(['_array_'], lastPath)) {
      paths.push(lastPath)
    }
    if (includes(['_label_', '_key_'], lastPath)) {
      paths.push(lastPath)
    }
    paths.push(key)
    paths = paths.join('.')

    return paths
  }
}

module.exports = DiffHelper