-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
177 lines (166 loc) · 5.76 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
* json-squash
* @module json-squash
* @license MIT
* @author Omar Alshaker <[email protected]>
*/
const clone = require('clone');
function isArraySplice(patch) {
// check of indices are numerical and sequential
for (let i = 0; i < patch.length - 1; i++) {
const op = patch[i];
const nextOp = patch[i + 1];
let hereIndex = op.path.match(/\d+$/);
if (!hereIndex) return false;
let nextIndex = nextOp.path.match(/\d+$/);
if (!nextIndex) return false;
hereIndex = Number(hereIndex);
nextIndex = Number(nextIndex);
if (Math.abs(hereIndex - nextIndex) !== 1) {
return false;
}
// all the operations from start to end INCONCLUSIVE should be replace
if (i > 0 && i < patch.length - 2 && op.op !== 'replace') {
return false;
}
}
// determine where the remove is (some diffs put it in the end, some in the start)
if (patch[0].op === 'remove') {
return patch[patch.length - 1].path;
}
if (patch[patch.length - 1].op === 'remove') {
return patch[0].path;
}
}
function isReplaceOrAdd(operation) {
return operation.op == 'add' || operation.op == 'replace';
}
/**
* Squashes a json-patch patch into a smaller one if possible
* @param {Array} patch Your input patch
* @returns {Array} The squash patch
*/
function squash(patch) {
if (patch.length < 2) {
return patch;
}
let isSingleElementSplice = isArraySplice(patch);
if (isSingleElementSplice) {
return [{ op: 'remove', path: isSingleElementSplice }];
}
const patchDictionary = {};
const resultPatch = [];
let index = Date.now() + 1;
patch.forEach(function(operation) {
switch (operation.op) {
case 'add':
// those `add`s with `-` index can have duplicates
if (operation.path.endsWith('-')) {
// make them non-overwritable
index++;
patchDictionary[operation.path + index] = operation;
} else {
patchDictionary[operation.path] = operation;
}
break;
case 'replace':
// if this replace came after an add, change it to an add and merge them
if (
patchDictionary[operation.path] &&
patchDictionary[operation.path].op == 'add'
) {
operation.op = 'add';
}
patchDictionary[operation.path] = operation;
break;
case 'move':
// do we have a value added recently?
if (
patchDictionary[operation.from] &&
isReplaceOrAdd(patchDictionary[operation.from])
) {
// discard the move operation, and change the add's operation path to the move's operation path
patchDictionary[operation.from].path = operation.path;
patchDictionary[operation.path] = patchDictionary[operation.from];
delete patchDictionary[operation.from];
break;
} else {
// just keep move op as is
patchDictionary[operation.path] = operation;
}
break;
case 'copy':
if (
// do we have the source value in history ?
patchDictionary[operation.from] &&
isReplaceOrAdd(patchDictionary[operation.from])
) {
// do we have the destination value in history ?
if (
patchDictionary[operation.path] &&
isReplaceOrAdd(patchDictionary[operation.path])
) {
// change the original operation to have the copied value and discard copy operation
patchDictionary[operation.path].value = clone(
patchDictionary[operation.from].value
);
} else {
// we have source value, but not destination, convert it to an add, it's faster
patchDictionary[operation.path] = {
op: 'add',
path: operation.path,
value: clone(patchDictionary[operation.from].value)
};
}
} else {
// we neither have source or destination values, just add operation as is
patchDictionary[operation.path] = operation;
}
break;
case 'test':
/* when a test operation comes, we need to preserve all operations to this point,
e.g: add + replace = one add with the new value
but add + test + replace = add + test + replace, we shouldn't merge add + replace in this case
and that's why we give the path a unique key to preserve it */
if (patchDictionary[operation.path]) {
++index;
patchDictionary[operation.path + index] =
patchDictionary[operation.path];
delete patchDictionary[operation.path];
}
// we don't want to overwrite (or be overwritten by) other operations with test operation, so we give a pseudo key
++index;
patchDictionary[operation.path + index] = operation; // push as is
break;
case 'remove':
// do we have it in history
if (patchDictionary[operation.path]) {
// if have an add, copy or move in history, they (op + remove) equalize to nothing
if (
['replace', 'add', 'copy', 'replace'].indexOf(
patchDictionary[operation.path].op
) > -1
) {
patchDictionary[operation.path] = operation; // push as is
}
} else {
// we don't have it in history
patchDictionary[operation.path] = operation; // push as is
break;
}
default:
patchDictionary[operation.path] = operation; // push as is
break;
}
});
const newPatches = [];
for (let path in patchDictionary) {
newPatches.push(patchDictionary[path]);
}
return newPatches;
}
if (typeof module !== 'undefined') {
Object.defineProperty(exports, '__esModule', { value: true });
module.exports = squash;
module.exports.default = squash;
}