@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/table.d.ts
CHANGED
|
@@ -7,10 +7,20 @@
|
|
|
7
7
|
* two rows compare equal the original order is kept, so the same input gives
|
|
8
8
|
* the same output every time.
|
|
9
9
|
*/
|
|
10
|
+
import { type RegexAnswers } from "@crewhaus/tool-schema";
|
|
10
11
|
export type Record_ = Record<string, unknown>;
|
|
11
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Read a dotted path out of a record. Returns undefined for any miss. Only
|
|
14
|
+
* the record's own fields are read: `constructor` on a record that has no
|
|
15
|
+
* such field is a miss, not `Object.prototype.constructor`.
|
|
16
|
+
*/
|
|
12
17
|
export declare function getPath(record: unknown, path: string): unknown;
|
|
13
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Write a dotted path into a record, creating intermediate objects. Every
|
|
20
|
+
* segment is an own field of the object it names — descent never follows an
|
|
21
|
+
* inherited member, so `__proto__.x` writes a field called `__proto__`, not
|
|
22
|
+
* into `Object.prototype`.
|
|
23
|
+
*/
|
|
14
24
|
export declare function setPath(target: Record_, path: string, value: unknown): void;
|
|
15
25
|
export type Comparison = "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith" | "in" | "matches" | "exists" | "empty";
|
|
16
26
|
export type Condition = {
|
|
@@ -26,8 +36,21 @@ export type Predicate = {
|
|
|
26
36
|
};
|
|
27
37
|
export declare class PredicateError extends Error {
|
|
28
38
|
}
|
|
29
|
-
/**
|
|
30
|
-
export
|
|
39
|
+
/** A predicate's answer for one record: a verdict, or undetermined (a pattern had no answer). */
|
|
40
|
+
export type PredicateVerdict = boolean | "undetermined";
|
|
41
|
+
/**
|
|
42
|
+
* Ask `regex` about every `matches` condition in `predicate` for every
|
|
43
|
+
* record, so one resolve in the regex worker answers them all before
|
|
44
|
+
* {@link testPredicate} reads them.
|
|
45
|
+
*/
|
|
46
|
+
export declare function askPredicatePatterns(records: ReadonlyArray<unknown>, predicate: Predicate, regex: RegexAnswers): void;
|
|
47
|
+
/**
|
|
48
|
+
* Evaluate one condition against a record. A `matches` pattern is never run
|
|
49
|
+
* here: its answer comes from `regex`, resolved in the worker by the tool
|
|
50
|
+
* (or, for a library caller with none, from the bounded fallback on this
|
|
51
|
+
* thread), and one with no answer is `"undetermined"`, never `false`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function testCondition(record: unknown, cond: Condition, regex?: RegexAnswers): PredicateVerdict;
|
|
31
54
|
/**
|
|
32
55
|
* Order two scalars, or null when they are not comparable. Numbers compare
|
|
33
56
|
* numerically, strings by code unit, booleans false-before-true; a
|
|
@@ -35,8 +58,16 @@ export declare function testCondition(record: unknown, cond: Condition): boolean
|
|
|
35
58
|
* quietly being true.
|
|
36
59
|
*/
|
|
37
60
|
export declare function compareValues(a: unknown, b: unknown): number | null;
|
|
38
|
-
/**
|
|
39
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Evaluate a whole predicate. An empty predicate matches everything.
|
|
63
|
+
*
|
|
64
|
+
* Three-valued, so a condition with no answer cannot decide a record either
|
|
65
|
+
* way on its own: `all` fails on any definite miss, `none` excludes on any
|
|
66
|
+
* definite hit, `any` holds on any definite hit; otherwise a condition with
|
|
67
|
+
* no answer leaves the record `"undetermined"`. Reading it as a miss would
|
|
68
|
+
* let a `none` exclusion pass the very row it was written to stop.
|
|
69
|
+
*/
|
|
70
|
+
export declare function testPredicate(record: unknown, predicate: Predicate, regex?: RegexAnswers): PredicateVerdict;
|
|
40
71
|
export type SortKey = {
|
|
41
72
|
field: string;
|
|
42
73
|
direction?: "asc" | "desc";
|
|
@@ -111,11 +142,17 @@ export type JoinOptions = {
|
|
|
111
142
|
* how a join quietly loses rows.
|
|
112
143
|
*
|
|
113
144
|
* Every right-hand match is emitted, so a one-to-many join multiplies rows,
|
|
114
|
-
* as SQL does. `maxRows` bounds that
|
|
145
|
+
* as SQL does. `maxRows` bounds that — the WORK as well as the output: a row
|
|
146
|
+
* past the cap is counted, never built, so a join on a low-cardinality key
|
|
147
|
+
* (every row `status: "open"`) costs the rows it returns plus one pass over
|
|
148
|
+
* each side, not |left| x |right| merges thrown away. `totalRows` is the size
|
|
149
|
+
* the uncapped join would have had, counted arithmetically, so a caller can
|
|
150
|
+
* tell ten rows cut from eleven apart from ten cut from millions.
|
|
115
151
|
*/
|
|
116
152
|
export declare function joinRecords(left: ReadonlyArray<Record_>, right: ReadonlyArray<Record_>, options: JoinOptions): {
|
|
117
153
|
rows: Record_[];
|
|
118
154
|
truncated: boolean;
|
|
155
|
+
totalRows: number;
|
|
119
156
|
unmatchedLeft: number;
|
|
120
157
|
unmatchedRight: number;
|
|
121
158
|
};
|
|
@@ -130,7 +167,7 @@ export declare function columnsToRecords(columns: Record<string, ReadonlyArray<u
|
|
|
130
167
|
ragged: string[];
|
|
131
168
|
};
|
|
132
169
|
/** Flatten nested objects into dotted keys. Arrays become `a.0`, `a.1` when expanded. */
|
|
133
|
-
export declare function flattenObject(value: unknown, separator: string, expandArrays: boolean, maxDepth: number): Record<string, unknown>;
|
|
170
|
+
export declare function flattenObject(value: unknown, separator: string, expandArrays: boolean, maxDepth: number, maxKeyChars?: number): Record<string, unknown>;
|
|
134
171
|
/**
|
|
135
172
|
* Rebuild nested objects from dotted keys. A numeric segment becomes an
|
|
136
173
|
* array index when `arraysFromNumericKeys` is on and the keys under that
|
package/dist/lib/table.js
CHANGED
|
@@ -7,15 +7,20 @@
|
|
|
7
7
|
* two rows compare equal the original order is kept, so the same input gives
|
|
8
8
|
* the same output every time.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import { testPatternSync } from "@crewhaus/tool-schema";
|
|
11
|
+
import { OutputLimitError, canonicalStringify, deepEqual, getOwn, isPlainObject, setOwn, } from "./json";
|
|
12
|
+
/**
|
|
13
|
+
* Read a dotted path out of a record. Returns undefined for any miss. Only
|
|
14
|
+
* the record's own fields are read: `constructor` on a record that has no
|
|
15
|
+
* such field is a miss, not `Object.prototype.constructor`.
|
|
16
|
+
*/
|
|
12
17
|
export function getPath(record, path) {
|
|
13
18
|
if (path === "")
|
|
14
19
|
return record;
|
|
15
20
|
let cur = record;
|
|
16
21
|
for (const seg of path.split(".")) {
|
|
17
22
|
if (isPlainObject(cur)) {
|
|
18
|
-
cur = cur
|
|
23
|
+
cur = getOwn(cur, seg);
|
|
19
24
|
continue;
|
|
20
25
|
}
|
|
21
26
|
if (Array.isArray(cur) && /^\d+$/.test(seg)) {
|
|
@@ -26,28 +31,59 @@ export function getPath(record, path) {
|
|
|
26
31
|
}
|
|
27
32
|
return cur;
|
|
28
33
|
}
|
|
29
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Write a dotted path into a record, creating intermediate objects. Every
|
|
36
|
+
* segment is an own field of the object it names — descent never follows an
|
|
37
|
+
* inherited member, so `__proto__.x` writes a field called `__proto__`, not
|
|
38
|
+
* into `Object.prototype`.
|
|
39
|
+
*/
|
|
30
40
|
export function setPath(target, path, value) {
|
|
31
41
|
const segs = path.split(".");
|
|
32
42
|
let cur = target;
|
|
33
43
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
34
44
|
const seg = segs[i];
|
|
35
|
-
const next = cur
|
|
45
|
+
const next = getOwn(cur, seg);
|
|
36
46
|
if (!isPlainObject(next)) {
|
|
37
47
|
const fresh = {};
|
|
38
|
-
cur
|
|
48
|
+
setOwn(cur, seg, fresh);
|
|
39
49
|
cur = fresh;
|
|
40
50
|
}
|
|
41
51
|
else {
|
|
42
52
|
cur = next;
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
|
-
cur
|
|
55
|
+
setOwn(cur, segs[segs.length - 1], value);
|
|
46
56
|
}
|
|
47
57
|
export class PredicateError extends Error {
|
|
48
58
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Ask `regex` about every `matches` condition in `predicate` for every
|
|
61
|
+
* record, so one resolve in the regex worker answers them all before
|
|
62
|
+
* {@link testPredicate} reads them.
|
|
63
|
+
*/
|
|
64
|
+
export function askPredicatePatterns(records, predicate, regex) {
|
|
65
|
+
const conditions = [
|
|
66
|
+
...(predicate.all ?? []),
|
|
67
|
+
...(predicate.any ?? []),
|
|
68
|
+
...(predicate.none ?? []),
|
|
69
|
+
].filter((c) => c.op === "matches" && typeof c.value === "string");
|
|
70
|
+
if (conditions.length === 0)
|
|
71
|
+
return;
|
|
72
|
+
for (const record of records) {
|
|
73
|
+
for (const cond of conditions) {
|
|
74
|
+
const actual = getPath(record, cond.field);
|
|
75
|
+
if (actual !== undefined && actual !== null)
|
|
76
|
+
regex.lookup(cond.value, "", String(actual));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Evaluate one condition against a record. A `matches` pattern is never run
|
|
82
|
+
* here: its answer comes from `regex`, resolved in the worker by the tool
|
|
83
|
+
* (or, for a library caller with none, from the bounded fallback on this
|
|
84
|
+
* thread), and one with no answer is `"undetermined"`, never `false`.
|
|
85
|
+
*/
|
|
86
|
+
export function testCondition(record, cond, regex) {
|
|
51
87
|
const actual = getPath(record, cond.field);
|
|
52
88
|
const expected = cond.value;
|
|
53
89
|
switch (cond.op) {
|
|
@@ -78,14 +114,25 @@ export function testCondition(record, cond) {
|
|
|
78
114
|
if (typeof expected !== "string") {
|
|
79
115
|
throw new PredicateError(`"matches" needs a regular-expression string for ${cond.field}`);
|
|
80
116
|
}
|
|
81
|
-
let re;
|
|
82
117
|
try {
|
|
83
|
-
|
|
118
|
+
// Compiling reads only the pattern; the match is what is not run here.
|
|
119
|
+
new RegExp(expected);
|
|
84
120
|
}
|
|
85
121
|
catch (err) {
|
|
86
122
|
throw new PredicateError(`invalid regex for ${cond.field}: ${err.message}`);
|
|
87
123
|
}
|
|
88
|
-
|
|
124
|
+
if (actual === undefined || actual === null)
|
|
125
|
+
return false;
|
|
126
|
+
const subject = String(actual);
|
|
127
|
+
const answer = regex === undefined
|
|
128
|
+
? testPatternSync(expected, "", subject)
|
|
129
|
+
: regex.lookup(expected, "", subject);
|
|
130
|
+
if (typeof answer === "boolean")
|
|
131
|
+
return answer;
|
|
132
|
+
if (answer !== undefined && "refused" in answer) {
|
|
133
|
+
throw new PredicateError(`invalid regex for ${cond.field}: ${answer.refused}`);
|
|
134
|
+
}
|
|
135
|
+
return "undetermined";
|
|
89
136
|
}
|
|
90
137
|
default:
|
|
91
138
|
return compareOrdered(actual, expected, cond.op);
|
|
@@ -120,20 +167,52 @@ export function compareValues(a, b) {
|
|
|
120
167
|
return a === b ? 0 : a ? 1 : -1;
|
|
121
168
|
return null;
|
|
122
169
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Evaluate a whole predicate. An empty predicate matches everything.
|
|
172
|
+
*
|
|
173
|
+
* Three-valued, so a condition with no answer cannot decide a record either
|
|
174
|
+
* way on its own: `all` fails on any definite miss, `none` excludes on any
|
|
175
|
+
* definite hit, `any` holds on any definite hit; otherwise a condition with
|
|
176
|
+
* no answer leaves the record `"undetermined"`. Reading it as a miss would
|
|
177
|
+
* let a `none` exclusion pass the very row it was written to stop.
|
|
178
|
+
*/
|
|
179
|
+
export function testPredicate(record, predicate, regex) {
|
|
125
180
|
const all = predicate.all ?? [];
|
|
126
181
|
const any = predicate.any ?? [];
|
|
127
182
|
const none = predicate.none ?? [];
|
|
128
|
-
|
|
129
|
-
|
|
183
|
+
let open = false;
|
|
184
|
+
for (const c of all) {
|
|
185
|
+
const v = testCondition(record, c, regex);
|
|
186
|
+
if (v === false)
|
|
130
187
|
return false;
|
|
131
|
-
|
|
132
|
-
|
|
188
|
+
if (v === "undetermined")
|
|
189
|
+
open = true;
|
|
190
|
+
}
|
|
191
|
+
for (const c of none) {
|
|
192
|
+
const v = testCondition(record, c, regex);
|
|
193
|
+
if (v === true)
|
|
133
194
|
return false;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
195
|
+
if (v === "undetermined")
|
|
196
|
+
open = true;
|
|
197
|
+
}
|
|
198
|
+
if (any.length > 0) {
|
|
199
|
+
let hit = false;
|
|
200
|
+
let anyOpen = false;
|
|
201
|
+
for (const c of any) {
|
|
202
|
+
const v = testCondition(record, c, regex);
|
|
203
|
+
if (v === true) {
|
|
204
|
+
hit = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (v === "undetermined")
|
|
208
|
+
anyOpen = true;
|
|
209
|
+
}
|
|
210
|
+
if (!hit && !anyOpen)
|
|
211
|
+
return false;
|
|
212
|
+
if (!hit)
|
|
213
|
+
open = true;
|
|
214
|
+
}
|
|
215
|
+
return open ? "undetermined" : true;
|
|
137
216
|
}
|
|
138
217
|
/**
|
|
139
218
|
* Sort records by one or more keys. The sort is stable and total: values of
|
|
@@ -243,10 +322,11 @@ export function omitFields(record, fields) {
|
|
|
243
322
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
244
323
|
if (!isPlainObject(cur))
|
|
245
324
|
break;
|
|
246
|
-
cur = cur
|
|
325
|
+
cur = getOwn(cur, segs[i]);
|
|
247
326
|
}
|
|
248
|
-
|
|
249
|
-
|
|
327
|
+
const last = segs[segs.length - 1];
|
|
328
|
+
if (isPlainObject(cur) && Object.hasOwn(cur, last))
|
|
329
|
+
delete cur[last];
|
|
250
330
|
}
|
|
251
331
|
return out;
|
|
252
332
|
}
|
|
@@ -254,7 +334,7 @@ function structuredCloneish(value) {
|
|
|
254
334
|
const out = {};
|
|
255
335
|
for (const k of Object.keys(value)) {
|
|
256
336
|
const v = value[k];
|
|
257
|
-
out
|
|
337
|
+
setOwn(out, k, isPlainObject(v) ? structuredCloneish(v) : v);
|
|
258
338
|
}
|
|
259
339
|
return out;
|
|
260
340
|
}
|
|
@@ -271,7 +351,7 @@ export function aggregate(records, groupBy, aggregations) {
|
|
|
271
351
|
for (const rec of records) {
|
|
272
352
|
const key = {};
|
|
273
353
|
for (const field of groupBy)
|
|
274
|
-
key
|
|
354
|
+
setOwn(key, field, getPath(rec, field) ?? null);
|
|
275
355
|
const id = canonicalStringify(key);
|
|
276
356
|
const existing = buckets.get(id);
|
|
277
357
|
if (existing === undefined)
|
|
@@ -288,37 +368,37 @@ export function aggregate(records, groupBy, aggregations) {
|
|
|
288
368
|
const values = bucket.rows.map((r) => getPath(r, field));
|
|
289
369
|
switch (agg.fn) {
|
|
290
370
|
case "count":
|
|
291
|
-
out
|
|
371
|
+
setOwn(out, agg.as, field === "" ? bucket.rows.length : values.filter(isPresent).length);
|
|
292
372
|
break;
|
|
293
373
|
case "sum":
|
|
294
374
|
case "avg": {
|
|
295
375
|
const nums = values.filter((v) => typeof v === "number");
|
|
296
376
|
skipped += values.filter(isPresent).length - nums.length;
|
|
297
377
|
const total = nums.reduce((a, b) => a + b, 0);
|
|
298
|
-
out
|
|
378
|
+
setOwn(out, agg.as, agg.fn === "sum" ? total : nums.length === 0 ? null : total / nums.length);
|
|
299
379
|
break;
|
|
300
380
|
}
|
|
301
381
|
case "min":
|
|
302
382
|
case "max": {
|
|
303
383
|
const present = values.filter(isPresent);
|
|
304
384
|
if (present.length === 0) {
|
|
305
|
-
out
|
|
385
|
+
setOwn(out, agg.as, null);
|
|
306
386
|
break;
|
|
307
387
|
}
|
|
308
|
-
out
|
|
388
|
+
setOwn(out, agg.as, present.reduce((best, v) => {
|
|
309
389
|
const cmp = totalCompare(v, best);
|
|
310
390
|
return (agg.fn === "min" ? cmp < 0 : cmp > 0) ? v : best;
|
|
311
|
-
});
|
|
391
|
+
}));
|
|
312
392
|
break;
|
|
313
393
|
}
|
|
314
394
|
case "first":
|
|
315
|
-
out
|
|
395
|
+
setOwn(out, agg.as, values[0] ?? null);
|
|
316
396
|
break;
|
|
317
397
|
case "last":
|
|
318
|
-
out
|
|
398
|
+
setOwn(out, agg.as, values[values.length - 1] ?? null);
|
|
319
399
|
break;
|
|
320
400
|
case "distinct":
|
|
321
|
-
out
|
|
401
|
+
setOwn(out, agg.as, new Set(values.filter(isPresent).map((v) => canonicalStringify(v))).size);
|
|
322
402
|
break;
|
|
323
403
|
default:
|
|
324
404
|
break;
|
|
@@ -337,7 +417,12 @@ function isPresent(v) {
|
|
|
337
417
|
* how a join quietly loses rows.
|
|
338
418
|
*
|
|
339
419
|
* Every right-hand match is emitted, so a one-to-many join multiplies rows,
|
|
340
|
-
* as SQL does. `maxRows` bounds that
|
|
420
|
+
* as SQL does. `maxRows` bounds that — the WORK as well as the output: a row
|
|
421
|
+
* past the cap is counted, never built, so a join on a low-cardinality key
|
|
422
|
+
* (every row `status: "open"`) costs the rows it returns plus one pass over
|
|
423
|
+
* each side, not |left| x |right| merges thrown away. `totalRows` is the size
|
|
424
|
+
* the uncapped join would have had, counted arithmetically, so a caller can
|
|
425
|
+
* tell ten rows cut from eleven apart from ten cut from millions.
|
|
341
426
|
*/
|
|
342
427
|
export function joinRecords(left, right, options) {
|
|
343
428
|
const index = new Map();
|
|
@@ -355,13 +440,14 @@ export function joinRecords(left, right, options) {
|
|
|
355
440
|
const rows = [];
|
|
356
441
|
const matchedRight = new Set();
|
|
357
442
|
let unmatchedLeft = 0;
|
|
358
|
-
let
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
rows.
|
|
443
|
+
let totalRows = 0;
|
|
444
|
+
// The cap is checked BEFORE a row is built. Building it first and then
|
|
445
|
+
// discarding it (the 0.7.0 shape) made maxRows bound the answer but not
|
|
446
|
+
// the work: every matching pair was merged whatever the cap said.
|
|
447
|
+
const keep = (build) => {
|
|
448
|
+
totalRows += 1;
|
|
449
|
+
if (rows.length < options.maxRows)
|
|
450
|
+
rows.push(build());
|
|
365
451
|
};
|
|
366
452
|
for (const l of left) {
|
|
367
453
|
const key = getPath(l, options.leftKey);
|
|
@@ -370,13 +456,16 @@ export function joinRecords(left, right, options) {
|
|
|
370
456
|
if (matches === undefined || matches.length === 0) {
|
|
371
457
|
unmatchedLeft += 1;
|
|
372
458
|
if (options.kind === "left" || options.kind === "full")
|
|
373
|
-
|
|
459
|
+
keep(() => ({ ...l }));
|
|
374
460
|
continue;
|
|
375
461
|
}
|
|
376
462
|
if (id !== null)
|
|
377
463
|
matchedRight.add(id);
|
|
378
|
-
|
|
379
|
-
|
|
464
|
+
const take = Math.min(Math.max(options.maxRows - rows.length, 0), matches.length);
|
|
465
|
+
for (let i = 0; i < take; i++) {
|
|
466
|
+
rows.push(mergeRow(l, matches[i], options.rightPrefix));
|
|
467
|
+
}
|
|
468
|
+
totalRows += matches.length;
|
|
380
469
|
}
|
|
381
470
|
let unmatchedRight = 0;
|
|
382
471
|
for (const r of right) {
|
|
@@ -385,15 +474,21 @@ export function joinRecords(left, right, options) {
|
|
|
385
474
|
if (id === null || !matchedRight.has(id)) {
|
|
386
475
|
unmatchedRight += 1;
|
|
387
476
|
if (options.kind === "right" || options.kind === "full")
|
|
388
|
-
|
|
477
|
+
keep(() => ({ ...r }));
|
|
389
478
|
}
|
|
390
479
|
}
|
|
391
|
-
return {
|
|
480
|
+
return {
|
|
481
|
+
rows,
|
|
482
|
+
truncated: totalRows > rows.length,
|
|
483
|
+
totalRows,
|
|
484
|
+
unmatchedLeft,
|
|
485
|
+
unmatchedRight,
|
|
486
|
+
};
|
|
392
487
|
}
|
|
393
488
|
function mergeRow(left, right, prefix) {
|
|
394
489
|
const out = { ...left };
|
|
395
490
|
for (const k of Object.keys(right)) {
|
|
396
|
-
out
|
|
491
|
+
setOwn(out, Object.hasOwn(left, k) ? `${prefix}${k}` : k, right[k]);
|
|
397
492
|
}
|
|
398
493
|
return out;
|
|
399
494
|
}
|
|
@@ -413,7 +508,7 @@ export function recordsToColumns(records) {
|
|
|
413
508
|
}
|
|
414
509
|
const out = {};
|
|
415
510
|
for (const k of keys)
|
|
416
|
-
out
|
|
511
|
+
setOwn(out, k, records.map((r) => (Object.hasOwn(r, k) ? r[k] : null)));
|
|
417
512
|
return out;
|
|
418
513
|
}
|
|
419
514
|
/**
|
|
@@ -430,25 +525,35 @@ export function columnsToRecords(columns) {
|
|
|
430
525
|
const rec = {};
|
|
431
526
|
for (const k of keys) {
|
|
432
527
|
const col = columns[k] ?? [];
|
|
433
|
-
rec
|
|
528
|
+
setOwn(rec, k, i < col.length ? col[i] : null);
|
|
434
529
|
}
|
|
435
530
|
records.push(rec);
|
|
436
531
|
}
|
|
437
532
|
return { records, ragged };
|
|
438
533
|
}
|
|
439
534
|
/** Flatten nested objects into dotted keys. Arrays become `a.0`, `a.1` when expanded. */
|
|
440
|
-
export function flattenObject(value, separator, expandArrays, maxDepth) {
|
|
535
|
+
export function flattenObject(value, separator, expandArrays, maxDepth, maxKeyChars = Number.POSITIVE_INFINITY) {
|
|
441
536
|
const out = {};
|
|
537
|
+
// Every flattened key spells its whole path, so the keys alone are
|
|
538
|
+
// depth x leaves characters; `maxKeyChars` stops the walk (throwing
|
|
539
|
+
// OutputLimitError) before they are all built.
|
|
540
|
+
let keyChars = 0;
|
|
541
|
+
const put = (key, v) => {
|
|
542
|
+
keyChars += key.length;
|
|
543
|
+
if (keyChars > maxKeyChars)
|
|
544
|
+
throw new OutputLimitError(maxKeyChars, "the flattened keys");
|
|
545
|
+
setOwn(out, key, v);
|
|
546
|
+
};
|
|
442
547
|
const walk = (node, prefix, depth) => {
|
|
443
548
|
if (depth >= maxDepth) {
|
|
444
|
-
|
|
549
|
+
put(prefix, node);
|
|
445
550
|
return;
|
|
446
551
|
}
|
|
447
552
|
if (isPlainObject(node)) {
|
|
448
553
|
const keys = Object.keys(node);
|
|
449
554
|
if (keys.length === 0) {
|
|
450
555
|
if (prefix !== "")
|
|
451
|
-
|
|
556
|
+
put(prefix, {});
|
|
452
557
|
return;
|
|
453
558
|
}
|
|
454
559
|
for (const k of keys)
|
|
@@ -458,13 +563,13 @@ export function flattenObject(value, separator, expandArrays, maxDepth) {
|
|
|
458
563
|
if (Array.isArray(node) && expandArrays) {
|
|
459
564
|
if (node.length === 0) {
|
|
460
565
|
if (prefix !== "")
|
|
461
|
-
|
|
566
|
+
put(prefix, []);
|
|
462
567
|
return;
|
|
463
568
|
}
|
|
464
569
|
node.forEach((el, i) => walk(el, prefix === "" ? String(i) : `${prefix}${separator}${i}`, depth + 1));
|
|
465
570
|
return;
|
|
466
571
|
}
|
|
467
|
-
|
|
572
|
+
put(prefix, node);
|
|
468
573
|
};
|
|
469
574
|
walk(value, "", 0);
|
|
470
575
|
return out;
|
|
@@ -482,17 +587,17 @@ export function unflattenObject(flat, separator, arraysFromNumericKeys) {
|
|
|
482
587
|
let cur = root;
|
|
483
588
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
484
589
|
const seg = segs[i];
|
|
485
|
-
const next = cur
|
|
590
|
+
const next = getOwn(cur, seg);
|
|
486
591
|
if (!isPlainObject(next)) {
|
|
487
592
|
const fresh = {};
|
|
488
|
-
cur
|
|
593
|
+
setOwn(cur, seg, fresh);
|
|
489
594
|
cur = fresh;
|
|
490
595
|
}
|
|
491
596
|
else {
|
|
492
597
|
cur = next;
|
|
493
598
|
}
|
|
494
599
|
}
|
|
495
|
-
cur
|
|
600
|
+
setOwn(cur, segs[segs.length - 1], flat[key]);
|
|
496
601
|
}
|
|
497
602
|
return arraysFromNumericKeys ? arrayify(root) : root;
|
|
498
603
|
}
|
|
@@ -502,7 +607,7 @@ function arrayify(node) {
|
|
|
502
607
|
const keys = Object.keys(node);
|
|
503
608
|
const converted = {};
|
|
504
609
|
for (const k of keys)
|
|
505
|
-
converted
|
|
610
|
+
setOwn(converted, k, arrayify(node[k]));
|
|
506
611
|
if (keys.length === 0)
|
|
507
612
|
return converted;
|
|
508
613
|
const allNumeric = keys.every((k) => /^(0|[1-9][0-9]*)$/.test(k));
|
package/dist/lib/toml.d.ts
CHANGED
|
@@ -43,6 +43,13 @@ export declare class TomlError extends Error {
|
|
|
43
43
|
readonly line: number;
|
|
44
44
|
constructor(message: string, line: number);
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* A key or table path with more parts than MAX_NESTING_DEPTH, refused as it
|
|
48
|
+
* is read: the value it builds would nest past the cap and be refused
|
|
49
|
+
* anyway, and building it first cost the square of the path's length.
|
|
50
|
+
*/
|
|
51
|
+
export declare class TomlTooDeepError extends TomlError {
|
|
52
|
+
}
|
|
46
53
|
/** Parse a TOML document into JSON values. Throws `TomlError` with a line number. */
|
|
47
54
|
export declare function parseToml(text: string): Record<string, unknown>;
|
|
48
55
|
/** Type a bare TOML token. Exported so the tests can pin the exact rules. */
|
|
@@ -56,4 +63,4 @@ export type TomlWriteResult = {
|
|
|
56
63
|
* keys that were dropped because TOML has no representation for them
|
|
57
64
|
* (`null` and `undefined`).
|
|
58
65
|
*/
|
|
59
|
-
export declare function stringifyToml(value: unknown): TomlWriteResult;
|
|
66
|
+
export declare function stringifyToml(value: unknown, maxChars?: number): TomlWriteResult;
|