|  | 
|  | 1 | +/** | 
|  | 2 | + * credit to https://github.com/fathomas/json-patch-to-mongodb-ops/tree/main | 
|  | 3 | + */ | 
|  | 4 | + | 
|  | 5 | +import { Operation, validate } from "fast-json-patch"; | 
|  | 6 | +import { PatchError } from "fast-json-patch/module/helpers"; | 
|  | 7 | +import { AnyBulkWriteOperation, Collection } from "mongodb"; | 
|  | 8 | + | 
|  | 9 | +export const jsonPatchPathToDot = (path: string): string => | 
|  | 10 | +    path.replace(/^\//, "").replace(/\//g, ".").replace(/~1/g, "/").replace(/~0/g, "~"); | 
|  | 11 | + | 
|  | 12 | +function getTrailingPos(path: string): number { | 
|  | 13 | +    const parts = path.split("."); | 
|  | 14 | +    const pathTail = parts.slice(-1)[0]; | 
|  | 15 | +    return parts.length > 1 ? (pathTail === "-" ? -1 : parseInt(pathTail, 10)) : NaN; | 
|  | 16 | +} | 
|  | 17 | + | 
|  | 18 | +function $remove(path: any) { | 
|  | 19 | +    const result: any[] = []; | 
|  | 20 | +    const trailingPos = getTrailingPos(path); | 
|  | 21 | +    result.push({ $unset: { [path]: 1 } }); | 
|  | 22 | +    if (!Number.isNaN(trailingPos)) { | 
|  | 23 | +        const pathHead = path.split(".").slice(0, -1).join("."); | 
|  | 24 | +        result.push({ $pull: { [pathHead]: null } }); | 
|  | 25 | +    } | 
|  | 26 | +    return result; | 
|  | 27 | +} | 
|  | 28 | + | 
|  | 29 | +function $add(path: any, value) { | 
|  | 30 | +    const trailingPos = getTrailingPos(path); | 
|  | 31 | +    if (Number.isNaN(trailingPos)) { | 
|  | 32 | +        return { $set: { [path]: value } }; | 
|  | 33 | +    } | 
|  | 34 | +    const pathHead = path.split(".").slice(0, -1).join("."); | 
|  | 35 | +    return { | 
|  | 36 | +        $push: { | 
|  | 37 | +            [pathHead]: trailingPos >= 0 ? { $each: [value], $position: trailingPos } : value | 
|  | 38 | +        } | 
|  | 39 | +    }; | 
|  | 40 | +} | 
|  | 41 | + | 
|  | 42 | +export async function jsonPatchToMongoDbOps( | 
|  | 43 | +    patch: Operation[], | 
|  | 44 | +    targetFilter: Record<string, any>, | 
|  | 45 | +    collection: Collection | 
|  | 46 | +): Promise<AnyBulkWriteOperation<{}>[]> { | 
|  | 47 | +    const error = validate(patch) as PatchError | undefined; | 
|  | 48 | +    if (error) throw error; | 
|  | 49 | + | 
|  | 50 | +    return ( | 
|  | 51 | +        await Promise.all( | 
|  | 52 | +            patch.map(async (operation) => { | 
|  | 53 | +                const path = jsonPatchPathToDot(operation.path); | 
|  | 54 | +                if (!path || path.endsWith(".")) { | 
|  | 55 | +                    throw new Error("Invalid update path."); | 
|  | 56 | +                } | 
|  | 57 | + | 
|  | 58 | +                switch (operation.op) { | 
|  | 59 | +                    case "add": { | 
|  | 60 | +                        return $add(path, operation.value); | 
|  | 61 | +                    } | 
|  | 62 | +                    case "remove": { | 
|  | 63 | +                        return $remove(path); | 
|  | 64 | +                    } | 
|  | 65 | +                    case "replace": { | 
|  | 66 | +                        return { $set: { [path]: operation.value } }; | 
|  | 67 | +                    } | 
|  | 68 | +                    case "copy": | 
|  | 69 | +                    case "move": { | 
|  | 70 | +                        const from = jsonPatchPathToDot(operation.from); | 
|  | 71 | +                        const fromParts = from.split("."); | 
|  | 72 | +                        const targetIndex = fromParts[1] && /^\d+$/.test(fromParts[1]) ? parseInt(fromParts[1]) : NaN; | 
|  | 73 | + | 
|  | 74 | +                        const currDoc = await collection.findOne(targetFilter, { | 
|  | 75 | +                            // Reduce memory usage and speed up query. | 
|  | 76 | +                            projection: { | 
|  | 77 | +                                // eslint-disable-next-line @typescript-eslint/naming-convention | 
|  | 78 | +                                _id: 0, | 
|  | 79 | +                                ...(Number.isNaN(targetIndex) | 
|  | 80 | +                                    ? { | 
|  | 81 | +                                          [fromParts[0]]: 1 | 
|  | 82 | +                                      } | 
|  | 83 | +                                    : { [fromParts[0]]: { $slice: [targetIndex, 1] } }) | 
|  | 84 | +                            } | 
|  | 85 | +                        }); | 
|  | 86 | + | 
|  | 87 | +                        let segmentsToWalk; | 
|  | 88 | +                        let startPoint; | 
|  | 89 | +                        if (Number.isNaN(targetIndex)) { | 
|  | 90 | +                            segmentsToWalk = fromParts; | 
|  | 91 | +                            startPoint = currDoc; | 
|  | 92 | +                        } else { | 
|  | 93 | +                            segmentsToWalk = fromParts.slice(2); | 
|  | 94 | +                            startPoint = currDoc![fromParts[0]][0]; | 
|  | 95 | +                        } | 
|  | 96 | + | 
|  | 97 | +                        const valueToTransfer = segmentsToWalk.reduce((curr, propName) => curr[propName], startPoint); | 
|  | 98 | +                        if (typeof valueToTransfer === "undefined") { | 
|  | 99 | +                            throw new Error("Can't move undefined value!"); | 
|  | 100 | +                        } | 
|  | 101 | + | 
|  | 102 | +                        const addOp = $add(path, valueToTransfer); | 
|  | 103 | +                        return operation.op === "copy" ? addOp : [...$remove(from), addOp]; | 
|  | 104 | +                    } | 
|  | 105 | +                    case "test": { | 
|  | 106 | +                        return undefined; | 
|  | 107 | +                    } | 
|  | 108 | +                    case "_get": | 
|  | 109 | +                    default: { | 
|  | 110 | +                        throw new Error(`Unsupported Operation! op = ${operation.op}`); | 
|  | 111 | +                    } | 
|  | 112 | +                } | 
|  | 113 | +            }) | 
|  | 114 | +        ) | 
|  | 115 | +    ) | 
|  | 116 | +        .flat() | 
|  | 117 | +        .filter((update) => update) | 
|  | 118 | +        .map((update) => ({ updateOne: { update, filter: targetFilter } })); | 
|  | 119 | +} | 
0 commit comments