@dudousxd/nestjs-catalog 0.26.0 → 0.28.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/dist/catalog.aggregate.d.ts +376 -0
- package/dist/catalog.aggregate.js +703 -0
- package/dist/catalog.pipeline.d.ts +786 -10
- package/dist/catalog.pipeline.js +1188 -63
- package/dist/client.d.ts +2 -2
- package/dist/client.js +49 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +49 -3
- package/package.json +1 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fold behind the `aggregate` node: one pass, one entry per group.
|
|
3
|
+
*
|
|
4
|
+
* ## The property this file exists to have
|
|
5
|
+
*
|
|
6
|
+
* A hash aggregate **consumes its input as a stream and holds only the groups.**
|
|
7
|
+
* It never needs the row it read two rows ago, and it never needs the row it is
|
|
8
|
+
* about to read. So the heap it occupies is a function of how many distinct
|
|
9
|
+
* groups there are, and not of how many rows there were.
|
|
10
|
+
*
|
|
11
|
+
* That is the entire argument for the node, and it is worth stating with the
|
|
12
|
+
* measurement that produced it. flip's `wo` derivation groups **44,720 rows into
|
|
13
|
+
* 16,119 groups** with about fifty aggregates over them. Today it is a
|
|
14
|
+
* whole-batch transform, and as a whole-batch transform it is one column away
|
|
15
|
+
* from dying:
|
|
16
|
+
*
|
|
17
|
+
* - the child is handed 65.51 MiB on stdin;
|
|
18
|
+
* - it answers with **one JSON line of 25.01 MiB — 78.2% of the hard 32 MiB
|
|
19
|
+
* output cap**, which the parent buffers as a single string before parsing;
|
|
20
|
+
* - and before any of that, the parent materialises all 44,720 rows through
|
|
21
|
+
* `readInputs`. A standalone equivalent peaked at 237 MiB heap, 406 MiB RSS.
|
|
22
|
+
*
|
|
23
|
+
* At **1.28×** that file it stops working, and it stops by being killed at the
|
|
24
|
+
* cap rather than by getting slower. SUBWO is already the largest file in the
|
|
25
|
+
* drop it comes from. Run through this file instead, the same derivation holds
|
|
26
|
+
* 16,119 accumulator rows and reads one staged batch at a time.
|
|
27
|
+
*
|
|
28
|
+
* ## Which is exactly why the bound is loud
|
|
29
|
+
*
|
|
30
|
+
* "Holds only the groups" is a *cheap* promise only while the groups are far
|
|
31
|
+
* fewer than the rows. Group by something near-unique and a hash aggregate holds
|
|
32
|
+
* the entire load, which is the thing being fixed rather than a corner of it. So
|
|
33
|
+
* {@link AggregateTable} carries a group ceiling and **refuses** when it is
|
|
34
|
+
* crossed, naming the columns it was grouping on. Peaking silently at the
|
|
35
|
+
* ceiling is the failure mode this node was written about.
|
|
36
|
+
*
|
|
37
|
+
* ## Every arithmetic decision in one place
|
|
38
|
+
*
|
|
39
|
+
* The node is the model; this is the semantics. Both are here rather than in the
|
|
40
|
+
* runner so that the canvas, a spec and a durable step get the same answer out
|
|
41
|
+
* of the same code, and so that the decisions below have one address:
|
|
42
|
+
*
|
|
43
|
+
* - **Comparison order for `min`/`max` over strings** — see {@link compareValues}.
|
|
44
|
+
* - **Summation error** — see {@link addToSum}.
|
|
45
|
+
* - **The bound on a `join`, and what happens at it** — see {@link appendJoin}.
|
|
46
|
+
* - **What makes two records the same group** — see {@link groupKeyOf}.
|
|
47
|
+
*
|
|
48
|
+
* None of them is inherited from MySQL, and where the answer differs from
|
|
49
|
+
* MySQL's the docblock says so and says why, because the alternative is a load
|
|
50
|
+
* that silently disagrees with the query it replaced.
|
|
51
|
+
*/
|
|
52
|
+
import { type WorkflowAggregate, type WorkflowAggregateNode } from './catalog.pipeline';
|
|
53
|
+
/**
|
|
54
|
+
* A refusal raised while folding, rather than a wrong answer carried forward.
|
|
55
|
+
*
|
|
56
|
+
* Its own class so a caller can tell "this data cannot be aggregated the way the
|
|
57
|
+
* node says" from a bug, and turn the first into a 400 with the sentence intact.
|
|
58
|
+
* Every message it carries names the column, the group and the values, because a
|
|
59
|
+
* refusal at row ninety thousand that says only "incomparable" is a refusal
|
|
60
|
+
* somebody has to reproduce before they can act on it.
|
|
61
|
+
*/
|
|
62
|
+
export declare class WorkflowAggregateError extends Error {
|
|
63
|
+
constructor(message: string);
|
|
64
|
+
}
|
|
65
|
+
/** What the fold learned on the way through, for the run log. */
|
|
66
|
+
export interface AggregateTableStats {
|
|
67
|
+
/** Records handed to {@link AggregateTable.push}. */
|
|
68
|
+
rowsIn: number;
|
|
69
|
+
/** Distinct groups held. The number the memory bound is about. */
|
|
70
|
+
groups: number;
|
|
71
|
+
/**
|
|
72
|
+
* Columns the node names that were present in **no** record of the whole run.
|
|
73
|
+
*
|
|
74
|
+
* Almost always a typo in a header, and the symptom otherwise is a column of
|
|
75
|
+
* nulls and a green run — the same trap `renameStagePayload` reports and for
|
|
76
|
+
* the same reason. Not a refusal, because a source legitimately omits a column
|
|
77
|
+
* for a whole load, and `sum`/`min`/`max` over nothing answers `null` rather
|
|
78
|
+
* than `0`, which is visibly different from a real zero.
|
|
79
|
+
*/
|
|
80
|
+
unseenColumns: string[];
|
|
81
|
+
/**
|
|
82
|
+
* How many values a `sum` or an `avg` had to read out of a string.
|
|
83
|
+
*
|
|
84
|
+
* A CSV delivers every number as text, so this is usually the whole column and
|
|
85
|
+
* is not a problem. It is reported because the one case that *is* a problem —
|
|
86
|
+
* a column that is numbers in one source and text in another — looks identical
|
|
87
|
+
* from the answer and different from here.
|
|
88
|
+
*/
|
|
89
|
+
coercedFromString: number;
|
|
90
|
+
/** The longest value any `join` produced, in characters. See {@link appendJoin}. */
|
|
91
|
+
longestJoin: number;
|
|
92
|
+
}
|
|
93
|
+
/** `sum` and `avg`. See {@link addToSum} for why there are two numbers. */
|
|
94
|
+
interface SumAcc {
|
|
95
|
+
/** The running total. */
|
|
96
|
+
sum: number;
|
|
97
|
+
/** Neumaier's compensation term: the low-order bits `sum` could not hold. */
|
|
98
|
+
compensation: number;
|
|
99
|
+
/** Non-null values seen. Zero means the answer is `null`, never `0`. */
|
|
100
|
+
n: number;
|
|
101
|
+
}
|
|
102
|
+
/** `join`. See {@link appendJoin} for the bound and what happens at it. */
|
|
103
|
+
interface JoinAcc {
|
|
104
|
+
text: string;
|
|
105
|
+
n: number;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The fold, as an object you push records into.
|
|
109
|
+
*
|
|
110
|
+
* An object rather than a `reduce` because the caller is a loop over staged
|
|
111
|
+
* batches with an `await` in it: the table has to survive between batches, and
|
|
112
|
+
* nothing about it may scale with how many batches there were.
|
|
113
|
+
*
|
|
114
|
+
* Built from the node, and it refuses a node the validator would refuse rather
|
|
115
|
+
* than folding under a configuration nobody could have saved — a graph can reach
|
|
116
|
+
* a runner from a database written by an older build, and `aggregateRefusals` is
|
|
117
|
+
* the one place that rule lives.
|
|
118
|
+
*/
|
|
119
|
+
export declare class AggregateTable {
|
|
120
|
+
private readonly node;
|
|
121
|
+
private readonly groups;
|
|
122
|
+
private readonly groupBy;
|
|
123
|
+
private readonly aggregates;
|
|
124
|
+
private readonly maxGroups;
|
|
125
|
+
/** Named columns not yet seen in any record. Emptied as they turn up. */
|
|
126
|
+
private readonly unseen;
|
|
127
|
+
private rowsIn;
|
|
128
|
+
private coercedFromString;
|
|
129
|
+
private longestJoin;
|
|
130
|
+
constructor(node: WorkflowAggregateNode);
|
|
131
|
+
/**
|
|
132
|
+
* Fold one record in.
|
|
133
|
+
*
|
|
134
|
+
* Allocates nothing per record except when a record opens a group, which is
|
|
135
|
+
* the property that makes the whole thing a stream: the cost of a row is the
|
|
136
|
+
* group-key encoding plus one accumulator update per aggregate.
|
|
137
|
+
*/
|
|
138
|
+
push(row: Record<string, unknown>): void;
|
|
139
|
+
/**
|
|
140
|
+
* One aggregate, one record. Narrowed off the function list, never asserted.
|
|
141
|
+
*
|
|
142
|
+
* A dispatch and four one-function methods rather than four branches inline,
|
|
143
|
+
* so each function's rule about nulls — which is the part that has to match
|
|
144
|
+
* SQL and the part that is easy to get subtly wrong — sits next to its own
|
|
145
|
+
* name. Ends in the exhaustiveness guard, so a seventh function is a compile
|
|
146
|
+
* error here rather than a value nobody computed.
|
|
147
|
+
*/
|
|
148
|
+
private accumulate;
|
|
149
|
+
/**
|
|
150
|
+
* `COUNT(*)` with no column and `COUNT(col)` with one.
|
|
151
|
+
*
|
|
152
|
+
* The two differ exactly where the data is sparse, which is where somebody is
|
|
153
|
+
* most likely to want to know — so both are offered rather than one being
|
|
154
|
+
* picked on the reader's behalf.
|
|
155
|
+
*/
|
|
156
|
+
private count;
|
|
157
|
+
/** `sum` and `avg`, which share an accumulator and differ only at the finish. */
|
|
158
|
+
private total;
|
|
159
|
+
/** `min` and `max`. Nulls are skipped, as SQL skips them. */
|
|
160
|
+
private extremum;
|
|
161
|
+
/** `join`. Nulls skipped, empty strings kept — which is what GROUP_CONCAT does. */
|
|
162
|
+
private join;
|
|
163
|
+
/**
|
|
164
|
+
* A value on its way into a `sum` or an `avg`, or nothing.
|
|
165
|
+
*
|
|
166
|
+
* Three answers rather than two, and the middle one is the decision:
|
|
167
|
+
*
|
|
168
|
+
* - **null, undefined, or a string that is only whitespace** — skipped, and
|
|
169
|
+
* `n` does not move. A blank cell in a CSV is an absent number and not a
|
|
170
|
+
* zero, and SQL agrees: `SUM` over nothing but nulls is `NULL`, which is
|
|
171
|
+
* visibly different from a real total of zero.
|
|
172
|
+
* - **a string that parses as a finite number** — accepted, and counted in
|
|
173
|
+
* {@link AggregateTableStats.coercedFromString}. Refusing would make the
|
|
174
|
+
* node useless on the exact data it was written for, since a CSV delivers
|
|
175
|
+
* every number as text.
|
|
176
|
+
* - **anything else** — refused, naming the column, the group and the value.
|
|
177
|
+
* MySQL reads `'n/a'` as `0` and raises a warning MikroORM does not surface,
|
|
178
|
+
* which is how a total ends up quietly too small. This is that behaviour,
|
|
179
|
+
* not reproduced.
|
|
180
|
+
*/
|
|
181
|
+
private toNumber;
|
|
182
|
+
/**
|
|
183
|
+
* Every group, as records, in the order the groups were first seen.
|
|
184
|
+
*
|
|
185
|
+
* **Insertion order rather than sorted**, and that is a decision about
|
|
186
|
+
* determinism rather than about cost. Sorting would need a total order over
|
|
187
|
+
* heterogeneous key values — the thing {@link compareValues} refuses to invent
|
|
188
|
+
* — and would buy an ordering nobody asked for. Insertion order is a function
|
|
189
|
+
* of the input order, and the input is a numbered list of staged batches, so
|
|
190
|
+
* two runs over the same staged rows emit the same rows in the same order.
|
|
191
|
+
* What it does **not** promise is stability across a source that returns its
|
|
192
|
+
* own rows in a different order; a `SELECT` without an `ORDER BY` promises
|
|
193
|
+
* nothing and this node cannot promise more than it was given.
|
|
194
|
+
*
|
|
195
|
+
* Every record carries **every** output column — the group keys, then the
|
|
196
|
+
* aggregates, in the node's order — including the ones whose answer is `null`.
|
|
197
|
+
* That is what makes an aggregate's output set *exact* rather than an upper
|
|
198
|
+
* bound, which is the claim `producedColumns` makes about this kind and about
|
|
199
|
+
* no other.
|
|
200
|
+
*/
|
|
201
|
+
emit(): Generator<Record<string, unknown>>;
|
|
202
|
+
/** What the run log says. Cheap: everything here was counted on the way past. */
|
|
203
|
+
stats(): AggregateTableStats;
|
|
204
|
+
}
|
|
205
|
+
/** The columns a node's aggregates read, deduplicated, `count(*)` excluded. */
|
|
206
|
+
export declare function aggregateInputColumns(node: WorkflowAggregateNode): string[];
|
|
207
|
+
/**
|
|
208
|
+
* What makes two records the same group.
|
|
209
|
+
*
|
|
210
|
+
* The key is the group-by values, encoded into one string so a `Map` can hold
|
|
211
|
+
* them. Every part is **length-prefixed and type-tagged**, which closes two
|
|
212
|
+
* holes that a naive `values.join('|')` leaves open:
|
|
213
|
+
*
|
|
214
|
+
* - `["a|b", "c"]` and `["a", "b|c"]` are different groups and must stay
|
|
215
|
+
* different. Length prefixes make the encoding unambiguous whatever the values
|
|
216
|
+
* contain.
|
|
217
|
+
* - `1` and `"1"` are different groups, and that is a **decision against SQL**,
|
|
218
|
+
* which would coerce them together. Merging them means merging two things the
|
|
219
|
+
* source considered distinct, on a rule nobody chose; within one load a column
|
|
220
|
+
* comes from one system and is one type, so the coercion buys nothing and
|
|
221
|
+
* risks a group that quietly swallowed another.
|
|
222
|
+
*
|
|
223
|
+
* **Missing and null are the same group**, and that is a decision *for* SQL.
|
|
224
|
+
* `GROUP BY` collects all NULLs into one group, and a record that simply lacks
|
|
225
|
+
* the column is a record whose column is null. The stage encoding makes "absent"
|
|
226
|
+
* and "null" a difference in physical layout rather than in meaning — see the
|
|
227
|
+
* shape dictionary in `catalog.stage-encoding.ts` — so grouping them apart would
|
|
228
|
+
* make the answer depend on which shape a row happened to land in, which is not
|
|
229
|
+
* a thing to build a load on.
|
|
230
|
+
*
|
|
231
|
+
* A value that is an object or an array is **refused**. Two objects are equal
|
|
232
|
+
* under some rule and unequal under others, every one of those rules is
|
|
233
|
+
* somebody's convention, and picking one silently decides how many rows a
|
|
234
|
+
* published type ends up holding.
|
|
235
|
+
*/
|
|
236
|
+
export declare function groupKeyOf(values: readonly unknown[], columns: readonly string[], node: {
|
|
237
|
+
id: string;
|
|
238
|
+
name: string;
|
|
239
|
+
}): string;
|
|
240
|
+
/**
|
|
241
|
+
* The order `min` and `max` compare in, and the one place it is decided.
|
|
242
|
+
*
|
|
243
|
+
* ## Strings compare by **code point**, and that is not MySQL's answer
|
|
244
|
+
*
|
|
245
|
+
* The column this node most often replaces sits in MySQL under
|
|
246
|
+
* `utf8mb4_0900_ai_ci` — case-insensitive and accent-insensitive — where
|
|
247
|
+
* `'apple' > 'Banana'` is false. JavaScript's `<` compares UTF-16 code points,
|
|
248
|
+
* where `'B'` is 66 and `'a'` is 97, so `'apple' > 'Banana'` is true. On the real
|
|
249
|
+
* SUBWO data the two answers agree on most columns and **differ on two**: 18 of
|
|
250
|
+
* 16,119 groups for `lastUpdatedBy` and 23 for `maintenanceLocation`, both of
|
|
251
|
+
* which hold mixed-case usernames and location codes.
|
|
252
|
+
*
|
|
253
|
+
* Code point order is implemented anyway, deliberately:
|
|
254
|
+
*
|
|
255
|
+
* - It is **total, stable and machine-independent.** The same rows produce the
|
|
256
|
+
* same answer on every Node build, in every locale, with no ICU data loaded.
|
|
257
|
+
* - The alternative is not "MySQL's answer" — it is *an approximation of one
|
|
258
|
+
* deployment's collation*. `Intl.Collator` implements the Unicode collation
|
|
259
|
+
* algorithm, MySQL implements its own table per collation, and a graph can
|
|
260
|
+
* read from a source that is not MySQL at all. Matching would mean carrying a
|
|
261
|
+
* collation name on the node and reimplementing it, which is a database inside
|
|
262
|
+
* a pipeline node.
|
|
263
|
+
* - Being *near* MySQL is worse than being clearly different. A comparison that
|
|
264
|
+
* agrees 99.8% of the time is one nobody checks and everybody trusts.
|
|
265
|
+
*
|
|
266
|
+
* So it is code point order, it is written down here, and the run log says
|
|
267
|
+
* nothing about it because there is nothing conditional to report — what a
|
|
268
|
+
* reader needs is this paragraph, which is why it is this long.
|
|
269
|
+
*
|
|
270
|
+
* ## Types are not compared across classes
|
|
271
|
+
*
|
|
272
|
+
* Numbers with numbers, strings with strings, dates with dates, booleans with
|
|
273
|
+
* booleans. A `max` over a column holding both `12` and `"12"` is **refused**,
|
|
274
|
+
* naming the column, the group and both values. There is no ordering between a
|
|
275
|
+
* number and a string that is not somebody's coercion rule, and every such rule
|
|
276
|
+
* produces a maximum that depends on which row arrived first — a wrong answer
|
|
277
|
+
* that reports success, which is the shape of failure this node was written
|
|
278
|
+
* about. The fix is one node upstream: normalise the column in a transform.
|
|
279
|
+
*/
|
|
280
|
+
export declare function compareValues(left: unknown, right: unknown, column: string, groupId: string): number;
|
|
281
|
+
/**
|
|
282
|
+
* Add one value to a running total, with the low-order bits kept.
|
|
283
|
+
*
|
|
284
|
+
* ## The measurement this is here for
|
|
285
|
+
*
|
|
286
|
+
* A prior comparison of flip's `wo` derivation against a JavaScript
|
|
287
|
+
* reimplementation found **17 of 16,119 groups differing in the last float64
|
|
288
|
+
* ulp** — `6442.999999999999` against `6443` — purely from the order the terms
|
|
289
|
+
* were added in. The grand totals reconciled exactly (212,192,113 on both
|
|
290
|
+
* sides), so the difference was tolerable. It was also nobody's decision, and
|
|
291
|
+
* that is the part worth fixing: a load that is off by an ulp because of an
|
|
292
|
+
* accident is one nobody can reason about the next time it is off by more.
|
|
293
|
+
*
|
|
294
|
+
* ## What is implemented, and what it does and does not promise
|
|
295
|
+
*
|
|
296
|
+
* **Neumaier summation.** One extra float per accumulator carries the bits the
|
|
297
|
+
* running total could not hold, and they are added back once at the end. On
|
|
298
|
+
* decimal money and hours — which is what every column this node sums actually
|
|
299
|
+
* is — the result is the correctly-rounded sum, so `6443` comes out as `6443`.
|
|
300
|
+
*
|
|
301
|
+
* What it promises:
|
|
302
|
+
*
|
|
303
|
+
* - The answer is **far** closer to the exact sum than `+=` is, and for values
|
|
304
|
+
* with a couple of decimal places it is the exact sum rounded once.
|
|
305
|
+
* - It costs one number per accumulator and two floating-point operations per
|
|
306
|
+
* row. At 44,720 rows that is unmeasurable.
|
|
307
|
+
*
|
|
308
|
+
* What it does **not** promise:
|
|
309
|
+
*
|
|
310
|
+
* - **Order independence.** Nothing short of exact arithmetic gives that, and
|
|
311
|
+
* exact arithmetic means holding a big decimal per accumulator, which trades
|
|
312
|
+
* the property the node is built on for a rounding difference nobody can
|
|
313
|
+
* observe in a report.
|
|
314
|
+
* - **Bit-for-bit agreement with MySQL.** MySQL sums in its own order and, for a
|
|
315
|
+
* `DECIMAL` column, not in binary floating point at all. Where the source
|
|
316
|
+
* column is `DECIMAL` the honest answer is that these are two different
|
|
317
|
+
* arithmetics and the difference is bounded by one rounding, not that they
|
|
318
|
+
* agree.
|
|
319
|
+
*/
|
|
320
|
+
export declare function addToSum(slot: SumAcc, value: number): void;
|
|
321
|
+
/**
|
|
322
|
+
* Append one value to a joined string, and refuse rather than truncate.
|
|
323
|
+
*
|
|
324
|
+
* ## The behaviour this is designed against, which is live today
|
|
325
|
+
*
|
|
326
|
+
* flip's `wo` derivation uses `GROUP_CONCAT(... SEPARATOR '; ')`. In that
|
|
327
|
+
* deployment `group_concat_max_len` is **1024**. Real values reach **1,700 and
|
|
328
|
+
* 1,883 characters**, and **5 of 16,119 groups exceed the limit on each of two
|
|
329
|
+
* columns**. MySQL truncates at the limit and raises a warning, and MikroORM
|
|
330
|
+
* does not surface the warning. So five rows per column have been silently
|
|
331
|
+
* missing their tail, in committed data, with a green run every time.
|
|
332
|
+
*
|
|
333
|
+
* That is not a MySQL bug and it is not a configuration slip anybody would
|
|
334
|
+
* notice: the default is 1024, the query does not mention it, and a truncated
|
|
335
|
+
* string is a perfectly plausible string.
|
|
336
|
+
*
|
|
337
|
+
* ## What this does instead
|
|
338
|
+
*
|
|
339
|
+
* **Refuses, at the bound, naming the group and the length.** Not truncation
|
|
340
|
+
* with a log line, and not an unbounded string.
|
|
341
|
+
*
|
|
342
|
+
* - Truncating loudly was the other candidate and it loses on the same argument
|
|
343
|
+
* as everything else here: the run would go green, the snapshot would commit,
|
|
344
|
+
* and the log line would be one of twenty on a node nobody reads unless
|
|
345
|
+
* something already went wrong. The value would still be wrong in the
|
|
346
|
+
* warehouse.
|
|
347
|
+
* - Unbounded loses on the node's own thesis. A `join` is the one accumulator
|
|
348
|
+
* whose size is not bounded by the group count, so an unbounded one is a hole
|
|
349
|
+
* straight through "holds only the groups".
|
|
350
|
+
*
|
|
351
|
+
* The default bound is {@link WORKFLOW_AGGREGATE_JOIN_MAX_LENGTH} — 65,535
|
|
352
|
+
* characters, which is what one MySQL `TEXT` column holds, because a value the
|
|
353
|
+
* target column cannot store is the same defect one layer further down. flip's
|
|
354
|
+
* real maximum of 1,883 is 2.9% of it, so the derivation this node was written
|
|
355
|
+
* for runs untouched and would have refused loudly at 35× the data instead of
|
|
356
|
+
* quietly at 1.5×.
|
|
357
|
+
*
|
|
358
|
+
* An author may lower it (a column that should never exceed 200 characters is
|
|
359
|
+
* worth saying so about) or raise it up to a hard ceiling, which is on
|
|
360
|
+
* {@link workflowAggregateJoinMaxLength}.
|
|
361
|
+
*/
|
|
362
|
+
export declare function appendJoin(slot: JoinAcc, text: string, aggregate: WorkflowAggregate, node: {
|
|
363
|
+
id: string;
|
|
364
|
+
name: string;
|
|
365
|
+
}, groupId: string): void;
|
|
366
|
+
/**
|
|
367
|
+
* A group key, back in something a person can look up.
|
|
368
|
+
*
|
|
369
|
+
* The key is a length-prefixed encoding, which is unreadable on purpose and
|
|
370
|
+
* useless in an error message. This walks it back into the values it was built
|
|
371
|
+
* from, so a refusal names the row somebody has to go and find.
|
|
372
|
+
*/
|
|
373
|
+
export declare function describeGroup(key: string): string;
|
|
374
|
+
/** A value, short enough for an error message and honest about its type. */
|
|
375
|
+
export declare function describeValue(value: unknown): string;
|
|
376
|
+
export {};
|