Skip to content

Commit 7e0ecfe

Browse files
committed
refactor: use jsonPatchToMongoDbOps script
1 parent d784d34 commit 7e0ecfe

File tree

2 files changed

+129
-32
lines changed

2 files changed

+129
-32
lines changed

packages/mongo/src/MongoDbCollection.ts

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "@js-soft/docdb-access-abstractions";
77
import jsonpatch from "fast-json-patch";
88
import { Collection } from "mongodb";
9+
import { jsonPatchToMongoDbOps } from "./jsonPatchToMongoDbOps";
910
import { removeContainsInQuery } from "./queryUtils";
1011

1112
export class MongoDbCollection implements IDatabaseCollection {
@@ -49,39 +50,16 @@ export class MongoDbCollection implements IDatabaseCollection {
4950
}
5051

5152
const patch = jsonpatch.compare(oldDoc, data);
53+
const filter = { id: oldDoc.id };
5254

53-
const patchMappedToMongoDbOps = patch
54-
.filter((v) => v.path !== "/_id")
55-
.map((op) => {
56-
const path = op.path.startsWith("/")
57-
? op.path.substring(1).replaceAll("/", ".")
58-
: op.path.replaceAll("/", ".");
59-
60-
switch (op.op) {
61-
case "add":
62-
case "replace":
63-
return { $set: { [path]: op.value } };
64-
case "remove":
65-
return { $unset: { [path]: "" } };
66-
case "move":
67-
// TODO: hard to implement
68-
// return { $set: { [op.path]: oldDoc[op.from] }, $unset: { [op.from]: "" } };
69-
throw new Error("Move operation is not supported in MongoDB");
70-
case "copy":
71-
// TODO: hard to implement
72-
// return { $set: { [op.path]: oldDoc[op.from] } };
73-
throw new Error("Move operation is not supported in MongoDB");
74-
case "test":
75-
// Test operations are not supported in MongoDB, so we ignore them
76-
case "_get":
77-
default:
78-
throw new Error(`Unsupported operation: ${op.op}`);
79-
}
80-
});
81-
82-
const updated = await this.collection.findOneAndUpdate({ id: oldDoc.id }, patchMappedToMongoDbOps, {
83-
returnDocument: "after"
84-
});
55+
const operations = await jsonPatchToMongoDbOps(
56+
patch.filter((v) => v.path !== "/_id"),
57+
filter,
58+
this.collection
59+
);
60+
await this.collection.bulkWrite(operations);
61+
62+
const updated = await this.collection.findOne({ id: oldDoc.id });
8563

8664
if (!updated) {
8765
throw new Error("Document not found for patching");
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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

Comments
 (0)