mongoose-diff-tracking
Version:
Manage Mongo Collection diff History and versions
485 lines (417 loc) • 13.4 kB
JavaScript
// @flow
import deepEqual from 'fast-deep-equal'
import { checkCondition } from 'power-filter/jsnext'
import {
getNestedValue,
sortByNotation,
parseDocumentPath,
createDocumentPath,
normalizeUpdateOperation,
} from 'oad-utils/jsnext'
import { retargetToProp } from './retarget-to-prop.js'
import { getObjectsToBeAssigned } from './get-objects-to-be-assigned.js'
import type {
BitOperator,
CurrentDateOperator,
DocumentPath,
IncOperator,
MaxOperator,
MinOperator,
MulOperator,
PopOperator,
RegularAddToSetOperator,
RegularPullOperator,
RegularPushOperator,
RegularUpdateOperation,
RenameOperator,
Restorable,
RestoreOperator,
SetOperator,
UnsetOperator,
UpdateOperation,
} from 'mongolike-operations'
/**
*
*/
export default class PowerAssign {
/**
*
*/
static assignUpdateOperation(obj: Object, uOp: RegularUpdateOperation): Object {
let updatedObj: Object = obj
const operatorNames = Object.keys(uOp)
for (const operatorName of operatorNames) {
switch (operatorName) {
case '$inc':
updatedObj = this.$inc(updatedObj, uOp.$inc)
break
case '$set':
updatedObj = this.$set(updatedObj, uOp.$set)
break
case '$min':
updatedObj = this.$min(updatedObj, uOp.$min)
break
case '$max':
updatedObj = this.$max(updatedObj, uOp.$max)
break
case '$mul':
updatedObj = this.$mul(updatedObj, uOp.$mul)
break
case '$addToSet':
updatedObj = this.$addToSet(updatedObj, uOp.$addToSet)
break
case '$pop':
updatedObj = this.$pop(updatedObj, uOp.$pop)
break
case '$pull':
updatedObj = this.$pull(updatedObj, uOp.$pull)
break
case '$push':
updatedObj = this.$push(updatedObj, uOp.$push)
break
case '$currentDate':
updatedObj = this.$currentDate(updatedObj, uOp.$currentDate)
break
case '$bit':
updatedObj = this.$bit(updatedObj, uOp.$bit)
break
case '$unset':
updatedObj = this.$unset(updatedObj, uOp.$unset)
break
case '$rename':
updatedObj = this.$rename(updatedObj, uOp.$rename)
break
case '$restore':
// this operation must run at the end of all other opreations
break
case '$setOnInsert':
throw new Error(`The given operator "${operatorName}" is not implemented yet.`)
default:
throw new Error(`Invalid operator: "${operatorName}"`)
}
}
if (uOp.$restore != null) {
updatedObj = this.$restore(obj, updatedObj, uOp.$restore)
}
return updatedObj
}
/**
*
*/
static $set<T: Object>(obj: T, setOp: SetOperator): T {
let updatedObj = obj
Object.keys(setOp).forEach(docPath => {
updatedObj = this.setValue(updatedObj, docPath, setOp[docPath])
})
return updatedObj
}
/**
*
*/
static $inc<T: Object>(obj: T, incOp: IncOperator): T {
const valuesToSet = {}
Object.keys(incOp).forEach(docPath => {
const currentVal = getNestedValue(obj, docPath)
const inc = incOp[docPath]
valuesToSet[docPath] = currentVal + inc
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $min<T: Object>(obj: T, minOp: MinOperator): T {
const valuesToSet = {}
Object.keys(minOp).forEach(docPath => {
const currentVal = getNestedValue(obj, docPath)
const newVal = minOp[docPath]
if (newVal < currentVal) {
valuesToSet[docPath] = newVal
}
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $max<T: Object>(obj: T, maxOp: MaxOperator): T {
const valuesToSet = {}
Object.keys(maxOp).forEach(docPath => {
const currentVal = getNestedValue(obj, docPath)
const newVal = maxOp[docPath]
if (newVal > currentVal) {
valuesToSet[docPath] = newVal
}
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $mul<T: Object>(obj: T, mulOp: MulOperator): T {
const valuesToSet = {}
Object.keys(mulOp).forEach(docPath => {
const currentNum = getNestedValue(obj, docPath)
if (currentNum == null) {
throw Error('operand must not be null')
}
valuesToSet[docPath] = currentNum * mulOp[docPath]
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $addToSet<T: Object>(obj: T, addToSetOp: RegularAddToSetOperator): T {
const valuesToSet = {}
Object.keys(addToSetOp).forEach(docPath => {
let arr: ?Array<any> = getNestedValue(obj, docPath)
if (arr == null) {
arr = [] // If the field is absent, empty array is set.
}
if (!Array.isArray(arr)) {
throw new Error(`"$addToSet" operator must be applied to an array. DocumentPath: "${docPath}".`)
}
let modifier = addToSetOp[docPath]
// $FlowIssue(arr-is-Array)
const newArr = modifier.$each.filter(element => !arr.some(arrEl => deepEqual(arrEl, element)))
valuesToSet[docPath] = arr.concat(newArr)
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $pop<T: Object>(obj: T, popOp: PopOperator): T {
const valuesToSet = {}
Object.keys(popOp).forEach(docPath => {
let arr: ?Array<any> = getNestedValue(obj, docPath).slice()
if (arr == null) {
arr = [] // If the field is absent, empty array is set.
}
if (!Array.isArray(arr)) {
throw new Error(`"$pop" operator must be applied to an array. DocumentPath: "${docPath}".`)
}
if (popOp[docPath] === 1) {
arr.pop()
} else {
arr.shift()
}
valuesToSet[docPath] = arr
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $pull<T: Object>(obj: T, pullOp: RegularPullOperator): T {
const valuesToSet = {}
Object.keys(pullOp).forEach(docPath => {
let arr: ?Array<any> = getNestedValue(obj, docPath)
if (arr == null) {
return // If the field is absent, no requests will be executed
}
if (!Array.isArray(arr)) {
throw new Error(`"$pull" operator must be applied to an array. DocumentPath: "${docPath}".`)
}
const condition = pullOp[docPath]
valuesToSet[docPath] = arr.filter(val => checkCondition(val, condition) === false)
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $push<T: Object>(obj: T, pushOp: RegularPushOperator): T {
const valuesToSet = {}
Object.keys(pushOp).forEach(docPath => {
let arr: ?Array<any> = getNestedValue(obj, docPath)
if (arr == null) {
arr = [] // If the field is absent, empty array is set.
}
if (!Array.isArray(arr)) {
throw new Error(`"$push" operator must be applied to an array. DocumentPath: "${docPath}".`)
}
let modifier = pushOp[docPath]
let position = (modifier.$position != null) ? modifier.$position : arr.length
let newArr = arr.slice()
newArr.splice(position, 0, ...modifier.$each)
if (modifier.$sort != null) {
newArr = sortByNotation(newArr, modifier.$sort)
}
if (modifier.$slice != null) {
newArr = (modifier.$slice >= 0)
? newArr.slice(0, modifier.$slice)
: newArr.slice(modifier.$slice)
}
valuesToSet[docPath] = newArr
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $currentDate<T: Object>(obj: T, curDateOp: CurrentDateOperator): T {
const valuesToSet = {}
Object.keys(curDateOp).forEach(docPath => {
let typeSpecification = curDateOp[docPath]
if (typeSpecification === true) {
typeSpecification = { $type: 'date'}
}
const now = new Date()
valuesToSet[docPath] = typeSpecification.$type === 'date' ? now : now.getTime()
})
return this.$set(obj, valuesToSet)
}
/**
*
*/
static $bit<T: Object>(obj: T, bitOp: BitOperator): T {
const valuesToSet = {}
Object.keys(bitOp).forEach(docPath => {
const currentNum = getNestedValue(obj, docPath) || 0 // If the field is absent, 0 is set.
const logicalOperator = Object.keys(bitOp[docPath])[0]
// $FlowIssue(return-number)
const operand: number = bitOp[docPath][logicalOperator]
switch (logicalOperator) {
case 'and':
valuesToSet[docPath] = currentNum & operand
break
case 'or':
valuesToSet[docPath] = currentNum | operand
break
case 'xor':
valuesToSet[docPath] = currentNum ^ operand
break
}
})
return this.$set(obj, valuesToSet)
}
/**
* Unset the value of the given DocumentPaths.
* NOTICE: The objects whose property are deleted will be converted into a plain object.
*/
static $unset(obj: Object, unsetOp: UnsetOperator): Object {
return Object.keys(unsetOp).reduce((newObj, docPath) => {
const attrs = parseDocumentPath(docPath)
const lastAttr = attrs.pop()
const pathToLast = createDocumentPath(...attrs)
const lastObj = pathToLast
? getNestedValue(newObj, pathToLast)
: newObj
let copiedLastObj: Array<any> | Object
if (Array.isArray(lastObj)) {
copiedLastObj = lastObj.slice()
copiedLastObj[lastAttr] = null
}
else {
copiedLastObj = Object.assign({}, lastObj)
delete copiedLastObj[lastAttr]
}
return pathToLast
? this.$set(newObj, { [pathToLast]: copiedLastObj })
: copiedLastObj
}, obj)
}
/**
* Unset the value of the given DocumentPaths.
* NOTICE: The objects whose property are renamed will be converted into a plain object.
*/
static $rename(obj: Object, renameOp: RenameOperator): Object {
return Object.keys(renameOp).reduce((newObj, docPath) => {
const attrs = parseDocumentPath(docPath)
const lastAttr = attrs.pop()
const pathToLast = createDocumentPath(...attrs)
const lastObj = pathToLast
? getNestedValue(newObj, pathToLast)
: newObj
if (Array.isArray(lastObj)) {
throw Error(`$rename operation cannot be applied to array field: "${pathToLast}".`)
}
if (!lastObj.hasOwnProperty(lastAttr)) {
return newObj
}
const copiedLastObj = Object.assign({}, lastObj)
delete copiedLastObj[lastAttr]
const newAttr = renameOp[docPath]
copiedLastObj[newAttr] = lastObj[lastAttr]
return pathToLast
? this.$set(newObj, { [pathToLast]: copiedLastObj })
: copiedLastObj
}, obj)
}
/**
*
*/
static $restore(originalObj: Object, targetObj: Object, restoreOp: RestoreOperator): Object {
const valuesToSet = {}
Object.keys(restoreOp).forEach(docPath => {
const currentValue = getNestedValue(targetObj, docPath)
if (isPrimitive(currentValue)) {
valuesToSet[docPath] = currentValue
return
}
let Constructor = restoreOp[docPath]
if (!Constructor) {
const originalValue = getNestedValue(originalObj, docPath)
Constructor = originalValue.constructor
}
valuesToSet[docPath] = new Constructor(currentValue)
})
return this.$set(targetObj, valuesToSet)
}
/**
*
*/
static setValue<T: Object>(obj: T, docPath: DocumentPath, value: any): T {
const revObjsToBeAssigned = getObjectsToBeAssigned(obj, docPath).reverse()
const revKeys = parseDocumentPath(docPath).reverse()
// assert(objsToBeAssigned.length === keys.length)
// $FlowIssue(return-T)
return revKeys.reduce((newValue, key, i) => {
const objToBeAssigned = revObjsToBeAssigned[i]
if (typeof key === 'number' && Array.isArray(objToBeAssigned)) {
const shallowCopied = objToBeAssigned.slice()
shallowCopied[key] = newValue
return shallowCopied
}
return Object.assign({}, objToBeAssigned, { [key]: newValue })
}, value)
}
}
/**
* @public
* Assign new values to object following the given operation(s).
*/
export function assign(obj: Object, ...uOps: Array<SetOperator | UpdateOperation>): Object {
return uOps.reduce((ret, uOp) => {
return PowerAssign.assignUpdateOperation(ret, normalizeUpdateOperation(uOp))
}, obj)
}
/**
* Assign new values to the property of the obj located at the given documentPath following the given operation.
*/
export function assignToProp(obj: Object, docPath: DocumentPath, uOp: SetOperator | UpdateOperation): Object {
const modifiedOps = retargetToProp(docPath, uOp)
return assign(obj, modifiedOps)
}
/**
*
*/
export function assignWithRestoration<T: Restorable>(obj: T, uOp: SetOperator | UpdateOperation): T {
const updatedObj = assign(obj, uOp)
const Constructor = obj.constructor
return new Constructor(updatedObj) // if Constructor is Object, it's OK!
}
/**
*
*/
export function assignToPropWithRestoration<T: Restorable>(obj: T, docPath: DocumentPath, uOp: SetOperator | UpdateOperation): T {
const updatedObj = assignToProp(obj, docPath, uOp)
const Constructor = obj.constructor
return new Constructor(updatedObj) // if Constructor is Object, it's OK!
}
function isPrimitive(value: any): boolean {
const t = typeof value
return value == null || (t != 'object' && t != 'function')
}