@jarenjs/json 0.46.5 → 0.49.2
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/ARCHITECTURE.md +1 -1
- package/README.md +47 -4
- package/dist/types/query/operators.d.ts +42 -0
- package/dist/types/query/series.d.ts +164 -0
- package/docs/QUERY-FORMAT.md +172 -4
- package/package.json +2 -2
- package/schemas/jaren-jslt.draft-07.schema.json +236 -0
- package/schemas/jaren-jslt.llm-profile.schema.json +229 -0
- package/schemas/jaren-jslt.schema.json +236 -0
- package/schemas/jaren-query.draft-07.schema.json +236 -0
- package/schemas/jaren-query.llm-profile.schema.json +229 -0
- package/schemas/jaren-query.schema.json +122 -2
- package/src/query/normalize.js +44 -1
- package/src/query/operators.js +332 -3
- package/src/query/series.js +415 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region series spec compilation (section 8.16)
|
|
4
|
+
// The literal half of the five temporal operators. Everything a
|
|
5
|
+
// `$resample`, `$rolling`, `$asof` or `$time-bucket` needs to know
|
|
6
|
+
// before it sees a single row — the width, the aggregate, the fill
|
|
7
|
+
// policy, the wall clock, where the instant lives in a row — is read
|
|
8
|
+
// here, ONCE, at query compile time.
|
|
9
|
+
//
|
|
10
|
+
// That is the whole design. A spec is a **literal**, never an
|
|
11
|
+
// expression: it is captured verbatim by the 'raw' argument kind
|
|
12
|
+
// (normalize.js `makeRaw`), so nothing inside it is evaluated, nothing
|
|
13
|
+
// inside it can vary per row, and a misspelled member is a broken
|
|
14
|
+
// document rather than a surprise on the ten-thousandth sample. The
|
|
15
|
+
// alternative — a spec computed from the data — would make the kernel
|
|
16
|
+
// re-compile a boundary ladder per call and would put a typo beyond the
|
|
17
|
+
// reach of every gate a document has.
|
|
18
|
+
//
|
|
19
|
+
// Three rules run through it:
|
|
20
|
+
//
|
|
21
|
+
// **Closed.** Each spec names its members exactly. An unknown one is
|
|
22
|
+
// `JQ0003` with the near-miss named, because `minPeriod` for
|
|
23
|
+
// `minPeriods` silently ignored is the bug that takes an afternoon.
|
|
24
|
+
//
|
|
25
|
+
// **Compile what is authored, refuse what is not.** A bad duration, a
|
|
26
|
+
// bad aggregate, a bad path, a zone with no provider: all `JQ0003`
|
|
27
|
+
// against the member that carried them. Only what the DATA decides —
|
|
28
|
+
// a row that is not a sample, an instant that names none — is
|
|
29
|
+
// `JQ2001` at runtime.
|
|
30
|
+
//
|
|
31
|
+
// **No second loop.** These helpers shape arguments for
|
|
32
|
+
// `@jarenjs/core/series` and read its answers back. The bucketing,
|
|
33
|
+
// the window, the join and the fill exist once, in the kernel; this
|
|
34
|
+
// module is the door they are reached through from a document.
|
|
35
|
+
|
|
36
|
+
import { parseJSONPath, JSONPathSyntaxError } from '../path.js';
|
|
37
|
+
import { isSingularSegments, compileSingularGetter, NOTHING } from '../segments.js';
|
|
38
|
+
import {
|
|
39
|
+
resolveClock,
|
|
40
|
+
CLOCK_MEMBERS as KERNEL_CLOCK_MEMBERS,
|
|
41
|
+
RESAMPLE_MEMBERS as KERNEL_RESAMPLE_MEMBERS,
|
|
42
|
+
ROLLING_MEMBERS as KERNEL_ROLLING_MEMBERS,
|
|
43
|
+
} from '@jarenjs/core/series';
|
|
44
|
+
import { JsonQueryCompileError, JsonQueryRuntimeError } from './errors.js';
|
|
45
|
+
import { EMPTY, Seq, describeItem } from './runtime.js';
|
|
46
|
+
|
|
47
|
+
const hasOwn = Object.hasOwn;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The one member of a kernel specification a DOCUMENT cannot carry.
|
|
51
|
+
* `provider` is a pair of functions and JSON has no such value, so a
|
|
52
|
+
* named zone reaches the kernel through `options.zoneProvider` at
|
|
53
|
+
* compile time instead. Everything else these operators admit is
|
|
54
|
+
* whatever `@jarenjs/core/series` admits, read from the kernel rather
|
|
55
|
+
* than restated here — one list, one place, and a member added to a
|
|
56
|
+
* kernel reaches the language in the same change.
|
|
57
|
+
*/
|
|
58
|
+
const NOT_IN_A_DOCUMENT = Object.freeze(['provider']);
|
|
59
|
+
|
|
60
|
+
/** @param {readonly string[]} members @returns {readonly string[]} */
|
|
61
|
+
const spellable = (members) =>
|
|
62
|
+
Object.freeze(members.filter((name) => !NOT_IN_A_DOCUMENT.includes(name)));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The calendar context every clock-reading spec carries, and the one
|
|
66
|
+
* `$time-bucket` takes as its own literal. `zone` is an IANA name — only
|
|
67
|
+
* `'UTC'` resolves without a provider (D7: this suite bundles no tzdb) —
|
|
68
|
+
* `offset` is minutes east of UTC, and `disambiguation` says what a
|
|
69
|
+
* local time that happens twice, or never, resolves to.
|
|
70
|
+
*/
|
|
71
|
+
export const CLOCK_MEMBERS = spellable(KERNEL_CLOCK_MEMBERS);
|
|
72
|
+
|
|
73
|
+
/** `$resample`: the D5 bucket contract, plus where a row keeps its members. */
|
|
74
|
+
export const RESAMPLE_MEMBERS = spellable(KERNEL_RESAMPLE_MEMBERS);
|
|
75
|
+
|
|
76
|
+
/** `$rolling`: a window measured in time, and how much of one counts. */
|
|
77
|
+
export const ROLLING_MEMBERS = spellable(KERNEL_ROLLING_MEMBERS);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `$asof`: which way to look, how far, and what makes two rows
|
|
81
|
+
* comparable. The one list NOT read from the kernel: `asOfJoin` takes
|
|
82
|
+
* nested `left`/`right` selector records, and a spec that stays one
|
|
83
|
+
* flat literal is what makes "compiled once" true — so the document
|
|
84
|
+
* spells `by`, `leftAt` and `rightAt`, and `compileAsOfSpec` below is
|
|
85
|
+
* the single place that translation happens.
|
|
86
|
+
*/
|
|
87
|
+
export const ASOF_MEMBERS = Object.freeze([
|
|
88
|
+
'direction', 'tolerance', 'by', 'leftAt', 'rightAt',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/** What a local time that happens twice, or never, may resolve to. */
|
|
92
|
+
const DISAMBIGUATIONS = Object.freeze(['reject', 'earlier', 'later']);
|
|
93
|
+
|
|
94
|
+
/** The spec members that are singular paths into a row rather than values. */
|
|
95
|
+
const SELECTOR_MEMBERS = Object.freeze(['at', 'value', 'by', 'leftAt', 'rightAt']);
|
|
96
|
+
|
|
97
|
+
/** @param {string} code @param {string} message @param {string} docPath @param {unknown} [cause] */
|
|
98
|
+
function compileError(code, message, docPath, cause) {
|
|
99
|
+
return new JsonQueryCompileError(code, message, docPath,
|
|
100
|
+
cause === undefined ? undefined : { cause });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The nearest admitted member to a misspelling, by a cheap edit-distance
|
|
105
|
+
* proxy: same first letter and a length within one, or a case-folded
|
|
106
|
+
* match. Enough to catch `minPeriod`, `Every` and `agregate`, and it
|
|
107
|
+
* never guesses when nothing is close.
|
|
108
|
+
* @param {string} name
|
|
109
|
+
* @param {readonly string[]} allowed
|
|
110
|
+
* @returns {string} `''`, or ` (did you mean 'x'?)`
|
|
111
|
+
*/
|
|
112
|
+
function nearMiss(name, allowed) {
|
|
113
|
+
const lower = name.toLowerCase();
|
|
114
|
+
for (const candidate of allowed) {
|
|
115
|
+
const other = candidate.toLowerCase();
|
|
116
|
+
if (other === lower
|
|
117
|
+
|| (other.startsWith(lower) && other.length - lower.length <= 2)
|
|
118
|
+
|| (lower.startsWith(other) && lower.length - other.length <= 2))
|
|
119
|
+
return ` (did you mean '${candidate}'?)`;
|
|
120
|
+
}
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* One captured spec literal, confirmed to be an object naming only
|
|
126
|
+
* admitted members.
|
|
127
|
+
* @param {any} value - the raw node's frozen value
|
|
128
|
+
* @param {readonly string[]} allowed - the closed member list
|
|
129
|
+
* @param {string} operator - the operator's name, for the message
|
|
130
|
+
* @param {string} docPath - the spec argument's pointer
|
|
131
|
+
* @returns {any} the same object
|
|
132
|
+
* @throws {JsonQueryCompileError} JQ0003
|
|
133
|
+
*/
|
|
134
|
+
export function requireSpec(value, allowed, operator, docPath) {
|
|
135
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
136
|
+
throw compileError('JQ0003',
|
|
137
|
+
`'${operator}' takes a literal spec object, got ${describeItem(value)}`, docPath);
|
|
138
|
+
}
|
|
139
|
+
for (const name of Object.keys(value)) {
|
|
140
|
+
if (!allowed.includes(name)) {
|
|
141
|
+
throw compileError('JQ0003',
|
|
142
|
+
`'${operator}' has no spec member '${name}'${nearMiss(name, allowed)}; it admits ${
|
|
143
|
+
allowed.map((m) => `'${m}'`).join(', ')}`, docPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A spec member that must be one of a closed set of names.
|
|
151
|
+
* @param {any} value
|
|
152
|
+
* @param {readonly string[]} allowed
|
|
153
|
+
* @param {string} member
|
|
154
|
+
* @param {string} docPath
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
export function requireEnum(value, allowed, member, docPath) {
|
|
158
|
+
if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
159
|
+
throw compileError('JQ0003',
|
|
160
|
+
`'${member}' is ${allowed.map((a) => `'${a}'`).join(', ')}, got ${
|
|
161
|
+
typeof value === 'string' ? JSON.stringify(value) : describeItem(value)}`,
|
|
162
|
+
docPath);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A spec member that must be a finite number.
|
|
169
|
+
* @param {any} value
|
|
170
|
+
* @param {string} member
|
|
171
|
+
* @param {string} docPath
|
|
172
|
+
* @returns {number}
|
|
173
|
+
*/
|
|
174
|
+
export function requireNumber(value, member, docPath) {
|
|
175
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
176
|
+
throw compileError('JQ0003',
|
|
177
|
+
`'${member}' is a finite number, got ${describeItem(value)}`, docPath);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A spec member naming an instant: epoch milliseconds or an RFC 3339
|
|
184
|
+
* string. The kernel converts it; this only refuses what is not one of
|
|
185
|
+
* the two spellings, so the message names the member rather than a row.
|
|
186
|
+
* @param {any} value
|
|
187
|
+
* @param {string} member
|
|
188
|
+
* @param {string} docPath
|
|
189
|
+
* @returns {number | string}
|
|
190
|
+
*/
|
|
191
|
+
export function requireInstant(value, member, docPath) {
|
|
192
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
193
|
+
return value;
|
|
194
|
+
if (typeof value === 'string')
|
|
195
|
+
return value;
|
|
196
|
+
throw compileError('JQ0003',
|
|
197
|
+
`'${member}' is epoch milliseconds or an RFC 3339 string, got ${describeItem(value)}`, docPath);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A spec member naming a span: a finite count of milliseconds or an ISO
|
|
202
|
+
* 8601 duration string. The kernel decides whether the duration is one
|
|
203
|
+
* it can walk; this refuses the shapes that are not spans at all.
|
|
204
|
+
* @param {any} value
|
|
205
|
+
* @param {string} member
|
|
206
|
+
* @param {string} docPath
|
|
207
|
+
* @returns {number | string}
|
|
208
|
+
*/
|
|
209
|
+
export function requireSpan(value, member, docPath) {
|
|
210
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
211
|
+
return value;
|
|
212
|
+
if (typeof value === 'string')
|
|
213
|
+
return value;
|
|
214
|
+
throw compileError('JQ0003',
|
|
215
|
+
`'${member}' is a duration string or a count of milliseconds, got ${describeItem(value)}`,
|
|
216
|
+
docPath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A **row selector**: the query language's singular-path spelling, with
|
|
221
|
+
* `$` reading as the ROW rather than as the document — `'$.on'`,
|
|
222
|
+
* `'$[\'recorded at\']'`, `'$.meta.at'`. Compiled once into a direct
|
|
223
|
+
* property walk (`compileSingularGetter`), so nothing is parsed per row.
|
|
224
|
+
*
|
|
225
|
+
* A bare member name is the common case, and it is handed to the kernel
|
|
226
|
+
* as a name rather than as a closure: `canonicalSeries`'s no-copy fast
|
|
227
|
+
* path is only available to a series already spelled `at`/`value`, and
|
|
228
|
+
* a closure would take it away from every caller who did not need one.
|
|
229
|
+
*
|
|
230
|
+
* @param {any} value - the raw spec member
|
|
231
|
+
* @param {string} member - its name, for the message
|
|
232
|
+
* @param {string} docPath - the spec argument's pointer
|
|
233
|
+
* @returns {string | ((item: any) => any)} a member name, or a reader
|
|
234
|
+
* @throws {JsonQueryCompileError} JQ0003 for a non-singular or invalid path
|
|
235
|
+
*/
|
|
236
|
+
export function compileSelector(value, member, docPath) {
|
|
237
|
+
if (typeof value !== 'string') {
|
|
238
|
+
throw compileError('JQ0003',
|
|
239
|
+
`'${member}' is a singular path into the row, got ${describeItem(value)}`, docPath);
|
|
240
|
+
}
|
|
241
|
+
let ast;
|
|
242
|
+
try {
|
|
243
|
+
ast = parseJSONPath(value);
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
throw compileError('JQ0003',
|
|
247
|
+
`'${member}': ${e instanceof JSONPathSyntaxError ? e.message : 'is not a path'}`,
|
|
248
|
+
docPath, e);
|
|
249
|
+
}
|
|
250
|
+
const segments = ast.segments;
|
|
251
|
+
if (segments.length === 0) {
|
|
252
|
+
throw compileError('JQ0003',
|
|
253
|
+
`'${member}' selects the whole row rather than a member of it`, docPath);
|
|
254
|
+
}
|
|
255
|
+
if (!isSingularSegments(segments)) {
|
|
256
|
+
throw compileError('JQ0003',
|
|
257
|
+
`'${member}' is a singular path — one name or index per segment, no wildcard,`
|
|
258
|
+
+ ' descendant or filter', docPath);
|
|
259
|
+
}
|
|
260
|
+
if (segments.length === 1 && segments[0].selectors[0].kind === 'name')
|
|
261
|
+
return segments[0].selectors[0].name;
|
|
262
|
+
const walk = compileSingularGetter(segments, true);
|
|
263
|
+
return (item) => {
|
|
264
|
+
const found = walk(item, item);
|
|
265
|
+
return found === NOTHING ? undefined : found;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The wall clock a spec's calendar boundaries fall on, resolved once.
|
|
271
|
+
*
|
|
272
|
+
* UTC and a fixed offset need nothing. A named zone needs the tzdb this
|
|
273
|
+
* suite deliberately does not bundle (D7), and a JSON document cannot
|
|
274
|
+
* carry one — so the provider arrives through the compilation's
|
|
275
|
+
* `zoneProvider` option, and a named zone without one is a refusal
|
|
276
|
+
* naming the seam rather than a silent fall back to UTC that is right
|
|
277
|
+
* for eight months of the year.
|
|
278
|
+
*
|
|
279
|
+
* @param {any} spec - the captured spec (or calendar context)
|
|
280
|
+
* @param {any} provider - `options.zoneProvider`, or null
|
|
281
|
+
* @param {string} docPath
|
|
282
|
+
* @returns {{ zone?: string, offset?: number, provider?: any, disambiguation?: string }}
|
|
283
|
+
* the clock options the kernel takes
|
|
284
|
+
* @throws {JsonQueryCompileError} JQ0003
|
|
285
|
+
*/
|
|
286
|
+
export function compileClock(spec, provider, docPath) {
|
|
287
|
+
/** @type {any} */
|
|
288
|
+
const clock = {};
|
|
289
|
+
if (hasOwn(spec, 'zone')) {
|
|
290
|
+
if (typeof spec.zone !== 'string') {
|
|
291
|
+
throw compileError('JQ0003',
|
|
292
|
+
`'zone' is an IANA zone name, got ${describeItem(spec.zone)}`, docPath);
|
|
293
|
+
}
|
|
294
|
+
clock.zone = spec.zone;
|
|
295
|
+
if (spec.zone !== 'UTC') {
|
|
296
|
+
if (provider === null) {
|
|
297
|
+
throw compileError('JQ0003',
|
|
298
|
+
`the zone '${spec.zone}' needs a time-zone provider: this suite bundles no tzdb, so`
|
|
299
|
+
+ ' a named zone is compiled with options.zoneProvider (toParts / toEpoch).'
|
|
300
|
+
+ " 'UTC' and a numeric 'offset' need none", docPath);
|
|
301
|
+
}
|
|
302
|
+
clock.provider = provider;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (hasOwn(spec, 'offset'))
|
|
306
|
+
clock.offset = requireNumber(spec.offset, 'offset', docPath);
|
|
307
|
+
if (hasOwn(spec, 'disambiguation'))
|
|
308
|
+
clock.disambiguation = requireEnum(spec.disambiguation, DISAMBIGUATIONS, 'disambiguation', docPath);
|
|
309
|
+
// the kernel owns the remaining refusals (a zone and an offset at
|
|
310
|
+
// once); raising them here keeps them compile-time
|
|
311
|
+
try {
|
|
312
|
+
resolveClock(clock);
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
throw compileError('JQ0003',
|
|
316
|
+
`the calendar context: ${e instanceof Error ? e.message : 'is invalid'}`, docPath, e);
|
|
317
|
+
}
|
|
318
|
+
return clock;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The seven aggregates D5 fixes, for both `$resample` and `$rolling`. */
|
|
322
|
+
export const AGGREGATES = Object.freeze([
|
|
323
|
+
'sum', 'mean', 'min', 'max', 'first', 'last', 'count']);
|
|
324
|
+
|
|
325
|
+
/** The five fill policies D5 fixes. */
|
|
326
|
+
export const FILLS = Object.freeze(['omit', 'null', 'zero', 'locf', 'linear']);
|
|
327
|
+
|
|
328
|
+
/** Which way an as-of join looks for its match. */
|
|
329
|
+
export const DIRECTIONS = Object.freeze(['backward', 'forward', 'nearest']);
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Copy the members a spec authored into the shape the kernel takes,
|
|
333
|
+
* compiling each through its own rule. Members absent from the document
|
|
334
|
+
* stay absent, so the kernel's own defaults are the only defaults.
|
|
335
|
+
* @param {any} spec - the captured literal
|
|
336
|
+
* @param {any} provider - `options.zoneProvider`, or null
|
|
337
|
+
* @param {string} docPath
|
|
338
|
+
* @param {Record<string, (value: any, member: string, docPath: string) => any>} rules
|
|
339
|
+
* @returns {any} the kernel spec
|
|
340
|
+
*/
|
|
341
|
+
export function buildKernelSpec(spec, provider, docPath, rules) {
|
|
342
|
+
/** @type {any} */
|
|
343
|
+
const out = compileClock(spec, provider, docPath);
|
|
344
|
+
for (const member of Object.keys(rules)) {
|
|
345
|
+
if (!hasOwn(spec, member))
|
|
346
|
+
continue;
|
|
347
|
+
const value = SELECTOR_MEMBERS.includes(member)
|
|
348
|
+
? compileSelector(spec[member], member, docPath)
|
|
349
|
+
: rules[member](spec[member], member, docPath);
|
|
350
|
+
out[member] = value;
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The rows a series operand carries.
|
|
357
|
+
*
|
|
358
|
+
* Both spellings a document actually has work, and neither is ambiguous
|
|
359
|
+
* because a sample is an object: a path that fans out (`$.rows[*]`)
|
|
360
|
+
* arrives as a sequence of rows, and a path that does not (`$.rows`)
|
|
361
|
+
* arrives as the one array item that holds them. The empty sequence is
|
|
362
|
+
* an empty series — no rows is data, not an error.
|
|
363
|
+
*
|
|
364
|
+
* @param {any} v - the evaluated operand
|
|
365
|
+
* @param {string} docPath
|
|
366
|
+
* @returns {any[]}
|
|
367
|
+
* @throws {JsonQueryRuntimeError} JQ2001 when it is not rows at all
|
|
368
|
+
*/
|
|
369
|
+
export function seriesArg(v, docPath) {
|
|
370
|
+
if (v === EMPTY)
|
|
371
|
+
return [];
|
|
372
|
+
if (v instanceof Seq)
|
|
373
|
+
return v.items;
|
|
374
|
+
if (Array.isArray(v))
|
|
375
|
+
return v;
|
|
376
|
+
if (v !== null && typeof v === 'object')
|
|
377
|
+
return [v];
|
|
378
|
+
throw new JsonQueryRuntimeError('JQ2001',
|
|
379
|
+
`expected a series (records with an instant and a reading), got ${describeItem(v)}`, docPath);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* One half-open interval operand: a record with a `start` and an `end`.
|
|
384
|
+
* The kernel decides whether the bounds name instants and whether the
|
|
385
|
+
* span is a span; this only refuses what is not a record.
|
|
386
|
+
* @param {any} v
|
|
387
|
+
* @param {string} docPath
|
|
388
|
+
* @returns {any}
|
|
389
|
+
* @throws {JsonQueryRuntimeError} JQ2001
|
|
390
|
+
*/
|
|
391
|
+
export function intervalArg(v, docPath) {
|
|
392
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v) || v instanceof Seq) {
|
|
393
|
+
throw new JsonQueryRuntimeError('JQ2001',
|
|
394
|
+
`expected an interval record { start, end }, got ${describeItem(v)}`, docPath);
|
|
395
|
+
}
|
|
396
|
+
return v;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The kernel's refusals, in this language's vocabulary. Everything the
|
|
401
|
+
* temporal kernel throws is a `TypeError` about the DATA it was handed —
|
|
402
|
+
* a row that is not a sample, an instant that names none, a local time
|
|
403
|
+
* that never happened — which is exactly what `JQ2001` is for. A host
|
|
404
|
+
* failure that is not a `TypeError` is not laundered.
|
|
405
|
+
* @param {unknown} e
|
|
406
|
+
* @param {string} docPath
|
|
407
|
+
* @returns {JsonQueryRuntimeError}
|
|
408
|
+
*/
|
|
409
|
+
export function seriesRefusal(e, docPath) {
|
|
410
|
+
if (!(e instanceof TypeError))
|
|
411
|
+
throw e;
|
|
412
|
+
return new JsonQueryRuntimeError('JQ2001', e.message, docPath, { cause: e });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
//#endregion
|