@crewhaus/tool-data 0.7.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/README.md +193 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +988 -0
- package/dist/lib/csv.d.ts +80 -0
- package/dist/lib/csv.js +240 -0
- package/dist/lib/diff.d.ts +44 -0
- package/dist/lib/diff.js +166 -0
- package/dist/lib/json.d.ts +65 -0
- package/dist/lib/json.js +133 -0
- package/dist/lib/jsonl.d.ts +39 -0
- package/dist/lib/jsonl.js +63 -0
- package/dist/lib/jsonpath.d.ts +91 -0
- package/dist/lib/jsonpath.js +430 -0
- package/dist/lib/patch.d.ts +72 -0
- package/dist/lib/patch.js +272 -0
- package/dist/lib/table.d.ts +156 -0
- package/dist/lib/table.js +571 -0
- package/dist/lib/toml.d.ts +59 -0
- package/dist/lib/toml.js +530 -0
- package/dist/lib/xml.d.ts +72 -0
- package/dist/lib/xml.js +344 -0
- package/dist/lib/yaml.d.ts +61 -0
- package/dist/lib/yaml.js +598 -0
- package/package.json +45 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Pointer (RFC 6901), JSON Patch (RFC 6902) and JSON Merge Patch
|
|
3
|
+
* (RFC 7386).
|
|
4
|
+
*
|
|
5
|
+
* All three are implemented in full. Patching is transactional: the document
|
|
6
|
+
* is cloned, every operation is applied in order, and a failure anywhere
|
|
7
|
+
* leaves the caller's input untouched and reports which operation index
|
|
8
|
+
* failed — which is the behaviour RFC 6902 §5 requires.
|
|
9
|
+
*/
|
|
10
|
+
import { deepClone, deepEqual, isPlainObject } from "./json";
|
|
11
|
+
export class PatchError extends Error {
|
|
12
|
+
opIndex;
|
|
13
|
+
constructor(message, opIndex) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.opIndex = opIndex;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Decode one JSON Pointer reference token: `~1` is `/`, `~0` is `~`. */
|
|
19
|
+
export function unescapeToken(token) {
|
|
20
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
21
|
+
}
|
|
22
|
+
/** Encode one reference token for embedding in a pointer. */
|
|
23
|
+
export function escapeToken(token) {
|
|
24
|
+
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
25
|
+
}
|
|
26
|
+
/** Split a JSON Pointer into its decoded tokens. `""` is the whole document. */
|
|
27
|
+
export function parsePointer(pointer) {
|
|
28
|
+
if (pointer === "")
|
|
29
|
+
return [];
|
|
30
|
+
if (!pointer.startsWith("/")) {
|
|
31
|
+
throw new Error(`JSON Pointer must be empty or start with '/', got ${JSON.stringify(pointer)}`);
|
|
32
|
+
}
|
|
33
|
+
return pointer.slice(1).split("/").map(unescapeToken);
|
|
34
|
+
}
|
|
35
|
+
/** Build a pointer from already-decoded tokens. */
|
|
36
|
+
export function formatPointer(tokens) {
|
|
37
|
+
return tokens.map((t) => `/${escapeToken(t)}`).join("");
|
|
38
|
+
}
|
|
39
|
+
/** Resolve a pointer, returning `{ found: false }` rather than throwing when it misses. */
|
|
40
|
+
export function resolvePointer(doc, pointer) {
|
|
41
|
+
let tokens;
|
|
42
|
+
try {
|
|
43
|
+
tokens = parsePointer(pointer);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { found: false, reason: err.message };
|
|
47
|
+
}
|
|
48
|
+
let cur = doc;
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
if (Array.isArray(cur)) {
|
|
51
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) {
|
|
52
|
+
return { found: false, reason: `"${token}" is not an array index` };
|
|
53
|
+
}
|
|
54
|
+
const i = Number(token);
|
|
55
|
+
if (i >= cur.length)
|
|
56
|
+
return { found: false, reason: `index ${i} is past the end` };
|
|
57
|
+
cur = cur[i];
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (isPlainObject(cur)) {
|
|
61
|
+
if (!Object.hasOwn(cur, token))
|
|
62
|
+
return { found: false, reason: `no member "${token}"` };
|
|
63
|
+
cur = cur[token];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return { found: false, reason: `cannot descend into a ${cur === null ? "null" : typeof cur}` };
|
|
67
|
+
}
|
|
68
|
+
return { found: true, value: cur };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The container that holds a pointer's final token, plus that token.
|
|
72
|
+
*
|
|
73
|
+
* A syntactically invalid pointer is the caller's mistake, so it comes back
|
|
74
|
+
* as a `PatchError` carrying the operation index — the same shape every
|
|
75
|
+
* other failure here has — rather than as the bare `Error` `parsePointer`
|
|
76
|
+
* raises, which would escape the tool as a crash.
|
|
77
|
+
*/
|
|
78
|
+
function locateParent(doc, pointer, opIndex) {
|
|
79
|
+
let tokens;
|
|
80
|
+
try {
|
|
81
|
+
tokens = parsePointer(pointer);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
throw new PatchError(err.message, opIndex);
|
|
85
|
+
}
|
|
86
|
+
if (tokens.length === 0)
|
|
87
|
+
throw new PatchError("cannot address the root's parent", opIndex);
|
|
88
|
+
const parentPointer = formatPointer(tokens.slice(0, -1));
|
|
89
|
+
const found = resolvePointer(doc, parentPointer);
|
|
90
|
+
if (!found.found) {
|
|
91
|
+
throw new PatchError(`path "${pointer}" does not exist: ${found.reason}`, opIndex);
|
|
92
|
+
}
|
|
93
|
+
return { parent: found.value, token: tokens[tokens.length - 1] };
|
|
94
|
+
}
|
|
95
|
+
function addAt(parent, token, value, pointer, i) {
|
|
96
|
+
if (Array.isArray(parent)) {
|
|
97
|
+
if (token === "-") {
|
|
98
|
+
parent.push(value);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) {
|
|
102
|
+
throw new PatchError(`"${token}" is not an array index in "${pointer}"`, i);
|
|
103
|
+
}
|
|
104
|
+
const idx = Number(token);
|
|
105
|
+
if (idx > parent.length) {
|
|
106
|
+
throw new PatchError(`index ${idx} is past the end of the array in "${pointer}"`, i);
|
|
107
|
+
}
|
|
108
|
+
parent.splice(idx, 0, value);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (isPlainObject(parent)) {
|
|
112
|
+
parent[token] = value;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
throw new PatchError(`cannot add to a ${typeof parent} at "${pointer}"`, i);
|
|
116
|
+
}
|
|
117
|
+
function removeAt(parent, token, pointer, i) {
|
|
118
|
+
if (Array.isArray(parent)) {
|
|
119
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) {
|
|
120
|
+
throw new PatchError(`"${token}" is not an array index in "${pointer}"`, i);
|
|
121
|
+
}
|
|
122
|
+
const idx = Number(token);
|
|
123
|
+
if (idx >= parent.length) {
|
|
124
|
+
throw new PatchError(`index ${idx} is past the end of the array in "${pointer}"`, i);
|
|
125
|
+
}
|
|
126
|
+
return parent.splice(idx, 1)[0];
|
|
127
|
+
}
|
|
128
|
+
if (isPlainObject(parent)) {
|
|
129
|
+
if (!Object.hasOwn(parent, token)) {
|
|
130
|
+
throw new PatchError(`no member "${token}" to remove at "${pointer}"`, i);
|
|
131
|
+
}
|
|
132
|
+
const old = parent[token];
|
|
133
|
+
delete parent[token];
|
|
134
|
+
return old;
|
|
135
|
+
}
|
|
136
|
+
throw new PatchError(`cannot remove from a ${typeof parent} at "${pointer}"`, i);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Apply an RFC 6902 patch. Returns the new document; the input is never
|
|
140
|
+
* mutated. Throws `PatchError` (carrying the failing operation's index) if
|
|
141
|
+
* any operation cannot be applied, leaving nothing half-done.
|
|
142
|
+
*/
|
|
143
|
+
export function applyJsonPatch(doc, ops) {
|
|
144
|
+
let working = deepClone(doc);
|
|
145
|
+
ops.forEach((op, i) => {
|
|
146
|
+
switch (op.op) {
|
|
147
|
+
case "test": {
|
|
148
|
+
const found = resolvePointer(working, op.path);
|
|
149
|
+
if (!found.found)
|
|
150
|
+
throw new PatchError(`test failed: "${op.path}" ${found.reason}`, i);
|
|
151
|
+
if (!deepEqual(found.value, op.value)) {
|
|
152
|
+
throw new PatchError(`test failed: "${op.path}" is not the expected value`, i);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case "add": {
|
|
157
|
+
if (op.path === "") {
|
|
158
|
+
working = deepClone(op.value);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { parent, token } = locateParent(working, op.path, i);
|
|
162
|
+
addAt(parent, token, deepClone(op.value), op.path, i);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
case "remove": {
|
|
166
|
+
if (op.path === "")
|
|
167
|
+
throw new PatchError("cannot remove the whole document", i);
|
|
168
|
+
const { parent, token } = locateParent(working, op.path, i);
|
|
169
|
+
removeAt(parent, token, op.path, i);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
case "replace": {
|
|
173
|
+
if (op.path === "") {
|
|
174
|
+
working = deepClone(op.value);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const existing = resolvePointer(working, op.path);
|
|
178
|
+
if (!existing.found) {
|
|
179
|
+
throw new PatchError(`cannot replace "${op.path}": ${existing.reason}`, i);
|
|
180
|
+
}
|
|
181
|
+
const { parent, token } = locateParent(working, op.path, i);
|
|
182
|
+
removeAt(parent, token, op.path, i);
|
|
183
|
+
addAt(parent, token, deepClone(op.value), op.path, i);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
case "move": {
|
|
187
|
+
if (op.path.startsWith(`${op.from}/`)) {
|
|
188
|
+
throw new PatchError(`cannot move "${op.from}" into its own child "${op.path}"`, i);
|
|
189
|
+
}
|
|
190
|
+
// RFC 6902 §4.4: the "from" location MUST exist, and that holds even
|
|
191
|
+
// when it equals "path". Taking the no-op shortcut first would let a
|
|
192
|
+
// move of something that is not there report success.
|
|
193
|
+
const src = resolvePointer(working, op.from);
|
|
194
|
+
if (!src.found)
|
|
195
|
+
throw new PatchError(`cannot move from "${op.from}": ${src.reason}`, i);
|
|
196
|
+
if (op.path === op.from)
|
|
197
|
+
return;
|
|
198
|
+
const from = locateParent(working, op.from, i);
|
|
199
|
+
const moved = removeAt(from.parent, from.token, op.from, i);
|
|
200
|
+
if (op.path === "") {
|
|
201
|
+
working = moved;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const dst = locateParent(working, op.path, i);
|
|
205
|
+
addAt(dst.parent, dst.token, moved, op.path, i);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case "copy": {
|
|
209
|
+
const src = resolvePointer(working, op.from);
|
|
210
|
+
if (!src.found)
|
|
211
|
+
throw new PatchError(`cannot copy from "${op.from}": ${src.reason}`, i);
|
|
212
|
+
if (op.path === "") {
|
|
213
|
+
working = deepClone(src.value);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const dst = locateParent(working, op.path, i);
|
|
217
|
+
addAt(dst.parent, dst.token, deepClone(src.value), op.path, i);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
default: {
|
|
221
|
+
const bad = op;
|
|
222
|
+
throw new PatchError(`unknown operation ${JSON.stringify(bad.op)}`, i);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return working;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Apply an RFC 7386 merge patch: an object merges key by key, `null` deletes
|
|
230
|
+
* a key, and any non-object patch replaces the target outright. Arrays are
|
|
231
|
+
* replaced whole — that is the RFC's behaviour, not a shortcut here.
|
|
232
|
+
*/
|
|
233
|
+
export function applyMergePatch(target, patch) {
|
|
234
|
+
if (!isPlainObject(patch))
|
|
235
|
+
return deepClone(patch);
|
|
236
|
+
const base = isPlainObject(target)
|
|
237
|
+
? deepClone(target)
|
|
238
|
+
: {};
|
|
239
|
+
for (const key of Object.keys(patch)) {
|
|
240
|
+
const value = patch[key];
|
|
241
|
+
if (value === null)
|
|
242
|
+
delete base[key];
|
|
243
|
+
else
|
|
244
|
+
base[key] = applyMergePatch(base[key], value);
|
|
245
|
+
}
|
|
246
|
+
return base;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Derive the RFC 7386 merge patch that turns `from` into `to`. Returns the
|
|
250
|
+
* smallest object that does so; `null` entries mark deletions. Where either
|
|
251
|
+
* side is not an object the result is `to` itself, since a merge patch
|
|
252
|
+
* cannot express a partial array or scalar change.
|
|
253
|
+
*/
|
|
254
|
+
export function diffMergePatch(from, to) {
|
|
255
|
+
if (!isPlainObject(from) || !isPlainObject(to))
|
|
256
|
+
return deepClone(to);
|
|
257
|
+
const out = {};
|
|
258
|
+
for (const key of Object.keys(to)) {
|
|
259
|
+
if (!Object.hasOwn(from, key)) {
|
|
260
|
+
out[key] = deepClone(to[key]);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (deepEqual(from[key], to[key]))
|
|
264
|
+
continue;
|
|
265
|
+
out[key] = diffMergePatch(from[key], to[key]);
|
|
266
|
+
}
|
|
267
|
+
for (const key of Object.keys(from)) {
|
|
268
|
+
if (!Object.hasOwn(to, key))
|
|
269
|
+
out[key] = null;
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operations over an array of records — the shape almost every API response,
|
|
3
|
+
* CSV file and query result arrives in.
|
|
4
|
+
*
|
|
5
|
+
* Field references are dotted paths (`user.name`), so nested records work
|
|
6
|
+
* without flattening them first. Ordering is always total and stable: where
|
|
7
|
+
* two rows compare equal the original order is kept, so the same input gives
|
|
8
|
+
* the same output every time.
|
|
9
|
+
*/
|
|
10
|
+
export type Record_ = Record<string, unknown>;
|
|
11
|
+
/** Read a dotted path out of a record. Returns undefined for any miss. */
|
|
12
|
+
export declare function getPath(record: unknown, path: string): unknown;
|
|
13
|
+
/** Write a dotted path into a record, creating intermediate objects. */
|
|
14
|
+
export declare function setPath(target: Record_, path: string, value: unknown): void;
|
|
15
|
+
export type Comparison = "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith" | "in" | "matches" | "exists" | "empty";
|
|
16
|
+
export type Condition = {
|
|
17
|
+
field: string;
|
|
18
|
+
op: Comparison;
|
|
19
|
+
value?: unknown;
|
|
20
|
+
};
|
|
21
|
+
/** All conditions must hold ("and"), or any one ("or"). */
|
|
22
|
+
export type Predicate = {
|
|
23
|
+
all?: Condition[];
|
|
24
|
+
any?: Condition[];
|
|
25
|
+
none?: Condition[];
|
|
26
|
+
};
|
|
27
|
+
export declare class PredicateError extends Error {
|
|
28
|
+
}
|
|
29
|
+
/** Evaluate one condition against a record. */
|
|
30
|
+
export declare function testCondition(record: unknown, cond: Condition): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Order two scalars, or null when they are not comparable. Numbers compare
|
|
33
|
+
* numerically, strings by code unit, booleans false-before-true; a
|
|
34
|
+
* number/string pair is not comparable, which is what stops `"10" < 9`
|
|
35
|
+
* quietly being true.
|
|
36
|
+
*/
|
|
37
|
+
export declare function compareValues(a: unknown, b: unknown): number | null;
|
|
38
|
+
/** Evaluate a whole predicate. An empty predicate matches everything. */
|
|
39
|
+
export declare function testPredicate(record: unknown, predicate: Predicate): boolean;
|
|
40
|
+
export type SortKey = {
|
|
41
|
+
field: string;
|
|
42
|
+
direction?: "asc" | "desc";
|
|
43
|
+
/** Compare strings case-insensitively. Has no effect on other types. */
|
|
44
|
+
caseInsensitive?: boolean;
|
|
45
|
+
/** Where undefined and null go. Default "last", whatever the direction. */
|
|
46
|
+
nulls?: "first" | "last";
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Sort records by one or more keys. The sort is stable and total: values of
|
|
50
|
+
* different types are ordered by a fixed type rank (null < boolean < number
|
|
51
|
+
* < string < array < object) so the result never depends on the input order
|
|
52
|
+
* of incomparable rows.
|
|
53
|
+
*/
|
|
54
|
+
export declare function sortRecords<T>(records: ReadonlyArray<T>, keys: ReadonlyArray<SortKey>): T[];
|
|
55
|
+
/**
|
|
56
|
+
* The type-rank order. `TYPE_ORDER[rankOf(v)]` is the name of `v`'s rank, so
|
|
57
|
+
* the documented order and the code that implements it cannot drift.
|
|
58
|
+
* `undefined` ranks with `null`, since neither is a JSON value a comparison
|
|
59
|
+
* can say anything else about.
|
|
60
|
+
*/
|
|
61
|
+
export declare const TYPE_ORDER: ReadonlyArray<string>;
|
|
62
|
+
/**
|
|
63
|
+
* A total order over JSON values, used so a sort is never arbitrary.
|
|
64
|
+
*
|
|
65
|
+
* Within a rank, scalars compare naturally. Arrays and objects compare by
|
|
66
|
+
* their canonical text, which is lexicographic and therefore *not*
|
|
67
|
+
* element-wise numeric: `[1, 10]` sorts before `[1, 2]`, because `"1"`
|
|
68
|
+
* precedes `"2"` at the fourth character. That is the price of an order that
|
|
69
|
+
* is total and never has to invent a comparison between a number and an
|
|
70
|
+
* object; sort by a scalar field when the order has to mean something.
|
|
71
|
+
*/
|
|
72
|
+
export declare function totalCompare(a: unknown, b: unknown): number;
|
|
73
|
+
/** Keep only the named fields (dotted paths), preserving the requested order. */
|
|
74
|
+
export declare function selectFields(record: unknown, fields: ReadonlyArray<string>): Record_;
|
|
75
|
+
/** Drop the named fields (dotted paths) from a shallow copy. */
|
|
76
|
+
export declare function omitFields(record: Record_, fields: ReadonlyArray<string>): Record_;
|
|
77
|
+
export type AggregateFn = "count" | "sum" | "min" | "max" | "avg" | "first" | "last" | "distinct";
|
|
78
|
+
export type Aggregation = {
|
|
79
|
+
as: string;
|
|
80
|
+
fn: AggregateFn;
|
|
81
|
+
field?: string;
|
|
82
|
+
};
|
|
83
|
+
export type GroupResult = {
|
|
84
|
+
key: Record_;
|
|
85
|
+
count: number;
|
|
86
|
+
} & Record_;
|
|
87
|
+
/**
|
|
88
|
+
* Group records by one or more fields and reduce each group.
|
|
89
|
+
*
|
|
90
|
+
* `sum` and `avg` ignore non-numeric values and report how many were skipped
|
|
91
|
+
* via the `_skipped` count, rather than producing NaN. `min`/`max` use the
|
|
92
|
+
* same total order as `sortRecords`. `distinct` counts distinct values by
|
|
93
|
+
* their canonical form, so `{a:1,b:2}` and `{b:2,a:1}` are one value.
|
|
94
|
+
*/
|
|
95
|
+
export declare function aggregate(records: ReadonlyArray<Record_>, groupBy: ReadonlyArray<string>, aggregations: ReadonlyArray<Aggregation>): {
|
|
96
|
+
groups: Record_[];
|
|
97
|
+
skipped: number;
|
|
98
|
+
};
|
|
99
|
+
export type JoinKind = "inner" | "left" | "right" | "full";
|
|
100
|
+
export type JoinOptions = {
|
|
101
|
+
kind: JoinKind;
|
|
102
|
+
leftKey: string;
|
|
103
|
+
rightKey: string;
|
|
104
|
+
/** Prefix applied to right-hand fields whose name collides with a left one. */
|
|
105
|
+
rightPrefix: string;
|
|
106
|
+
maxRows: number;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Join two record arrays on a key. Matching is by the canonical form of the
|
|
110
|
+
* key value, so `1` and `"1"` are different keys — a silent coercion here is
|
|
111
|
+
* how a join quietly loses rows.
|
|
112
|
+
*
|
|
113
|
+
* Every right-hand match is emitted, so a one-to-many join multiplies rows,
|
|
114
|
+
* as SQL does. `maxRows` bounds that.
|
|
115
|
+
*/
|
|
116
|
+
export declare function joinRecords(left: ReadonlyArray<Record_>, right: ReadonlyArray<Record_>, options: JoinOptions): {
|
|
117
|
+
rows: Record_[];
|
|
118
|
+
truncated: boolean;
|
|
119
|
+
unmatchedLeft: number;
|
|
120
|
+
unmatchedRight: number;
|
|
121
|
+
};
|
|
122
|
+
/** Records to a column-oriented object: `[{a:1},{a:2}]` becomes `{a:[1,2]}`. */
|
|
123
|
+
export declare function recordsToColumns(records: ReadonlyArray<Record_>): Record<string, unknown[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Columns back to records. Columns of differing length are padded with null
|
|
126
|
+
* up to the longest, and the caller is told how many were short.
|
|
127
|
+
*/
|
|
128
|
+
export declare function columnsToRecords(columns: Record<string, ReadonlyArray<unknown>>): {
|
|
129
|
+
records: Record_[];
|
|
130
|
+
ragged: string[];
|
|
131
|
+
};
|
|
132
|
+
/** 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>;
|
|
134
|
+
/**
|
|
135
|
+
* Rebuild nested objects from dotted keys. A numeric segment becomes an
|
|
136
|
+
* array index when `arraysFromNumericKeys` is on and the keys under that
|
|
137
|
+
* prefix are a contiguous run from 0; otherwise it stays an object key, so
|
|
138
|
+
* `{"a.2": 1}` does not silently produce two nulls.
|
|
139
|
+
*/
|
|
140
|
+
export declare function unflattenObject(flat: Record<string, unknown>, separator: string, arraysFromNumericKeys: boolean): unknown;
|
|
141
|
+
/**
|
|
142
|
+
* De-duplicate records by a key path, or by the whole value when no key is
|
|
143
|
+
* given. Identity is the canonical (key-sorted) serialization, so two
|
|
144
|
+
* records that differ only in key order are one record.
|
|
145
|
+
*/
|
|
146
|
+
export declare function dedupeRecords<T>(records: ReadonlyArray<T>, keyFields: ReadonlyArray<string>, keep: "first" | "last"): {
|
|
147
|
+
records: T[];
|
|
148
|
+
removed: number;
|
|
149
|
+
};
|
|
150
|
+
export type SampleMode = "head" | "tail" | "everyNth" | "evenly";
|
|
151
|
+
/**
|
|
152
|
+
* Take a deterministic sample. There is no random mode on purpose: a tool
|
|
153
|
+
* that returned a different subset on each call could not be cached, retried
|
|
154
|
+
* or compared between runs. `evenly` spreads picks across the whole array.
|
|
155
|
+
*/
|
|
156
|
+
export declare function sampleRecords<T>(records: ReadonlyArray<T>, mode: SampleMode, count: number, step: number): T[];
|