Created
November 23, 2021 14:04
-
-
Save saswatds/714d4c0553931d79eee7c56f24d6740c to your computer and use it in GitHub Desktop.
JSON Patch to MongoDB Patch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* NOTE: This algorithm only works for Mongodb version 4.2 where aggregate queries were introduced. | |
* | |
*/ | |
const ZERO = 0, | |
ONE = 1, | |
unescape = (str) => str.replace(/~1/g, '/').replace(/~0/g, '~'), | |
parse = (pointer) => { | |
if (pointer === '') { return []; } | |
if (pointer.charAt(ZERO) !== '/') { throw new Error(`Invalid JSON pointer: ${pointer}`); } | |
return pointer.substring(ONE).split(/\//).map(unescape); | |
}, | |
{ isEmpty } = require('./nodash'); | |
module.exports = { | |
toMongo (patches) { | |
const update = {}, | |
arrayUpdates = new Map(), | |
aggregates = []; | |
// For each of the patched | |
patches.forEach(({ op, path, value }) => { | |
// Get the object accessor array | |
const parts = parse(path), | |
// Detect if the path is an array or not, | |
// It will be an array if the last element is a number or - | |
isArray = (/^(([1-9]\d*)|0|-)$/).test(parts.slice(-ONE).pop()); | |
if (isArray) { | |
const pa = parts.slice(ZERO, -ONE).join('.'), | |
i = parts.slice(-ONE).pop(); | |
if (i === '-') { | |
throw new Error('Unbounded array operations are not supported'); | |
} | |
arrayUpdates.has(pa) ? | |
arrayUpdates.get(pa).push({ i: parseInt(i, 10), op, value }) : | |
arrayUpdates.set(pa, [ | |
{ i: parseInt(i, 10), op, value } | |
]); | |
return; | |
} | |
switch (op) { | |
case 'add': | |
case 'replace': | |
update.$set = { ...update.$set, [parts.join('.')]: value }; | |
break; | |
case 'remove': { | |
const key = parts.join('.'); | |
update.$unset = { | |
...update.$unset, | |
[key]: '' | |
}; | |
} | |
break; | |
default: | |
} | |
}); | |
arrayUpdates.forEach((ops, path) => { | |
// We will have a bunch of replace operations with some remove or adds at the end | |
const replaceOps = ops.filter(({ op }) => op === 'replace'), | |
removeOps = ops.filter(({ op }) => op === 'remove'), | |
addOps = ops.filter(({ op }) => op === 'add'); | |
// Now we have these different situations | |
// 1. Only adds | |
// 2. Only removes | |
// 3. Only replace | |
// 4. Replace with add (array size increased) | |
// 5. Replace with remove (array size decreased) | |
if (!removeOps.length) { | |
update.$set = [ | |
...addOps, | |
...replaceOps | |
].reduce((acc, { i, value }) => { | |
acc[`${path}.${i}`] = value; | |
return acc; | |
}, update.$set || {}); | |
} | |
else { | |
// Things become very complicated here. But we can we certain that there will be | |
// no add operations here. We will have a bunch of replaces and then some removes | |
// Create splices of each replace and remove operation | |
const updates = [ | |
...replaceOps, | |
...removeOps | |
].sort((a, b) => a.i - b.i).reduce((acc, { i, value = null }) => { | |
// If it is a consecutive number then push to the last injected value | |
if (acc.li === i) { | |
!i ? | |
acc.arr.push([ | |
value | |
]) : | |
acc.arr[acc.arr.length - ONE].push(value); | |
} | |
else { | |
acc.arr.push({ | |
$slice: [ | |
`$${path}`, | |
acc.li, | |
i | |
] | |
}); | |
acc.arr.push([ | |
value | |
]); | |
} | |
acc.li = i + ONE; | |
return acc; | |
}, { li: 0, arr: [] }); | |
// If there are some updates then perform them | |
aggregates.push({ | |
$set: { | |
[path]: { | |
$concatArrays: [ | |
...updates.arr, | |
{ | |
$slice: [ | |
`$${path}`, | |
updates.li, | |
{ $size: `$${path}` } | |
] | |
} | |
] | |
} | |
} | |
}); | |
aggregates.push({ | |
$addFields: { | |
[path]: { | |
$filter: { | |
input: `$${path}`, | |
as: 'd', | |
cond: { | |
$ne: [ | |
'$$d', | |
null | |
] | |
} | |
} | |
} | |
} | |
}); | |
} | |
}); | |
return aggregates.length ? | |
(isEmpty(update) ? | |
[] : | |
[ | |
update | |
]).concat(aggregates) : | |
update; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment