lib/update.js
import isPlainObject from 'lodash/isPlainObject'
import _omitBy from 'lodash/omitBy'
import wrap from './wrap'
import constant from './constant'
const innerOmitted = { __omitted: true }
export const omitted = constant(innerOmitted)
function isEmpty(object) {
return !Object.keys(object).length
}
function reduce(object, callback, initialValue) {
return Object.keys(object).reduce(
(acc, key) => callback(acc, object[key], key),
initialValue
)
}
function resolveUpdates(updates, object) {
return reduce(
updates,
(acc, value, key) => {
let updatedValue = value
if (
!Array.isArray(value) &&
value !== null &&
typeof value === 'object'
) {
updatedValue = update(value, object[key]) // eslint-disable-line no-use-before-define
} else if (typeof value === 'function') {
updatedValue = value(object[key])
}
if (isUpdateChange(updatedValue, key, object)) {
acc[key] = updatedValue // eslint-disable-line no-param-reassign
}
return acc
},
{}
)
}
function isUpdateChange(value, key, object) {
// Omitting a property that does not exist is not a change
if (value === innerOmitted) {
return object.hasOwnProperty(key)
} else {
return object[key] !== value
}
}
function updateArray(updates, object) {
const newArray = [...object]
Object.keys(updates).forEach((key) => {
newArray[key] = updates[key]
})
return newArray
}
/**
* Recursively update an object or array.
*
* Can update with values:
* update({ foo: 3 }, { foo: 1, bar: 2 });
* // => { foo: 3, bar: 2 }
*
* Or with a function:
* update({ foo: x => (x + 1) }, { foo: 2 });
* // => { foo: 3 }
*
* @function
* @name update
* @param {Object|Function} updates
* @param {Object|Array} object to update
* @return {Object|Array} new object with modifications
*/
function update(updates, object, ...args) {
if (typeof updates === 'function') {
return updates(object, ...args)
}
if (!isPlainObject(updates)) {
return updates
}
const defaultedObject =
typeof object === 'undefined' || object === null ? {} : object
const resolvedUpdates = resolveUpdates(updates, defaultedObject)
if (isEmpty(resolvedUpdates)) {
return defaultedObject
}
if (Array.isArray(defaultedObject)) {
return updateArray(resolvedUpdates, defaultedObject).filter(
(value) => value !== innerOmitted
)
}
return _omitBy(
{ ...defaultedObject, ...resolvedUpdates },
(value) => value === innerOmitted
)
}
export default wrap(update, 2)