@haste-health/fhir-patch-building 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +102 -0
- package/lib/index.js.map +1 -0
- package/package.json +35 -0
- package/src/index.test.ts +188 -0
- package/src/index.ts +144 -0
package/README.md
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Operation } from "fast-json-patch";
|
|
2
|
+
import * as fpt from "@haste-health/fhir-pointer";
|
|
3
|
+
import { Resource } from "@haste-health/fhir-types/r4/types";
|
|
4
|
+
export interface Mutation<T, R> {
|
|
5
|
+
path: fpt.Loc<T, R, any>;
|
|
6
|
+
op: "add" | "remove" | "replace";
|
|
7
|
+
value?: R;
|
|
8
|
+
}
|
|
9
|
+
export default function buildPatches<T extends Resource, R>(value: T, mutation: Mutation<T, R>): Operation[];
|
|
10
|
+
export declare function applyMutation<T extends Resource, R>(value: T, mutation: Mutation<T, R>): T;
|
|
11
|
+
export declare function applyMutationImmutable<T extends Resource, R>(value: T, mutation: Mutation<T, R>): T;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import jsonpatch from "fast-json-patch";
|
|
2
|
+
import { produce } from "immer";
|
|
3
|
+
import * as fpt from "@haste-health/fhir-pointer";
|
|
4
|
+
import { R4 } from "@haste-health/fhir-types/versions";
|
|
5
|
+
function getValue(value, pointer) {
|
|
6
|
+
return fpt.get(pointer, value);
|
|
7
|
+
}
|
|
8
|
+
function valueExists(value, json_pointer) {
|
|
9
|
+
return getValue(value, json_pointer) !== undefined;
|
|
10
|
+
}
|
|
11
|
+
function deriveNextValuePlaceHolder(fields) {
|
|
12
|
+
if (typeof fields[1] === "number") {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
function createPatchesNonExistantFields(resource, path) {
|
|
18
|
+
const fields = fpt.fields(path);
|
|
19
|
+
let patches = [];
|
|
20
|
+
let curValue = resource;
|
|
21
|
+
let curPointer = fpt.pointer(R4, resource.resourceType, resource.id);
|
|
22
|
+
for (let i = 0; i < fields.length; i++) {
|
|
23
|
+
curPointer = fpt.descend(curPointer, fields[i]);
|
|
24
|
+
curValue = getValue(resource, curPointer);
|
|
25
|
+
if (curValue === undefined) {
|
|
26
|
+
const nextValue = deriveNextValuePlaceHolder(fields.slice(i));
|
|
27
|
+
patches = [
|
|
28
|
+
...patches,
|
|
29
|
+
{ op: "add", path: fpt.toJSONPointer(curPointer), value: nextValue },
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return patches;
|
|
34
|
+
}
|
|
35
|
+
export default function buildPatches(value, mutation) {
|
|
36
|
+
// Builds patches with a given mutation to include non existant values up to the point in the path
|
|
37
|
+
// where the mutation occurs
|
|
38
|
+
switch (mutation.op) {
|
|
39
|
+
case "remove": {
|
|
40
|
+
if (valueExists(value, mutation.path)) {
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
op: "remove",
|
|
44
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
case "add": {
|
|
51
|
+
const patches = createPatchesNonExistantFields(value, mutation.path);
|
|
52
|
+
//If last is adding value remove here as collection will only add once.
|
|
53
|
+
if (patches[patches.length - 1]?.op === "add" &&
|
|
54
|
+
patches[patches.length - 1]?.path === fpt.toJSONPointer(mutation.path)) {
|
|
55
|
+
return [
|
|
56
|
+
...patches.slice(0, patches.length - 1),
|
|
57
|
+
{
|
|
58
|
+
op: "add",
|
|
59
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
60
|
+
value: mutation.value,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
return [
|
|
65
|
+
...patches,
|
|
66
|
+
{
|
|
67
|
+
op: "add",
|
|
68
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
69
|
+
value: mutation.value,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
case "replace": {
|
|
74
|
+
if (mutation.value === undefined || mutation.value === null) {
|
|
75
|
+
return buildPatches(value, {
|
|
76
|
+
op: "remove",
|
|
77
|
+
path: mutation.path,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const patches = createPatchesNonExistantFields(value, mutation.path);
|
|
81
|
+
return [
|
|
82
|
+
...patches,
|
|
83
|
+
{
|
|
84
|
+
op: "replace",
|
|
85
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
86
|
+
value: mutation.value,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
throw new Error(`Invalid operation '${mutation.op}'`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function applyMutation(value, mutation) {
|
|
95
|
+
return jsonpatch.applyPatch(value, buildPatches(value, mutation)).newDocument;
|
|
96
|
+
}
|
|
97
|
+
export function applyMutationImmutable(value, mutation) {
|
|
98
|
+
return produce(value, (v) => {
|
|
99
|
+
applyMutation(v, mutation);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haste-health/fhir-patch-building",
|
|
3
|
+
"version": "0.10.1",
|
|
4
|
+
"homepage": "https://haste.health",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/HasteHealth/HasteHealth.git"
|
|
8
|
+
},
|
|
9
|
+
"description": "JSON Patch building with typesafe fhir pointers.",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./lib/index.js",
|
|
12
|
+
"types": "./lib/index.d.ts",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "pnpm tsc",
|
|
15
|
+
"test": "pnpm node --experimental-vm-modules $(pnpm bin jest)",
|
|
16
|
+
"publish": "pnpm build && pnpm npm publish --access public --tolerate-republish"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@haste-health/fhir-types": "workspace:^",
|
|
20
|
+
"@jest/globals": "^29.7.0",
|
|
21
|
+
"jest": "^29.7.0",
|
|
22
|
+
"ts-jest": "^29.3.2",
|
|
23
|
+
"typescript": "5.9.2"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@haste-health/fhir-pointer": "workspace:^",
|
|
27
|
+
"fast-json-patch": "^3.1.1",
|
|
28
|
+
"immer": "^10.1.1"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"readme.md",
|
|
32
|
+
"lib/**",
|
|
33
|
+
"src/**"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { expect, test } from "@jest/globals";
|
|
2
|
+
import jsonpatch from "fast-json-patch";
|
|
3
|
+
|
|
4
|
+
import { descend, pointer } from "@haste-health/fhir-pointer";
|
|
5
|
+
import { Patient, id } from "@haste-health/fhir-types/r4/types";
|
|
6
|
+
import { R4 } from "@haste-health/fhir-types/versions";
|
|
7
|
+
|
|
8
|
+
import buildPatches, { applyMutationImmutable } from "./index.js";
|
|
9
|
+
|
|
10
|
+
test("Adding a value.", () => {
|
|
11
|
+
const loc = pointer(R4, "Patient", "123" as id);
|
|
12
|
+
const patient: Patient = { resourceType: "Patient", id: "123" } as Patient;
|
|
13
|
+
|
|
14
|
+
descend(descend(descend(descend(loc, "name"), 0), "given"), 0);
|
|
15
|
+
expect(
|
|
16
|
+
buildPatches(patient, {
|
|
17
|
+
op: "add",
|
|
18
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 0),
|
|
19
|
+
value: "test",
|
|
20
|
+
})
|
|
21
|
+
).toEqual([
|
|
22
|
+
{
|
|
23
|
+
op: "add",
|
|
24
|
+
path: "/name",
|
|
25
|
+
value: [],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
op: "add",
|
|
29
|
+
path: "/name/0",
|
|
30
|
+
value: {},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
op: "add",
|
|
34
|
+
path: "/name/0/given",
|
|
35
|
+
value: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
op: "add",
|
|
39
|
+
path: "/name/0/given/0",
|
|
40
|
+
value: "test",
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
expect(
|
|
45
|
+
buildPatches(patient, {
|
|
46
|
+
op: "add",
|
|
47
|
+
path: descend(loc, "name"),
|
|
48
|
+
value: [{ given: ["John"] }],
|
|
49
|
+
})
|
|
50
|
+
).toEqual([
|
|
51
|
+
{
|
|
52
|
+
op: "add",
|
|
53
|
+
path: "/name",
|
|
54
|
+
value: [{ given: ["John"] }],
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
expect(
|
|
59
|
+
buildPatches(
|
|
60
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
61
|
+
{
|
|
62
|
+
op: "add",
|
|
63
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 1),
|
|
64
|
+
value: "Jake",
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
).toEqual([
|
|
68
|
+
{
|
|
69
|
+
op: "add",
|
|
70
|
+
path: "/name/0/given/1",
|
|
71
|
+
value: "Jake",
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
expect(
|
|
76
|
+
jsonpatch.applyPatch(
|
|
77
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
78
|
+
buildPatches(
|
|
79
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
80
|
+
{
|
|
81
|
+
op: "add",
|
|
82
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 1),
|
|
83
|
+
value: "Jake",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
).newDocument
|
|
87
|
+
).toEqual({ ...patient, name: [{ given: ["bob", "Jake"] }] });
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
jsonpatch.applyPatch(
|
|
91
|
+
patient,
|
|
92
|
+
buildPatches(patient, {
|
|
93
|
+
op: "add",
|
|
94
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 0),
|
|
95
|
+
value: "test",
|
|
96
|
+
})
|
|
97
|
+
).newDocument
|
|
98
|
+
).toEqual({ ...patient, name: [{ given: ["test"] }] });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("replace", () => {
|
|
102
|
+
const loc = pointer(R4, "Patient", "123" as id);
|
|
103
|
+
const patient: Patient = { resourceType: "Patient", id: "123" } as Patient;
|
|
104
|
+
expect(
|
|
105
|
+
jsonpatch.applyPatch(
|
|
106
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
107
|
+
buildPatches(
|
|
108
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
109
|
+
{
|
|
110
|
+
op: "replace",
|
|
111
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 0),
|
|
112
|
+
value: "Jake",
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
).newDocument
|
|
116
|
+
).toEqual({ ...patient, name: [{ given: ["Jake"] }] });
|
|
117
|
+
|
|
118
|
+
expect(
|
|
119
|
+
jsonpatch.applyPatch(
|
|
120
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
121
|
+
buildPatches(
|
|
122
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
123
|
+
{
|
|
124
|
+
op: "replace",
|
|
125
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 1),
|
|
126
|
+
value: "Jake",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
).newDocument
|
|
130
|
+
).toEqual({ ...patient, name: [{ given: ["bob", "Jake"] }] });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("removal", () => {
|
|
134
|
+
const loc = pointer(R4, "Patient", "123" as id);
|
|
135
|
+
const patient: Patient = { resourceType: "Patient", id: "123" } as Patient;
|
|
136
|
+
expect(
|
|
137
|
+
jsonpatch.applyPatch(
|
|
138
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
139
|
+
buildPatches(
|
|
140
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
141
|
+
{
|
|
142
|
+
op: "remove",
|
|
143
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 0),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
).newDocument
|
|
147
|
+
).toEqual({ ...patient, name: [{ given: [] }] });
|
|
148
|
+
|
|
149
|
+
expect(
|
|
150
|
+
jsonpatch.applyPatch(
|
|
151
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
152
|
+
buildPatches(
|
|
153
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
154
|
+
{
|
|
155
|
+
op: "remove",
|
|
156
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 1),
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
).newDocument
|
|
160
|
+
).toEqual({ ...patient, name: [{ given: ["bob"] }] });
|
|
161
|
+
expect(
|
|
162
|
+
jsonpatch.applyPatch(
|
|
163
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
164
|
+
buildPatches(
|
|
165
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
166
|
+
{
|
|
167
|
+
op: "remove",
|
|
168
|
+
path: descend(descend(descend(loc, "identifier"), 0), "system"),
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
).newDocument
|
|
172
|
+
).toEqual({ ...patient, name: [{ given: ["bob"] }] });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("immutable Patch", () => {
|
|
176
|
+
const loc = pointer(R4, "Patient", "123" as id);
|
|
177
|
+
const patient: Patient = { resourceType: "Patient", id: "123" } as Patient;
|
|
178
|
+
expect(
|
|
179
|
+
applyMutationImmutable(
|
|
180
|
+
{ ...patient, name: [{ given: ["bob"] }] },
|
|
181
|
+
{
|
|
182
|
+
op: "replace",
|
|
183
|
+
path: descend(descend(descend(descend(loc, "name"), 0), "given"), 1),
|
|
184
|
+
value: "Jake",
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
).toEqual({ ...patient, name: [{ given: ["bob", "Jake"] }] });
|
|
188
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import jsonpatch, { Operation } from "fast-json-patch";
|
|
2
|
+
import { produce } from "immer";
|
|
3
|
+
|
|
4
|
+
import * as fpt from "@haste-health/fhir-pointer";
|
|
5
|
+
import { Resource, id } from "@haste-health/fhir-types/r4/types";
|
|
6
|
+
import { R4 } from "@haste-health/fhir-types/versions";
|
|
7
|
+
|
|
8
|
+
export interface Mutation<T, R> {
|
|
9
|
+
path: fpt.Loc<T, R, any>;
|
|
10
|
+
op: "add" | "remove" | "replace";
|
|
11
|
+
value?: R;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getValue<T extends object, R>(
|
|
15
|
+
value: T,
|
|
16
|
+
pointer: fpt.Loc<T, R, any>
|
|
17
|
+
): R {
|
|
18
|
+
return fpt.get(pointer, value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function valueExists<T extends object, R>(
|
|
22
|
+
value: T,
|
|
23
|
+
json_pointer: fpt.Loc<T, R, any>
|
|
24
|
+
) {
|
|
25
|
+
return getValue(value, json_pointer) !== undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function deriveNextValuePlaceHolder(
|
|
29
|
+
fields: (string | number | symbol)[]
|
|
30
|
+
): Array<unknown> | Record<string, unknown> {
|
|
31
|
+
if (typeof fields[1] === "number") {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createPatchesNonExistantFields<T extends Record<string, any>, R>(
|
|
38
|
+
resource: T,
|
|
39
|
+
path: fpt.Loc<T, R, any>
|
|
40
|
+
) {
|
|
41
|
+
const fields = fpt.fields(path);
|
|
42
|
+
|
|
43
|
+
let patches: Operation[] = [];
|
|
44
|
+
let curValue = resource as unknown;
|
|
45
|
+
let curPointer: fpt.Loc<T, any, any> = fpt.pointer(
|
|
46
|
+
R4,
|
|
47
|
+
resource.resourceType,
|
|
48
|
+
resource.id as id
|
|
49
|
+
);
|
|
50
|
+
for (let i = 0; i < fields.length; i++) {
|
|
51
|
+
curPointer = fpt.descend(curPointer, fields[i]);
|
|
52
|
+
curValue = getValue(resource, curPointer);
|
|
53
|
+
if (curValue === undefined) {
|
|
54
|
+
const nextValue = deriveNextValuePlaceHolder(fields.slice(i));
|
|
55
|
+
patches = [
|
|
56
|
+
...patches,
|
|
57
|
+
{ op: "add", path: fpt.toJSONPointer(curPointer), value: nextValue },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return patches;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default function buildPatches<T extends Resource, R>(
|
|
65
|
+
value: T,
|
|
66
|
+
mutation: Mutation<T, R>
|
|
67
|
+
): Operation[] {
|
|
68
|
+
// Builds patches with a given mutation to include non existant values up to the point in the path
|
|
69
|
+
// where the mutation occurs
|
|
70
|
+
|
|
71
|
+
switch (mutation.op) {
|
|
72
|
+
case "remove": {
|
|
73
|
+
if (valueExists(value, mutation.path)) {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
op: "remove",
|
|
77
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
case "add": {
|
|
84
|
+
const patches = createPatchesNonExistantFields(value, mutation.path);
|
|
85
|
+
//If last is adding value remove here as collection will only add once.
|
|
86
|
+
if (
|
|
87
|
+
patches[patches.length - 1]?.op === "add" &&
|
|
88
|
+
patches[patches.length - 1]?.path === fpt.toJSONPointer(mutation.path)
|
|
89
|
+
) {
|
|
90
|
+
return [
|
|
91
|
+
...patches.slice(0, patches.length - 1),
|
|
92
|
+
{
|
|
93
|
+
op: "add",
|
|
94
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
95
|
+
value: mutation.value,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
return [
|
|
100
|
+
...patches,
|
|
101
|
+
{
|
|
102
|
+
op: "add",
|
|
103
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
104
|
+
value: mutation.value,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
case "replace": {
|
|
109
|
+
if (mutation.value === undefined || mutation.value === null) {
|
|
110
|
+
return buildPatches(value, {
|
|
111
|
+
op: "remove",
|
|
112
|
+
path: mutation.path,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const patches = createPatchesNonExistantFields(value, mutation.path);
|
|
116
|
+
return [
|
|
117
|
+
...patches,
|
|
118
|
+
{
|
|
119
|
+
op: "replace",
|
|
120
|
+
path: fpt.toJSONPointer(mutation.path),
|
|
121
|
+
value: mutation.value,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Invalid operation '${mutation.op}'`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function applyMutation<T extends Resource, R>(
|
|
131
|
+
value: T,
|
|
132
|
+
mutation: Mutation<T, R>
|
|
133
|
+
): T {
|
|
134
|
+
return jsonpatch.applyPatch(value, buildPatches(value, mutation)).newDocument;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function applyMutationImmutable<T extends Resource, R>(
|
|
138
|
+
value: T,
|
|
139
|
+
mutation: Mutation<T, R>
|
|
140
|
+
): T {
|
|
141
|
+
return produce(value, (v) => {
|
|
142
|
+
applyMutation(v as T, mutation);
|
|
143
|
+
});
|
|
144
|
+
}
|