• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

qlik-oss / enigma.js / 23801101998

31 Mar 2026 01:55PM UTC coverage: 95.945% (+0.6%) from 95.374%
23801101998

push

github

web-flow
chore(test): replace after-work with vitest (#1034)

* chore(test): replace after-work with vitest

* chore(test): update test files after vitest migration

* chore(test): fix lint issues

Signed-off-by: Johan Enell <johan.enell@qlik.com>

---------

Signed-off-by: Johan Enell <johan.enell@qlik.com>

604 of 673 branches covered (89.75%)

Branch coverage included in aggregate %.

2188 of 2237 relevant lines covered (97.81%)

68.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.41
/src/json-patch.js
1
import originalExtend from 'extend';
3✔
2

3
import createEnigmaError from './error';
3✔
4
import errorCodes from './error-codes';
3✔
5

6
const extend = originalExtend.bind(null, true);
3✔
7
const JSONPatch = {};
3✔
8
const { isArray } = Array;
3✔
9
function isObject(v) { return v != null && !Array.isArray(v) && typeof v === 'object'; }
502✔
10
function isUndef(v) { return typeof v === 'undefined'; }
120✔
11
function isFunction(v) { return typeof v === 'function'; }
26✔
12

13
/**
3✔
14
* Generate an exact duplicate (with no references) of a specific value.
3✔
15
*
3✔
16
* @private
3✔
17
* @param {Object} The value to duplicate
3✔
18
* @returns {Object} a unique, duplicated value
3✔
19
*/
3✔
20
function generateValue(val) {
83✔
21
  if (val) {
83✔
22
    return extend({}, { val }).val;
82✔
23
  }
82✔
24
  return val;
1✔
25
}
1✔
26

27
/**
3✔
28
* An additional type checker used to determine if the property is of internal
3✔
29
* use or not a type that can be translated into JSON (like functions).
3✔
30
*
3✔
31
* @private
3✔
32
* @param {Object} obj The object which has the property to check
3✔
33
* @param {String} The property name to check
3✔
34
* @returns {Boolean} Whether the property is deemed special or not
3✔
35
*/
3✔
36
function isSpecialProperty(obj, key) {
26✔
37
  return isFunction(obj[key])
26✔
38
    || key.substring(0, 2) === '$$'
26✔
39
    || key.substring(0, 1) === '_';
26✔
40
}
26✔
41

42
/**
3✔
43
* Finds the parent object from a JSON-Pointer ("/foo/bar/baz" = "bar" is "baz" parent),
3✔
44
* also creates the object structure needed.
3✔
45
*
3✔
46
* @private
3✔
47
* @param {Object} data The root object to traverse through
3✔
48
* @param {String} The JSON-Pointer string to use when traversing
3✔
49
* @returns {Object} The parent object
3✔
50
*/
3✔
51
function getParent(data, str) {
62✔
52
  const seperator = '/';
62✔
53
  const parts = str.substring(1).split(seperator).slice(0, -1);
62✔
54
  let numPart;
62✔
55

56
  parts.forEach((part, i) => {
62✔
57
    if (i === parts.length) {
6!
58
      return;
×
59
    }
×
60
    numPart = +part;
6✔
61
    const newPart = !isNaN(numPart) ? [] : {};
6!
62
    data[numPart || part] = isUndef(data[numPart || part])
6✔
63
      ? newPart
6!
64
      : data[part];
6✔
65
    data = data[numPart || part];
6✔
66
  });
62✔
67

68
  return data;
62✔
69
}
62✔
70

71
/**
3✔
72
* Cleans an object of all its properties, unless they're deemed special or
3✔
73
* cannot be removed by configuration.
3✔
74
*
3✔
75
* @private
3✔
76
* @param {Object} obj The object to clean
3✔
77
*/
3✔
78
function emptyObject(obj) {
47✔
79
  Object.keys(obj).forEach((key) => {
47✔
80
    const config = Object.getOwnPropertyDescriptor(obj, key);
13✔
81

82
    if (config.configurable && !isSpecialProperty(obj, key)) {
13✔
83
      delete obj[key];
13✔
84
    }
13✔
85
  });
47✔
86
}
47✔
87

88
/**
3✔
89
* Compare an object with another, could be object, array, number, string, bool.
3✔
90
* @private
3✔
91
* @param {Object} a The first object to compare
3✔
92
* @param {Object} a The second object to compare
3✔
93
* @returns {Boolean} Whether the objects are identical
3✔
94
*/
3✔
95
function compare(a, b) {
307✔
96
  let isIdentical = true;
307✔
97

98
  if (isObject(a) && isObject(b)) {
307✔
99
    if (Object.keys(a).length !== Object.keys(b).length) {
85✔
100
      return false;
4✔
101
    }
4✔
102
    Object.keys(a).forEach((key) => {
81✔
103
      if (!compare(a[key], b[key])) {
135✔
104
        isIdentical = false;
1✔
105
      }
1✔
106
    });
81✔
107
    return isIdentical;
81✔
108
  }
81✔
109
  if (isArray(a) && isArray(b)) {
307✔
110
    if (a.length !== b.length) {
42✔
111
      return false;
8✔
112
    }
8✔
113
    for (let i = 0, l = a.length; i < l; i += 1) {
42✔
114
      if (!compare(a[i], b[i])) {
84✔
115
        return false;
2✔
116
      }
2✔
117
    }
84✔
118
    return true;
32✔
119
  }
32✔
120
  return a === b;
180✔
121
}
180✔
122

123
/**
3✔
124
* Generates patches by comparing two arrays.
3✔
125
*
3✔
126
* @private
3✔
127
* @param {Array} oldA The old (original) array, which will be patched
3✔
128
* @param {Array} newA The new array, which will be used to compare against
3✔
129
* @returns {Array} An array of patches (if any)
3✔
130
*/
3✔
131
function patchArray(original, newA, basePath) {
5✔
132
  let patches = [];
5✔
133
  const oldA = original.slice();
5✔
134
  let tmpIdx = -1;
5✔
135

136
  function findIndex(a, id, idx) {
5✔
137
    if (a[idx] && isUndef(a[idx].qInfo)) {
21!
138
      return null;
×
139
    }
×
140
    if (a[idx] && a[idx].qInfo.qId === id) {
21✔
141
      // shortcut if identical
6✔
142
      return idx;
6✔
143
    }
6✔
144
    for (let ii = 0, ll = a.length; ii < ll; ii += 1) {
21✔
145
      if (a[ii] && a[ii].qInfo.qId === id) {
30✔
146
        return ii;
10✔
147
      }
10✔
148
    }
30✔
149
    return -1;
5✔
150
  }
21✔
151

152
  if (compare(newA, oldA)) {
5!
153
    // array is unchanged
×
154
    return patches;
×
155
  }
×
156

157
  if (!isUndef(newA[0]) && isUndef(newA[0].qInfo)) {
5✔
158
    // we cannot create patches without unique identifiers, replace array...
1✔
159
    patches.push({
1✔
160
      op: 'replace',
1✔
161
      path: basePath,
1✔
162
      value: newA,
1✔
163
    });
1✔
164
    return patches;
1✔
165
  }
1✔
166

167
  for (let i = oldA.length - 1; i >= 0; i -= 1) {
5✔
168
    tmpIdx = findIndex(newA, oldA[i].qInfo && oldA[i].qInfo.qId, i);
10✔
169
    if (tmpIdx === -1) {
10✔
170
      patches.push({
2✔
171
        op: 'remove',
2✔
172
        path: `${basePath}/${i}`,
2✔
173
      });
2✔
174
      oldA.splice(i, 1);
2✔
175
    } else {
10✔
176
      patches = patches.concat(JSONPatch.generate(oldA[i], newA[tmpIdx], `${basePath}/${i}`));
8✔
177
    }
8✔
178
  }
10✔
179

180
  for (let i = 0, l = newA.length; i < l; i += 1) {
5✔
181
    tmpIdx = findIndex(oldA, newA[i].qInfo && newA[i].qInfo.qId);
11✔
182
    if (tmpIdx === -1) {
11✔
183
      patches.push({
3✔
184
        op: 'add',
3✔
185
        path: `${basePath}/${i}`,
3✔
186
        value: newA[i],
3✔
187
      });
3✔
188
      oldA.splice(i, 0, newA[i]);
3✔
189
    } else if (tmpIdx !== i) {
11!
190
      patches.push({
×
191
        op: 'move',
×
192
        path: `${basePath}/${i}`,
×
193
        from: `${basePath}/${tmpIdx}`,
×
194
      });
×
195
      oldA.splice(i, 0, oldA.splice(tmpIdx, 1)[0]);
×
196
    }
×
197
  }
11✔
198
  return patches;
4✔
199
}
4✔
200

201
/**
3✔
202
* Generate an array of JSON-Patch:es following the JSON-Patch Specification Draft.
3✔
203
*
3✔
204
* See [specification draft](http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-10)
3✔
205
*
3✔
206
* Does NOT currently generate patches for arrays (will replace them)
3✔
207
* @private
3✔
208
* @param {Object} original The object to patch to
3✔
209
* @param {Object} newData The object to patch from
3✔
210
* @param {String} [basePath] The base path to use when generating the paths for
3✔
211
*                            the patches (normally not used)
3✔
212
* @returns {Array} An array of patches
3✔
213
*/
3✔
214
JSONPatch.generate = function generate(original, newData, basePath) {
3✔
215
  basePath = basePath || '';
19✔
216
  let patches = [];
19✔
217

218
  Object.keys(newData).forEach((key) => {
19✔
219
    const val = generateValue(newData[key]);
83✔
220
    const oldVal = original[key];
83✔
221
    const tmpPath = `${basePath}/${key}`;
83✔
222

223
    if (compare(val, oldVal) || isSpecialProperty(newData, key)) {
83✔
224
      return;
72✔
225
    }
72✔
226
    if (isUndef(oldVal)) {
83✔
227
      // property does not previously exist
1✔
228
      patches.push({
1✔
229
        op: 'add',
1✔
230
        path: tmpPath,
1✔
231
        value: val,
1✔
232
      });
1✔
233
    } else if (isObject(val) && isObject(oldVal)) {
83✔
234
      // we need to generate sub-patches for this, since it already exist
2✔
235
      patches = patches.concat(JSONPatch.generate(oldVal, val, tmpPath));
2✔
236
    } else if (isArray(val) && isArray(oldVal)) {
10✔
237
      patches = patches.concat(patchArray(oldVal, val, tmpPath));
5✔
238
    } else {
8✔
239
      // it's a simple property (bool, string, number)
3✔
240
      patches.push({
3✔
241
        op: 'replace',
3✔
242
        path: `${basePath}/${key}`,
3✔
243
        value: val,
3✔
244
      });
3✔
245
    }
3✔
246
  });
19✔
247

248
  Object.keys(original).forEach((key) => {
19✔
249
    if (isUndef(newData[key]) && !isSpecialProperty(original, key)) {
84✔
250
      // this property does not exist anymore
2✔
251
      patches.push({
2✔
252
        op: 'remove',
2✔
253
        path: `${basePath}/${key}`,
2✔
254
      });
2✔
255
    }
2✔
256
  });
19✔
257

258
  return patches;
19✔
259
};
19✔
260

261
/**
3✔
262
* Apply a list of patches to an object.
3✔
263
* @private
3✔
264
* @param {Object} original The object to patch
3✔
265
* @param {Array} patches The list of patches to apply
3✔
266
*/
3✔
267
JSONPatch.apply = function apply(original, patches) {
3✔
268
  patches.forEach((patch) => {
62✔
269
    let parent = getParent(original, patch.path);
62✔
270
    let key = patch.path.split('/').splice(-1)[0];
62✔
271
    let target = key && isNaN(+key) ? parent[key] : parent[+key] || parent;
62✔
272
    const from = patch.from ? patch.from.split('/').splice(-1)[0] : null;
62!
273

274
    if (patch.path === '/') {
62✔
275
      parent = null;
50✔
276
      target = original;
50✔
277
    }
50✔
278

279
    if (patch.op === 'add' || patch.op === 'replace') {
62✔
280
      if (isArray(parent)) {
59!
281
        // trust indexes from patches, so don't replace the index if it's an add
3✔
282
        if (key === '-') {
3✔
283
          key = parent.length;
1✔
284
        }
1✔
285
        parent.splice(+key, patch.op === 'add' ? 0 : 1, patch.value);
3✔
286
      } else if (isArray(target) && isArray(patch.value)) {
59✔
287
        // keep array reference if possible...
5✔
288
        target.length = 0;
5✔
289

290
        const chunkSize = 1000;
5✔
291
        for (let i = 0; i < patch.value.length; i += chunkSize) {
5✔
292
          const chunk = patch.value.slice(i, i + chunkSize);
6✔
293
          target.push(...chunk);
6✔
294
        }
6✔
295
      } else if (isObject(target) && isObject(patch.value)) {
56✔
296
        // keep object reference if possible...
47✔
297
        emptyObject(target);
47✔
298
        extend(target, patch.value);
47✔
299
      } else if (!parent) {
51!
300
        throw createEnigmaError(errorCodes.PATCH_HAS_NO_PARENT, 'Patchee is not an object we can patch');
×
301
      } else {
4✔
302
        // simple value
4✔
303
        parent[key] = patch.value;
4✔
304
      }
4✔
305
    } else if (patch.op === 'move') {
62!
306
      const oldParent = getParent(original, patch.from);
×
307
      if (isArray(parent)) {
×
308
        parent.splice(+key, 0, oldParent.splice(+from, 1)[0]);
×
309
      } else {
×
310
        parent[key] = oldParent[from];
×
311
        delete oldParent[from];
×
312
      }
×
313
    } else if (patch.op === 'remove') {
3✔
314
      if (isArray(parent)) {
3✔
315
        parent.splice(+key, 1);
1✔
316
      } else {
3✔
317
        delete parent[key];
2✔
318
      }
2✔
319
    }
3✔
320
  });
62✔
321
};
62✔
322

323
/**
3✔
324
* Deep clone an object.
3✔
325
* @private
3✔
326
* @param {Object} obj The object to clone
3✔
327
* @returns {Object} A new object identical to the `obj`
3✔
328
*/
3✔
329
JSONPatch.clone = function clone(obj) {
3✔
330
  return extend({}, obj);
×
331
};
×
332

333
/**
3✔
334
* Creates a JSON-patch.
3✔
335
* @private
3✔
336
* @param {String} op The operation of the patch. Available values: "add", "remove", "move"
3✔
337
* @param {Object} [val] The value to set the `path` to. If `op` is `move`, `val`
3✔
338
*                       is the "from JSON-path" path
3✔
339
* @param {String} path The JSON-path for the property to change (e.g. "/qHyperCubeDef/columnOrder")
3✔
340
* @returns {Object} A patch following the JSON-patch specification
3✔
341
*/
3✔
342
JSONPatch.createPatch = function createPatch(op, val, path) {
3✔
343
  const patch = {
×
344
    op: op.toLowerCase(),
×
345
    path,
×
346
  };
×
347
  if (patch.op === 'move') {
×
348
    patch.from = val;
×
349
  } else if (typeof val !== 'undefined') {
×
350
    patch.value = val;
×
351
  }
×
352
  return patch;
×
353
};
×
354

355
/**
3✔
356
* Apply the differences of two objects (keeping references if possible).
3✔
357
* Identical to running `JSONPatch.apply(original, JSONPatch.generate(original, newData));`
3✔
358
* @private
3✔
359
* @param {Object} original The object to update/patch
3✔
360
* @param {Object} newData the object to diff against
3✔
361
*
3✔
362
* @example
3✔
363
* var obj1 = { foo: [1,2,3], bar: { baz: true, qux: 1 } };
3✔
364
* var obj2 = { foo: [4,5,6], bar: { baz: false } };
3✔
365
* JSONPatch.updateObject(obj1, obj2);
3✔
366
* // => { foo: [4,5,6], bar: { baz: false } };
3✔
367
*/
3✔
368
JSONPatch.updateObject = function updateObject(original, newData) {
3✔
369
  if (!Object.keys(original).length) {
×
370
    extend(original, newData);
×
371
    return;
×
372
  }
×
373
  JSONPatch.apply(original, JSONPatch.generate(original, newData));
×
374
};
×
375

376
export default JSONPatch;
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc