@crewhaus/tool-data 0.7.0 → 0.8.0
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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +38 -1
- package/dist/index.js +280 -63
- package/dist/lib/csv.d.ts +27 -2
- package/dist/lib/csv.js +77 -14
- package/dist/lib/diff.js +3 -3
- package/dist/lib/json.d.ts +51 -0
- package/dist/lib/json.js +174 -2
- package/dist/lib/jsonl.d.ts +1 -10
- package/dist/lib/jsonl.js +15 -1
- package/dist/lib/jsonpath.d.ts +38 -2
- package/dist/lib/jsonpath.js +166 -53
- package/dist/lib/own-record.d.ts +22 -0
- package/dist/lib/own-record.js +66 -0
- package/dist/lib/patch.d.ts +18 -2
- package/dist/lib/patch.js +72 -21
- package/dist/lib/table.d.ts +45 -8
- package/dist/lib/table.js +164 -59
- package/dist/lib/toml.d.ts +8 -1
- package/dist/lib/toml.js +77 -48
- package/dist/lib/xml.js +23 -10
- package/dist/lib/yaml.d.ts +19 -2
- package/dist/lib/yaml.js +88 -42
- package/package.json +11 -6
package/dist/lib/jsonpath.js
CHANGED
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
* `&&`. Anything unsupported raises a parse error naming the offending
|
|
34
34
|
* position, never a silent empty result.
|
|
35
35
|
*/
|
|
36
|
-
import {
|
|
36
|
+
import { testPatternSync } from "@crewhaus/tool-schema";
|
|
37
|
+
import { getOwn, isPlainObject } from "./json";
|
|
37
38
|
export class PathError extends Error {
|
|
38
39
|
}
|
|
39
40
|
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$-]*/;
|
|
@@ -233,85 +234,160 @@ function parseLiteral(text, whole, at) {
|
|
|
233
234
|
return n;
|
|
234
235
|
throw new PathError(`cannot read ${JSON.stringify(text)} as a literal at position ${at} of ${JSON.stringify(whole)}`);
|
|
235
236
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
237
|
+
/**
|
|
238
|
+
* The most nodes one query may visit, counting every node a step produces
|
|
239
|
+
* and every node a `..` walk passes. A single pass over the largest document
|
|
240
|
+
* this package accepts is well inside it; a path that walks the document
|
|
241
|
+
* again for each node it found (`$..*..x` over a deep document) is what it
|
|
242
|
+
* stops.
|
|
243
|
+
*/
|
|
244
|
+
export const MAX_QUERY_VISITS = 5_000_000;
|
|
245
|
+
/**
|
|
246
|
+
* Run a parsed path over a value, returning matches in document order.
|
|
247
|
+
*
|
|
248
|
+
* Evaluated lazily, depth first: each node a step produces runs through the
|
|
249
|
+
* remaining steps before the next is produced, which gives exactly the
|
|
250
|
+
* step-by-step, document-order result, and lets the query stop as soon as it
|
|
251
|
+
* has one match more than `limit` (to know it truncated). 0.7.0 built each
|
|
252
|
+
* step's whole node list first and then cut it at 20 x limit — so `$..*`
|
|
253
|
+
* materialized every node with its path, and a cut in the middle of a path
|
|
254
|
+
* silently dropped real matches: `$[*].b` over thirty records with `b` only
|
|
255
|
+
* in the last answered zero matches, not truncated.
|
|
256
|
+
*/
|
|
257
|
+
export function queryPath(root, steps, limit, maxVisits = MAX_QUERY_VISITS, regex) {
|
|
258
|
+
const found = [];
|
|
259
|
+
const want = Math.max(limit, 0) + 1;
|
|
260
|
+
const budget = { left: maxVisits };
|
|
261
|
+
const filters = { regex, undetermined: [] };
|
|
262
|
+
const start = { value: root, parent: null, seg: null };
|
|
263
|
+
let stopped = false;
|
|
264
|
+
if (steps.length === 0) {
|
|
265
|
+
found.push(start);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// One iterator per step: iterators[i] yields step i's outputs for the
|
|
269
|
+
// node the level above handed it. An explicit stack, not recursion, so a
|
|
270
|
+
// long path cannot overflow it.
|
|
271
|
+
const iterators = [expand(steps[0], start, budget, filters)];
|
|
272
|
+
while (iterators.length > 0) {
|
|
273
|
+
const next = iterators[iterators.length - 1].next();
|
|
274
|
+
if (budget.left < 0) {
|
|
275
|
+
stopped = true;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (next.done === true) {
|
|
279
|
+
iterators.pop();
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (iterators.length === steps.length) {
|
|
283
|
+
found.push(next.value);
|
|
284
|
+
if (found.length >= want)
|
|
285
|
+
break;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
iterators.push(expand(steps[iterators.length], next.value, budget, filters));
|
|
289
|
+
}
|
|
248
290
|
}
|
|
249
|
-
const
|
|
291
|
+
const shown = found.slice(0, Math.max(limit, 0));
|
|
292
|
+
const open = filters.undetermined;
|
|
250
293
|
return {
|
|
251
|
-
matches:
|
|
252
|
-
truncated,
|
|
294
|
+
matches: shown.map((n) => ({ path: renderPath(segmentsOf(n)), value: n.value })),
|
|
295
|
+
truncated: stopped || found.length > shown.length,
|
|
296
|
+
...(stopped ? { stoppedAtVisits: maxVisits } : {}),
|
|
297
|
+
...(open.length > 0
|
|
298
|
+
? {
|
|
299
|
+
undetermined: {
|
|
300
|
+
count: open.length,
|
|
301
|
+
paths: open.slice(0, 50).map((n) => renderPath(segmentsOf(n))),
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
: {}),
|
|
253
305
|
};
|
|
254
306
|
}
|
|
255
|
-
function
|
|
307
|
+
function segmentsOf(node) {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (let n = node; n !== null && n.seg !== null; n = n.parent)
|
|
310
|
+
out.push(n.seg);
|
|
311
|
+
return out.reverse();
|
|
312
|
+
}
|
|
313
|
+
const child = (parent, seg, value) => ({ value, parent, seg });
|
|
314
|
+
/** The nodes one step produces from `node`, lazily, charging each to the budget. */
|
|
315
|
+
function* expand(step, node, budget, filters) {
|
|
256
316
|
const v = node.value;
|
|
257
317
|
switch (step.kind) {
|
|
258
318
|
case "child": {
|
|
259
319
|
if (isPlainObject(v) && Object.hasOwn(v, step.name)) {
|
|
260
|
-
|
|
320
|
+
budget.left -= 1;
|
|
321
|
+
yield child(node, step.name, v[step.name]);
|
|
261
322
|
}
|
|
262
323
|
return;
|
|
263
324
|
}
|
|
264
325
|
case "wildcard": {
|
|
265
326
|
if (Array.isArray(v)) {
|
|
266
|
-
|
|
327
|
+
for (let i = 0; i < v.length; i++) {
|
|
328
|
+
budget.left -= 1;
|
|
329
|
+
yield child(node, i, v[i]);
|
|
330
|
+
}
|
|
267
331
|
}
|
|
268
332
|
else if (isPlainObject(v)) {
|
|
269
|
-
for (const k of Object.keys(v))
|
|
270
|
-
|
|
333
|
+
for (const k of Object.keys(v)) {
|
|
334
|
+
budget.left -= 1;
|
|
335
|
+
yield child(node, k, v[k]);
|
|
336
|
+
}
|
|
271
337
|
}
|
|
272
338
|
return;
|
|
273
339
|
}
|
|
274
340
|
case "descend": {
|
|
275
341
|
const name = step.name;
|
|
276
|
-
|
|
342
|
+
for (const n of descendants(node, budget)) {
|
|
277
343
|
if (name === null) {
|
|
278
|
-
if (n
|
|
279
|
-
|
|
280
|
-
return;
|
|
344
|
+
if (n !== node)
|
|
345
|
+
yield n;
|
|
281
346
|
}
|
|
282
|
-
if (isPlainObject(n.value) && Object.hasOwn(n.value, name)) {
|
|
283
|
-
|
|
347
|
+
else if (isPlainObject(n.value) && Object.hasOwn(n.value, name)) {
|
|
348
|
+
yield child(n, name, n.value[name]);
|
|
284
349
|
}
|
|
285
|
-
}
|
|
350
|
+
}
|
|
286
351
|
return;
|
|
287
352
|
}
|
|
288
353
|
case "index": {
|
|
289
354
|
if (!Array.isArray(v))
|
|
290
355
|
return;
|
|
291
356
|
const i = step.index < 0 ? v.length + step.index : step.index;
|
|
292
|
-
if (i >= 0 && i < v.length)
|
|
293
|
-
|
|
357
|
+
if (i >= 0 && i < v.length) {
|
|
358
|
+
budget.left -= 1;
|
|
359
|
+
yield child(node, i, v[i]);
|
|
360
|
+
}
|
|
294
361
|
return;
|
|
295
362
|
}
|
|
296
363
|
case "slice": {
|
|
297
364
|
if (!Array.isArray(v))
|
|
298
365
|
return;
|
|
299
366
|
for (const i of sliceIndices(v.length, step.start, step.end, step.step)) {
|
|
300
|
-
|
|
367
|
+
budget.left -= 1;
|
|
368
|
+
yield child(node, i, v[i]);
|
|
301
369
|
}
|
|
302
370
|
return;
|
|
303
371
|
}
|
|
304
372
|
case "filter": {
|
|
373
|
+
const keep = (seg, value) => {
|
|
374
|
+
const verdict = matchesFilter(value, step, filters.regex);
|
|
375
|
+
if (verdict === "undetermined")
|
|
376
|
+
filters.undetermined.push(child(node, seg, value));
|
|
377
|
+
return verdict === true;
|
|
378
|
+
};
|
|
305
379
|
if (Array.isArray(v)) {
|
|
306
|
-
v.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
380
|
+
for (let i = 0; i < v.length; i++) {
|
|
381
|
+
budget.left -= 1;
|
|
382
|
+
if (keep(i, v[i]))
|
|
383
|
+
yield child(node, i, v[i]);
|
|
384
|
+
}
|
|
310
385
|
}
|
|
311
386
|
else if (isPlainObject(v)) {
|
|
312
387
|
for (const k of Object.keys(v)) {
|
|
313
|
-
|
|
314
|
-
|
|
388
|
+
budget.left -= 1;
|
|
389
|
+
if (keep(k, v[k]))
|
|
390
|
+
yield child(node, k, v[k]);
|
|
315
391
|
}
|
|
316
392
|
}
|
|
317
393
|
return;
|
|
@@ -320,16 +396,41 @@ function applyStep(step, node, out) {
|
|
|
320
396
|
return;
|
|
321
397
|
}
|
|
322
398
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
399
|
+
/**
|
|
400
|
+
* `node` and everything under it, depth-first pre-order, lazily. An explicit
|
|
401
|
+
* stack of child cursors, one per level, so a deep document costs one frame
|
|
402
|
+
* per level and never a recursion.
|
|
403
|
+
*/
|
|
404
|
+
function* descendants(node, budget) {
|
|
405
|
+
budget.left -= 1;
|
|
406
|
+
yield node;
|
|
407
|
+
const cursorOf = (n) => Array.isArray(n.value)
|
|
408
|
+
? { of: n, keys: null, at: 0 }
|
|
409
|
+
: isPlainObject(n.value)
|
|
410
|
+
? { of: n, keys: Object.keys(n.value), at: 0 }
|
|
411
|
+
: null;
|
|
412
|
+
const stack = [];
|
|
413
|
+
const first = cursorOf(node);
|
|
414
|
+
if (first !== null)
|
|
415
|
+
stack.push(first);
|
|
416
|
+
while (stack.length > 0) {
|
|
417
|
+
const top = stack[stack.length - 1];
|
|
418
|
+
const container = top.of.value;
|
|
419
|
+
const size = top.keys === null ? container.length : top.keys.length;
|
|
420
|
+
if (top.at >= size) {
|
|
421
|
+
stack.pop();
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const at = top.at;
|
|
425
|
+
top.at += 1;
|
|
426
|
+
const next = top.keys === null
|
|
427
|
+
? child(top.of, at, container[at])
|
|
428
|
+
: child(top.of, top.keys[at], container[top.keys[at]]);
|
|
429
|
+
budget.left -= 1;
|
|
430
|
+
yield next;
|
|
431
|
+
const deeper = cursorOf(next);
|
|
432
|
+
if (deeper !== null)
|
|
433
|
+
stack.push(deeper);
|
|
333
434
|
}
|
|
334
435
|
}
|
|
335
436
|
/** Python slice semantics, including negative bounds and a negative step. */
|
|
@@ -350,11 +451,19 @@ export function sliceIndices(length, start, end, step) {
|
|
|
350
451
|
}
|
|
351
452
|
return out;
|
|
352
453
|
}
|
|
353
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Does `value` pass the filter? A `=~` pattern is never run here: its
|
|
456
|
+
* answer comes from `regex`, resolved in the worker by the tool, and one
|
|
457
|
+
* not answered yet reads as a miss for this pass (the tool runs the query
|
|
458
|
+
* again once it is answered). One that could not be answered is
|
|
459
|
+
* `"undetermined"`; one the screen refuses, or that does not compile, is a
|
|
460
|
+
* bad path, where 0.7.0 silently matched nothing.
|
|
461
|
+
*/
|
|
462
|
+
function matchesFilter(value, step, regex) {
|
|
354
463
|
let cur = value;
|
|
355
464
|
for (const seg of step.field) {
|
|
356
465
|
if (isPlainObject(cur))
|
|
357
|
-
cur = cur
|
|
466
|
+
cur = getOwn(cur, seg);
|
|
358
467
|
else if (Array.isArray(cur) && /^-?\d+$/.test(seg)) {
|
|
359
468
|
const i = Number(seg);
|
|
360
469
|
cur = cur[i < 0 ? cur.length + i : i];
|
|
@@ -372,12 +481,16 @@ function matchesFilter(value, step) {
|
|
|
372
481
|
if (step.op === "=~") {
|
|
373
482
|
if (typeof lit !== "string")
|
|
374
483
|
return false;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
catch {
|
|
484
|
+
const subject = String(cur);
|
|
485
|
+
const answer = regex === undefined ? testPatternSync(lit, "", subject) : regex.lookup(lit, "", subject);
|
|
486
|
+
if (answer === undefined)
|
|
379
487
|
return false;
|
|
488
|
+
if (typeof answer === "boolean")
|
|
489
|
+
return answer;
|
|
490
|
+
if ("refused" in answer) {
|
|
491
|
+
throw new PathError(`the filter's regex /${lit}/ was not run: ${answer.refused}`);
|
|
380
492
|
}
|
|
493
|
+
return "undetermined";
|
|
381
494
|
}
|
|
382
495
|
if (typeof cur === "number" && typeof lit === "number") {
|
|
383
496
|
return compare(cur < lit, cur > lit, step.op);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `z.record` that keeps a key named `__proto__`.
|
|
3
|
+
*
|
|
4
|
+
* zod rebuilds a record into a fresh object and skips `__proto__` while it
|
|
5
|
+
* does (so the rebuild cannot set the new object's prototype). That is the
|
|
6
|
+
* right call for zod, and the wrong one for a data tool: `{"__proto__": 1}`
|
|
7
|
+
* is valid JSON, `JSON.parse` keeps it as an own property, and CsvParse,
|
|
8
|
+
* FlattenObject and JsonParse hand it back as one. Passed on to TableQuery,
|
|
9
|
+
* CsvWrite or UnflattenObject, the field vanished before the tool ran, with
|
|
10
|
+
* no error: the documented FlattenObject -> UnflattenObject round trip lost
|
|
11
|
+
* it, and a `__proto__` column read by CsvParse was not written by CsvWrite.
|
|
12
|
+
*
|
|
13
|
+
* `ownRecord(values)` is a `ZodRecord` in every way a reader of the schema
|
|
14
|
+
* can see (the model is shown the same JSON Schema, and introspection finds
|
|
15
|
+
* a record), so only the parse differs: after zod's own checks, an own
|
|
16
|
+
* `__proto__` key is validated with the same value schema and put back as
|
|
17
|
+
* an own data property, at its place in the key order. Nothing is ever
|
|
18
|
+
* assigned through `obj.__proto__`, so no prototype changes.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
/** `z.record(values)`, except that a key named `__proto__` is kept as data. */
|
|
22
|
+
export declare function ownRecord<Value extends z.ZodTypeAny>(values: Value): z.ZodRecord<z.ZodString, Value>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `z.record` that keeps a key named `__proto__`.
|
|
3
|
+
*
|
|
4
|
+
* zod rebuilds a record into a fresh object and skips `__proto__` while it
|
|
5
|
+
* does (so the rebuild cannot set the new object's prototype). That is the
|
|
6
|
+
* right call for zod, and the wrong one for a data tool: `{"__proto__": 1}`
|
|
7
|
+
* is valid JSON, `JSON.parse` keeps it as an own property, and CsvParse,
|
|
8
|
+
* FlattenObject and JsonParse hand it back as one. Passed on to TableQuery,
|
|
9
|
+
* CsvWrite or UnflattenObject, the field vanished before the tool ran, with
|
|
10
|
+
* no error: the documented FlattenObject -> UnflattenObject round trip lost
|
|
11
|
+
* it, and a `__proto__` column read by CsvParse was not written by CsvWrite.
|
|
12
|
+
*
|
|
13
|
+
* `ownRecord(values)` is a `ZodRecord` in every way a reader of the schema
|
|
14
|
+
* can see (the model is shown the same JSON Schema, and introspection finds
|
|
15
|
+
* a record), so only the parse differs: after zod's own checks, an own
|
|
16
|
+
* `__proto__` key is validated with the same value schema and put back as
|
|
17
|
+
* an own data property, at its place in the key order. Nothing is ever
|
|
18
|
+
* assigned through `obj.__proto__`, so no prototype changes.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
const PROTO_KEY = "__proto__";
|
|
22
|
+
function isRecordLike(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
class ZodOwnRecord extends z.ZodRecord {
|
|
26
|
+
_parse(input) {
|
|
27
|
+
const result = super._parse(input);
|
|
28
|
+
const raw = input.data;
|
|
29
|
+
if (!isRecordLike(raw) || !Object.hasOwn(raw, PROTO_KEY))
|
|
30
|
+
return result;
|
|
31
|
+
const restore = (parsed) => {
|
|
32
|
+
if (parsed.status === "aborted")
|
|
33
|
+
return parsed;
|
|
34
|
+
const ownValue = this._def.valueType.safeParse(raw[PROTO_KEY]);
|
|
35
|
+
if (!ownValue.success) {
|
|
36
|
+
const ctx = this._getOrReturnCtx(input);
|
|
37
|
+
for (const issue of ownValue.error.issues) {
|
|
38
|
+
z.addIssueToContext(ctx, { ...issue, path: [...input.path, PROTO_KEY, ...issue.path] });
|
|
39
|
+
}
|
|
40
|
+
return z.INVALID;
|
|
41
|
+
}
|
|
42
|
+
// Rebuild in the caller's key order, with `__proto__` as a plain own
|
|
43
|
+
// property where it stood.
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const key of Object.keys(raw)) {
|
|
46
|
+
const value = key === PROTO_KEY ? ownValue.data : parsed.value[key];
|
|
47
|
+
Object.defineProperty(out, key, {
|
|
48
|
+
value,
|
|
49
|
+
enumerable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { status: parsed.status, value: out };
|
|
55
|
+
};
|
|
56
|
+
return result instanceof Promise ? result.then(restore) : restore(result);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** `z.record(values)`, except that a key named `__proto__` is kept as data. */
|
|
60
|
+
export function ownRecord(values) {
|
|
61
|
+
return new ZodOwnRecord({
|
|
62
|
+
keyType: z.string(),
|
|
63
|
+
valueType: values,
|
|
64
|
+
typeName: z.ZodFirstPartyTypeKind.ZodRecord,
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/lib/patch.d.ts
CHANGED
|
@@ -51,16 +51,32 @@ export type PatchOp = {
|
|
|
51
51
|
path: string;
|
|
52
52
|
value: unknown;
|
|
53
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* The most values a patch may add to a document, over all its operations:
|
|
56
|
+
* about as many as the largest document this package reads holds. Each
|
|
57
|
+
* `copy` of the root doubles the document, so twenty of them on a
|
|
58
|
+
* seven-character document made 13.6 million characters.
|
|
59
|
+
*/
|
|
60
|
+
export declare const MAX_PATCH_ADDED_NODES = 2000000;
|
|
54
61
|
/**
|
|
55
62
|
* Apply an RFC 6902 patch. Returns the new document; the input is never
|
|
56
63
|
* mutated. Throws `PatchError` (carrying the failing operation's index) if
|
|
57
|
-
* any operation cannot be applied, leaving nothing half-done
|
|
64
|
+
* any operation cannot be applied, leaving nothing half-done — including an
|
|
65
|
+
* operation that would take the values the patch adds (by add, replace or
|
|
66
|
+
* copy) past `maxAddedNodes`.
|
|
58
67
|
*/
|
|
59
|
-
export declare function applyJsonPatch(doc: unknown, ops: ReadonlyArray<PatchOp
|
|
68
|
+
export declare function applyJsonPatch(doc: unknown, ops: ReadonlyArray<PatchOp>, maxAddedNodes?: number): unknown;
|
|
60
69
|
/**
|
|
61
70
|
* Apply an RFC 7386 merge patch: an object merges key by key, `null` deletes
|
|
62
71
|
* a key, and any non-object patch replaces the target outright. Arrays are
|
|
63
72
|
* replaced whole — that is the RFC's behaviour, not a shortcut here.
|
|
73
|
+
*
|
|
74
|
+
* The result is a fresh value that shares nothing with either input, and
|
|
75
|
+
* every node of it is built exactly once: a target key the patch leaves
|
|
76
|
+
* alone is cloned, and one it touches is merged from the ORIGINAL child.
|
|
77
|
+
* (0.7.0 deep-cloned the target and then recursed into the clone, cloning
|
|
78
|
+
* the rest of the tree again at every level — quadratic in depth.) Keys keep
|
|
79
|
+
* the target's order, with new keys after them in the patch's order.
|
|
64
80
|
*/
|
|
65
81
|
export declare function applyMergePatch(target: unknown, patch: unknown): unknown;
|
|
66
82
|
/**
|
package/dist/lib/patch.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* leaves the caller's input untouched and reports which operation index
|
|
8
8
|
* failed — which is the behaviour RFC 6902 §5 requires.
|
|
9
9
|
*/
|
|
10
|
-
import { deepClone, deepEqual, isPlainObject } from "./json";
|
|
10
|
+
import { deepClone, deepEqual, isPlainObject, setOwn } from "./json";
|
|
11
11
|
export class PatchError extends Error {
|
|
12
12
|
opIndex;
|
|
13
13
|
constructor(message, opIndex) {
|
|
@@ -109,7 +109,9 @@ function addAt(parent, token, value, pointer, i) {
|
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
if (isPlainObject(parent)) {
|
|
112
|
-
|
|
112
|
+
// An own member even for `__proto__`: assignment would call the setter,
|
|
113
|
+
// change nothing, and report success.
|
|
114
|
+
setOwn(parent, token, value);
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
throw new PatchError(`cannot add to a ${typeof parent} at "${pointer}"`, i);
|
|
@@ -135,13 +137,45 @@ function removeAt(parent, token, pointer, i) {
|
|
|
135
137
|
}
|
|
136
138
|
throw new PatchError(`cannot remove from a ${typeof parent} at "${pointer}"`, i);
|
|
137
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The most values a patch may add to a document, over all its operations:
|
|
142
|
+
* about as many as the largest document this package reads holds. Each
|
|
143
|
+
* `copy` of the root doubles the document, so twenty of them on a
|
|
144
|
+
* seven-character document made 13.6 million characters.
|
|
145
|
+
*/
|
|
146
|
+
export const MAX_PATCH_ADDED_NODES = 2_000_000;
|
|
147
|
+
/** Charge the values in `value` to the patch's budget; throw once it is spent. */
|
|
148
|
+
function chargeNodes(value, budget, opIndex) {
|
|
149
|
+
const stack = [value];
|
|
150
|
+
while (stack.length > 0) {
|
|
151
|
+
const node = stack.pop();
|
|
152
|
+
budget.left -= 1;
|
|
153
|
+
if (budget.left < 0) {
|
|
154
|
+
throw new PatchError(`the patch would add more than ${MAX_PATCH_ADDED_NODES} values to the document`, opIndex);
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(node))
|
|
157
|
+
for (const el of node)
|
|
158
|
+
stack.push(el);
|
|
159
|
+
else if (isPlainObject(node))
|
|
160
|
+
for (const k of Object.keys(node))
|
|
161
|
+
stack.push(node[k]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
138
164
|
/**
|
|
139
165
|
* Apply an RFC 6902 patch. Returns the new document; the input is never
|
|
140
166
|
* mutated. Throws `PatchError` (carrying the failing operation's index) if
|
|
141
|
-
* any operation cannot be applied, leaving nothing half-done
|
|
167
|
+
* any operation cannot be applied, leaving nothing half-done — including an
|
|
168
|
+
* operation that would take the values the patch adds (by add, replace or
|
|
169
|
+
* copy) past `maxAddedNodes`.
|
|
142
170
|
*/
|
|
143
|
-
export function applyJsonPatch(doc, ops) {
|
|
171
|
+
export function applyJsonPatch(doc, ops, maxAddedNodes = MAX_PATCH_ADDED_NODES) {
|
|
144
172
|
let working = deepClone(doc);
|
|
173
|
+
const budget = { left: maxAddedNodes };
|
|
174
|
+
// Every value an operation adds is counted before it is cloned in.
|
|
175
|
+
const added = (value, i) => {
|
|
176
|
+
chargeNodes(value, budget, i);
|
|
177
|
+
return deepClone(value);
|
|
178
|
+
};
|
|
145
179
|
ops.forEach((op, i) => {
|
|
146
180
|
switch (op.op) {
|
|
147
181
|
case "test": {
|
|
@@ -155,11 +189,11 @@ export function applyJsonPatch(doc, ops) {
|
|
|
155
189
|
}
|
|
156
190
|
case "add": {
|
|
157
191
|
if (op.path === "") {
|
|
158
|
-
working =
|
|
192
|
+
working = added(op.value, i);
|
|
159
193
|
return;
|
|
160
194
|
}
|
|
161
195
|
const { parent, token } = locateParent(working, op.path, i);
|
|
162
|
-
addAt(parent, token,
|
|
196
|
+
addAt(parent, token, added(op.value, i), op.path, i);
|
|
163
197
|
return;
|
|
164
198
|
}
|
|
165
199
|
case "remove": {
|
|
@@ -171,7 +205,7 @@ export function applyJsonPatch(doc, ops) {
|
|
|
171
205
|
}
|
|
172
206
|
case "replace": {
|
|
173
207
|
if (op.path === "") {
|
|
174
|
-
working =
|
|
208
|
+
working = added(op.value, i);
|
|
175
209
|
return;
|
|
176
210
|
}
|
|
177
211
|
const existing = resolvePointer(working, op.path);
|
|
@@ -180,7 +214,7 @@ export function applyJsonPatch(doc, ops) {
|
|
|
180
214
|
}
|
|
181
215
|
const { parent, token } = locateParent(working, op.path, i);
|
|
182
216
|
removeAt(parent, token, op.path, i);
|
|
183
|
-
addAt(parent, token,
|
|
217
|
+
addAt(parent, token, added(op.value, i), op.path, i);
|
|
184
218
|
return;
|
|
185
219
|
}
|
|
186
220
|
case "move": {
|
|
@@ -210,11 +244,11 @@ export function applyJsonPatch(doc, ops) {
|
|
|
210
244
|
if (!src.found)
|
|
211
245
|
throw new PatchError(`cannot copy from "${op.from}": ${src.reason}`, i);
|
|
212
246
|
if (op.path === "") {
|
|
213
|
-
working =
|
|
247
|
+
working = added(src.value, i);
|
|
214
248
|
return;
|
|
215
249
|
}
|
|
216
250
|
const dst = locateParent(working, op.path, i);
|
|
217
|
-
addAt(dst.parent, dst.token,
|
|
251
|
+
addAt(dst.parent, dst.token, added(src.value, i), op.path, i);
|
|
218
252
|
return;
|
|
219
253
|
}
|
|
220
254
|
default: {
|
|
@@ -229,21 +263,38 @@ export function applyJsonPatch(doc, ops) {
|
|
|
229
263
|
* Apply an RFC 7386 merge patch: an object merges key by key, `null` deletes
|
|
230
264
|
* a key, and any non-object patch replaces the target outright. Arrays are
|
|
231
265
|
* replaced whole — that is the RFC's behaviour, not a shortcut here.
|
|
266
|
+
*
|
|
267
|
+
* The result is a fresh value that shares nothing with either input, and
|
|
268
|
+
* every node of it is built exactly once: a target key the patch leaves
|
|
269
|
+
* alone is cloned, and one it touches is merged from the ORIGINAL child.
|
|
270
|
+
* (0.7.0 deep-cloned the target and then recursed into the clone, cloning
|
|
271
|
+
* the rest of the tree again at every level — quadratic in depth.) Keys keep
|
|
272
|
+
* the target's order, with new keys after them in the patch's order.
|
|
232
273
|
*/
|
|
233
274
|
export function applyMergePatch(target, patch) {
|
|
234
275
|
if (!isPlainObject(patch))
|
|
235
276
|
return deepClone(patch);
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
277
|
+
const out = {};
|
|
278
|
+
const source = isPlainObject(target) ? target : null;
|
|
279
|
+
if (source !== null) {
|
|
280
|
+
for (const key of Object.keys(source)) {
|
|
281
|
+
if (!Object.hasOwn(patch, key)) {
|
|
282
|
+
setOwn(out, key, deepClone(source[key]));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const value = patch[key];
|
|
286
|
+
if (value !== null)
|
|
287
|
+
setOwn(out, key, applyMergePatch(source[key], value));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
239
290
|
for (const key of Object.keys(patch)) {
|
|
291
|
+
if (source !== null && Object.hasOwn(source, key))
|
|
292
|
+
continue;
|
|
240
293
|
const value = patch[key];
|
|
241
|
-
if (value
|
|
242
|
-
|
|
243
|
-
else
|
|
244
|
-
base[key] = applyMergePatch(base[key], value);
|
|
294
|
+
if (value !== null)
|
|
295
|
+
setOwn(out, key, applyMergePatch(undefined, value));
|
|
245
296
|
}
|
|
246
|
-
return
|
|
297
|
+
return out;
|
|
247
298
|
}
|
|
248
299
|
/**
|
|
249
300
|
* Derive the RFC 7386 merge patch that turns `from` into `to`. Returns the
|
|
@@ -257,16 +308,16 @@ export function diffMergePatch(from, to) {
|
|
|
257
308
|
const out = {};
|
|
258
309
|
for (const key of Object.keys(to)) {
|
|
259
310
|
if (!Object.hasOwn(from, key)) {
|
|
260
|
-
out
|
|
311
|
+
setOwn(out, key, deepClone(to[key]));
|
|
261
312
|
continue;
|
|
262
313
|
}
|
|
263
314
|
if (deepEqual(from[key], to[key]))
|
|
264
315
|
continue;
|
|
265
|
-
out
|
|
316
|
+
setOwn(out, key, diffMergePatch(from[key], to[key]));
|
|
266
317
|
}
|
|
267
318
|
for (const key of Object.keys(from)) {
|
|
268
319
|
if (!Object.hasOwn(to, key))
|
|
269
|
-
out
|
|
320
|
+
setOwn(out, key, null);
|
|
270
321
|
}
|
|
271
322
|
return out;
|
|
272
323
|
}
|