@pythia-software/query-table-core 0.6.0 → 0.7.1
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 +22 -0
- package/dist/index.d.ts +455 -15
- package/dist/index.js +2391 -463
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,438 @@
|
|
|
1
|
+
// src/cellDisplay.ts
|
|
2
|
+
var calendarOptions = {
|
|
3
|
+
style: ["human", "iso", "custom"],
|
|
4
|
+
pattern: "string",
|
|
5
|
+
timeZone: "string",
|
|
6
|
+
locale: "string"
|
|
7
|
+
};
|
|
8
|
+
var numberOptions = { decimals: [0, 20], locale: "string" };
|
|
9
|
+
var calendarUnits = ["iso", "epochSeconds", "epochMilliseconds"];
|
|
10
|
+
var options = {
|
|
11
|
+
json: { encoding: ["auto", "value", "json", "escaped"], maxDecodeDepth: [1, 8], previewLength: [20, 500], expandDepth: [0, 8] },
|
|
12
|
+
date: { ...calendarOptions, sourceUnit: calendarUnits },
|
|
13
|
+
datetime: { ...calendarOptions, sourceUnit: calendarUnits },
|
|
14
|
+
time: { ...calendarOptions, sourceUnit: [...calendarUnits, "secondsOfDay", "millisecondsOfDay"] },
|
|
15
|
+
duration: { sourceUnit: ["milliseconds", "seconds", "minutes", "hours", "days"], style: ["human", "long", "clock", "custom"], pattern: "string", decimals: [0, 20] },
|
|
16
|
+
number: numberOptions,
|
|
17
|
+
percent: numberOptions,
|
|
18
|
+
chip: { labels: "labels", tones: "tones" },
|
|
19
|
+
text: { truncate: "boolean" }
|
|
20
|
+
};
|
|
21
|
+
function parseCellDisplay(raw, context = "cell display") {
|
|
22
|
+
const fail = (reason) => {
|
|
23
|
+
throw Error(`${context}: ${reason}`);
|
|
24
|
+
};
|
|
25
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("expected an object");
|
|
26
|
+
const record2 = raw;
|
|
27
|
+
if (typeof record2.kind !== "string" || !Object.prototype.hasOwnProperty.call(options, record2.kind)) return fail("unsupported kind");
|
|
28
|
+
const allowed = options[record2.kind];
|
|
29
|
+
for (const [key, value] of Object.entries(record2)) {
|
|
30
|
+
if (key === "kind") continue;
|
|
31
|
+
const rule = Object.prototype.hasOwnProperty.call(allowed, key) ? allowed[key] : void 0;
|
|
32
|
+
if (!rule) return fail(`unsupported option ${key}`);
|
|
33
|
+
if (Array.isArray(rule)) {
|
|
34
|
+
if (typeof rule[0] === "number") {
|
|
35
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < Number(rule[0]) || value > Number(rule[1])) return fail(`${key} must be an integer from ${rule[0]} to ${rule[1]}`);
|
|
36
|
+
} else if (!rule.includes(value)) return fail(`unsupported ${key}`);
|
|
37
|
+
} else if (rule === "labels" || rule === "tones") {
|
|
38
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !Object.values(value).every((item) => typeof item === "string" && (rule === "labels" || ["neutral", "success", "warning", "danger"].includes(item)))) return fail(`invalid ${key} map`);
|
|
39
|
+
} else if (typeof value !== rule) return fail(`${key} must be ${rule}`);
|
|
40
|
+
if (key === "pattern" && value.length > 160) return fail("pattern must be at most 160 characters");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...record2,
|
|
44
|
+
...record2.labels ? { labels: { ...record2.labels } } : {},
|
|
45
|
+
...record2.tones ? { tones: { ...record2.tones } } : {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/properties.ts
|
|
50
|
+
var MAX_PROPERTIES = 200;
|
|
51
|
+
var string = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max;
|
|
52
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
53
|
+
function normalizePropertyFormat(input) {
|
|
54
|
+
if (!record(input)) return;
|
|
55
|
+
const kind = input.kind;
|
|
56
|
+
if (kind === "json" || kind === "chip") {
|
|
57
|
+
try {
|
|
58
|
+
return parseCellDisplay(input);
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (input.decimals !== void 0 && !(typeof input.decimals === "number" && Number.isInteger(input.decimals) && input.decimals >= 0 && input.decimals <= 20))
|
|
64
|
+
return;
|
|
65
|
+
if (["text", "code", "boolean", "chips"].includes(String(kind)))
|
|
66
|
+
return { kind };
|
|
67
|
+
if (kind === "relative") {
|
|
68
|
+
if (input.sourceUnit !== void 0 && !["iso", "epochSeconds", "epochMilliseconds"].includes(
|
|
69
|
+
String(input.sourceUnit)
|
|
70
|
+
))
|
|
71
|
+
return;
|
|
72
|
+
return {
|
|
73
|
+
kind,
|
|
74
|
+
...input.sourceUnit ? { sourceUnit: input.sourceUnit } : {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (kind === "currency") {
|
|
78
|
+
if (typeof input.currency !== "string" || !/^[A-Z]{3}$/.test(input.currency))
|
|
79
|
+
return;
|
|
80
|
+
return {
|
|
81
|
+
kind,
|
|
82
|
+
currency: input.currency,
|
|
83
|
+
...typeof input.decimals === "number" && Number.isInteger(input.decimals) && input.decimals >= 0 && input.decimals <= 20 ? { decimals: input.decimals } : {}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!["number", "percent", "duration", "date", "time", "datetime"].includes(
|
|
87
|
+
String(kind)
|
|
88
|
+
))
|
|
89
|
+
return;
|
|
90
|
+
if (input.sourceUnit !== void 0 && ![
|
|
91
|
+
"milliseconds",
|
|
92
|
+
"seconds",
|
|
93
|
+
"minutes",
|
|
94
|
+
"hours",
|
|
95
|
+
"days",
|
|
96
|
+
"iso",
|
|
97
|
+
"epochSeconds",
|
|
98
|
+
"epochMilliseconds",
|
|
99
|
+
"secondsOfDay",
|
|
100
|
+
"millisecondsOfDay"
|
|
101
|
+
].includes(String(input.sourceUnit)))
|
|
102
|
+
return;
|
|
103
|
+
if (input.style !== void 0 && !["human", "long", "clock", "custom", "iso"].includes(String(input.style)))
|
|
104
|
+
return;
|
|
105
|
+
if (input.pattern !== void 0 && !string(input.pattern, 1e3)) return;
|
|
106
|
+
if (input.timeZone !== void 0 && !string(input.timeZone)) return;
|
|
107
|
+
const out = { kind };
|
|
108
|
+
if (typeof input.decimals === "number" && Number.isInteger(input.decimals) && input.decimals >= 0 && input.decimals <= 20)
|
|
109
|
+
out.decimals = input.decimals;
|
|
110
|
+
if ([
|
|
111
|
+
"milliseconds",
|
|
112
|
+
"seconds",
|
|
113
|
+
"minutes",
|
|
114
|
+
"hours",
|
|
115
|
+
"days",
|
|
116
|
+
"iso",
|
|
117
|
+
"epochSeconds",
|
|
118
|
+
"epochMilliseconds",
|
|
119
|
+
"secondsOfDay",
|
|
120
|
+
"millisecondsOfDay"
|
|
121
|
+
].includes(String(input.sourceUnit)))
|
|
122
|
+
out.sourceUnit = input.sourceUnit;
|
|
123
|
+
if (["human", "long", "clock", "custom", "iso"].includes(String(input.style)))
|
|
124
|
+
out.style = input.style;
|
|
125
|
+
if (string(input.pattern, 1e3)) out.pattern = input.pattern;
|
|
126
|
+
if (string(input.timeZone)) out.timeZone = input.timeZone;
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function normalizePropertyView(input) {
|
|
130
|
+
if (!record(input) || input.version !== 1 || !Array.isArray(input.properties))
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Unsupported or invalid property view (expected version 1)."
|
|
133
|
+
);
|
|
134
|
+
if (input.properties.length > MAX_PROPERTIES)
|
|
135
|
+
throw new Error(`At most ${MAX_PROPERTIES} properties are allowed.`);
|
|
136
|
+
const ids = /* @__PURE__ */ new Set();
|
|
137
|
+
const properties = input.properties.map((item) => {
|
|
138
|
+
if (!record(item) || !string(item.id) || !string(item.field) || ids.has(item.id))
|
|
139
|
+
throw new Error("Property fields and unique instance IDs are required.");
|
|
140
|
+
ids.add(item.id);
|
|
141
|
+
const property = { id: item.id, field: item.field };
|
|
142
|
+
if (string(item.label, 1e3)) property.label = item.label;
|
|
143
|
+
if (string(item.render)) property.render = item.render;
|
|
144
|
+
if (item.format !== void 0) {
|
|
145
|
+
const format = normalizePropertyFormat(item.format);
|
|
146
|
+
if (!format) throw new Error(`Invalid format for property ${item.id}.`);
|
|
147
|
+
property.format = format;
|
|
148
|
+
}
|
|
149
|
+
return property;
|
|
150
|
+
});
|
|
151
|
+
const labelWidth = typeof input.labelWidth === "number" && Number.isFinite(input.labelWidth) ? Math.max(15, Math.min(75, Math.round(input.labelWidth))) : void 0;
|
|
152
|
+
return { version: 1, properties, ...labelWidth !== void 0 ? { labelWidth } : {} };
|
|
153
|
+
}
|
|
154
|
+
function createPropertyView(schema, names) {
|
|
155
|
+
const enabled = schema.fields.filter((f2) => f2.property?.enabled !== false);
|
|
156
|
+
const hasPropertyDefaults = enabled.some(
|
|
157
|
+
(f2) => f2.property?.default !== void 0
|
|
158
|
+
);
|
|
159
|
+
const defaults = hasPropertyDefaults ? enabled.filter((f2) => f2.property?.default).map((f2) => f2.name) : schema.defaultSelect?.map((column) => column.field) ?? enabled.filter((f2) => f2.select?.default).map((f2) => f2.name);
|
|
160
|
+
const fields = names ?? (hasPropertyDefaults || defaults.length ? defaults.filter((name) => enabled.some((field) => field.name === name)) : enabled.map((f2) => f2.name));
|
|
161
|
+
return normalizePropertyView({
|
|
162
|
+
version: 1,
|
|
163
|
+
properties: fields.map((field, i) => ({ id: `property-${i}`, field }))
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function objectReferenceKey(reference) {
|
|
167
|
+
return JSON.stringify([
|
|
168
|
+
reference.objectType,
|
|
169
|
+
Object.entries(reference.key).sort(([a], [b]) => a.localeCompare(b))
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
function readObjectReference(field, fields, row) {
|
|
173
|
+
const relationship = field.relationship;
|
|
174
|
+
if (!relationship) return null;
|
|
175
|
+
const key = /* @__PURE__ */ Object.create(null);
|
|
176
|
+
for (const [target, local] of Object.entries(relationship.key)) {
|
|
177
|
+
const source = fields.find((f2) => f2.name === local);
|
|
178
|
+
if (!source) return null;
|
|
179
|
+
const value = readFieldValue(source, row);
|
|
180
|
+
if (!(typeof value === "string" && value !== "" || typeof value === "number" && Number.isFinite(value)))
|
|
181
|
+
return null;
|
|
182
|
+
key[target] = value;
|
|
183
|
+
}
|
|
184
|
+
return Object.keys(key).length ? { objectType: relationship.objectType, key } : null;
|
|
185
|
+
}
|
|
186
|
+
function discoverFieldRelationships(fields, keys, types) {
|
|
187
|
+
return fields.map((field) => {
|
|
188
|
+
if (field.relationship || !field.origin) return field;
|
|
189
|
+
const candidates = [];
|
|
190
|
+
for (const fk of keys) {
|
|
191
|
+
if (fk.sourceTable !== field.origin.table || !fk.sourceColumns.includes(field.origin.column) || fk.sourceColumns.length === 0 || fk.sourceColumns.length !== fk.targetColumns.length)
|
|
192
|
+
continue;
|
|
193
|
+
const targets = types.filter(
|
|
194
|
+
(type) => type.table === fk.targetTable && type.keyFields.length === fk.targetColumns.length && type.keyFields.every((k) => fk.targetColumns.includes(k))
|
|
195
|
+
);
|
|
196
|
+
if (targets.length !== 1) continue;
|
|
197
|
+
const key = /* @__PURE__ */ Object.create(null);
|
|
198
|
+
let complete = true;
|
|
199
|
+
fk.sourceColumns.forEach((column, i) => {
|
|
200
|
+
const local = fields.filter(
|
|
201
|
+
(f2) => f2.origin?.table === fk.sourceTable && f2.origin.column === column
|
|
202
|
+
);
|
|
203
|
+
if (local.length !== 1) complete = false;
|
|
204
|
+
else key[fk.targetColumns[i]] = local[0].name;
|
|
205
|
+
});
|
|
206
|
+
if (complete) candidates.push({ objectType: targets[0].name, key });
|
|
207
|
+
}
|
|
208
|
+
return candidates.length === 1 ? { ...field, relationship: candidates[0] } : field;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/schema.ts
|
|
213
|
+
function isFilterable(f2) {
|
|
214
|
+
if (f2.source.kind === "derived" && f2.source.computedId) return false;
|
|
215
|
+
return f2.filter?.enabled ?? f2.source.kind === "backend";
|
|
216
|
+
}
|
|
217
|
+
function isPushdownFilter(f2) {
|
|
218
|
+
return isFilterable(f2) && (f2.filter?.pushdown ?? f2.source.kind === "backend");
|
|
219
|
+
}
|
|
220
|
+
function isSortable(f2) {
|
|
221
|
+
if (f2.source.kind === "derived" && f2.source.computedId) return f2.sort?.enabled === true;
|
|
222
|
+
return f2.sort?.enabled ?? f2.source.kind === "backend";
|
|
223
|
+
}
|
|
224
|
+
function isSelectable(f2) {
|
|
225
|
+
return f2.select?.enabled ?? true;
|
|
226
|
+
}
|
|
227
|
+
function filterValues(f2) {
|
|
228
|
+
return f2.filter?.values ?? { source: "autocomplete" };
|
|
229
|
+
}
|
|
230
|
+
function indexFields(schema) {
|
|
231
|
+
return new Map(schema.fields.map((f2) => [f2.name, f2]));
|
|
232
|
+
}
|
|
233
|
+
function selectedFields(schema, q) {
|
|
234
|
+
const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((f2) => f2.select?.default).map((f2) => ({ field: f2.name }))).map(
|
|
235
|
+
(c) => c.field
|
|
236
|
+
);
|
|
237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const name of order) {
|
|
240
|
+
if (seen.has(name)) continue;
|
|
241
|
+
const f2 = resolveField(schema, name);
|
|
242
|
+
if (!f2 || !isSelectable(f2)) continue;
|
|
243
|
+
seen.add(name);
|
|
244
|
+
seen.add(f2.name);
|
|
245
|
+
out.push(f2);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
function resolveField(schema, field) {
|
|
250
|
+
const token = field.trim();
|
|
251
|
+
if (!token) return void 0;
|
|
252
|
+
const byName = indexFields(schema);
|
|
253
|
+
const byExactName = byName.get(token);
|
|
254
|
+
if (byExactName) return byExactName;
|
|
255
|
+
const needle = token.toLowerCase();
|
|
256
|
+
for (const f2 of schema.fields) {
|
|
257
|
+
if (f2.name.toLowerCase() === needle) return f2;
|
|
258
|
+
if (aliasMatches(f2, needle)) return f2;
|
|
259
|
+
}
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
function resolveFieldName(schema, field) {
|
|
263
|
+
return resolveField(schema, field)?.name;
|
|
264
|
+
}
|
|
265
|
+
function aliasTermsFor(field) {
|
|
266
|
+
const out = [];
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
const add = (raw) => {
|
|
269
|
+
if (typeof raw !== "string") return;
|
|
270
|
+
const value = raw.trim();
|
|
271
|
+
if (!value) return;
|
|
272
|
+
const key = value.toLowerCase();
|
|
273
|
+
if (seen.has(key)) return;
|
|
274
|
+
seen.add(key);
|
|
275
|
+
out.push(value);
|
|
276
|
+
};
|
|
277
|
+
add(field.alias);
|
|
278
|
+
if (field.aliases) {
|
|
279
|
+
for (const alias of field.aliases) add(alias);
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
function aliasMatches(field, needle) {
|
|
284
|
+
for (const alias of aliasTermsFor(field)) {
|
|
285
|
+
if (alias.toLowerCase() === needle) return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
function readFieldValue(field, row) {
|
|
290
|
+
if (row == null) return null;
|
|
291
|
+
if (field.source.kind === "derived" && field.source.accessor) {
|
|
292
|
+
return field.source.accessor(row);
|
|
293
|
+
}
|
|
294
|
+
const path = field.source.kind === "backend" && field.source.path || field.name;
|
|
295
|
+
const r = row;
|
|
296
|
+
if (path in r) return r[path] ?? null;
|
|
297
|
+
let cur = row;
|
|
298
|
+
for (const part of path.split(".")) {
|
|
299
|
+
if (cur == null || typeof cur !== "object") return null;
|
|
300
|
+
cur = cur[part];
|
|
301
|
+
}
|
|
302
|
+
return cur ?? null;
|
|
303
|
+
}
|
|
304
|
+
function loadSchema(doc) {
|
|
305
|
+
const d = doc;
|
|
306
|
+
if (!d || typeof d !== "object") throw new Error("schema: document is not an object");
|
|
307
|
+
if (typeof d.name !== "string") throw new Error("schema: missing `name`");
|
|
308
|
+
if (typeof d.idField !== "string") throw new Error("schema: missing `idField`");
|
|
309
|
+
if (!Array.isArray(d.fields) || d.fields.length === 0) throw new Error("schema: `fields` must be a non-empty array");
|
|
310
|
+
const fields = d.fields.map((raw, i) => projectField(raw, i));
|
|
311
|
+
return {
|
|
312
|
+
name: d.name,
|
|
313
|
+
idField: d.idField,
|
|
314
|
+
fields,
|
|
315
|
+
defaultSort: d.defaultSort,
|
|
316
|
+
defaultSelect: d.defaultSelect,
|
|
317
|
+
defaultLimit: d.defaultLimit
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function projectField(raw, i) {
|
|
321
|
+
if (!raw || typeof raw.name !== "string") throw new Error(`schema: field[${i}] missing \`name\``);
|
|
322
|
+
if (typeof raw.label !== "string") throw new Error(`schema: field ${raw.name} missing \`label\``);
|
|
323
|
+
if (typeof raw.type !== "string") throw new Error(`schema: field ${raw.name} missing \`type\``);
|
|
324
|
+
if (raw.property !== void 0) {
|
|
325
|
+
const property = raw.property;
|
|
326
|
+
if (!property || typeof property !== "object" || Array.isArray(property) || property.enabled !== void 0 && typeof property.enabled !== "boolean" || property.default !== void 0 && typeof property.default !== "boolean" || property.render !== void 0 && (typeof property.render !== "string" || !property.render)) {
|
|
327
|
+
throw new Error(`schema: field ${raw.name} has invalid property metadata`);
|
|
328
|
+
}
|
|
329
|
+
if (property.format !== void 0 && !normalizePropertyFormat(property.format)) {
|
|
330
|
+
throw new Error(`schema: field ${raw.name} has an invalid property format`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (raw.origin !== void 0 && (!raw.origin || typeof raw.origin.table !== "string" || !raw.origin.table || typeof raw.origin.column !== "string" || !raw.origin.column)) {
|
|
334
|
+
throw new Error(`schema: field ${raw.name} has an invalid origin`);
|
|
335
|
+
}
|
|
336
|
+
if (raw.relationship !== void 0 && (!raw.relationship || typeof raw.relationship.objectType !== "string" || !raw.relationship.objectType || !raw.relationship.key || typeof raw.relationship.key !== "object" || Array.isArray(raw.relationship.key) || Object.keys(raw.relationship.key).length === 0 || Object.entries(raw.relationship.key).some(([key, value]) => !key || typeof value !== "string" || !value))) {
|
|
337
|
+
throw new Error(`schema: field ${raw.name} has an invalid relationship`);
|
|
338
|
+
}
|
|
339
|
+
const rs = raw.source ?? (raw.bindings ? "backend" : "derived");
|
|
340
|
+
let source;
|
|
341
|
+
if (typeof rs === "string") {
|
|
342
|
+
source = rs === "derived" ? { kind: "derived" } : { kind: "backend" };
|
|
343
|
+
} else if (rs.kind === "derived") {
|
|
344
|
+
source = { kind: "derived" };
|
|
345
|
+
} else {
|
|
346
|
+
source = { kind: "backend", path: rs.path, synthetic: rs.synthetic };
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
name: raw.name,
|
|
350
|
+
label: raw.label,
|
|
351
|
+
type: raw.type,
|
|
352
|
+
source,
|
|
353
|
+
filter: raw.filter,
|
|
354
|
+
sort: raw.sort,
|
|
355
|
+
select: raw.select,
|
|
356
|
+
aggregate: raw.aggregate,
|
|
357
|
+
group: raw.group,
|
|
358
|
+
alias: raw.alias,
|
|
359
|
+
aliases: normalizeAliases(raw.aliases, raw.alias),
|
|
360
|
+
...raw.display === void 0 ? {} : { display: parseCellDisplay(raw.display, `schema: field ${raw.name} display`) },
|
|
361
|
+
render: raw.render,
|
|
362
|
+
// string key from JSON; consumer may override with a fn
|
|
363
|
+
property: raw.property ? { ...raw.property, format: raw.property.format === void 0 ? void 0 : normalizePropertyFormat(raw.property.format) } : void 0,
|
|
364
|
+
origin: raw.origin,
|
|
365
|
+
relationship: raw.relationship
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function normalizeAliases(rawAliases, rawAlias) {
|
|
369
|
+
const aliases = [];
|
|
370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
371
|
+
const add = (raw) => {
|
|
372
|
+
if (typeof raw !== "string") return;
|
|
373
|
+
const value = raw.trim();
|
|
374
|
+
if (!value) return;
|
|
375
|
+
const key = value.toLowerCase();
|
|
376
|
+
if (seen.has(key)) return;
|
|
377
|
+
seen.add(key);
|
|
378
|
+
aliases.push(value);
|
|
379
|
+
};
|
|
380
|
+
add(rawAlias);
|
|
381
|
+
if (Array.isArray(rawAliases)) {
|
|
382
|
+
for (const item of rawAliases) add(item);
|
|
383
|
+
}
|
|
384
|
+
return aliases;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/itemPage.ts
|
|
388
|
+
function itemPageFields(template) {
|
|
389
|
+
return [...new Set(Array.from(template.matchAll(/\{field:([^{}]+)\}/g), (match) => match[1]))];
|
|
390
|
+
}
|
|
391
|
+
function validateItemPageTemplate(template, schema) {
|
|
392
|
+
if (template.length > 8e3) return "Use a URL template of at most 8,000 characters.";
|
|
393
|
+
if (!template.trim()) return null;
|
|
394
|
+
const remainder = template.replace(/\{(id|query|field:[^{}]+)\}/g, "value");
|
|
395
|
+
if (/[{}]/.test(remainder)) return "Use {id}, {query}, or {field:field_name} placeholders.";
|
|
396
|
+
for (const name of itemPageFields(template)) {
|
|
397
|
+
if (!schema.fields.some((field) => field.name === name)) return `Unknown URL field: ${name}.`;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
if (!["http:", "https:"].includes(new URL(remainder, "https://query-table.invalid/").protocol))
|
|
401
|
+
return "Use an HTTP, HTTPS, or relative page URL.";
|
|
402
|
+
} catch {
|
|
403
|
+
return "Enter a valid page URL.";
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
function renderItemPageTemplate(template, schema, row, id, query) {
|
|
408
|
+
if (!template.trim() || validateItemPageTemplate(template, schema)) return null;
|
|
409
|
+
let missing = false;
|
|
410
|
+
const href = template.replace(/\{(id|query|field:[^{}]+)\}/g, (_match, token) => {
|
|
411
|
+
const value = token === "id" ? id : token === "query" ? query : readFieldValue(schema.fields.find((field) => field.name === token.slice(6)), row);
|
|
412
|
+
if (value == null || !["string", "number", "boolean"].includes(typeof value)) {
|
|
413
|
+
missing = true;
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
416
|
+
return encodeURIComponent(String(value));
|
|
417
|
+
});
|
|
418
|
+
try {
|
|
419
|
+
if (missing || !["http:", "https:"].includes(new URL(href, "https://query-table.invalid/").protocol)) return null;
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
return href;
|
|
424
|
+
}
|
|
425
|
+
|
|
1
426
|
// src/metricNormalize.ts
|
|
2
427
|
function normalizeMetricOptions(raw) {
|
|
3
428
|
const out = {}, diagnostics = [];
|
|
4
|
-
const
|
|
429
|
+
const record2 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
5
430
|
const copy = (input, specs) => {
|
|
6
431
|
const result = {};
|
|
7
432
|
for (const [key, spec] of Object.entries(specs)) {
|
|
8
433
|
const v = input[key];
|
|
9
434
|
if (v === void 0) continue;
|
|
10
|
-
if (spec === "string" ? typeof v === "string" && v.length <= 1e4 : spec === "number" ? typeof v === "number" && Number.isFinite(v) : spec === "boolean" ? typeof v === "boolean" : spec === "integer" ? typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= 1e6 : typeof v === "string" && spec.includes(v))
|
|
435
|
+
if (spec === "string" ? typeof v === "string" && v.length <= 1e4 : spec === "number" ? typeof v === "number" && Number.isFinite(v) : spec === "boolean" ? typeof v === "boolean" : spec === "integer" || spec === "positiveInteger" ? typeof v === "number" && Number.isSafeInteger(v) && v >= (spec === "positiveInteger" ? 1 : 0) && v <= 1e6 : typeof v === "string" && spec.includes(v))
|
|
11
436
|
result[key] = v;
|
|
12
437
|
else diagnostics.push(`Invalid metric ${key}.`);
|
|
13
438
|
}
|
|
@@ -22,8 +447,24 @@ function normalizeMetricOptions(raw) {
|
|
|
22
447
|
groupLimit: "integer"
|
|
23
448
|
})
|
|
24
449
|
);
|
|
450
|
+
if (raw.dateGrouping !== void 0) {
|
|
451
|
+
if (record2(raw.dateGrouping)) {
|
|
452
|
+
const d = copy(raw.dateGrouping, {
|
|
453
|
+
field: "string",
|
|
454
|
+
interval: ["hour", "day", "week", "month", "year"],
|
|
455
|
+
timeZone: "string",
|
|
456
|
+
includeEmpty: "boolean",
|
|
457
|
+
range: ["query", "observed", "custom"],
|
|
458
|
+
start: "string",
|
|
459
|
+
end: "string"
|
|
460
|
+
});
|
|
461
|
+
if (typeof d.field !== "string" || !d.field || d.field.length > 256 || !d.interval || typeof d.includeEmpty !== "boolean" || !d.range)
|
|
462
|
+
diagnostics.push("Invalid metric date grouping.");
|
|
463
|
+
out.dateGrouping = d;
|
|
464
|
+
} else diagnostics.push("Invalid metric date grouping.");
|
|
465
|
+
}
|
|
25
466
|
if (raw.display !== void 0) {
|
|
26
|
-
if (
|
|
467
|
+
if (record2(raw.display)) {
|
|
27
468
|
if (typeof raw.display.kind !== "string")
|
|
28
469
|
diagnostics.push("Invalid metric display kind.");
|
|
29
470
|
const d = copy(raw.display, {
|
|
@@ -45,10 +486,12 @@ function normalizeMetricOptions(raw) {
|
|
|
45
486
|
xLabel: "string",
|
|
46
487
|
yLabel: "string",
|
|
47
488
|
legendPosition: ["left", "right", "top", "bottom", "none"],
|
|
489
|
+
maxGroups: "positiveInteger",
|
|
490
|
+
showOther: "boolean",
|
|
48
491
|
boxMean: "boolean"
|
|
49
492
|
});
|
|
50
493
|
for (const k of ["xScale", "yScale"]) {
|
|
51
|
-
if (
|
|
494
|
+
if (record2(raw.display[k]))
|
|
52
495
|
d[k] = copy(raw.display[k], {
|
|
53
496
|
mode: ["linear", "log"],
|
|
54
497
|
min: "number",
|
|
@@ -58,228 +501,88 @@ function normalizeMetricOptions(raw) {
|
|
|
58
501
|
diagnostics.push(`Invalid metric ${k}.`);
|
|
59
502
|
}
|
|
60
503
|
for (const k of ["format", "xFormat", "yFormat"])
|
|
61
|
-
if (
|
|
504
|
+
if (record2(raw.display[k]))
|
|
62
505
|
d[k] = copy(raw.display[k], {
|
|
63
506
|
kind: ["number", "percent", "duration", "date", "time", "datetime"],
|
|
64
507
|
decimals: "integer",
|
|
65
|
-
sourceUnit: [
|
|
66
|
-
"milliseconds",
|
|
67
|
-
"seconds",
|
|
68
|
-
"minutes",
|
|
69
|
-
"hours",
|
|
70
|
-
"days",
|
|
71
|
-
"iso",
|
|
72
|
-
"epochSeconds",
|
|
73
|
-
"epochMilliseconds",
|
|
74
|
-
"secondsOfDay",
|
|
75
|
-
"millisecondsOfDay"
|
|
76
|
-
],
|
|
77
|
-
style: ["human", "long", "clock", "custom", "iso"],
|
|
78
|
-
pattern: "string",
|
|
79
|
-
timeZone: "string"
|
|
80
|
-
});
|
|
81
|
-
if (
|
|
82
|
-
d.list = copy(raw.display.list, {
|
|
83
|
-
showBars: "boolean",
|
|
84
|
-
useGroupColors: "boolean",
|
|
85
|
-
showValues: "boolean"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (Array.isArray(raw.sort))
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// src/computed.ts
|
|
144
|
-
var COMPUTED_PREFIX = "@computed/";
|
|
145
|
-
var computedFieldName = (id) => COMPUTED_PREFIX + id;
|
|
146
|
-
var isComputedField = (name) => name.trim().toLowerCase().startsWith(COMPUTED_PREFIX);
|
|
147
|
-
function validateComputedColumn(input) {
|
|
148
|
-
const c = input;
|
|
149
|
-
if (!c || typeof c.id !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 200 || typeof c.revision !== "string" || !c.revision || c.revision.length > 256 || c.expression?.language !== "qt-expr" || c.expression.version !== 1 || typeof c.expression.source !== "string" || !c.expression.source.trim() || c.expression.source.length > 1e4) {
|
|
150
|
-
throw new Error(
|
|
151
|
-
"Invalid computed column definition or unsupported formula version."
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
id: c.id,
|
|
156
|
-
label: c.label,
|
|
157
|
-
revision: c.revision,
|
|
158
|
-
expression: { ...c.expression }
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
function memoryComputedColumnStore(seed = {}) {
|
|
162
|
-
const data = new Map(
|
|
163
|
-
Object.entries(seed).map(([k, v]) => [
|
|
164
|
-
k,
|
|
165
|
-
new Map(v.map((c) => [c.id, validateComputedColumn(c)]))
|
|
166
|
-
])
|
|
167
|
-
);
|
|
168
|
-
const seedRevisions = new Set(
|
|
169
|
-
[...data.values()].flatMap(
|
|
170
|
-
(entries) => [...entries.values()].map((c) => c.revision)
|
|
171
|
-
)
|
|
172
|
-
);
|
|
173
|
-
let revisionSequence = 0n;
|
|
174
|
-
function freshRevision() {
|
|
175
|
-
let revision;
|
|
176
|
-
do {
|
|
177
|
-
revision = `memory:${++revisionSequence}`;
|
|
178
|
-
} while (seedRevisions.has(revision));
|
|
179
|
-
return revision;
|
|
180
|
-
}
|
|
181
|
-
const listeners = /* @__PURE__ */ new Map();
|
|
182
|
-
return {
|
|
183
|
-
async list(dataset) {
|
|
184
|
-
return [...data.get(dataset)?.values() ?? []].map(
|
|
185
|
-
validateComputedColumn
|
|
186
|
-
);
|
|
187
|
-
},
|
|
188
|
-
async save(dataset, column, expectedRevision) {
|
|
189
|
-
const entries = data.get(dataset) ?? /* @__PURE__ */ new Map();
|
|
190
|
-
const current = entries.get(column.id);
|
|
191
|
-
if ((current?.revision ?? null) !== expectedRevision)
|
|
192
|
-
throw new Error(
|
|
193
|
-
"This definition changed. Reload it before saving again."
|
|
194
|
-
);
|
|
195
|
-
const next = validateComputedColumn({
|
|
196
|
-
...column,
|
|
197
|
-
revision: freshRevision()
|
|
198
|
-
});
|
|
199
|
-
entries.set(next.id, next);
|
|
200
|
-
data.set(dataset, entries);
|
|
201
|
-
listeners.get(dataset)?.forEach((fn) => fn());
|
|
202
|
-
return validateComputedColumn(next);
|
|
203
|
-
},
|
|
204
|
-
subscribe(dataset, listener) {
|
|
205
|
-
const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
|
|
206
|
-
set.add(listener);
|
|
207
|
-
listeners.set(dataset, set);
|
|
208
|
-
return () => {
|
|
209
|
-
set.delete(listener);
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
function httpComputedColumnStore(base, fetcher = fetch) {
|
|
215
|
-
const listeners = /* @__PURE__ */ new Map();
|
|
216
|
-
const url = (dataset) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(dataset)}`;
|
|
217
|
-
async function read(response) {
|
|
218
|
-
if (response.status === 409)
|
|
219
|
-
throw new Error(
|
|
220
|
-
"This definition changed. Reload it before saving again."
|
|
221
|
-
);
|
|
222
|
-
if (!response.ok)
|
|
223
|
-
throw new Error(
|
|
224
|
-
`Computed column store request failed (${response.status}).`
|
|
225
|
-
);
|
|
226
|
-
return response.json();
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
229
|
-
async list(dataset, signal) {
|
|
230
|
-
const raw = await read(
|
|
231
|
-
await fetcher(url(dataset), { ...signal ? { signal } : {} })
|
|
232
|
-
);
|
|
233
|
-
if (!Array.isArray(raw))
|
|
234
|
-
throw new Error("Invalid computed column catalogue.");
|
|
235
|
-
return raw.map(validateComputedColumn);
|
|
236
|
-
},
|
|
237
|
-
async save(dataset, column, expectedRevision) {
|
|
238
|
-
const result = validateComputedColumn(
|
|
239
|
-
await read(
|
|
240
|
-
await fetcher(url(dataset), {
|
|
241
|
-
method: "PUT",
|
|
242
|
-
headers: { "Content-Type": "application/json" },
|
|
243
|
-
body: JSON.stringify({ column, expectedRevision })
|
|
244
|
-
})
|
|
245
|
-
)
|
|
246
|
-
);
|
|
247
|
-
listeners.get(dataset)?.forEach((fn) => fn());
|
|
248
|
-
return result;
|
|
249
|
-
},
|
|
250
|
-
subscribe(dataset, listener) {
|
|
251
|
-
const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
|
|
252
|
-
set.add(listener);
|
|
253
|
-
listeners.set(dataset, set);
|
|
254
|
-
return () => {
|
|
255
|
-
set.delete(listener);
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
function isComputedCellError(value) {
|
|
261
|
-
return value !== null && typeof value === "object" && "computedError" in value && typeof value.computedError === "string";
|
|
262
|
-
}
|
|
263
|
-
function groupPreview(dependencies, inputs, results, total) {
|
|
264
|
-
const groups = /* @__PURE__ */ new Map();
|
|
265
|
-
let nulls = 0, errors = 0;
|
|
266
|
-
inputs.forEach((row, i) => {
|
|
267
|
-
const values = dependencies.map((name) => row[name] ?? null), result = results[i] ?? { value: null, error: "No result." };
|
|
268
|
-
const key = JSON.stringify([values, result.value, result.error ?? null]);
|
|
269
|
-
const group = groups.get(key);
|
|
270
|
-
if (group) group.count++;
|
|
271
|
-
else groups.set(key, { inputs: values, result, count: 1 });
|
|
272
|
-
if (result.error) errors++;
|
|
273
|
-
else if (result.value === null) nulls++;
|
|
274
|
-
});
|
|
275
|
-
return {
|
|
276
|
-
dependencies,
|
|
277
|
-
groups: [...groups.values()].sort((a, b) => b.count - a.count),
|
|
278
|
-
processed: inputs.length,
|
|
279
|
-
total,
|
|
280
|
-
nulls,
|
|
281
|
-
errors
|
|
282
|
-
};
|
|
508
|
+
sourceUnit: [
|
|
509
|
+
"milliseconds",
|
|
510
|
+
"seconds",
|
|
511
|
+
"minutes",
|
|
512
|
+
"hours",
|
|
513
|
+
"days",
|
|
514
|
+
"iso",
|
|
515
|
+
"epochSeconds",
|
|
516
|
+
"epochMilliseconds",
|
|
517
|
+
"secondsOfDay",
|
|
518
|
+
"millisecondsOfDay"
|
|
519
|
+
],
|
|
520
|
+
style: ["human", "long", "clock", "custom", "iso"],
|
|
521
|
+
pattern: "string",
|
|
522
|
+
timeZone: "string"
|
|
523
|
+
});
|
|
524
|
+
if (record2(raw.display.list))
|
|
525
|
+
d.list = copy(raw.display.list, {
|
|
526
|
+
showBars: "boolean",
|
|
527
|
+
useGroupColors: "boolean",
|
|
528
|
+
showValues: "boolean",
|
|
529
|
+
barPosition: ["inline", "below"],
|
|
530
|
+
maxLabelLines: "positiveInteger"
|
|
531
|
+
});
|
|
532
|
+
if (record2(raw.display.pivot))
|
|
533
|
+
d.pivot = copy(raw.display.pivot, {
|
|
534
|
+
swap: "boolean",
|
|
535
|
+
rowDir: ["asc", "desc"],
|
|
536
|
+
columnDir: ["asc", "desc"]
|
|
537
|
+
});
|
|
538
|
+
out.display = { kind: "auto", ...d };
|
|
539
|
+
} else diagnostics.push("Invalid metric display.");
|
|
540
|
+
}
|
|
541
|
+
if (raw.distribution !== void 0) {
|
|
542
|
+
if (record2(raw.distribution))
|
|
543
|
+
out.distribution = copy(raw.distribution, {
|
|
544
|
+
kind: ["box", "histogram"],
|
|
545
|
+
input: "string",
|
|
546
|
+
whiskers: ["minmax", "tukey"],
|
|
547
|
+
bins: "integer"
|
|
548
|
+
});
|
|
549
|
+
else diagnostics.push("Invalid metric distribution.");
|
|
550
|
+
}
|
|
551
|
+
if (record2(raw.layout)) {
|
|
552
|
+
const l = copy(raw.layout, {
|
|
553
|
+
widthRem: "integer",
|
|
554
|
+
heightRem: "integer",
|
|
555
|
+
minWidthRem: "integer",
|
|
556
|
+
minHeightRem: "integer"
|
|
557
|
+
});
|
|
558
|
+
for (const k of ["widthRem", "heightRem", "minWidthRem", "minHeightRem"])
|
|
559
|
+
l[k] = Math.max(1, Math.min(1e3, Number(l[k] ?? 1)));
|
|
560
|
+
l.widthRem = Math.max(Number(l.widthRem), Number(l.minWidthRem));
|
|
561
|
+
l.heightRem = Math.max(Number(l.heightRem), Number(l.minHeightRem));
|
|
562
|
+
out.layout = l;
|
|
563
|
+
}
|
|
564
|
+
if (raw.sort !== void 0 && !Array.isArray(raw.sort))
|
|
565
|
+
diagnostics.push("Invalid metric sort.");
|
|
566
|
+
if (Array.isArray(raw.sort))
|
|
567
|
+
out.sort = raw.sort.slice(0, 20).flatMap((s) => {
|
|
568
|
+
if (!record2(s) || typeof s.key !== "string" || !/^(value|y|count|samples|group(?:0|[1-9][0-9]?))$/.test(s.key) || !["asc", "desc"].includes(String(s.dir))) {
|
|
569
|
+
diagnostics.push("Invalid metric sort.");
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
return [
|
|
573
|
+
copy(s, {
|
|
574
|
+
key: [s.key],
|
|
575
|
+
dir: ["asc", "desc"],
|
|
576
|
+
nulls: ["first", "last"]
|
|
577
|
+
})
|
|
578
|
+
];
|
|
579
|
+
});
|
|
580
|
+
if (Array.isArray(raw.diagnostics))
|
|
581
|
+
diagnostics.push(
|
|
582
|
+
...raw.diagnostics.filter((s) => typeof s === "string" && s.length <= 1e3).slice(0, 20)
|
|
583
|
+
);
|
|
584
|
+
if (diagnostics.length) out.diagnostics = [...new Set(diagnostics)];
|
|
585
|
+
return out;
|
|
283
586
|
}
|
|
284
587
|
|
|
285
588
|
// src/query.ts
|
|
@@ -289,6 +592,7 @@ function isOrGroup(term) {
|
|
|
289
592
|
function predicatesOf(term) {
|
|
290
593
|
return isOrGroup(term) ? term.any : [term];
|
|
291
594
|
}
|
|
595
|
+
var ITEM_DETAILS_COLUMN = "__qt_item_details__";
|
|
292
596
|
var EMPTY_QUERY = {
|
|
293
597
|
select: [],
|
|
294
598
|
where: [],
|
|
@@ -343,7 +647,7 @@ function normalizeWhereClause(item) {
|
|
|
343
647
|
if (!isRecord(item)) return null;
|
|
344
648
|
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
345
649
|
const value = typeof item.value === "string" && item.value.length <= MAX_FILTER_VALUE_LENGTH ? item.value : null;
|
|
346
|
-
if (!field ||
|
|
650
|
+
if (!field || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) {
|
|
347
651
|
return null;
|
|
348
652
|
}
|
|
349
653
|
const clause = { field, op: item.op, value };
|
|
@@ -437,6 +741,8 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
437
741
|
limit: boundedInteger(raw.limit, fallback.limit, 1, MAX_QUERY_LIMIT),
|
|
438
742
|
offset: boundedInteger(raw.offset, fallback.offset, 0, MAX_QUERY_OFFSET)
|
|
439
743
|
};
|
|
744
|
+
if (raw.fitToScreen === true) out.fitToScreen = true;
|
|
745
|
+
if (raw.grouping != null) out.grouping = normalizeGroupingState(raw.grouping);
|
|
440
746
|
if (Array.isArray(raw.aggregations)) {
|
|
441
747
|
const aggregations = [];
|
|
442
748
|
for (const item of raw.aggregations.slice(0, MAX_AGGREGATIONS)) {
|
|
@@ -463,179 +769,113 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
463
769
|
} else if (fallback.aggregations?.length) {
|
|
464
770
|
out.aggregations = normalizeQueryState({ aggregations: fallback.aggregations }).aggregations;
|
|
465
771
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return f2.select?.enabled ?? true;
|
|
499
|
-
}
|
|
500
|
-
function filterValues(f2) {
|
|
501
|
-
return f2.filter?.values ?? { source: "autocomplete" };
|
|
502
|
-
}
|
|
503
|
-
function indexFields(schema) {
|
|
504
|
-
return new Map(schema.fields.map((f2) => [f2.name, f2]));
|
|
505
|
-
}
|
|
506
|
-
function selectedFields(schema, q) {
|
|
507
|
-
const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((f2) => f2.select?.default).map((f2) => ({ field: f2.name }))).map(
|
|
508
|
-
(c) => c.field
|
|
509
|
-
);
|
|
510
|
-
const seen = /* @__PURE__ */ new Set();
|
|
511
|
-
const out = [];
|
|
512
|
-
for (const name of order) {
|
|
513
|
-
if (seen.has(name)) continue;
|
|
514
|
-
const f2 = resolveField(schema, name);
|
|
515
|
-
if (!f2 || !isSelectable(f2)) continue;
|
|
516
|
-
seen.add(name);
|
|
517
|
-
seen.add(f2.name);
|
|
518
|
-
out.push(f2);
|
|
772
|
+
if (isRecord(raw.itemDetails) && Array.isArray(raw.itemDetails.modes)) {
|
|
773
|
+
const modes = [...new Set(raw.itemDetails.modes.filter(
|
|
774
|
+
(mode) => typeof mode === "string" && ["hover", "modal", "expand", "page"].includes(mode)
|
|
775
|
+
))];
|
|
776
|
+
const details = { modes };
|
|
777
|
+
if (isRecord(raw.itemDetails.page) && typeof raw.itemDetails.page.urlTemplate === "string" && raw.itemDetails.page.urlTemplate.length <= 8e3) {
|
|
778
|
+
details.page = {
|
|
779
|
+
urlTemplate: raw.itemDetails.page.urlTemplate,
|
|
780
|
+
target: raw.itemDetails.page.target === "_self" ? "_self" : "_blank"
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (raw.itemDetails.view !== void 0) {
|
|
784
|
+
try {
|
|
785
|
+
details.view = normalizePropertyView(raw.itemDetails.view);
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (isRecord(raw.itemDetails.views)) {
|
|
790
|
+
const views = {};
|
|
791
|
+
for (const mode of ["hover", "modal", "expand"]) {
|
|
792
|
+
try {
|
|
793
|
+
if (raw.itemDetails.views[mode] !== void 0) views[mode] = normalizePropertyView(raw.itemDetails.views[mode]);
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (Object.keys(views).length) details.views = views;
|
|
798
|
+
}
|
|
799
|
+
const active = raw.itemDetails.active;
|
|
800
|
+
if (isRecord(active) && (active.mode === "modal" || active.mode === "expand") && modes.includes(active.mode) && (typeof active.id === "string" && active.id.length > 0 && active.id.length <= 1e3 || typeof active.id === "number" && Number.isFinite(active.id))) {
|
|
801
|
+
details.active = { id: active.id, mode: active.mode };
|
|
802
|
+
}
|
|
803
|
+
out.itemDetails = details;
|
|
519
804
|
}
|
|
520
805
|
return out;
|
|
521
806
|
}
|
|
522
|
-
function
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
if (byExactName) return byExactName;
|
|
528
|
-
const needle = token.toLowerCase();
|
|
529
|
-
for (const f2 of schema.fields) {
|
|
530
|
-
if (f2.name.toLowerCase() === needle) return f2;
|
|
531
|
-
if (aliasMatches(f2, needle)) return f2;
|
|
532
|
-
}
|
|
533
|
-
return void 0;
|
|
534
|
-
}
|
|
535
|
-
function resolveFieldName(schema, field) {
|
|
536
|
-
return resolveField(schema, field)?.name;
|
|
537
|
-
}
|
|
538
|
-
function aliasTermsFor(field) {
|
|
539
|
-
const out = [];
|
|
540
|
-
const seen = /* @__PURE__ */ new Set();
|
|
541
|
-
const add = (raw) => {
|
|
542
|
-
if (typeof raw !== "string") return;
|
|
543
|
-
const value = raw.trim();
|
|
544
|
-
if (!value) return;
|
|
545
|
-
const key = value.toLowerCase();
|
|
546
|
-
if (seen.has(key)) return;
|
|
547
|
-
seen.add(key);
|
|
548
|
-
out.push(value);
|
|
807
|
+
function normalizeGroupingState(value) {
|
|
808
|
+
const raw = isRecord(value) ? value : {};
|
|
809
|
+
const diagnostics = [];
|
|
810
|
+
const fail = () => {
|
|
811
|
+
diagnostics.push("Invalid grouping definition.");
|
|
549
812
|
};
|
|
550
|
-
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
813
|
+
const keys = Array.isArray(raw.keys) ? raw.keys.slice(0, MAX_GROUP_BY_FIELDS).filter((key) => !!boundedString(key, MAX_FIELD_NAME_LENGTH)) : [];
|
|
814
|
+
if (!Array.isArray(raw.keys) || !keys.length || keys.length !== raw.keys.length || new Set(keys).size !== keys.length) fail();
|
|
815
|
+
const summaries = [];
|
|
816
|
+
if (!Array.isArray(raw.summaries) || raw.summaries.length > 64) fail();
|
|
817
|
+
for (const item of Array.isArray(raw.summaries) ? raw.summaries.slice(0, 64) : []) {
|
|
818
|
+
if (!isRecord(item) || !boundedString(item.id, MAX_FIELD_NAME_LENGTH) || typeof item.op !== "string" || !AGG_OPS.has(item.op) && item.op !== "row" && item.op !== "formula") {
|
|
819
|
+
fail();
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const column = { id: item.id, op: item.op };
|
|
823
|
+
if (item.field !== void 0) {
|
|
824
|
+
if (!boundedString(item.field, MAX_FIELD_NAME_LENGTH)) fail();
|
|
825
|
+
else column.field = item.field;
|
|
826
|
+
}
|
|
827
|
+
if (item.op === "formula") {
|
|
828
|
+
if (typeof item.expression !== "string" || item.expression.length > 1e4) fail();
|
|
829
|
+
else column.expression = item.expression;
|
|
830
|
+
}
|
|
831
|
+
if (item.label !== void 0) {
|
|
832
|
+
if (typeof item.label !== "string" || item.label.length > MAX_LABEL_LENGTH) fail();
|
|
833
|
+
else column.label = item.label;
|
|
834
|
+
}
|
|
835
|
+
summaries.push(column);
|
|
566
836
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
837
|
+
let representative = null;
|
|
838
|
+
if (raw.representative != null) {
|
|
839
|
+
const ordering = isRecord(raw.representative) ? raw.representative.orderBy : void 0;
|
|
840
|
+
const orderBy = [];
|
|
841
|
+
if (!Array.isArray(ordering) || !ordering.length || ordering.length > 8) fail();
|
|
842
|
+
for (const term of Array.isArray(ordering) ? ordering.slice(0, 8) : []) {
|
|
843
|
+
if (!isRecord(term) || !boundedString(term.field, MAX_FIELD_NAME_LENGTH) || !["asc", "desc"].includes(String(term.dir)) || term.nulls !== void 0 && !["first", "last"].includes(String(term.nulls))) {
|
|
844
|
+
fail();
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
orderBy.push({ field: term.field, dir: term.dir, nulls: term.nulls === "first" ? "first" : "last" });
|
|
848
|
+
}
|
|
849
|
+
representative = { orderBy };
|
|
574
850
|
}
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (!Array.isArray(d.fields) || d.fields.length === 0) throw new Error("schema: `fields` must be a non-empty array");
|
|
583
|
-
const fields = d.fields.map((raw, i) => projectField(raw, i));
|
|
584
|
-
return {
|
|
585
|
-
name: d.name,
|
|
586
|
-
idField: d.idField,
|
|
587
|
-
fields,
|
|
588
|
-
defaultSort: d.defaultSort,
|
|
589
|
-
defaultSelect: d.defaultSelect,
|
|
590
|
-
defaultLimit: d.defaultLimit
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
function projectField(raw, i) {
|
|
594
|
-
if (!raw || typeof raw.name !== "string") throw new Error(`schema: field[${i}] missing \`name\``);
|
|
595
|
-
if (typeof raw.label !== "string") throw new Error(`schema: field ${raw.name} missing \`label\``);
|
|
596
|
-
if (typeof raw.type !== "string") throw new Error(`schema: field ${raw.name} missing \`type\``);
|
|
597
|
-
const rs = raw.source ?? (raw.bindings ? "backend" : "derived");
|
|
598
|
-
let source;
|
|
599
|
-
if (typeof rs === "string") {
|
|
600
|
-
source = rs === "derived" ? { kind: "derived" } : { kind: "backend" };
|
|
601
|
-
} else if (rs.kind === "derived") {
|
|
602
|
-
source = { kind: "derived" };
|
|
603
|
-
} else {
|
|
604
|
-
source = { kind: "backend", path: rs.path, synthetic: rs.synthetic };
|
|
851
|
+
if (!isRecord(raw.source) || raw.source.grouping != null) fail();
|
|
852
|
+
const { grouping: _nested, ...source } = isRecord(raw.source) ? raw.source : {};
|
|
853
|
+
if (Array.isArray(raw.diagnostics) && raw.diagnostics.length) fail();
|
|
854
|
+
const normalizedSource = normalizeQueryState(source);
|
|
855
|
+
if (Array.isArray(source.where)) {
|
|
856
|
+
const supplied = source.where.reduce((count, term) => count + (isRecord(term) && Array.isArray(term.any) ? term.any.length : 1), 0);
|
|
857
|
+
if (supplied !== normalizedSource.where.reduce((count, term) => count + predicatesOf(term).length, 0)) fail();
|
|
605
858
|
}
|
|
606
|
-
return {
|
|
607
|
-
name: raw.name,
|
|
608
|
-
label: raw.label,
|
|
609
|
-
type: raw.type,
|
|
610
|
-
source,
|
|
611
|
-
filter: raw.filter,
|
|
612
|
-
sort: raw.sort,
|
|
613
|
-
select: raw.select,
|
|
614
|
-
aggregate: raw.aggregate,
|
|
615
|
-
group: raw.group,
|
|
616
|
-
alias: raw.alias,
|
|
617
|
-
aliases: normalizeAliases(raw.aliases, raw.alias),
|
|
618
|
-
render: raw.render
|
|
619
|
-
// string key from JSON; consumer may override with a fn
|
|
620
|
-
};
|
|
859
|
+
return { keys, summaries, representative, source: normalizedSource, ...diagnostics.length ? { diagnostics: [...new Set(diagnostics)] } : {} };
|
|
621
860
|
}
|
|
622
|
-
function
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (
|
|
636
|
-
|
|
861
|
+
function queriesEqual(a, b) {
|
|
862
|
+
if (a === b) return true;
|
|
863
|
+
if (a.limit !== b.limit || a.offset !== b.offset) return false;
|
|
864
|
+
if (a.fitToScreen === true !== (b.fitToScreen === true)) return false;
|
|
865
|
+
if (!sameArray(a.where, b.where)) return false;
|
|
866
|
+
if (!sameArray(a.orderBy, b.orderBy)) return false;
|
|
867
|
+
if (!sameArray(a.select, b.select)) return false;
|
|
868
|
+
if (!sameArray(a.aggregations ?? [], b.aggregations ?? [])) return false;
|
|
869
|
+
if (JSON.stringify(a.itemDetails) !== JSON.stringify(b.itemDetails)) return false;
|
|
870
|
+
if (JSON.stringify(a.grouping) !== JSON.stringify(b.grouping)) return false;
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
function sameArray(a, b) {
|
|
874
|
+
if (a.length !== b.length) return false;
|
|
875
|
+
for (let i = 0; i < a.length; i++) {
|
|
876
|
+
if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) return false;
|
|
637
877
|
}
|
|
638
|
-
return
|
|
878
|
+
return true;
|
|
639
879
|
}
|
|
640
880
|
|
|
641
881
|
// src/relativeTime.ts
|
|
@@ -655,8 +895,8 @@ function parseRelativeDuration(value) {
|
|
|
655
895
|
}
|
|
656
896
|
return value[0] === "-" && total !== 0 ? -total : total;
|
|
657
897
|
}
|
|
658
|
-
function evaluationTime(
|
|
659
|
-
const now =
|
|
898
|
+
function evaluationTime(options2) {
|
|
899
|
+
const now = options2.now ?? Date.now();
|
|
660
900
|
if (!Number.isSafeInteger(now) || Math.abs(now) > MAX_RELATIVE_TIME_MS) throw new Error("Invalid query evaluation time");
|
|
661
901
|
return now;
|
|
662
902
|
}
|
|
@@ -807,6 +1047,9 @@ function isGroupable(field) {
|
|
|
807
1047
|
return field.type === "enum" || field.type === "text" || field.type === "bool";
|
|
808
1048
|
}
|
|
809
1049
|
|
|
1050
|
+
// src/encode.ts
|
|
1051
|
+
import { deflateSync, Inflate } from "fflate";
|
|
1052
|
+
|
|
810
1053
|
// src/setFilter.ts
|
|
811
1054
|
function createSetFilter(field, mode, values, id) {
|
|
812
1055
|
const keys = [...new Set(values)];
|
|
@@ -872,7 +1115,7 @@ function fromBase64Url(s) {
|
|
|
872
1115
|
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
873
1116
|
return new TextDecoder().decode(bytes);
|
|
874
1117
|
}
|
|
875
|
-
function
|
|
1118
|
+
function compactQuery(q) {
|
|
876
1119
|
q = normalizeQueryState(q);
|
|
877
1120
|
const c = {};
|
|
878
1121
|
if (q.select.length)
|
|
@@ -882,25 +1125,157 @@ function encodeQuery(q) {
|
|
|
882
1125
|
c.l = q.limit;
|
|
883
1126
|
if (q.offset) c.f = q.offset;
|
|
884
1127
|
if (q.aggregations?.length) c.g = q.aggregations;
|
|
885
|
-
if (
|
|
886
|
-
|
|
1128
|
+
if (q.itemDetails) c.d = q.itemDetails;
|
|
1129
|
+
if (q.fitToScreen) c.fs = true;
|
|
1130
|
+
if (q.grouping) c.b = q.grouping;
|
|
1131
|
+
return c;
|
|
1132
|
+
}
|
|
1133
|
+
var MAX_QUERY_STATE_BYTES = 4 * 1024 * 1024;
|
|
1134
|
+
var QueryDecodeError = class extends Error {
|
|
1135
|
+
constructor(code, message) {
|
|
1136
|
+
super(message);
|
|
1137
|
+
this.code = code;
|
|
1138
|
+
this.name = "QueryDecodeError";
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
function packQuery(c) {
|
|
1142
|
+
const packed = { ...c };
|
|
1143
|
+
if (c.l === EMPTY_QUERY.limit) delete packed.l;
|
|
1144
|
+
if (c.w) {
|
|
1145
|
+
const terms = [];
|
|
1146
|
+
for (let i = 0; i < c.w.length; i++) {
|
|
1147
|
+
const set = readSetFilter(c.w, i);
|
|
1148
|
+
if (set) {
|
|
1149
|
+
terms.push(["set", set.field, set.metadata.mode, set.metadata.values, set.metadata.id]);
|
|
1150
|
+
i += set.count - 1;
|
|
1151
|
+
} else terms.push(c.w[i]);
|
|
1152
|
+
}
|
|
1153
|
+
packed.w = terms;
|
|
1154
|
+
}
|
|
1155
|
+
if (c.d?.views) {
|
|
1156
|
+
const pool = [];
|
|
1157
|
+
const refs = {};
|
|
1158
|
+
for (const [mode, view] of Object.entries(c.d.views)) {
|
|
1159
|
+
let index = pool.findIndex((v) => JSON.stringify(v) === JSON.stringify(view));
|
|
1160
|
+
if (index < 0) {
|
|
1161
|
+
index = pool.length;
|
|
1162
|
+
pool.push(view);
|
|
1163
|
+
}
|
|
1164
|
+
refs[mode] = index;
|
|
1165
|
+
}
|
|
1166
|
+
packed.d = { ...c.d, views: { pool, refs } };
|
|
1167
|
+
}
|
|
1168
|
+
return packed;
|
|
1169
|
+
}
|
|
1170
|
+
function unpackQuery(c) {
|
|
1171
|
+
if (Array.isArray(c.w)) c.w = c.w.flatMap((term) => {
|
|
1172
|
+
if (!Array.isArray(term)) return [term];
|
|
1173
|
+
if (term.length !== 5 || term[0] !== "set" || typeof term[1] !== "string" || typeof term[2] !== "string" || !["any", "all", "none", "empty"].includes(term[2]) || !Array.isArray(term[3]) || term[3].length > 100 || !term[3].every((v) => typeof v === "string" && v.length <= 1e4) || typeof term[4] !== "string")
|
|
1174
|
+
throw new QueryDecodeError("invalid", "Invalid set filter in query link.");
|
|
1175
|
+
return createSetFilter(term[1], term[2], term[3], term[4]);
|
|
1176
|
+
});
|
|
1177
|
+
const details = c.d;
|
|
1178
|
+
const views = details && typeof details === "object" && "views" in details ? details.views : void 0;
|
|
1179
|
+
if (details && typeof details === "object" && views && typeof views === "object" && "pool" in views) {
|
|
1180
|
+
const pool = views.pool;
|
|
1181
|
+
const refs = "refs" in views ? views.refs : void 0;
|
|
1182
|
+
if (!Array.isArray(pool) || pool.length > 3 || !refs || typeof refs !== "object" || Array.isArray(refs))
|
|
1183
|
+
throw new QueryDecodeError("invalid", "Invalid property views in query link.");
|
|
1184
|
+
c.d = { ...details, views: Object.fromEntries(Object.entries(refs).map(([mode, index]) => {
|
|
1185
|
+
if (!["hover", "modal", "expand"].includes(mode) || !Number.isInteger(index) || index < 0 || index >= pool.length)
|
|
1186
|
+
throw new QueryDecodeError("invalid", "Invalid property view reference.");
|
|
1187
|
+
return [mode, pool[index]];
|
|
1188
|
+
})) };
|
|
1189
|
+
}
|
|
1190
|
+
return c;
|
|
1191
|
+
}
|
|
1192
|
+
function encodeQuery(q) {
|
|
1193
|
+
const json = JSON.stringify(packQuery(compactQuery(q)));
|
|
1194
|
+
const bytes = new TextEncoder().encode(json);
|
|
1195
|
+
if (bytes.length > MAX_QUERY_STATE_BYTES) throw new QueryDecodeError("too_large", "Query state exceeds the storage limit.");
|
|
1196
|
+
const plain = "2j." + toBase64Url(json);
|
|
1197
|
+
const compressed = "2d." + bytesToBase64(deflateSync(bytes));
|
|
1198
|
+
const token = compressed.length < plain.length ? compressed : plain;
|
|
1199
|
+
if (token.length > MAX_QUERY_TOKEN_LENGTH) throw new QueryDecodeError("too_large", "Encoded query state exceeds the storage limit.");
|
|
1200
|
+
return token;
|
|
1201
|
+
}
|
|
1202
|
+
function bytesToBase64(bytes) {
|
|
1203
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
1204
|
+
let bin = "";
|
|
1205
|
+
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
1206
|
+
return btoa(bin).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
1207
|
+
}
|
|
1208
|
+
function inflateToken(token) {
|
|
1209
|
+
const bin = atob(token.replace(/-/g, "+").replace(/_/g, "/"));
|
|
1210
|
+
const input = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
1211
|
+
const chunks = [];
|
|
1212
|
+
let size = 0;
|
|
1213
|
+
let complete = false;
|
|
1214
|
+
const stream = new Inflate((chunk, final) => {
|
|
1215
|
+
size += chunk.length;
|
|
1216
|
+
if (size > MAX_QUERY_STATE_BYTES) throw new QueryDecodeError("too_large", "Expanded query state exceeds the storage limit.");
|
|
1217
|
+
chunks.push(chunk);
|
|
1218
|
+
complete = final;
|
|
1219
|
+
});
|
|
1220
|
+
for (let i = 0; i < input.length; i += 256) stream.push(input.subarray(i, i + 256), i + 256 >= input.length);
|
|
1221
|
+
if (!complete) throw new QueryDecodeError("invalid", "Incomplete compressed query link.");
|
|
1222
|
+
const bytes = new Uint8Array(size);
|
|
1223
|
+
let offset = 0;
|
|
1224
|
+
for (const chunk of chunks) {
|
|
1225
|
+
bytes.set(chunk, offset);
|
|
1226
|
+
offset += chunk.length;
|
|
1227
|
+
}
|
|
1228
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
887
1229
|
}
|
|
888
1230
|
function decodeQuery(token) {
|
|
1231
|
+
try {
|
|
1232
|
+
return decodeToken(token, false);
|
|
1233
|
+
} catch {
|
|
1234
|
+
return { ...EMPTY_QUERY };
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
function decodeQueryStrict(token) {
|
|
1238
|
+
return decodeToken(token, true);
|
|
1239
|
+
}
|
|
1240
|
+
function decodeToken(token, strict) {
|
|
889
1241
|
if (!token) return { ...EMPTY_QUERY };
|
|
890
|
-
if (token.length > MAX_QUERY_TOKEN_LENGTH)
|
|
1242
|
+
if (token.length > MAX_QUERY_TOKEN_LENGTH) throw new QueryDecodeError("too_large", "Query link exceeds the storage limit.");
|
|
891
1243
|
try {
|
|
892
|
-
const
|
|
1244
|
+
const versioned = token.includes(".");
|
|
1245
|
+
if (versioned && !token.startsWith("2j.") && !token.startsWith("2d."))
|
|
1246
|
+
throw new QueryDecodeError("unsupported_version", "Unsupported query link version.");
|
|
1247
|
+
const payload = versioned ? token.slice(3) : token;
|
|
1248
|
+
if (!/^[A-Za-z0-9_-]+$/.test(payload) || payload.length % 4 === 1) throw new QueryDecodeError("invalid", "Invalid query link encoding.");
|
|
1249
|
+
const json = token.startsWith("2d.") ? inflateToken(payload) : fromBase64Url(payload);
|
|
1250
|
+
if (new TextEncoder().encode(json).length > MAX_QUERY_STATE_BYTES) throw new QueryDecodeError("too_large", "Query state exceeds the storage limit.");
|
|
1251
|
+
const raw = JSON.parse(json);
|
|
1252
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || ["w", "g"].some((k) => raw[k] !== void 0 && !Array.isArray(raw[k])))
|
|
1253
|
+
throw new QueryDecodeError("invalid", "Invalid query link state.");
|
|
1254
|
+
const c = versioned ? unpackQuery(raw) : raw;
|
|
1255
|
+
if (strict && c.w) {
|
|
1256
|
+
if (c.w.some((term) => !term || typeof term !== "object")) throw new QueryDecodeError("invalid", "Invalid query predicates.");
|
|
1257
|
+
const count = c.w.reduce((n, term) => n + predicatesOf(term).length, 0);
|
|
1258
|
+
const normalized = normalizeQueryState({ where: c.w });
|
|
1259
|
+
const normalizedCount = normalized.where.reduce((n, term) => n + predicatesOf(term).length, 0);
|
|
1260
|
+
if (count > 100 || count !== normalizedCount || c.w.some((term) => predicatesOf(term).some((clause) => !clause || typeof clause !== "object" || clause.negated !== void 0 && typeof clause.negated !== "boolean")))
|
|
1261
|
+
throw new QueryDecodeError("invalid", "Invalid query predicates or predicate budget exceeded.");
|
|
1262
|
+
}
|
|
893
1263
|
const out = normalizeQueryState({
|
|
894
1264
|
select: normalizeSelect(c.s ?? c.c),
|
|
895
1265
|
where: Array.isArray(c.w) ? c.w : [],
|
|
896
1266
|
orderBy: normalizeOrderBy(c.o),
|
|
897
1267
|
limit: c.l,
|
|
898
1268
|
offset: c.f,
|
|
899
|
-
aggregations: c.g
|
|
1269
|
+
aggregations: c.g,
|
|
1270
|
+
itemDetails: c.d,
|
|
1271
|
+
fitToScreen: c.fs,
|
|
1272
|
+
grouping: c.b
|
|
900
1273
|
});
|
|
1274
|
+
if (strict && c.g && out.aggregations?.length !== c.g.length) throw new QueryDecodeError("invalid", "Invalid query metrics.");
|
|
901
1275
|
return out;
|
|
902
|
-
} catch {
|
|
903
|
-
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (error instanceof QueryDecodeError) throw error;
|
|
1278
|
+
throw new QueryDecodeError("invalid", "Invalid or incomplete query link.");
|
|
904
1279
|
}
|
|
905
1280
|
}
|
|
906
1281
|
function normalizeSelect(s) {
|
|
@@ -919,6 +1294,7 @@ function normalizeOrderBy(o) {
|
|
|
919
1294
|
return [o];
|
|
920
1295
|
}
|
|
921
1296
|
function toServerQuery(q, schema) {
|
|
1297
|
+
if (q.grouping) throw new Error("This transport does not support grouped queries. Enable the v3 query executor.");
|
|
922
1298
|
q = normalizeQueryState(q);
|
|
923
1299
|
const byName = indexFields(schema);
|
|
924
1300
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
@@ -957,8 +1333,10 @@ function termIsPushdown(term, byName, resolveField2) {
|
|
|
957
1333
|
});
|
|
958
1334
|
}
|
|
959
1335
|
function toAggregationQuery(q, schema) {
|
|
1336
|
+
if (q.grouping) throw new Error("Metrics over grouped rows require grouped-query execution.");
|
|
960
1337
|
const suppliedPredicateCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
|
|
961
1338
|
q = normalizeQueryState(q);
|
|
1339
|
+
if (q.aggregations?.some((a) => a.dateGrouping)) throw new Error("Date grouping currently requires local metric execution.");
|
|
962
1340
|
if (q.aggregations?.some((a) => a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.distribution || a.scope === "shownRows" || a.sort?.length || a.groupLimit)) throw new Error("Metric definition requires the version 2 metric transport.");
|
|
963
1341
|
const byName = indexFields(schema);
|
|
964
1342
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
@@ -969,13 +1347,453 @@ function toAggregationQuery(q, schema) {
|
|
|
969
1347
|
const f2 = byName.get(field);
|
|
970
1348
|
return f2 != null && f2.source.kind === "backend";
|
|
971
1349
|
};
|
|
972
|
-
const aggregations = (q.aggregations ?? []).filter(
|
|
973
|
-
(a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
|
|
974
|
-
);
|
|
975
|
-
const diagnostics = [];
|
|
976
|
-
for (const c of q.aggregations ?? []) if (!aggregations.includes(c)) diagnostics.push({ metricId: c.id, code: "unsupported_metric", message: "Metric requires unavailable backend fields.", from: 0, to: 1 });
|
|
977
|
-
if (where.length !== q.where.length || suppliedPredicateCount !== q.where.reduce((n, t) => n + predicatesOf(t).length, 0)) diagnostics.push({ code: "residual_filter", message: "All-matching metrics are unavailable with residual client-only filters.", from: 0, to: 1 });
|
|
978
|
-
return { where, aggregations, ...diagnostics.length ? { diagnostics } : {} };
|
|
1350
|
+
const aggregations = (q.aggregations ?? []).filter(
|
|
1351
|
+
(a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
|
|
1352
|
+
);
|
|
1353
|
+
const diagnostics = [];
|
|
1354
|
+
for (const c of q.aggregations ?? []) if (!aggregations.includes(c)) diagnostics.push({ metricId: c.id, code: "unsupported_metric", message: "Metric requires unavailable backend fields.", from: 0, to: 1 });
|
|
1355
|
+
if (where.length !== q.where.length || suppliedPredicateCount !== q.where.reduce((n, t) => n + predicatesOf(t).length, 0)) diagnostics.push({ code: "residual_filter", message: "All-matching metrics are unavailable with residual client-only filters.", from: 0, to: 1 });
|
|
1356
|
+
return { where, aggregations, ...diagnostics.length ? { diagnostics } : {} };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/queryLinks.ts
|
|
1360
|
+
var codePattern = /^[A-Za-z0-9_-]{22}$/;
|
|
1361
|
+
function validateQuerySnapshot(input) {
|
|
1362
|
+
const s = input;
|
|
1363
|
+
if (!s || s.version !== 1 || !codePattern.test(s.code) || typeof s.token !== "string" || s.token.length > MAX_QUERY_TOKEN_LENGTH || !Number.isFinite(s.createdAt)) throw Error("Invalid query snapshot.");
|
|
1364
|
+
decodeQueryStrict(s.token);
|
|
1365
|
+
return { version: 1, code: s.code, token: s.token, createdAt: s.createdAt };
|
|
1366
|
+
}
|
|
1367
|
+
function newSnapshot(token) {
|
|
1368
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
1369
|
+
let bin = "";
|
|
1370
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
1371
|
+
return { version: 1, code: btoa(bin).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"), token, createdAt: Date.now() };
|
|
1372
|
+
}
|
|
1373
|
+
function localQuerySnapshotCache(options2 = {}) {
|
|
1374
|
+
const storageKey = options2.storageKey ?? "query-table:link-cache:v1";
|
|
1375
|
+
const maxEntries = options2.maxEntries ?? 100;
|
|
1376
|
+
const maxBytes = options2.maxBytes ?? 2 * 1024 * 1024;
|
|
1377
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1 || !Number.isFinite(maxBytes) || maxBytes < 1) throw Error("Invalid query link cache limits.");
|
|
1378
|
+
const storage = () => options2.storage ?? globalThis.localStorage;
|
|
1379
|
+
function read() {
|
|
1380
|
+
const text = storage().getItem(storageKey);
|
|
1381
|
+
if (!text) return [];
|
|
1382
|
+
let raw;
|
|
1383
|
+
try {
|
|
1384
|
+
raw = JSON.parse(text);
|
|
1385
|
+
} catch {
|
|
1386
|
+
return [];
|
|
1387
|
+
}
|
|
1388
|
+
if (!Array.isArray(raw)) return [];
|
|
1389
|
+
const entries = raw.filter((e) => e && typeof e.key === "string" && e.snapshot && e.snapshot.version === 1 && codePattern.test(e.snapshot.code) && typeof e.snapshot.token === "string" && e.snapshot.token.length <= MAX_QUERY_TOKEN_LENGTH && Number.isFinite(e.snapshot.createdAt)).slice(-maxEntries);
|
|
1390
|
+
while (entries.length && JSON.stringify(entries).length * 2 > maxBytes) entries.shift();
|
|
1391
|
+
return entries;
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
isDurable(key, code) {
|
|
1395
|
+
return read().some((e) => e.key === key && e.snapshot.code === code && e.durable);
|
|
1396
|
+
},
|
|
1397
|
+
markDurable(key, code) {
|
|
1398
|
+
const entries = read();
|
|
1399
|
+
const entry = entries.find((e) => e.key === key && e.snapshot.code === code);
|
|
1400
|
+
if (entry) {
|
|
1401
|
+
entry.durable = true;
|
|
1402
|
+
let text = JSON.stringify(entries);
|
|
1403
|
+
while (text.length * 2 > maxBytes && entries.length > 1) {
|
|
1404
|
+
entries.splice(entries[0] === entry ? 1 : 0, 1);
|
|
1405
|
+
text = JSON.stringify(entries);
|
|
1406
|
+
}
|
|
1407
|
+
if (text.length * 2 <= maxBytes) storage().setItem(storageKey, text);
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
get(key, code) {
|
|
1411
|
+
return read().find((e) => e.key === key && e.snapshot.code === code)?.snapshot ?? null;
|
|
1412
|
+
},
|
|
1413
|
+
find(key, token) {
|
|
1414
|
+
return read().find((e) => e.key === key && e.snapshot.token === token)?.snapshot ?? null;
|
|
1415
|
+
},
|
|
1416
|
+
put(key, snapshot) {
|
|
1417
|
+
const entries = read();
|
|
1418
|
+
const old = entries.find((e) => e.key === key && e.snapshot.code === snapshot.code);
|
|
1419
|
+
if (old) {
|
|
1420
|
+
if (old.snapshot.token !== snapshot.token) throw Error("A query link code cannot be overwritten.");
|
|
1421
|
+
const text2 = JSON.stringify(entries);
|
|
1422
|
+
if (storage().getItem(storageKey) !== text2) storage().setItem(storageKey, text2);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
entries.push({ key, snapshot: validateQuerySnapshot(snapshot) });
|
|
1426
|
+
while (entries.length > maxEntries) entries.shift();
|
|
1427
|
+
let text = JSON.stringify(entries);
|
|
1428
|
+
while (text.length * 2 > maxBytes && entries.length > 1) {
|
|
1429
|
+
entries.shift();
|
|
1430
|
+
text = JSON.stringify(entries);
|
|
1431
|
+
}
|
|
1432
|
+
if (text.length * 2 > maxBytes) throw Error("This query is too large for the local link cache.");
|
|
1433
|
+
storage().setItem(storageKey, text);
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function memoryQuerySnapshotCache() {
|
|
1438
|
+
let text = null;
|
|
1439
|
+
return localQuerySnapshotCache({ storage: { getItem: () => text, setItem: (_key, value) => {
|
|
1440
|
+
text = value;
|
|
1441
|
+
} } });
|
|
1442
|
+
}
|
|
1443
|
+
var QuerySnapshotHttpError = class extends Error {
|
|
1444
|
+
constructor(status, code, message, detail, serverCode) {
|
|
1445
|
+
super(message);
|
|
1446
|
+
this.status = status;
|
|
1447
|
+
this.code = code;
|
|
1448
|
+
this.detail = detail;
|
|
1449
|
+
this.serverCode = serverCode;
|
|
1450
|
+
this.name = "QuerySnapshotHttpError";
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
async function snapshotHttpError(response) {
|
|
1454
|
+
let detail;
|
|
1455
|
+
let serverCode;
|
|
1456
|
+
const type = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
|
|
1457
|
+
if (response.body && (type === "text/plain" || type === "application/json")) {
|
|
1458
|
+
const reader = response.body.getReader();
|
|
1459
|
+
try {
|
|
1460
|
+
const chunks = [];
|
|
1461
|
+
let size = 0;
|
|
1462
|
+
let complete = false;
|
|
1463
|
+
while (size <= 4096) {
|
|
1464
|
+
const next = await reader.read();
|
|
1465
|
+
if (next.done) {
|
|
1466
|
+
complete = true;
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
size += next.value.byteLength;
|
|
1470
|
+
if (size <= 4096) chunks.push(next.value);
|
|
1471
|
+
}
|
|
1472
|
+
if (complete) {
|
|
1473
|
+
const bytes = new Uint8Array(size);
|
|
1474
|
+
let offset = 0;
|
|
1475
|
+
for (const chunk of chunks) {
|
|
1476
|
+
bytes.set(chunk, offset);
|
|
1477
|
+
offset += chunk.length;
|
|
1478
|
+
}
|
|
1479
|
+
const text = new TextDecoder().decode(bytes);
|
|
1480
|
+
let reason = text;
|
|
1481
|
+
if (type === "application/json") {
|
|
1482
|
+
const body = JSON.parse(text);
|
|
1483
|
+
reason = body?.message ?? body?.error?.message ?? body?.error;
|
|
1484
|
+
const code2 = body?.code ?? body?.error?.code;
|
|
1485
|
+
if (typeof code2 === "string" && /^[A-Za-z0-9_.-]{1,64}$/.test(code2)) serverCode = code2;
|
|
1486
|
+
}
|
|
1487
|
+
if (typeof reason === "string" && !/[<>]/.test(reason)) {
|
|
1488
|
+
const safe = reason.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
|
|
1489
|
+
if (safe) detail = safe.slice(0, 512);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
} catch {
|
|
1493
|
+
} finally {
|
|
1494
|
+
try {
|
|
1495
|
+
await reader.cancel();
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
reader.releaseLock();
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const status = response.status;
|
|
1502
|
+
const code = status === 507 ? "quota" : status === 401 || status === 403 ? "forbidden" : status === 409 ? "conflict" : status === 404 ? "unavailable" : status >= 500 ? "storage" : "http";
|
|
1503
|
+
const messages = {
|
|
1504
|
+
quota: "Query snapshot storage is full. Ask your host to increase its quota, then retry saving. Existing links still work.",
|
|
1505
|
+
forbidden: "You do not have permission to save or open this query link. Sign in or ask your host for access.",
|
|
1506
|
+
conflict: "This query link code already identifies different state. Ask your host to check its snapshot store.",
|
|
1507
|
+
unavailable: "This query link is unavailable. Check its address or ask its author for a new link.",
|
|
1508
|
+
storage: "Query snapshot storage is temporarily unavailable. Retry later; your previous saved query is unchanged.",
|
|
1509
|
+
http: `Query snapshot request failed (${status}). Check the request or retry later.`
|
|
1510
|
+
};
|
|
1511
|
+
const message = messages[code] + (code !== "http" ? ` (HTTP ${status})` : "") + (detail && (status < 500 || status === 507) ? ` ${detail}` : "");
|
|
1512
|
+
return new QuerySnapshotHttpError(status, code, message, detail, serverCode);
|
|
1513
|
+
}
|
|
1514
|
+
function httpQuerySnapshotStore(base, fetcher = fetch) {
|
|
1515
|
+
const url = (key, code) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(key)}&code=${encodeURIComponent(code)}`;
|
|
1516
|
+
async function responseSnapshot(response) {
|
|
1517
|
+
if (!response.ok) throw await snapshotHttpError(response);
|
|
1518
|
+
return validateQuerySnapshot(await response.json());
|
|
1519
|
+
}
|
|
1520
|
+
return {
|
|
1521
|
+
async save(key, snapshot, signal) {
|
|
1522
|
+
const result = await responseSnapshot(await fetcher(url(key, snapshot.code), {
|
|
1523
|
+
method: "PUT",
|
|
1524
|
+
headers: { "Content-Type": "application/json" },
|
|
1525
|
+
body: JSON.stringify(snapshot),
|
|
1526
|
+
...signal ? { signal } : {}
|
|
1527
|
+
}));
|
|
1528
|
+
if (result.code !== snapshot.code || result.token !== snapshot.token) throw Error("The snapshot store changed this immutable query link.");
|
|
1529
|
+
return result;
|
|
1530
|
+
},
|
|
1531
|
+
async resolve(key, code, signal) {
|
|
1532
|
+
const response = await fetcher(url(key, code), signal ? { signal } : {});
|
|
1533
|
+
if (response.status === 404) return null;
|
|
1534
|
+
const result = await responseSnapshot(response);
|
|
1535
|
+
if (result.code !== code) throw Error("The snapshot store returned a different query link.");
|
|
1536
|
+
return result;
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function createQueryLinkService(key, store, options2 = {}) {
|
|
1541
|
+
const budget = options2.maxUrlBytes ?? 2e3;
|
|
1542
|
+
if (!Number.isInteger(budget) || budget < 1) throw Error("Invalid query URL budget.");
|
|
1543
|
+
const cache = options2.cache ?? localQuerySnapshotCache();
|
|
1544
|
+
const promoted = /* @__PURE__ */ new Map();
|
|
1545
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
1546
|
+
const known = /* @__PURE__ */ new Map();
|
|
1547
|
+
function remember(snapshot2) {
|
|
1548
|
+
known.set(snapshot2.code, snapshot2);
|
|
1549
|
+
while (known.size > 100) known.delete(known.keys().next().value);
|
|
1550
|
+
try {
|
|
1551
|
+
cache.put(key, snapshot2);
|
|
1552
|
+
cache.markDurable?.(key, snapshot2.code);
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function link(base, parameter, value, kind) {
|
|
1557
|
+
const url = new URL(base);
|
|
1558
|
+
url.searchParams.set(parameter, value);
|
|
1559
|
+
const bytes = new TextEncoder().encode(url.toString()).length;
|
|
1560
|
+
if (bytes > budget) throw Error("The query link exceeds the URL budget, including the page address.");
|
|
1561
|
+
return { url: url.toString(), kind, bytes, ...kind !== "inline" ? { snapshotCode: value.slice(1) } : {} };
|
|
1562
|
+
}
|
|
1563
|
+
function cached(token) {
|
|
1564
|
+
try {
|
|
1565
|
+
return cache.find(key, token);
|
|
1566
|
+
} catch {
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async function snapshot(query) {
|
|
1571
|
+
if (!store) return null;
|
|
1572
|
+
const token = encodeQuery(query);
|
|
1573
|
+
const existing = promoted.get(token);
|
|
1574
|
+
if (existing) return existing;
|
|
1575
|
+
const local = attempts.get(token) ?? cached(token) ?? newSnapshot(token);
|
|
1576
|
+
attempts.set(token, local);
|
|
1577
|
+
while (attempts.size > 100) attempts.delete(attempts.keys().next().value);
|
|
1578
|
+
try {
|
|
1579
|
+
cache.put(key, local);
|
|
1580
|
+
} catch {
|
|
1581
|
+
}
|
|
1582
|
+
const pending = store.save(key, local).then((result) => {
|
|
1583
|
+
const valid = validateQuerySnapshot(result);
|
|
1584
|
+
if (valid.code !== local.code || valid.token !== token) throw Error("The snapshot store changed this immutable query link.");
|
|
1585
|
+
remember(valid);
|
|
1586
|
+
return valid;
|
|
1587
|
+
});
|
|
1588
|
+
promoted.set(token, pending);
|
|
1589
|
+
while (promoted.size > 100) promoted.delete(promoted.keys().next().value);
|
|
1590
|
+
try {
|
|
1591
|
+
return await pending;
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
promoted.delete(token);
|
|
1594
|
+
throw error;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
local(query, baseUrl, parameter = "q") {
|
|
1599
|
+
const token = encodeQuery(query);
|
|
1600
|
+
const current = new URL(baseUrl).searchParams.get(parameter);
|
|
1601
|
+
if (current?.startsWith("@") && known.get(current.slice(1))?.token === token)
|
|
1602
|
+
return link(baseUrl, parameter, current, "durable");
|
|
1603
|
+
if (current?.startsWith("@")) {
|
|
1604
|
+
try {
|
|
1605
|
+
const code = current.slice(1);
|
|
1606
|
+
if (cache.isDurable?.(key, code) && cache.get(key, code)?.token === token)
|
|
1607
|
+
return link(baseUrl, parameter, current, "durable");
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const url = new URL(baseUrl);
|
|
1612
|
+
url.searchParams.set(parameter, token);
|
|
1613
|
+
if (new TextEncoder().encode(url.toString()).length <= budget) return link(baseUrl, parameter, token, "inline");
|
|
1614
|
+
const local = cached(token) ?? newSnapshot(token);
|
|
1615
|
+
const result = link(baseUrl, parameter, "@" + local.code, "local");
|
|
1616
|
+
cache.put(key, local);
|
|
1617
|
+
return result;
|
|
1618
|
+
},
|
|
1619
|
+
async durable(query, baseUrl, parameter = "q") {
|
|
1620
|
+
const saved = await snapshot(query);
|
|
1621
|
+
if (saved) return link(baseUrl, parameter, "@" + saved.code, "durable");
|
|
1622
|
+
const token = encodeQuery(query);
|
|
1623
|
+
return link(baseUrl, parameter, token, "inline");
|
|
1624
|
+
},
|
|
1625
|
+
snapshot,
|
|
1626
|
+
async resolve(baseUrl, parameter = "q", signal) {
|
|
1627
|
+
const values = new URL(baseUrl).searchParams.getAll(parameter);
|
|
1628
|
+
if (!values.length || values.length === 1 && values[0] === "") return null;
|
|
1629
|
+
if (values.length !== 1) throw Error("Invalid or conflicting query link parameters.");
|
|
1630
|
+
const value = values[0];
|
|
1631
|
+
if (!value.startsWith("@")) return decodeQueryStrict(value);
|
|
1632
|
+
const code = value.slice(1);
|
|
1633
|
+
if (!codePattern.test(code)) throw Error("Invalid query link code.");
|
|
1634
|
+
let local = null;
|
|
1635
|
+
try {
|
|
1636
|
+
local = cache.get(key, code);
|
|
1637
|
+
if (local) local = validateQuerySnapshot(local);
|
|
1638
|
+
} catch {
|
|
1639
|
+
local = null;
|
|
1640
|
+
}
|
|
1641
|
+
const result = local ?? (store ? await store.resolve(key, code, signal) : null);
|
|
1642
|
+
if (!result) throw Error("This query link is unavailable. Its local cache entry may have expired; use a saved or shared link for permanent access.");
|
|
1643
|
+
const valid = validateQuerySnapshot(result);
|
|
1644
|
+
if (valid.code !== code) throw Error("Invalid query snapshot code.");
|
|
1645
|
+
if (!local) remember(valid);
|
|
1646
|
+
else {
|
|
1647
|
+
try {
|
|
1648
|
+
if (cache.isDurable?.(key, code)) remember(valid);
|
|
1649
|
+
} catch {
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return decodeQueryStrict(valid.token);
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/computed.ts
|
|
1658
|
+
var COMPUTED_PREFIX = "@computed/";
|
|
1659
|
+
var computedFieldName = (id) => COMPUTED_PREFIX + id;
|
|
1660
|
+
var isComputedField = (name) => name.trim().toLowerCase().startsWith(COMPUTED_PREFIX);
|
|
1661
|
+
function validateComputedColumn(input) {
|
|
1662
|
+
const c = input;
|
|
1663
|
+
if (!c || typeof c.id !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 200 || typeof c.revision !== "string" || !c.revision || c.revision.length > 256 || c.expression?.language !== "qt-expr" || c.expression.version !== 1 || typeof c.expression.source !== "string" || !c.expression.source.trim() || c.expression.source.length > 1e4) {
|
|
1664
|
+
throw new Error(
|
|
1665
|
+
"Invalid computed column definition or unsupported formula version."
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
return {
|
|
1669
|
+
id: c.id,
|
|
1670
|
+
label: c.label,
|
|
1671
|
+
revision: c.revision,
|
|
1672
|
+
expression: { ...c.expression }
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function memoryComputedColumnStore(seed = {}) {
|
|
1676
|
+
const data = new Map(
|
|
1677
|
+
Object.entries(seed).map(([k, v]) => [
|
|
1678
|
+
k,
|
|
1679
|
+
new Map(v.map((c) => [c.id, validateComputedColumn(c)]))
|
|
1680
|
+
])
|
|
1681
|
+
);
|
|
1682
|
+
const seedRevisions = new Set(
|
|
1683
|
+
[...data.values()].flatMap(
|
|
1684
|
+
(entries) => [...entries.values()].map((c) => c.revision)
|
|
1685
|
+
)
|
|
1686
|
+
);
|
|
1687
|
+
let revisionSequence = 0n;
|
|
1688
|
+
function freshRevision() {
|
|
1689
|
+
let revision;
|
|
1690
|
+
do {
|
|
1691
|
+
revision = `memory:${++revisionSequence}`;
|
|
1692
|
+
} while (seedRevisions.has(revision));
|
|
1693
|
+
return revision;
|
|
1694
|
+
}
|
|
1695
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
1696
|
+
return {
|
|
1697
|
+
async list(dataset) {
|
|
1698
|
+
return [...data.get(dataset)?.values() ?? []].map(
|
|
1699
|
+
validateComputedColumn
|
|
1700
|
+
);
|
|
1701
|
+
},
|
|
1702
|
+
async save(dataset, column, expectedRevision) {
|
|
1703
|
+
const entries = data.get(dataset) ?? /* @__PURE__ */ new Map();
|
|
1704
|
+
const current = entries.get(column.id);
|
|
1705
|
+
if ((current?.revision ?? null) !== expectedRevision)
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
"This definition changed. Reload it before saving again."
|
|
1708
|
+
);
|
|
1709
|
+
const next = validateComputedColumn({
|
|
1710
|
+
...column,
|
|
1711
|
+
revision: freshRevision()
|
|
1712
|
+
});
|
|
1713
|
+
entries.set(next.id, next);
|
|
1714
|
+
data.set(dataset, entries);
|
|
1715
|
+
listeners.get(dataset)?.forEach((fn) => fn());
|
|
1716
|
+
return validateComputedColumn(next);
|
|
1717
|
+
},
|
|
1718
|
+
subscribe(dataset, listener) {
|
|
1719
|
+
const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
|
|
1720
|
+
set.add(listener);
|
|
1721
|
+
listeners.set(dataset, set);
|
|
1722
|
+
return () => {
|
|
1723
|
+
set.delete(listener);
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
function httpComputedColumnStore(base, fetcher = fetch) {
|
|
1729
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
1730
|
+
const url = (dataset) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(dataset)}`;
|
|
1731
|
+
async function read(response) {
|
|
1732
|
+
if (response.status === 409)
|
|
1733
|
+
throw new Error(
|
|
1734
|
+
"This definition changed. Reload it before saving again."
|
|
1735
|
+
);
|
|
1736
|
+
if (!response.ok)
|
|
1737
|
+
throw new Error(
|
|
1738
|
+
`Computed column store request failed (${response.status}).`
|
|
1739
|
+
);
|
|
1740
|
+
return response.json();
|
|
1741
|
+
}
|
|
1742
|
+
return {
|
|
1743
|
+
async list(dataset, signal) {
|
|
1744
|
+
const raw = await read(
|
|
1745
|
+
await fetcher(url(dataset), { ...signal ? { signal } : {} })
|
|
1746
|
+
);
|
|
1747
|
+
if (!Array.isArray(raw))
|
|
1748
|
+
throw new Error("Invalid computed column catalogue.");
|
|
1749
|
+
return raw.map(validateComputedColumn);
|
|
1750
|
+
},
|
|
1751
|
+
async save(dataset, column, expectedRevision) {
|
|
1752
|
+
const result = validateComputedColumn(
|
|
1753
|
+
await read(
|
|
1754
|
+
await fetcher(url(dataset), {
|
|
1755
|
+
method: "PUT",
|
|
1756
|
+
headers: { "Content-Type": "application/json" },
|
|
1757
|
+
body: JSON.stringify({ column, expectedRevision })
|
|
1758
|
+
})
|
|
1759
|
+
)
|
|
1760
|
+
);
|
|
1761
|
+
listeners.get(dataset)?.forEach((fn) => fn());
|
|
1762
|
+
return result;
|
|
1763
|
+
},
|
|
1764
|
+
subscribe(dataset, listener) {
|
|
1765
|
+
const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
|
|
1766
|
+
set.add(listener);
|
|
1767
|
+
listeners.set(dataset, set);
|
|
1768
|
+
return () => {
|
|
1769
|
+
set.delete(listener);
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function isComputedCellError(value) {
|
|
1775
|
+
return value !== null && typeof value === "object" && "computedError" in value && typeof value.computedError === "string";
|
|
1776
|
+
}
|
|
1777
|
+
function groupPreview(dependencies, inputs, results, total) {
|
|
1778
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1779
|
+
let nulls = 0, errors = 0;
|
|
1780
|
+
inputs.forEach((row, i) => {
|
|
1781
|
+
const values = dependencies.map((name) => row[name] ?? null), result = results[i] ?? { value: null, error: "No result." };
|
|
1782
|
+
const key = JSON.stringify([values, result.value, result.error ?? null]);
|
|
1783
|
+
const group = groups.get(key);
|
|
1784
|
+
if (group) group.count++;
|
|
1785
|
+
else groups.set(key, { inputs: values, result, count: 1 });
|
|
1786
|
+
if (result.error) errors++;
|
|
1787
|
+
else if (result.value === null) nulls++;
|
|
1788
|
+
});
|
|
1789
|
+
return {
|
|
1790
|
+
dependencies,
|
|
1791
|
+
groups: [...groups.values()].sort((a, b) => b.count - a.count),
|
|
1792
|
+
processed: inputs.length,
|
|
1793
|
+
total,
|
|
1794
|
+
nulls,
|
|
1795
|
+
errors
|
|
1796
|
+
};
|
|
979
1797
|
}
|
|
980
1798
|
|
|
981
1799
|
// src/formula.ts
|
|
@@ -2089,6 +2907,455 @@ function formulaRuntime(ast, inputs, extension) {
|
|
|
2089
2907
|
}
|
|
2090
2908
|
}
|
|
2091
2909
|
|
|
2910
|
+
// src/grouping.ts
|
|
2911
|
+
var GROUP_ID_FIELD = "__qt_group_id__";
|
|
2912
|
+
var MAX_REPRESENTATIVE_SORTS = 8;
|
|
2913
|
+
var MAX_SUMMARY_COLUMNS = 64;
|
|
2914
|
+
var MAX_RESULT_GROUPS = 1e4;
|
|
2915
|
+
function withoutGrouping(query) {
|
|
2916
|
+
const { grouping: _grouping, ...result } = query;
|
|
2917
|
+
return result;
|
|
2918
|
+
}
|
|
2919
|
+
var OP_LABEL = {
|
|
2920
|
+
count: "COUNT",
|
|
2921
|
+
count_distinct: "DISTINCT",
|
|
2922
|
+
sum: "SUM",
|
|
2923
|
+
avg: "AVG",
|
|
2924
|
+
min: "MIN",
|
|
2925
|
+
max: "MAX",
|
|
2926
|
+
row: "ROW",
|
|
2927
|
+
formula: "\u0192x"
|
|
2928
|
+
};
|
|
2929
|
+
function createGroupingModel(inputSchema) {
|
|
2930
|
+
const sourceField = (name) => inputSchema.fields.find((f2) => f2.name === name);
|
|
2931
|
+
const groupableFields = inputSchema.fields.filter(
|
|
2932
|
+
(field) => isGroupable(field) && isSelectable(field)
|
|
2933
|
+
);
|
|
2934
|
+
const summaryFields = inputSchema.fields.filter(
|
|
2935
|
+
(f2) => f2.type !== "textarray" && isSelectable(f2)
|
|
2936
|
+
);
|
|
2937
|
+
const representativeFields = inputSchema.fields.filter(
|
|
2938
|
+
(field) => isSortable(field) && field.type !== "textarray"
|
|
2939
|
+
);
|
|
2940
|
+
function representativeLabel(rule) {
|
|
2941
|
+
return rule.orderBy.map(
|
|
2942
|
+
(term) => `${sourceField(term.field)?.label ?? term.field} ${term.dir === "asc" ? "\u2191" : "\u2193"}${term.nulls === "first" ? " (empty first)" : ""}`
|
|
2943
|
+
).join(", ");
|
|
2944
|
+
}
|
|
2945
|
+
function allowedOps(field) {
|
|
2946
|
+
if (!field) return ["count"];
|
|
2947
|
+
const f2 = sourceField(field);
|
|
2948
|
+
return f2 && isSelectable(f2) ? [...isMeasurable(f2) ? aggOpsForField(f2) : [], "row"] : [];
|
|
2949
|
+
}
|
|
2950
|
+
function summaryExpression(column) {
|
|
2951
|
+
if (column.op === "formula") return column.expression ?? "";
|
|
2952
|
+
const field = column.field ? `[${column.field.replace(/\]/g, "]]")}]` : "";
|
|
2953
|
+
return `${column.op === "row" ? "COUNT" : column.op.toUpperCase()}(${field})`;
|
|
2954
|
+
}
|
|
2955
|
+
function summaryProblem(column) {
|
|
2956
|
+
if (column.op !== "formula") return null;
|
|
2957
|
+
try {
|
|
2958
|
+
const plan = compileMetric(summaryExpression(column), inputSchema);
|
|
2959
|
+
if (plan.dependencies.some((name) => !isSelectable(sourceField(name))))
|
|
2960
|
+
throw new Error("Aggregate fields must be selectable.");
|
|
2961
|
+
if (hasRegex(plan))
|
|
2962
|
+
throw new Error(
|
|
2963
|
+
"Regex aggregate formulas require isolated execution and are not supported in grouped columns."
|
|
2964
|
+
);
|
|
2965
|
+
return null;
|
|
2966
|
+
} catch (error) {
|
|
2967
|
+
return error instanceof Error ? error.message : String(error);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
function summaryLabel(column) {
|
|
2971
|
+
if (column.op === "formula")
|
|
2972
|
+
return column.label?.trim() || "Custom aggregate";
|
|
2973
|
+
if (!column.field) return "Row count";
|
|
2974
|
+
const label = sourceField(column.field)?.label ?? column.field;
|
|
2975
|
+
const prefix = {
|
|
2976
|
+
count: "Count of",
|
|
2977
|
+
count_distinct: "Distinct",
|
|
2978
|
+
sum: "Total",
|
|
2979
|
+
avg: "Average",
|
|
2980
|
+
min: "Min",
|
|
2981
|
+
max: "Max",
|
|
2982
|
+
row: "Representative \xB7",
|
|
2983
|
+
formula: ""
|
|
2984
|
+
};
|
|
2985
|
+
return `${prefix[column.op]} ${label}`;
|
|
2986
|
+
}
|
|
2987
|
+
function resultSchema(keys, summaries) {
|
|
2988
|
+
const fields = [
|
|
2989
|
+
{
|
|
2990
|
+
name: GROUP_ID_FIELD,
|
|
2991
|
+
label: "Group identity",
|
|
2992
|
+
type: "text",
|
|
2993
|
+
source: { kind: "backend" },
|
|
2994
|
+
select: { enabled: false },
|
|
2995
|
+
filter: { enabled: false },
|
|
2996
|
+
sort: { enabled: false },
|
|
2997
|
+
aggregate: { measure: false, groupable: false }
|
|
2998
|
+
},
|
|
2999
|
+
...keys.flatMap((key) => {
|
|
3000
|
+
const field = sourceField(key);
|
|
3001
|
+
if (!field) return [];
|
|
3002
|
+
return [{
|
|
3003
|
+
name: key,
|
|
3004
|
+
label: field.label,
|
|
3005
|
+
type: field.type,
|
|
3006
|
+
source: { kind: "backend" },
|
|
3007
|
+
...field.select ? { select: { ...field.select } } : {},
|
|
3008
|
+
...field.filter ? { filter: { ...field.filter } } : {},
|
|
3009
|
+
...field.aggregate ? { aggregate: { ...field.aggregate } } : {},
|
|
3010
|
+
...field.sort?.enabled !== void 0 ? { sort: { enabled: field.sort.enabled } } : {},
|
|
3011
|
+
...typeof field.render === "string" ? { render: field.render } : {},
|
|
3012
|
+
...field.display ? { display: field.display } : {},
|
|
3013
|
+
group: "Group keys"
|
|
3014
|
+
}];
|
|
3015
|
+
})
|
|
3016
|
+
];
|
|
3017
|
+
for (const column of summaries) {
|
|
3018
|
+
if (column.op === "formula") {
|
|
3019
|
+
let type = "number";
|
|
3020
|
+
try {
|
|
3021
|
+
const compiled = compileMetric(
|
|
3022
|
+
summaryExpression(column),
|
|
3023
|
+
inputSchema
|
|
3024
|
+
);
|
|
3025
|
+
type = compiled.type === "null" ? "number" : compiled.type;
|
|
3026
|
+
} catch {
|
|
3027
|
+
}
|
|
3028
|
+
fields.push({
|
|
3029
|
+
name: column.id,
|
|
3030
|
+
label: summaryLabel(column),
|
|
3031
|
+
type,
|
|
3032
|
+
source: { kind: "backend" },
|
|
3033
|
+
group: "Aggregates",
|
|
3034
|
+
select: { align: type === "number" ? "right" : "left" }
|
|
3035
|
+
});
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
const input = column.field ? sourceField(column.field) : void 0;
|
|
3039
|
+
const counting = column.op === "count" || column.op === "count_distinct";
|
|
3040
|
+
const numeric = counting || column.op === "sum" || column.op === "avg";
|
|
3041
|
+
const example = column.op === "row";
|
|
3042
|
+
fields.push({
|
|
3043
|
+
name: column.id,
|
|
3044
|
+
label: summaryLabel(column),
|
|
3045
|
+
type: numeric ? "number" : input?.type ?? "number",
|
|
3046
|
+
source: { kind: "backend" },
|
|
3047
|
+
select: {
|
|
3048
|
+
align: numeric || input?.type === "number" ? "right" : "left"
|
|
3049
|
+
},
|
|
3050
|
+
group: example ? "Representative row" : "Summaries",
|
|
3051
|
+
...!numeric && input?.aggregate?.groupable !== void 0 ? { aggregate: { groupable: input.aggregate.groupable } } : {},
|
|
3052
|
+
...typeof input?.render === "string" && !counting ? { render: input.render } : {},
|
|
3053
|
+
...input?.display && !counting ? { display: input.display } : {}
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
return {
|
|
3057
|
+
name: `${inputSchema.name}:groups`,
|
|
3058
|
+
idField: GROUP_ID_FIELD,
|
|
3059
|
+
fields,
|
|
3060
|
+
defaultSelect: keys.filter((key) => sourceField(key)).map((field) => ({ field })),
|
|
3061
|
+
defaultLimit: 25
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
function keyValue(value) {
|
|
3065
|
+
if (value == null) return null;
|
|
3066
|
+
if (value instanceof Date) return value.toISOString();
|
|
3067
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
|
|
3068
|
+
return value;
|
|
3069
|
+
throw new Error("Group keys must be finite scalar values.");
|
|
3070
|
+
}
|
|
3071
|
+
function groupId(row, keys, schema) {
|
|
3072
|
+
const tuple = [...keys].sort().map((key) => [
|
|
3073
|
+
key,
|
|
3074
|
+
keyValue(
|
|
3075
|
+
readFieldValue(schema.fields.find((f2) => f2.name === key), row)
|
|
3076
|
+
)
|
|
3077
|
+
]);
|
|
3078
|
+
if (tuple.some(
|
|
3079
|
+
([key, value]) => String(key).length + String(value).length > 1e4
|
|
3080
|
+
))
|
|
3081
|
+
throw new Error("Grouping key length budget exceeded.");
|
|
3082
|
+
const id = JSON.stringify(tuple);
|
|
3083
|
+
if (id.length > 65536)
|
|
3084
|
+
throw new Error("Grouping key length budget exceeded.");
|
|
3085
|
+
return id;
|
|
3086
|
+
}
|
|
3087
|
+
function compare2(a, b) {
|
|
3088
|
+
if (a === b) return 0;
|
|
3089
|
+
if (a == null) return -1;
|
|
3090
|
+
if (b == null) return 1;
|
|
3091
|
+
return a < b ? -1 : 1;
|
|
3092
|
+
}
|
|
3093
|
+
function representative(rows, rule, schema) {
|
|
3094
|
+
const identityField = schema.fields.find(
|
|
3095
|
+
(field) => field.name === schema.idField
|
|
3096
|
+
);
|
|
3097
|
+
const identities = /* @__PURE__ */ new Set();
|
|
3098
|
+
for (const row of rows) {
|
|
3099
|
+
const id = identityField ? readFieldValue(identityField, row) : row[schema.idField];
|
|
3100
|
+
if (!(typeof id === "string" || typeof id === "number" && Number.isFinite(id)) || identities.has(id))
|
|
3101
|
+
throw new Error(
|
|
3102
|
+
"Representative rows require unique string or finite numeric source IDs."
|
|
3103
|
+
);
|
|
3104
|
+
identities.add(id);
|
|
3105
|
+
}
|
|
3106
|
+
const order = rule.orderBy.some((term) => term.field === schema.idField) ? rule.orderBy : [
|
|
3107
|
+
...rule.orderBy,
|
|
3108
|
+
{ field: schema.idField, dir: "desc", nulls: "last" }
|
|
3109
|
+
];
|
|
3110
|
+
const terms = order.map((term) => ({
|
|
3111
|
+
...term,
|
|
3112
|
+
def: schema.fields.find((field) => field.name === term.field)
|
|
3113
|
+
}));
|
|
3114
|
+
return rows.reduce((best, row) => {
|
|
3115
|
+
for (const term of terms) {
|
|
3116
|
+
const value = (record2) => {
|
|
3117
|
+
const raw = term.def ? readFieldValue(term.def, record2) : record2[schema.idField];
|
|
3118
|
+
if (raw == null || term.def?.type !== "datetime") return raw;
|
|
3119
|
+
const instant = Date.parse(String(raw));
|
|
3120
|
+
return Number.isFinite(instant) ? instant : null;
|
|
3121
|
+
};
|
|
3122
|
+
const a = value(row), b = value(best);
|
|
3123
|
+
if (a == null || b == null) {
|
|
3124
|
+
if (a == null && b == null) continue;
|
|
3125
|
+
return a == null === (term.nulls === "first") ? row : best;
|
|
3126
|
+
}
|
|
3127
|
+
const rank = compare2(a, b);
|
|
3128
|
+
if (rank !== 0)
|
|
3129
|
+
return (term.dir === "asc" ? rank < 0 : rank > 0) ? row : best;
|
|
3130
|
+
}
|
|
3131
|
+
return best;
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
function groupRows(rows, schema, keys, summaries, rule = null) {
|
|
3135
|
+
if (!rule && summaries.some((column) => column.op === "row"))
|
|
3136
|
+
throw new Error(
|
|
3137
|
+
"Configure representative-row ordering before selecting row values."
|
|
3138
|
+
);
|
|
3139
|
+
if (keys.length === 0) return [];
|
|
3140
|
+
if (keys.length > MAX_GROUP_BY_FIELDS || summaries.length > MAX_SUMMARY_COLUMNS)
|
|
3141
|
+
throw new Error("Grouping definition exceeds column limits.");
|
|
3142
|
+
if (rows.length > 1e6)
|
|
3143
|
+
throw new Error("Grouping row budget exceeded.");
|
|
3144
|
+
let bytes = rows.length * 16;
|
|
3145
|
+
let operations = 0;
|
|
3146
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3147
|
+
for (const row of rows) {
|
|
3148
|
+
const id = groupId(row, keys, schema);
|
|
3149
|
+
const bucket = groups.get(id);
|
|
3150
|
+
if (bucket) bucket.push(row);
|
|
3151
|
+
else {
|
|
3152
|
+
if (groups.size >= MAX_RESULT_GROUPS)
|
|
3153
|
+
throw new Error(
|
|
3154
|
+
`Grouping exceeds ${MAX_RESULT_GROUPS.toLocaleString()} groups.`
|
|
3155
|
+
);
|
|
3156
|
+
bytes += id.length * 2 + 256;
|
|
3157
|
+
if (bytes > 16e6)
|
|
3158
|
+
throw new Error("Grouping retained-memory budget exceeded.");
|
|
3159
|
+
groups.set(id, [row]);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
const formulas = new Map(
|
|
3163
|
+
summaries.filter((c) => c.op !== "row").map((column) => {
|
|
3164
|
+
try {
|
|
3165
|
+
const problem = summaryProblem(column);
|
|
3166
|
+
if (problem) throw new Error(problem);
|
|
3167
|
+
return [
|
|
3168
|
+
column.id,
|
|
3169
|
+
{ plan: compileMetric(summaryExpression(column), schema) }
|
|
3170
|
+
];
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
return [
|
|
3173
|
+
column.id,
|
|
3174
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
3175
|
+
];
|
|
3176
|
+
}
|
|
3177
|
+
})
|
|
3178
|
+
);
|
|
3179
|
+
const nodeCost = (node) => 1 + (node.kind === "call" ? node.args.reduce((cost, child) => cost + nodeCost(child), 0) : 0);
|
|
3180
|
+
const costs = new Map(
|
|
3181
|
+
[...formulas].map(([id, compiled]) => [
|
|
3182
|
+
id,
|
|
3183
|
+
compiled.plan?.aggregates.map(nodeCost) ?? []
|
|
3184
|
+
])
|
|
3185
|
+
);
|
|
3186
|
+
return [...groups].map(([id, members]) => {
|
|
3187
|
+
const output = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
3188
|
+
[GROUP_ID_FIELD]: id
|
|
3189
|
+
});
|
|
3190
|
+
for (const key of keys)
|
|
3191
|
+
output[key] = readFieldValue(
|
|
3192
|
+
schema.fields.find((f2) => f2.name === key),
|
|
3193
|
+
members[0]
|
|
3194
|
+
);
|
|
3195
|
+
const chosen = rule && summaries.some((column) => column.op === "row") ? representative(members, rule, schema) : null;
|
|
3196
|
+
for (const column of summaries) {
|
|
3197
|
+
if (column.op !== "row") {
|
|
3198
|
+
const compiled = formulas.get(column.id);
|
|
3199
|
+
if (!compiled.plan) {
|
|
3200
|
+
output[column.id] = { computedError: compiled.error };
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
const plan = compiled.plan;
|
|
3204
|
+
const rowPlans = metricAggregatePlans(plan);
|
|
3205
|
+
const reductions = plan.aggregates.map(
|
|
3206
|
+
(node, i) => reduceMetricValues(
|
|
3207
|
+
node,
|
|
3208
|
+
members.map((row) => {
|
|
3209
|
+
if ((operations += costs.get(column.id)?.[i] ?? 1) > 1e7)
|
|
3210
|
+
throw new Error("Grouping operation budget exceeded.");
|
|
3211
|
+
const rowPlan = rowPlans[i];
|
|
3212
|
+
if (!rowPlan) return { value: 1 };
|
|
3213
|
+
const values = Object.fromEntries(
|
|
3214
|
+
rowPlan.dependencies.map((name) => {
|
|
3215
|
+
const value = readFieldValue(
|
|
3216
|
+
schema.fields.find((f2) => f2.name === name),
|
|
3217
|
+
row
|
|
3218
|
+
);
|
|
3219
|
+
return [
|
|
3220
|
+
name,
|
|
3221
|
+
value instanceof Date ? value.toISOString() : value ?? null
|
|
3222
|
+
];
|
|
3223
|
+
})
|
|
3224
|
+
);
|
|
3225
|
+
return formulaRuntime(rowPlan.ast, values);
|
|
3226
|
+
})
|
|
3227
|
+
)
|
|
3228
|
+
);
|
|
3229
|
+
const result = evaluateMetricReductions(plan, reductions);
|
|
3230
|
+
output[column.id] = result.error ? { computedError: result.error } : result.value;
|
|
3231
|
+
continue;
|
|
3232
|
+
}
|
|
3233
|
+
const field = schema.fields.find((f2) => f2.name === column.field);
|
|
3234
|
+
if (column.op === "row") {
|
|
3235
|
+
output[column.id] = field && chosen ? readFieldValue(field, chosen) : null;
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
return output;
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
function filteredInput(rows, query, schema, options2 = {}) {
|
|
3243
|
+
return selectMetricShownRows(
|
|
3244
|
+
rows,
|
|
3245
|
+
{
|
|
3246
|
+
...withoutGrouping(query),
|
|
3247
|
+
aggregations: [],
|
|
3248
|
+
orderBy: [],
|
|
3249
|
+
offset: 0,
|
|
3250
|
+
limit: Math.max(1, rows.length)
|
|
3251
|
+
},
|
|
3252
|
+
{ ...schema, defaultSort: [] },
|
|
3253
|
+
options2
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
function reconcileRepresentative(query, summaries, schema, remove) {
|
|
3257
|
+
const affected = new Set(
|
|
3258
|
+
summaries.filter((s) => s.op === "row").map((s) => s.id)
|
|
3259
|
+
);
|
|
3260
|
+
if (!affected.size) return query;
|
|
3261
|
+
let grew = true;
|
|
3262
|
+
while (grew) {
|
|
3263
|
+
grew = false;
|
|
3264
|
+
for (const f2 of schema.fields) {
|
|
3265
|
+
if (!affected.has(f2.name) && f2.source.kind === "derived" && f2.source.dependencies?.some((name) => affected.has(name))) {
|
|
3266
|
+
affected.add(f2.name);
|
|
3267
|
+
grew = true;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
return {
|
|
3272
|
+
...query,
|
|
3273
|
+
offset: 0,
|
|
3274
|
+
select: remove ? query.select.filter((c) => !affected.has(c.field)) : query.select,
|
|
3275
|
+
where: query.where.filter(
|
|
3276
|
+
(term) => !predicatesOf(term).some((p) => affected.has(p.field))
|
|
3277
|
+
),
|
|
3278
|
+
orderBy: query.orderBy.filter((term) => !affected.has(term.field)),
|
|
3279
|
+
aggregations: (query.aggregations ?? []).filter((metric) => {
|
|
3280
|
+
if (metric.groupBy.some((name) => affected.has(name))) return false;
|
|
3281
|
+
try {
|
|
3282
|
+
return !metricPlans(metric, schema).some(
|
|
3283
|
+
(plan) => plan.dependencies.some((name) => affected.has(name))
|
|
3284
|
+
);
|
|
3285
|
+
} catch {
|
|
3286
|
+
return false;
|
|
3287
|
+
}
|
|
3288
|
+
})
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
function validate(definition) {
|
|
3292
|
+
if (definition.diagnostics?.length)
|
|
3293
|
+
throw new Error(definition.diagnostics.join(" "));
|
|
3294
|
+
if (!definition.keys.length || definition.keys.length > MAX_GROUP_BY_FIELDS || new Set(definition.keys).size !== definition.keys.length || definition.keys.some(
|
|
3295
|
+
(key) => key === GROUP_ID_FIELD || !groupableFields.some((f2) => f2.name === key)
|
|
3296
|
+
))
|
|
3297
|
+
throw new Error("Choose valid group keys from the source schema.");
|
|
3298
|
+
if (definition.summaries.length > MAX_SUMMARY_COLUMNS)
|
|
3299
|
+
throw new Error("Too many aggregate columns.");
|
|
3300
|
+
const ids = /* @__PURE__ */ new Set([
|
|
3301
|
+
GROUP_ID_FIELD,
|
|
3302
|
+
...inputSchema.fields.map((field) => field.name)
|
|
3303
|
+
]);
|
|
3304
|
+
for (const column of definition.summaries) {
|
|
3305
|
+
if (!column.id || column.id.length > 256 || ids.has(column.id) || isComputedField(column.id) || ["__proto__", "constructor", "prototype"].includes(column.id))
|
|
3306
|
+
throw new Error(
|
|
3307
|
+
"Aggregate column IDs must be unique and distinct from source fields."
|
|
3308
|
+
);
|
|
3309
|
+
ids.add(column.id);
|
|
3310
|
+
if (column.op === "formula") {
|
|
3311
|
+
const error = summaryProblem(column);
|
|
3312
|
+
if (error) throw new Error(error);
|
|
3313
|
+
} else if (!allowedOps(column.field).includes(column.op) || column.op === "row" && (!column.field || !definition.representative))
|
|
3314
|
+
throw new Error("Invalid aggregate or representative column.");
|
|
3315
|
+
}
|
|
3316
|
+
const rule = definition.representative;
|
|
3317
|
+
if (rule && (!rule.orderBy.length || rule.orderBy.length > MAX_REPRESENTATIVE_SORTS || new Set(rule.orderBy.map((t) => t.field)).size !== rule.orderBy.length || rule.orderBy.some(
|
|
3318
|
+
(t) => !representativeFields.some((f2) => f2.name === t.field) || !["asc", "desc"].includes(t.dir) || !["first", "last"].includes(t.nulls)
|
|
3319
|
+
)))
|
|
3320
|
+
throw new Error("Choose valid representative-row ordering.");
|
|
3321
|
+
}
|
|
3322
|
+
return {
|
|
3323
|
+
inputSchema,
|
|
3324
|
+
sourceField,
|
|
3325
|
+
groupableFields,
|
|
3326
|
+
summaryFields,
|
|
3327
|
+
representativeFields,
|
|
3328
|
+
representativeLabel,
|
|
3329
|
+
allowedOps,
|
|
3330
|
+
summaryExpression,
|
|
3331
|
+
summaryProblem,
|
|
3332
|
+
summaryLabel,
|
|
3333
|
+
resultSchema,
|
|
3334
|
+
groupRows,
|
|
3335
|
+
filteredInput,
|
|
3336
|
+
reconcileRepresentative,
|
|
3337
|
+
validate
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
function prepareGrouping(rows, schema, grouping, options2 = {}) {
|
|
3341
|
+
const model = createGroupingModel(schema);
|
|
3342
|
+
if (grouping.source.grouping)
|
|
3343
|
+
throw new Error("Only one grouping stage is supported.");
|
|
3344
|
+
model.validate(grouping);
|
|
3345
|
+
const inputs = model.filteredInput(rows, grouping.source, schema, options2);
|
|
3346
|
+
return {
|
|
3347
|
+
inputs,
|
|
3348
|
+
rows: model.groupRows(
|
|
3349
|
+
inputs,
|
|
3350
|
+
schema,
|
|
3351
|
+
grouping.keys,
|
|
3352
|
+
grouping.summaries,
|
|
3353
|
+
grouping.representative
|
|
3354
|
+
),
|
|
3355
|
+
schema: model.resultSchema(grouping.keys, grouping.summaries)
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
|
|
2092
3359
|
// src/execution.ts
|
|
2093
3360
|
function toServerQueryV2(q, schema, execution) {
|
|
2094
3361
|
const normalized = normalizeQueryState(q), legacy = toServerQuery(normalized, schema);
|
|
@@ -2115,7 +3382,7 @@ function toServerQueryV2(q, schema, execution) {
|
|
|
2115
3382
|
}
|
|
2116
3383
|
const select = [...legacy.select];
|
|
2117
3384
|
for (const c of normalized.select.length ? normalized.select : selectedFields(schema, normalized).map((f2) => ({ field: f2.name })))
|
|
2118
|
-
if (isComputedField(c.field) && requireField(c.field, "select"))
|
|
3385
|
+
if (isComputedField(c.field) && execution.fields[c.field]?.select && requireField(c.field, "select"))
|
|
2119
3386
|
select.push(c.field);
|
|
2120
3387
|
const orderBy = [];
|
|
2121
3388
|
for (const term of normalized.orderBy.length ? normalized.orderBy : schema.defaultSort ?? []) {
|
|
@@ -2286,6 +3553,171 @@ function histogramCounts(values, edges) {
|
|
|
2286
3553
|
return counts;
|
|
2287
3554
|
}
|
|
2288
3555
|
|
|
3556
|
+
// src/metricDates.ts
|
|
3557
|
+
import { Temporal } from "temporal-polyfill";
|
|
3558
|
+
var METRIC_DATE_INTERVALS = [
|
|
3559
|
+
"hour",
|
|
3560
|
+
"day",
|
|
3561
|
+
"week",
|
|
3562
|
+
"month",
|
|
3563
|
+
"year"
|
|
3564
|
+
];
|
|
3565
|
+
function metricDateTime(value, timeZone = "UTC") {
|
|
3566
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(
|
|
3567
|
+
value
|
|
3568
|
+
))
|
|
3569
|
+
throw Error(
|
|
3570
|
+
"Date grouping requires ISO dates or timestamps with a timezone."
|
|
3571
|
+
);
|
|
3572
|
+
const time = Date.parse(value), month = Number(value.slice(5, 7)), day = Number(value.slice(8, 10));
|
|
3573
|
+
const calendar = /* @__PURE__ */ new Date(0);
|
|
3574
|
+
calendar.setUTCFullYear(Number(value.slice(0, 4)), month, 0);
|
|
3575
|
+
if (!Number.isFinite(time) || month < 1 || month > 12 || day < 1 || day > calendar.getUTCDate())
|
|
3576
|
+
throw Error("Invalid date grouping value.");
|
|
3577
|
+
return value.length === 10 && timeZone !== "UTC" ? Temporal.PlainDate.from(value).toZonedDateTime(timeZone).epochMilliseconds : time;
|
|
3578
|
+
}
|
|
3579
|
+
function metricOffsetTransition(start, end, timeZone) {
|
|
3580
|
+
if (end <= start) return void 0;
|
|
3581
|
+
const offset = (time) => Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone).offsetNanoseconds;
|
|
3582
|
+
const before = offset(start);
|
|
3583
|
+
if (before === offset(end)) return void 0;
|
|
3584
|
+
while (end - start > 1) {
|
|
3585
|
+
const middle = Math.floor((start + end) / 2);
|
|
3586
|
+
if (offset(middle) === before) start = middle;
|
|
3587
|
+
else end = middle;
|
|
3588
|
+
}
|
|
3589
|
+
return end;
|
|
3590
|
+
}
|
|
3591
|
+
function metricDateFloor(time, interval, timeZone = "UTC") {
|
|
3592
|
+
if (timeZone !== "UTC") {
|
|
3593
|
+
const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
|
|
3594
|
+
if (interval === "hour") {
|
|
3595
|
+
const floor = time - zoned.minute * 6e4 - zoned.second * 1e3 - zoned.millisecond;
|
|
3596
|
+
return metricOffsetTransition(floor, time, timeZone) ?? floor;
|
|
3597
|
+
}
|
|
3598
|
+
let date2 = zoned.toPlainDate();
|
|
3599
|
+
if (interval === "week") date2 = date2.subtract({ days: date2.dayOfWeek - 1 });
|
|
3600
|
+
if (interval === "month" || interval === "year")
|
|
3601
|
+
date2 = date2.with({ day: 1 });
|
|
3602
|
+
if (interval === "year") date2 = date2.with({ month: 1 });
|
|
3603
|
+
return date2.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3604
|
+
}
|
|
3605
|
+
const date = new Date(time);
|
|
3606
|
+
date.setUTCMinutes(0, 0, 0);
|
|
3607
|
+
if (interval !== "hour") date.setUTCHours(0);
|
|
3608
|
+
if (interval === "week")
|
|
3609
|
+
date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
|
|
3610
|
+
if (interval === "month" || interval === "year") date.setUTCDate(1);
|
|
3611
|
+
if (interval === "year") date.setUTCMonth(0);
|
|
3612
|
+
return date.getTime();
|
|
3613
|
+
}
|
|
3614
|
+
function metricDateNext(time, interval, timeZone = "UTC") {
|
|
3615
|
+
if (timeZone !== "UTC") {
|
|
3616
|
+
const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
|
|
3617
|
+
if (interval === "hour") {
|
|
3618
|
+
const target = zoned.toPlainDateTime().with({
|
|
3619
|
+
minute: 0,
|
|
3620
|
+
second: 0,
|
|
3621
|
+
millisecond: 0,
|
|
3622
|
+
microsecond: 0,
|
|
3623
|
+
nanosecond: 0
|
|
3624
|
+
}).add({ hours: 1 });
|
|
3625
|
+
let next = target.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3626
|
+
if (next <= time)
|
|
3627
|
+
next = target.toZonedDateTime(timeZone, {
|
|
3628
|
+
disambiguation: "later"
|
|
3629
|
+
}).epochMilliseconds;
|
|
3630
|
+
return metricOffsetTransition(time, next, timeZone) ?? next;
|
|
3631
|
+
}
|
|
3632
|
+
let date2 = zoned.toPlainDate();
|
|
3633
|
+
if (interval === "month") date2 = date2.with({ day: 1 }).add({ months: 1 });
|
|
3634
|
+
else if (interval === "year")
|
|
3635
|
+
date2 = date2.with({ month: 1, day: 1 }).add({ years: 1 });
|
|
3636
|
+
else if (interval === "week")
|
|
3637
|
+
date2 = date2.subtract({ days: date2.dayOfWeek - 1 }).add({ weeks: 1 });
|
|
3638
|
+
else date2 = date2.add({ days: 1 });
|
|
3639
|
+
return date2.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3640
|
+
}
|
|
3641
|
+
const date = new Date(time);
|
|
3642
|
+
if (interval === "hour") date.setUTCHours(date.getUTCHours() + 1);
|
|
3643
|
+
else if (interval === "month") date.setUTCMonth(date.getUTCMonth() + 1);
|
|
3644
|
+
else if (interval === "year") date.setUTCFullYear(date.getUTCFullYear() + 1);
|
|
3645
|
+
else date.setUTCDate(date.getUTCDate() + (interval === "week" ? 7 : 1));
|
|
3646
|
+
return date.getTime();
|
|
3647
|
+
}
|
|
3648
|
+
function customEndTime(value, timeZone = "UTC") {
|
|
3649
|
+
const time = metricDateTime(value, timeZone);
|
|
3650
|
+
return value.length === 10 ? metricDateNext(time, "day", timeZone) - 1 : time;
|
|
3651
|
+
}
|
|
3652
|
+
function validateDateGrouping(config) {
|
|
3653
|
+
if (typeof config.field !== "string" || !config.field || config.field.length > 256 || !METRIC_DATE_INTERVALS.includes(config.interval) || typeof config.includeEmpty !== "boolean" || !["query", "observed", "custom"].includes(config.range))
|
|
3654
|
+
throw Error("Invalid date grouping definition.");
|
|
3655
|
+
if (config.timeZone !== void 0) {
|
|
3656
|
+
if (typeof config.timeZone !== "string" || !config.timeZone || config.timeZone.length > 80 || /^[+-]/.test(config.timeZone))
|
|
3657
|
+
throw Error(
|
|
3658
|
+
"Use a valid IANA grouping timezone, such as America/Denver or UTC."
|
|
3659
|
+
);
|
|
3660
|
+
try {
|
|
3661
|
+
new Intl.DateTimeFormat("en-US", { timeZone: config.timeZone });
|
|
3662
|
+
} catch {
|
|
3663
|
+
throw Error(
|
|
3664
|
+
"Use a valid IANA grouping timezone, such as America/Denver or UTC."
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
if (config.includeEmpty && config.range === "custom") {
|
|
3669
|
+
const start = metricDateTime(config.start ?? "", config.timeZone), end = customEndTime(config.end ?? "", config.timeZone);
|
|
3670
|
+
if (end < start)
|
|
3671
|
+
throw Error("The end date must be on or after the start date.");
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
function metricDateRange(config, query, observed, now, canonical) {
|
|
3675
|
+
let min = Infinity, max = -Infinity;
|
|
3676
|
+
for (const time of observed) {
|
|
3677
|
+
min = Math.min(min, time);
|
|
3678
|
+
max = Math.max(max, time);
|
|
3679
|
+
}
|
|
3680
|
+
if (config.range === "observed")
|
|
3681
|
+
return observed.length ? [min, max] : void 0;
|
|
3682
|
+
if (config.range === "custom") {
|
|
3683
|
+
const start2 = metricDateFloor(
|
|
3684
|
+
metricDateTime(config.start ?? "", config.timeZone),
|
|
3685
|
+
config.interval,
|
|
3686
|
+
config.timeZone
|
|
3687
|
+
);
|
|
3688
|
+
const end2 = metricDateFloor(
|
|
3689
|
+
customEndTime(config.end ?? "", config.timeZone),
|
|
3690
|
+
config.interval,
|
|
3691
|
+
config.timeZone
|
|
3692
|
+
);
|
|
3693
|
+
if (observed.length && (start2 > min || end2 < max))
|
|
3694
|
+
throw Error(
|
|
3695
|
+
"Custom date bounds must include all observed buckets. Narrow the query filters to exclude data."
|
|
3696
|
+
);
|
|
3697
|
+
return [start2, end2];
|
|
3698
|
+
}
|
|
3699
|
+
let start = -Infinity, end = Infinity;
|
|
3700
|
+
for (const term of query.where) {
|
|
3701
|
+
if (isOrGroup(term) || term.negated || canonical(term.field) !== canonical(config.field))
|
|
3702
|
+
continue;
|
|
3703
|
+
if (!["=", ">", ">=", "<", "<="].includes(term.op)) continue;
|
|
3704
|
+
const time = Number(coerceValue("datetime", term.value, now));
|
|
3705
|
+
if (term.op === ">" || term.op === ">=" || term.op === "=")
|
|
3706
|
+
start = Math.max(start, time + (term.op === ">" ? 1 : 0));
|
|
3707
|
+
if (term.op === "<" || term.op === "<=" || term.op === "=")
|
|
3708
|
+
end = Math.min(end, time - (term.op === "<" ? 1 : 0));
|
|
3709
|
+
}
|
|
3710
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
3711
|
+
throw Error(
|
|
3712
|
+
"Include empty dates needs a bounded date filter. Choose observed dates or a custom range."
|
|
3713
|
+
);
|
|
3714
|
+
if (end < start) return void 0;
|
|
3715
|
+
return [
|
|
3716
|
+
metricDateFloor(start, config.interval, config.timeZone),
|
|
3717
|
+
metricDateFloor(end, config.interval, config.timeZone)
|
|
3718
|
+
];
|
|
3719
|
+
}
|
|
3720
|
+
|
|
2289
3721
|
// src/metrics.ts
|
|
2290
3722
|
var aggregates = /* @__PURE__ */ new Set([
|
|
2291
3723
|
"COUNT",
|
|
@@ -2293,8 +3725,29 @@ var aggregates = /* @__PURE__ */ new Set([
|
|
|
2293
3725
|
"SUM",
|
|
2294
3726
|
"AVG",
|
|
2295
3727
|
"MIN",
|
|
2296
|
-
"MAX"
|
|
3728
|
+
"MAX",
|
|
3729
|
+
"MEDIAN"
|
|
2297
3730
|
]);
|
|
3731
|
+
var METRIC_FUNCTIONS = [
|
|
3732
|
+
["COUNT", "Count rows, or non-empty values."],
|
|
3733
|
+
["COUNT_DISTINCT", "Count distinct non-empty values."],
|
|
3734
|
+
["SUM", "Total numeric values in each group."],
|
|
3735
|
+
["AVG", "Average numeric values in each group."],
|
|
3736
|
+
["MIN", "Smallest non-empty value in each group."],
|
|
3737
|
+
["MAX", "Largest non-empty value in each group."],
|
|
3738
|
+
[
|
|
3739
|
+
"MEDIAN",
|
|
3740
|
+
"Middle numeric value; averages the middle pair. Ignores empty values."
|
|
3741
|
+
]
|
|
3742
|
+
].map(([name, description]) => ({
|
|
3743
|
+
name,
|
|
3744
|
+
description,
|
|
3745
|
+
signature: `${name}(row expression${name === "COUNT" ? "?" : ""})`,
|
|
3746
|
+
min: name === "COUNT" ? 0 : 1,
|
|
3747
|
+
max: 1,
|
|
3748
|
+
result: name === "MIN" || name === "MAX" ? "branch" : "number",
|
|
3749
|
+
args: ["SUM", "AVG", "MEDIAN"].includes(name) ? ["number"] : "any"
|
|
3750
|
+
}));
|
|
2298
3751
|
function compileMetric(expression, schema, resolve) {
|
|
2299
3752
|
const fields = Array.isArray(schema) ? schema : schema.fields;
|
|
2300
3753
|
const nodes = [];
|
|
@@ -2336,7 +3789,7 @@ function compileMetric(expression, schema, resolve) {
|
|
|
2336
3789
|
n.from,
|
|
2337
3790
|
n.to
|
|
2338
3791
|
);
|
|
2339
|
-
if (!isMeasurable(field) || (field.aggregate?.ops !== void 0 || node.args[0] === n) && !aggOpsForField(field).includes(node.name.toLowerCase()))
|
|
3792
|
+
if (!isMeasurable(field) || (field.aggregate?.ops !== void 0 || node.args[0] === n && node.name !== "MEDIAN") && !aggOpsForField(field).includes(node.name.toLowerCase()))
|
|
2340
3793
|
throw new FormulaError(
|
|
2341
3794
|
`Field ${n.name} does not allow ${node.name}.`,
|
|
2342
3795
|
n.from,
|
|
@@ -2353,7 +3806,7 @@ function compileMetric(expression, schema, resolve) {
|
|
|
2353
3806
|
}
|
|
2354
3807
|
};
|
|
2355
3808
|
node.args.forEach(visit);
|
|
2356
|
-
if (["SUM", "AVG"].includes(node.name) && !["number", "null"].includes(type))
|
|
3809
|
+
if (["SUM", "AVG", "MEDIAN"].includes(node.name) && !["number", "null"].includes(type))
|
|
2357
3810
|
throw new FormulaError(
|
|
2358
3811
|
`${node.name} requires numeric input.`,
|
|
2359
3812
|
node.from,
|
|
@@ -2367,7 +3820,9 @@ function compileMetric(expression, schema, resolve) {
|
|
|
2367
3820
|
);
|
|
2368
3821
|
nodes.push(node);
|
|
2369
3822
|
aggregateTypes.push(type);
|
|
2370
|
-
return ["COUNT", "COUNT_DISTINCT", "SUM", "AVG"].includes(
|
|
3823
|
+
return ["COUNT", "COUNT_DISTINCT", "SUM", "AVG", "MEDIAN"].includes(
|
|
3824
|
+
node.name
|
|
3825
|
+
) ? "number" : type;
|
|
2371
3826
|
}
|
|
2372
3827
|
);
|
|
2373
3828
|
const groupCheck = (node) => {
|
|
@@ -2410,11 +3865,11 @@ function metricAggregatePlans(plan) {
|
|
|
2410
3865
|
};
|
|
2411
3866
|
});
|
|
2412
3867
|
}
|
|
2413
|
-
function distinctAccumulator(
|
|
3868
|
+
function distinctAccumulator(options2) {
|
|
2414
3869
|
const values = /* @__PURE__ */ new Set();
|
|
2415
3870
|
let bytes = 0;
|
|
2416
|
-
const cardinality =
|
|
2417
|
-
const byteLimit =
|
|
3871
|
+
const cardinality = options2.maxDistinctValues ?? 1e5;
|
|
3872
|
+
const byteLimit = options2.maxDistinctBytes ?? 16e6;
|
|
2418
3873
|
if (!Number.isSafeInteger(cardinality) || cardinality < 0 || !Number.isSafeInteger(byteLimit) || byteLimit < 0)
|
|
2419
3874
|
throw Error("Invalid distinct budget.");
|
|
2420
3875
|
return {
|
|
@@ -2431,13 +3886,13 @@ function distinctAccumulator(options) {
|
|
|
2431
3886
|
result: () => ({ value: values.size })
|
|
2432
3887
|
};
|
|
2433
3888
|
}
|
|
2434
|
-
function reduceMetricValues(node, results,
|
|
2435
|
-
return reduceResults(node, results,
|
|
3889
|
+
function reduceMetricValues(node, results, options2 = {}) {
|
|
3890
|
+
return reduceResults(node, results, options2);
|
|
2436
3891
|
}
|
|
2437
|
-
function reduceResults(node, results,
|
|
3892
|
+
function reduceResults(node, results, options2) {
|
|
2438
3893
|
try {
|
|
2439
3894
|
if (!aggregates.has(node.name)) throw Error("Expected an aggregate node.");
|
|
2440
|
-
const distinct = node.name === "COUNT_DISTINCT" ? distinctAccumulator(
|
|
3895
|
+
const distinct = node.name === "COUNT_DISTINCT" ? distinctAccumulator(options2) : void 0;
|
|
2441
3896
|
const values = [];
|
|
2442
3897
|
let count = 0, nonnull = 0, error;
|
|
2443
3898
|
for (const r of results) {
|
|
@@ -2458,6 +3913,17 @@ function reduceResults(node, results, options) {
|
|
|
2458
3913
|
if (node.name === "COUNT")
|
|
2459
3914
|
return { value: node.args.length ? nonnull : count };
|
|
2460
3915
|
if (!values.length) return { value: null };
|
|
3916
|
+
if (node.name === "MEDIAN") {
|
|
3917
|
+
const sorted = values.map((value2) => {
|
|
3918
|
+
if (typeof value2 !== "number" || !Number.isFinite(value2))
|
|
3919
|
+
throw Error("Expected a finite number.");
|
|
3920
|
+
return value2;
|
|
3921
|
+
}).sort((a, b) => a - b);
|
|
3922
|
+
const mid = Math.floor(sorted.length / 2);
|
|
3923
|
+
const value = sorted.length % 2 ? sorted[mid] : sorted[mid - 1] / 2 + sorted[mid] / 2;
|
|
3924
|
+
assertMetricNumber(value);
|
|
3925
|
+
return { value };
|
|
3926
|
+
}
|
|
2461
3927
|
if (node.name === "SUM" || node.name === "AVG") {
|
|
2462
3928
|
let sum = 0;
|
|
2463
3929
|
for (const v of values) {
|
|
@@ -2559,6 +4025,18 @@ function validateMetric(c, schema, resolve) {
|
|
|
2559
4025
|
if (!f2 || !isGroupable(f2))
|
|
2560
4026
|
throw new FormulaError(`Field ${key} cannot group metrics.`);
|
|
2561
4027
|
}
|
|
4028
|
+
if (c.dateGrouping) {
|
|
4029
|
+
validateDateGrouping(c.dateGrouping);
|
|
4030
|
+
const field = resolveFieldName(schema, c.dateGrouping.field) ?? c.dateGrouping.field;
|
|
4031
|
+
if (field !== (resolveFieldName(schema, c.groupBy[0] ?? "") ?? c.groupBy[0]) || schema.fields.find((f2) => f2.name === field)?.type !== "datetime")
|
|
4032
|
+
throw new FormulaError(
|
|
4033
|
+
"Date grouping must use the first grouping field, with a datetime type."
|
|
4034
|
+
);
|
|
4035
|
+
if (distributionActive(c))
|
|
4036
|
+
throw new FormulaError(
|
|
4037
|
+
"Date grouping currently supports scalar metrics, not distributions."
|
|
4038
|
+
);
|
|
4039
|
+
}
|
|
2562
4040
|
if (c.groupLimit !== void 0 && (!Number.isInteger(c.groupLimit) || c.groupLimit < 1 || c.groupLimit > 1e4))
|
|
2563
4041
|
throw new FormulaError("Group limit must be 1\u201310,000.");
|
|
2564
4042
|
for (const s of c.sort ?? []) {
|
|
@@ -2603,6 +4081,7 @@ function metricComputationKey(clauses) {
|
|
|
2603
4081
|
expressionY: c.display?.kind === "scatter" ? c.expressionY : void 0,
|
|
2604
4082
|
scope: c.scope ?? "allMatching",
|
|
2605
4083
|
groupBy: c.groupBy,
|
|
4084
|
+
dateGrouping: c.dateGrouping,
|
|
2606
4085
|
sort: c.sort,
|
|
2607
4086
|
groupLimit: c.groupLimit,
|
|
2608
4087
|
distribution: distributionActive(c) ? c.distribution : void 0
|
|
@@ -2648,6 +4127,7 @@ function toMetricQuery(q, schema, execution) {
|
|
|
2648
4127
|
distribution,
|
|
2649
4128
|
expression,
|
|
2650
4129
|
expressionY,
|
|
4130
|
+
dateGrouping,
|
|
2651
4131
|
...rest
|
|
2652
4132
|
} = c;
|
|
2653
4133
|
return {
|
|
@@ -2667,6 +4147,15 @@ function toMetricQuery(q, schema, execution) {
|
|
|
2667
4147
|
code: "invalid_metric"
|
|
2668
4148
|
}))
|
|
2669
4149
|
);
|
|
4150
|
+
for (const c of q.aggregations ?? [])
|
|
4151
|
+
if (c.dateGrouping)
|
|
4152
|
+
diagnostics.push({
|
|
4153
|
+
metricId: c.id,
|
|
4154
|
+
code: "unsupported_date_grouping",
|
|
4155
|
+
from: 0,
|
|
4156
|
+
to: 1,
|
|
4157
|
+
message: "Date grouping currently requires local metric execution."
|
|
4158
|
+
});
|
|
2670
4159
|
try {
|
|
2671
4160
|
assertMetricQueryState(raw, originalSchema);
|
|
2672
4161
|
} catch (e) {
|
|
@@ -2774,24 +4263,23 @@ function assertMetricQueryState(q, schema) {
|
|
|
2774
4263
|
if (queryStateKey(q.where) !== queryStateKey(normalized.where) || queryStateKey(orderBy) !== queryStateKey(normalizedOrder) || q.limit !== normalized.limit || q.offset !== normalized.offset)
|
|
2775
4264
|
throw Error("Malformed metric query state cannot be executed exactly.");
|
|
2776
4265
|
}
|
|
2777
|
-
function selectMetricShownRows(rows, q, schema,
|
|
2778
|
-
if (
|
|
4266
|
+
function selectMetricShownRows(rows, q, schema, options2 = {}) {
|
|
4267
|
+
if (options2.signal?.aborted) throw Error("Metric evaluation cancelled.");
|
|
2779
4268
|
assertMetricQueryState(q, schema);
|
|
2780
|
-
if (
|
|
4269
|
+
if (options2.shownRows) return options2.shownRows;
|
|
2781
4270
|
const orderBy = q.orderBy.length ? q.orderBy : schema.defaultSort ?? [];
|
|
2782
4271
|
assertMetricPopulation(q, schema, orderBy);
|
|
2783
|
-
if (rows.length > (
|
|
4272
|
+
if (rows.length > (options2.maxRows ?? 1e6))
|
|
2784
4273
|
throw Error("Metric row budget exceeded.");
|
|
2785
|
-
if ((
|
|
4274
|
+
if ((options2.maxOperations ?? 1e7) <= 0)
|
|
2786
4275
|
throw Error("Metric operation budget exceeded.");
|
|
2787
4276
|
return applyQuery(
|
|
2788
4277
|
rows,
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
},
|
|
4278
|
+
// Local tables use a stable sort: ties retain input order. An extra ID
|
|
4279
|
+
// sort here would choose a different page than the one the table shows.
|
|
4280
|
+
{ ...q, orderBy },
|
|
2793
4281
|
schema,
|
|
2794
|
-
|
|
4282
|
+
options2
|
|
2795
4283
|
).rows;
|
|
2796
4284
|
}
|
|
2797
4285
|
function assertMetricPopulation(q, schema, orderBy) {
|
|
@@ -2806,12 +4294,12 @@ function assertMetricPopulation(q, schema, orderBy) {
|
|
|
2806
4294
|
);
|
|
2807
4295
|
if (q.where.some(
|
|
2808
4296
|
(t) => predicatesOf(t).some(
|
|
2809
|
-
(p) => isComputedField(p.field) || !fields.has(resolveFieldName(schema, p.field) ?? p.field)
|
|
4297
|
+
(p) => isComputedField(p.field) && !computedInputAllowed(schema, p.field, "filter") || !fields.has(resolveFieldName(schema, p.field) ?? p.field)
|
|
2810
4298
|
)
|
|
2811
4299
|
))
|
|
2812
4300
|
throw Error("Metric filter references an unavailable field.");
|
|
2813
4301
|
if (orderBy.some(
|
|
2814
|
-
(s) => isComputedField(s.field) || !fields.has(resolveFieldName(schema, s.field) ?? s.field)
|
|
4302
|
+
(s) => isComputedField(s.field) && !computedInputAllowed(schema, s.field, "sort") || !fields.has(resolveFieldName(schema, s.field) ?? s.field)
|
|
2815
4303
|
))
|
|
2816
4304
|
throw Error("Shown-row ordering requires an authoritative committed page.");
|
|
2817
4305
|
}
|
|
@@ -2854,39 +4342,53 @@ function compareMetricValues(a, b) {
|
|
|
2854
4342
|
}
|
|
2855
4343
|
return a < b ? -1 : 1;
|
|
2856
4344
|
}
|
|
2857
|
-
function evaluateMetrics(rows, q, schema,
|
|
2858
|
-
|
|
4345
|
+
function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
4346
|
+
if (q.grouping) {
|
|
4347
|
+
try {
|
|
4348
|
+
if (options2.signal?.aborted) throw new Error("Metric evaluation cancelled.");
|
|
4349
|
+
if (rows.length > (options2.maxRows ?? 1e6)) throw new Error("Metric row budget exceeded.");
|
|
4350
|
+
const now2 = evaluationTime(options2);
|
|
4351
|
+
const prepared = prepareGrouping(rows, schema, q.grouping, { now: now2 });
|
|
4352
|
+
return evaluateMetrics(prepared.rows, withoutGrouping(q), prepared.schema, {
|
|
4353
|
+
...options2,
|
|
4354
|
+
now: now2
|
|
4355
|
+
});
|
|
4356
|
+
} catch (error) {
|
|
4357
|
+
return { metrics: (q.aggregations ?? []).map((metric) => ({ id: metric.id, buckets: [], error: error instanceof Error ? error.message : String(error) })) };
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
const now = evaluationTime(options2), fields = indexFields(schema);
|
|
2859
4361
|
let matching;
|
|
2860
4362
|
let shown;
|
|
2861
4363
|
return {
|
|
2862
4364
|
metrics: (q.aggregations ?? []).map((c) => {
|
|
2863
|
-
let budget =
|
|
4365
|
+
let budget = options2.maxOperations ?? 1e7;
|
|
2864
4366
|
const tick = () => {
|
|
2865
4367
|
if (--budget < 0) throw Error("Metric operation budget exceeded.");
|
|
2866
|
-
if (
|
|
4368
|
+
if (options2.signal?.aborted)
|
|
2867
4369
|
throw Error("Metric evaluation cancelled.");
|
|
2868
4370
|
};
|
|
2869
4371
|
try {
|
|
2870
|
-
if (
|
|
4372
|
+
if (options2.signal?.aborted)
|
|
2871
4373
|
throw Error("Metric evaluation cancelled.");
|
|
2872
4374
|
assertMetricQueryState(q, schema);
|
|
2873
4375
|
if ((q.aggregations?.length ?? 0) > 20)
|
|
2874
4376
|
throw Error("At most 20 metrics are allowed.");
|
|
2875
|
-
const diagnostics = validateMetric(c, schema,
|
|
4377
|
+
const diagnostics = validateMetric(c, schema, options2.resolveComputed);
|
|
2876
4378
|
if (diagnostics.length)
|
|
2877
4379
|
throw Error(diagnostics.map((d) => d.message).join(" "));
|
|
2878
|
-
const ps = metricPlans(c, schema,
|
|
2879
|
-
if (ps.some(hasRegex) && !
|
|
4380
|
+
const ps = metricPlans(c, schema, options2.resolveComputed);
|
|
4381
|
+
if (ps.some(hasRegex) && !options2.evaluateInput)
|
|
2880
4382
|
throw Error(
|
|
2881
4383
|
"Regex metrics require an isolated worker with a watchdog."
|
|
2882
4384
|
);
|
|
2883
4385
|
let inputRows;
|
|
2884
4386
|
if (c.scope === "shownRows")
|
|
2885
|
-
inputRows = shown ?? (shown = selectMetricShownRows(rows, q, schema,
|
|
4387
|
+
inputRows = shown ?? (shown = selectMetricShownRows(rows, q, schema, options2));
|
|
2886
4388
|
else {
|
|
2887
4389
|
if (!matching) {
|
|
2888
4390
|
assertMetricPopulation(q, schema, []);
|
|
2889
|
-
if (rows.length > (
|
|
4391
|
+
if (rows.length > (options2.maxRows ?? 1e6))
|
|
2890
4392
|
throw Error("Metric row budget exceeded.");
|
|
2891
4393
|
matching = applyQuery(
|
|
2892
4394
|
rows,
|
|
@@ -2897,10 +4399,10 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
2897
4399
|
}
|
|
2898
4400
|
inputRows = matching;
|
|
2899
4401
|
}
|
|
2900
|
-
if (inputRows.length > (
|
|
4402
|
+
if (inputRows.length > (options2.maxRows ?? 1e6))
|
|
2901
4403
|
throw Error("Metric row budget exceeded.");
|
|
2902
4404
|
const groups = /* @__PURE__ */ new Map();
|
|
2903
|
-
const keyLimit =
|
|
4405
|
+
const keyLimit = options2.maxGroupKeyLength ?? 65536, byteLimit = options2.maxGroupBytes ?? 16e6;
|
|
2904
4406
|
if (![keyLimit, byteLimit].every((v) => Number.isSafeInteger(v) && v >= 0))
|
|
2905
4407
|
throw Error("Invalid grouping budget.");
|
|
2906
4408
|
let groupBytes = 0;
|
|
@@ -2915,6 +4417,8 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
2915
4417
|
groupBytes = 256;
|
|
2916
4418
|
groups.set("[]", { keys: [], rows: [] });
|
|
2917
4419
|
}
|
|
4420
|
+
let dateStart = Infinity, dateEnd = -Infinity, dateKey = "";
|
|
4421
|
+
let dateInput, dateTime = NaN;
|
|
2918
4422
|
for (const row of inputRows) {
|
|
2919
4423
|
tick();
|
|
2920
4424
|
if (!c.groupBy.length) {
|
|
@@ -2931,6 +4435,27 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
2931
4435
|
)
|
|
2932
4436
|
)
|
|
2933
4437
|
);
|
|
4438
|
+
if (c.dateGrouping && keys[0] !== null) {
|
|
4439
|
+
const config = c.dateGrouping;
|
|
4440
|
+
if (keys[0] !== dateInput) {
|
|
4441
|
+
dateTime = metricDateTime(keys[0], config.timeZone);
|
|
4442
|
+
dateInput = keys[0];
|
|
4443
|
+
}
|
|
4444
|
+
if (dateTime < dateStart || dateTime >= dateEnd) {
|
|
4445
|
+
dateStart = metricDateFloor(
|
|
4446
|
+
dateTime,
|
|
4447
|
+
config.interval,
|
|
4448
|
+
config.timeZone
|
|
4449
|
+
);
|
|
4450
|
+
dateEnd = metricDateNext(
|
|
4451
|
+
dateStart,
|
|
4452
|
+
config.interval,
|
|
4453
|
+
config.timeZone
|
|
4454
|
+
);
|
|
4455
|
+
dateKey = new Date(dateStart).toISOString();
|
|
4456
|
+
}
|
|
4457
|
+
keys[0] = dateKey;
|
|
4458
|
+
}
|
|
2934
4459
|
let keyLength = 1, payloadBytes = 0;
|
|
2935
4460
|
for (const value of keys) {
|
|
2936
4461
|
keyLength += 1 + (typeof value === "string" ? groupStringLength(value, keyLimit - keyLength - 1) : value === null ? 4 : typeof value === "boolean" ? value ? 4 : 5 : String(value).length);
|
|
@@ -2943,7 +4468,7 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
2943
4468
|
const key = JSON.stringify(keys);
|
|
2944
4469
|
let g = groups.get(key);
|
|
2945
4470
|
if (!g) {
|
|
2946
|
-
if (groups.size >= (
|
|
4471
|
+
if (groups.size >= (options2.maxGroups ?? 1e4))
|
|
2947
4472
|
throw Error("Metric group budget exceeded.");
|
|
2948
4473
|
g = { keys, rows: [] };
|
|
2949
4474
|
groups.set(key, g);
|
|
@@ -2952,6 +4477,68 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
2952
4477
|
groupBytes += 16;
|
|
2953
4478
|
g.rows.push(row);
|
|
2954
4479
|
}
|
|
4480
|
+
if (c.dateGrouping?.includeEmpty) {
|
|
4481
|
+
const config = c.dateGrouping;
|
|
4482
|
+
const observed = [];
|
|
4483
|
+
const series = /* @__PURE__ */ new Map();
|
|
4484
|
+
for (const group of groups.values())
|
|
4485
|
+
if (group.keys[0] !== null) {
|
|
4486
|
+
tick();
|
|
4487
|
+
reserve(8);
|
|
4488
|
+
groupBytes += 8;
|
|
4489
|
+
observed.push(metricDateTime(group.keys[0]));
|
|
4490
|
+
const tuple = group.keys.slice(1);
|
|
4491
|
+
const key = JSON.stringify(tuple);
|
|
4492
|
+
if (!series.has(key)) {
|
|
4493
|
+
const bytes = 128 + 16 * tuple.length + 2 * key.length;
|
|
4494
|
+
reserve(bytes);
|
|
4495
|
+
groupBytes += bytes;
|
|
4496
|
+
series.set(key, tuple);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
if (c.groupBy.length === 1 && !series.size) {
|
|
4500
|
+
reserve(132);
|
|
4501
|
+
groupBytes += 132;
|
|
4502
|
+
series.set("[]", []);
|
|
4503
|
+
}
|
|
4504
|
+
const range = metricDateRange(
|
|
4505
|
+
config,
|
|
4506
|
+
q,
|
|
4507
|
+
observed,
|
|
4508
|
+
now,
|
|
4509
|
+
(name) => resolveFieldName(schema, name) ?? name
|
|
4510
|
+
);
|
|
4511
|
+
if (range)
|
|
4512
|
+
for (let time = range[0]; time <= range[1]; ) {
|
|
4513
|
+
tick();
|
|
4514
|
+
for (const tuple of series.values()) {
|
|
4515
|
+
tick();
|
|
4516
|
+
const keys = [
|
|
4517
|
+
new Date(time).toISOString(),
|
|
4518
|
+
...tuple
|
|
4519
|
+
];
|
|
4520
|
+
const key = JSON.stringify(keys);
|
|
4521
|
+
if (key.length > keyLimit)
|
|
4522
|
+
throw Error("Metric grouping key length budget exceeded.");
|
|
4523
|
+
if (!groups.has(key)) {
|
|
4524
|
+
if (groups.size >= (options2.maxGroups ?? 1e4))
|
|
4525
|
+
throw Error("Metric group budget exceeded.");
|
|
4526
|
+
const bytes = 256 + 16 * keys.length + 4 * key.length;
|
|
4527
|
+
reserve(bytes);
|
|
4528
|
+
groupBytes += bytes;
|
|
4529
|
+
groups.set(key, { keys, rows: [] });
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
const next = metricDateNext(
|
|
4533
|
+
time,
|
|
4534
|
+
config.interval,
|
|
4535
|
+
config.timeZone
|
|
4536
|
+
);
|
|
4537
|
+
if (!Number.isFinite(next) || next <= time)
|
|
4538
|
+
throw Error("Date grouping exceeds calendar limits.");
|
|
4539
|
+
time = next;
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
2955
4542
|
const inputs = (row, p) => Object.fromEntries(
|
|
2956
4543
|
p.dependencies.map((name) => [
|
|
2957
4544
|
name,
|
|
@@ -2972,7 +4559,13 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
2972
4559
|
const rowEval = (node, row, p) => {
|
|
2973
4560
|
budget -= cost(node);
|
|
2974
4561
|
tick();
|
|
2975
|
-
|
|
4562
|
+
if (node.kind === "field") {
|
|
4563
|
+
const value = readFieldValue(fields.get(node.name), row);
|
|
4564
|
+
return formulaRuntime(node, {
|
|
4565
|
+
[node.name]: value instanceof Date ? value.toISOString() : value ?? null
|
|
4566
|
+
});
|
|
4567
|
+
}
|
|
4568
|
+
return options2.evaluateInput && hasRegex({ ...p, ast: node }) ? options2.evaluateInput(node, row, p) : formulaRuntime(node, inputs(row, p));
|
|
2976
4569
|
};
|
|
2977
4570
|
const reduce = (p, rs, inputErrors) => {
|
|
2978
4571
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2990,7 +4583,7 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
2990
4583
|
yield v;
|
|
2991
4584
|
}
|
|
2992
4585
|
}
|
|
2993
|
-
result = reduceResults(node, results(),
|
|
4586
|
+
result = reduceResults(node, results(), options2);
|
|
2994
4587
|
} catch (e) {
|
|
2995
4588
|
result = {
|
|
2996
4589
|
value: null,
|
|
@@ -3023,6 +4616,12 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
3023
4616
|
}
|
|
3024
4617
|
populations.set(bucket, values);
|
|
3025
4618
|
} else {
|
|
4619
|
+
if (c.dateGrouping && !g.rows.length) {
|
|
4620
|
+
const empty2 = (p) => p.ast.kind === "call" && ["COUNT", "COUNT_DISTINCT", "SUM"].includes(p.ast.name) ? 0 : null;
|
|
4621
|
+
bucket.value = empty2(ps[0]);
|
|
4622
|
+
if (ps[1]) bucket.y = empty2(ps[1]);
|
|
4623
|
+
return bucket;
|
|
4624
|
+
}
|
|
3026
4625
|
const inputErrors = /* @__PURE__ */ new Set();
|
|
3027
4626
|
const x = reduce(ps[0], g.rows, inputErrors);
|
|
3028
4627
|
bucket.value = scalar(x.value);
|
|
@@ -3064,7 +4663,7 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
3064
4663
|
}
|
|
3065
4664
|
}
|
|
3066
4665
|
}
|
|
3067
|
-
const ordering = c.sort?.length ? c.sort : [{ key: "value", dir: "desc", nulls: "last" }];
|
|
4666
|
+
const ordering = c.sort?.length ? c.sort : c.dateGrouping ? [{ key: "group0", dir: "asc", nulls: "last" }] : [{ key: "value", dir: "desc", nulls: "last" }];
|
|
3068
4667
|
const target = (b, s) => s.key === "samples" ? b.count - (b.nullCount ?? 0) - (b.inputErrorCount ?? 0) : s.key.startsWith("group") ? b.keys[Number(s.key.slice(5))] ?? null : s.key === "count" ? b.count : s.key === "y" ? b.y ?? null : b.value;
|
|
3069
4668
|
buckets.sort((a, b) => {
|
|
3070
4669
|
if (!!(a.error || a.yError) !== !!(b.error || b.yError))
|
|
@@ -3119,13 +4718,17 @@ function evaluateMetrics(rows, q, schema, options = {}) {
|
|
|
3119
4718
|
}
|
|
3120
4719
|
|
|
3121
4720
|
// src/apply.ts
|
|
3122
|
-
function applyQuery(rows, q, schema,
|
|
3123
|
-
const now = evaluationTime(
|
|
4721
|
+
function applyQuery(rows, q, schema, options2 = {}) {
|
|
4722
|
+
const now = evaluationTime(options2);
|
|
4723
|
+
if (q.grouping) {
|
|
4724
|
+
const grouped = prepareGrouping(rows, schema, q.grouping, { now });
|
|
4725
|
+
return applyQuery(grouped.rows, withoutGrouping(q), grouped.schema, { now });
|
|
4726
|
+
}
|
|
3124
4727
|
const byName = indexFields(schema);
|
|
3125
4728
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
3126
|
-
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2, now));
|
|
4729
|
+
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field) && !computedInputAllowed(schema, c.field, "filter"))).map((term) => prepareWhereTerm(term, resolveField2, now));
|
|
3127
4730
|
prepareDatetimePredicates(whereTerms, byName);
|
|
3128
|
-
const orderBy = q.orderBy.filter((term) => !isComputedField(term.field)).map((term) => ({ ...term, field: resolveField2(term.field) }));
|
|
4731
|
+
const orderBy = q.orderBy.filter((term) => !isComputedField(term.field) || computedInputAllowed(schema, term.field, "sort")).map((term) => ({ ...term, field: resolveField2(term.field) }));
|
|
3129
4732
|
let out = rows.filter(
|
|
3130
4733
|
(row) => whereTerms.every((term) => matchesTerm(byName, row, term))
|
|
3131
4734
|
);
|
|
@@ -3159,42 +4762,47 @@ function applyQuery(rows, q, schema, options = {}) {
|
|
|
3159
4762
|
const end = q.limit > 0 ? start + q.limit : out.length;
|
|
3160
4763
|
return { rows: out.slice(start, end), total };
|
|
3161
4764
|
}
|
|
3162
|
-
function
|
|
4765
|
+
function computedInputAllowed(schema, name, operation) {
|
|
4766
|
+
const field = schema.fields.find((field2) => field2.name === name);
|
|
4767
|
+
return !!field && (field.source.kind === "backend" && field[operation]?.enabled !== false || field[operation]?.enabled === true);
|
|
4768
|
+
}
|
|
4769
|
+
function matchesClause(row, clause, schema, options2 = {}) {
|
|
3163
4770
|
const field = resolveFieldName(schema, clause.field) ?? clause.field;
|
|
3164
4771
|
const byName = indexFields(schema);
|
|
3165
4772
|
const prepared = prepareWhereClause(
|
|
3166
4773
|
{ ...clause, field },
|
|
3167
|
-
evaluationTime(
|
|
4774
|
+
evaluationTime(options2)
|
|
3168
4775
|
);
|
|
3169
4776
|
prepareDatetimePredicates([{ kind: "lit", predicate: prepared }], byName);
|
|
3170
4777
|
return matchesWith(byName, row, prepared);
|
|
3171
4778
|
}
|
|
3172
|
-
function applyAggregations(rows, q, schema,
|
|
3173
|
-
if (
|
|
4779
|
+
function applyAggregations(rows, q, schema, options2 = {}) {
|
|
4780
|
+
if (q.grouping) return evaluateMetrics(rows, q, schema, options2);
|
|
4781
|
+
if (options2.signal?.aborted || q.where.some(
|
|
3174
4782
|
(t) => predicatesOf(t).some(
|
|
3175
4783
|
(p) => p.op === "matches_regex" || p.op === "not_matches_regex"
|
|
3176
4784
|
)
|
|
3177
4785
|
))
|
|
3178
|
-
return evaluateMetrics(rows, q, schema,
|
|
4786
|
+
return evaluateMetrics(rows, q, schema, options2);
|
|
3179
4787
|
const advanced = (q.aggregations ?? []).filter(
|
|
3180
|
-
(a) => a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0
|
|
4788
|
+
(a) => a.dateGrouping || a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0
|
|
3181
4789
|
);
|
|
3182
4790
|
if (advanced.length) {
|
|
3183
4791
|
if ((q.aggregations?.length ?? 0) > 20 || normalizeQueryState(q).aggregations?.length !== q.aggregations?.length)
|
|
3184
|
-
return evaluateMetrics(rows, q, schema,
|
|
4792
|
+
return evaluateMetrics(rows, q, schema, options2);
|
|
3185
4793
|
const legacy = (q.aggregations ?? []).filter((a) => !advanced.includes(a));
|
|
3186
4794
|
const results = [
|
|
3187
4795
|
...evaluateMetrics(
|
|
3188
4796
|
rows,
|
|
3189
4797
|
{ ...q, aggregations: advanced },
|
|
3190
4798
|
schema,
|
|
3191
|
-
|
|
4799
|
+
options2
|
|
3192
4800
|
).metrics,
|
|
3193
4801
|
...legacy.length ? applyAggregations(
|
|
3194
4802
|
rows,
|
|
3195
4803
|
{ ...q, aggregations: legacy },
|
|
3196
4804
|
schema,
|
|
3197
|
-
|
|
4805
|
+
options2
|
|
3198
4806
|
).metrics : []
|
|
3199
4807
|
];
|
|
3200
4808
|
const byId = new Map(results.map((result) => [result.id, result]));
|
|
@@ -3202,7 +4810,7 @@ function applyAggregations(rows, q, schema, options = {}) {
|
|
|
3202
4810
|
metrics: (q.aggregations ?? []).flatMap((a) => byId.get(a.id) ?? [])
|
|
3203
4811
|
};
|
|
3204
4812
|
}
|
|
3205
|
-
const now = evaluationTime(
|
|
4813
|
+
const now = evaluationTime(options2);
|
|
3206
4814
|
const byName = indexFields(schema);
|
|
3207
4815
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
3208
4816
|
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2, now));
|
|
@@ -3491,7 +5099,13 @@ function normalizeSavedQuery(value) {
|
|
|
3491
5099
|
if (typeof raw.id !== "string" || !raw.id || raw.id.length > 256 || typeof raw.name !== "string" || !raw.name || raw.name.length > MAX_SAVED_NAME_LENGTH || typeof raw.savedAt !== "number" || !Number.isFinite(raw.savedAt) || raw.query == null || typeof raw.query !== "object") {
|
|
3492
5100
|
return null;
|
|
3493
5101
|
}
|
|
3494
|
-
return {
|
|
5102
|
+
return {
|
|
5103
|
+
id: raw.id,
|
|
5104
|
+
name: raw.name,
|
|
5105
|
+
savedAt: raw.savedAt,
|
|
5106
|
+
query: normalizeQueryState(raw.query),
|
|
5107
|
+
...typeof raw.snapshotCode === "string" ? { snapshotCode: raw.snapshotCode } : {}
|
|
5108
|
+
};
|
|
3495
5109
|
}
|
|
3496
5110
|
function normalizeSavedQueries(value) {
|
|
3497
5111
|
if (!Array.isArray(value)) return [];
|
|
@@ -3524,7 +5138,7 @@ function memoryStorageAdapter() {
|
|
|
3524
5138
|
if (!(saved.get(key) ?? []).some((item) => item.id === id)) throw new Error(`Saved query "${id}" does not exist.`);
|
|
3525
5139
|
defaults.set(key, id);
|
|
3526
5140
|
},
|
|
3527
|
-
async saveNamed(key, name, query, savedAt) {
|
|
5141
|
+
async saveNamed(key, name, query, savedAt, snapshotCode) {
|
|
3528
5142
|
const normalizedName = name.trim();
|
|
3529
5143
|
if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
|
|
3530
5144
|
throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
|
|
@@ -3533,6 +5147,7 @@ function memoryStorageAdapter() {
|
|
|
3533
5147
|
if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
|
|
3534
5148
|
if (items.some((item2) => item2.name === normalizedName)) throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
3535
5149
|
const item = {
|
|
5150
|
+
...snapshotCode ? { snapshotCode } : {},
|
|
3536
5151
|
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
3537
5152
|
name: normalizedName,
|
|
3538
5153
|
savedAt,
|
|
@@ -3541,6 +5156,20 @@ function memoryStorageAdapter() {
|
|
|
3541
5156
|
saved.set(key, [...items, item]);
|
|
3542
5157
|
return item;
|
|
3543
5158
|
},
|
|
5159
|
+
async updateNamed(key, id, query, savedAt, snapshotCode) {
|
|
5160
|
+
const items = saved.get(key) ?? [];
|
|
5161
|
+
const original = items.find((item2) => item2.id === id);
|
|
5162
|
+
if (!original) throw new Error(`Saved query "${id}" does not exist.`);
|
|
5163
|
+
const item = {
|
|
5164
|
+
id,
|
|
5165
|
+
name: original.name,
|
|
5166
|
+
savedAt,
|
|
5167
|
+
query: normalizeQueryState(query),
|
|
5168
|
+
...snapshotCode ? { snapshotCode } : {}
|
|
5169
|
+
};
|
|
5170
|
+
saved.set(key, items.map((entry) => entry.id === id ? item : entry));
|
|
5171
|
+
return item;
|
|
5172
|
+
},
|
|
3544
5173
|
async deleteSaved(key, id) {
|
|
3545
5174
|
saved.set(key, (saved.get(key) ?? []).filter((item) => item.id !== id));
|
|
3546
5175
|
if (defaults.get(key) === id) defaults.delete(key);
|
|
@@ -3605,7 +5234,7 @@ function localStorageAdapter() {
|
|
|
3605
5234
|
}
|
|
3606
5235
|
writeDefaultId(key, id);
|
|
3607
5236
|
},
|
|
3608
|
-
async saveNamed(key, name, query, savedAt) {
|
|
5237
|
+
async saveNamed(key, name, query, savedAt, snapshotCode) {
|
|
3609
5238
|
const normalizedName = name.trim();
|
|
3610
5239
|
if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
|
|
3611
5240
|
throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
|
|
@@ -3616,6 +5245,7 @@ function localStorageAdapter() {
|
|
|
3616
5245
|
throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
3617
5246
|
}
|
|
3618
5247
|
const item = {
|
|
5248
|
+
...snapshotCode ? { snapshotCode } : {},
|
|
3619
5249
|
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
3620
5250
|
name: normalizedName,
|
|
3621
5251
|
savedAt,
|
|
@@ -3625,6 +5255,20 @@ function localStorageAdapter() {
|
|
|
3625
5255
|
writeSaved(key, items);
|
|
3626
5256
|
return item;
|
|
3627
5257
|
},
|
|
5258
|
+
async updateNamed(key, id, query, savedAt, snapshotCode) {
|
|
5259
|
+
const items = readSaved(key);
|
|
5260
|
+
const original = items.find((item2) => item2.id === id);
|
|
5261
|
+
if (!original) throw new Error(`Saved query "${id}" does not exist.`);
|
|
5262
|
+
const item = {
|
|
5263
|
+
id,
|
|
5264
|
+
name: original.name,
|
|
5265
|
+
savedAt,
|
|
5266
|
+
query: normalizeQueryState(query),
|
|
5267
|
+
...snapshotCode ? { snapshotCode } : {}
|
|
5268
|
+
};
|
|
5269
|
+
writeSaved(key, items.map((entry) => entry.id === id ? item : entry));
|
|
5270
|
+
return item;
|
|
5271
|
+
},
|
|
3628
5272
|
async deleteSaved(key, id) {
|
|
3629
5273
|
writeSaved(
|
|
3630
5274
|
key,
|
|
@@ -3634,6 +5278,248 @@ function localStorageAdapter() {
|
|
|
3634
5278
|
}
|
|
3635
5279
|
};
|
|
3636
5280
|
}
|
|
5281
|
+
|
|
5282
|
+
// src/propertyStorage.ts
|
|
5283
|
+
var empty = () => ({ last: null, saved: [], defaultId: null });
|
|
5284
|
+
var clone = (saved) => ({
|
|
5285
|
+
...saved,
|
|
5286
|
+
view: normalizePropertyView(saved.view)
|
|
5287
|
+
});
|
|
5288
|
+
function jsonPropertyViewStorage(read, write) {
|
|
5289
|
+
const keyFor = (type) => `query-table:properties:${type}`;
|
|
5290
|
+
function load(type) {
|
|
5291
|
+
const value = read(keyFor(type));
|
|
5292
|
+
if (value === null) return empty();
|
|
5293
|
+
const raw = JSON.parse(value);
|
|
5294
|
+
if (!raw || !Array.isArray(raw.saved) || raw.saved.length > 100 || !(raw.defaultId === null || typeof raw.defaultId === "string"))
|
|
5295
|
+
throw new Error("Invalid saved property views.");
|
|
5296
|
+
const saved = raw.saved.map((item) => {
|
|
5297
|
+
if (!item || typeof item.id !== "string" || !item.id || typeof item.name !== "string" || !item.name.trim() || item.name.length > 200 || !Number.isFinite(item.savedAt))
|
|
5298
|
+
throw new Error("Invalid saved property view.");
|
|
5299
|
+
return clone(item);
|
|
5300
|
+
});
|
|
5301
|
+
return {
|
|
5302
|
+
last: raw.last === null ? null : normalizePropertyView(raw.last),
|
|
5303
|
+
saved,
|
|
5304
|
+
defaultId: raw.defaultId
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5307
|
+
const store = (type, value) => write(keyFor(type), JSON.stringify(value));
|
|
5308
|
+
return {
|
|
5309
|
+
async loadLast(type) {
|
|
5310
|
+
return load(type).last;
|
|
5311
|
+
},
|
|
5312
|
+
async saveLast(type, view) {
|
|
5313
|
+
const data = load(type);
|
|
5314
|
+
data.last = normalizePropertyView(view);
|
|
5315
|
+
store(type, data);
|
|
5316
|
+
},
|
|
5317
|
+
async listSaved(type) {
|
|
5318
|
+
return load(type).saved;
|
|
5319
|
+
},
|
|
5320
|
+
async saveNamed(type, name, view, savedAt) {
|
|
5321
|
+
name = name.trim();
|
|
5322
|
+
if (!name || name.length > 200 || !Number.isFinite(savedAt))
|
|
5323
|
+
throw new Error(
|
|
5324
|
+
"A view name (1\u2013200 characters) and valid save time are required."
|
|
5325
|
+
);
|
|
5326
|
+
const data = load(type);
|
|
5327
|
+
if (data.saved.some((v) => v.name === name))
|
|
5328
|
+
throw new Error(`A property view named \u201C${name}\u201D already exists.`);
|
|
5329
|
+
if (data.saved.length >= 100)
|
|
5330
|
+
throw new Error("At most 100 saved property views are allowed.");
|
|
5331
|
+
let suffix = 0;
|
|
5332
|
+
while (data.saved.some((v) => v.id === `${savedAt}-${suffix}`)) suffix++;
|
|
5333
|
+
const saved = {
|
|
5334
|
+
id: `${savedAt}-${suffix}`,
|
|
5335
|
+
name,
|
|
5336
|
+
savedAt,
|
|
5337
|
+
view: normalizePropertyView(view)
|
|
5338
|
+
};
|
|
5339
|
+
data.saved.push(saved);
|
|
5340
|
+
store(type, data);
|
|
5341
|
+
return clone(saved);
|
|
5342
|
+
},
|
|
5343
|
+
async deleteSaved(type, id) {
|
|
5344
|
+
const data = load(type);
|
|
5345
|
+
data.saved = data.saved.filter((v) => v.id !== id);
|
|
5346
|
+
if (data.defaultId === id) data.defaultId = null;
|
|
5347
|
+
store(type, data);
|
|
5348
|
+
},
|
|
5349
|
+
async loadDefaultSaved(type) {
|
|
5350
|
+
const data = load(type);
|
|
5351
|
+
return data.saved.find((v) => v.id === data.defaultId) ?? null;
|
|
5352
|
+
},
|
|
5353
|
+
async setDefaultSaved(type, id) {
|
|
5354
|
+
const data = load(type);
|
|
5355
|
+
if (id !== null && !data.saved.some((v) => v.id === id))
|
|
5356
|
+
throw new Error("Unknown property view.");
|
|
5357
|
+
data.defaultId = id;
|
|
5358
|
+
store(type, data);
|
|
5359
|
+
}
|
|
5360
|
+
};
|
|
5361
|
+
}
|
|
5362
|
+
function memoryPropertyViewStorage() {
|
|
5363
|
+
const values = /* @__PURE__ */ new Map();
|
|
5364
|
+
return jsonPropertyViewStorage(
|
|
5365
|
+
(key) => values.get(key) ?? null,
|
|
5366
|
+
(key, value) => {
|
|
5367
|
+
values.set(key, value);
|
|
5368
|
+
}
|
|
5369
|
+
);
|
|
5370
|
+
}
|
|
5371
|
+
function localPropertyViewStorage() {
|
|
5372
|
+
return jsonPropertyViewStorage(
|
|
5373
|
+
(key) => localStorage.getItem(key),
|
|
5374
|
+
(key, value) => localStorage.setItem(key, value)
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
// src/source.ts
|
|
5379
|
+
function sourceDependencies(query, schema, resolveComputed) {
|
|
5380
|
+
const fields = /* @__PURE__ */ new Set([schema.idField]);
|
|
5381
|
+
const addSort = (name) => {
|
|
5382
|
+
fields.add(name);
|
|
5383
|
+
const sortField = schema.fields.find((field) => field.name === name)?.sort?.field;
|
|
5384
|
+
if (sortField) fields.add(sortField);
|
|
5385
|
+
};
|
|
5386
|
+
const source = query.grouping?.source ?? query;
|
|
5387
|
+
for (const term of source.where) for (const predicate of predicatesOf(term)) fields.add(predicate.field);
|
|
5388
|
+
if (query.grouping) {
|
|
5389
|
+
const model = createGroupingModel(schema);
|
|
5390
|
+
model.validate(query.grouping);
|
|
5391
|
+
for (const key of query.grouping.keys) fields.add(key);
|
|
5392
|
+
for (const summary of query.grouping.summaries) {
|
|
5393
|
+
if (summary.op === "row") fields.add(summary.field);
|
|
5394
|
+
else for (const dependency of compileMetric(model.summaryExpression(summary), schema, resolveComputed).dependencies) fields.add(dependency);
|
|
5395
|
+
}
|
|
5396
|
+
for (const term of query.grouping.representative?.orderBy ?? []) addSort(term.field);
|
|
5397
|
+
} else {
|
|
5398
|
+
for (const column of source.select) if (column.field !== ITEM_DETAILS_COLUMN) fields.add(column.field);
|
|
5399
|
+
for (const term of source.orderBy) addSort(term.field);
|
|
5400
|
+
for (const metric of source.aggregations ?? []) for (const dependency of metricDependencies(metric, schema, resolveComputed)) fields.add(dependency);
|
|
5401
|
+
}
|
|
5402
|
+
const base = /* @__PURE__ */ new Set();
|
|
5403
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
5404
|
+
const visit = (name) => {
|
|
5405
|
+
if (visiting.has(name)) throw Error("Source dependency cycle.");
|
|
5406
|
+
const field = schema.fields.find((field2) => field2.name === name);
|
|
5407
|
+
if (!field && name === schema.idField) {
|
|
5408
|
+
base.add(name);
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
const dependencies = field?.source.kind === "derived" ? field.source.dependencies : resolveComputed?.(name)?.dependencies;
|
|
5412
|
+
if (!field && !dependencies) throw Error(`Unavailable source dependency: ${name}.`);
|
|
5413
|
+
if (dependencies?.length) {
|
|
5414
|
+
visiting.add(name);
|
|
5415
|
+
for (const dependency of dependencies) visit(dependency);
|
|
5416
|
+
visiting.delete(name);
|
|
5417
|
+
} else base.add(name);
|
|
5418
|
+
};
|
|
5419
|
+
for (const name of fields) visit(name);
|
|
5420
|
+
const paths = Object.fromEntries([...base].map((name) => {
|
|
5421
|
+
const field = schema.fields.find((field2) => field2.name === name);
|
|
5422
|
+
return [name, field?.source.kind === "backend" ? field.source.path ?? name : name];
|
|
5423
|
+
}));
|
|
5424
|
+
return { fields: [...fields].sort(), baseFields: [...base].sort(), identity: schema.idField, paths };
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5427
|
+
// src/queryV3.ts
|
|
5428
|
+
function sourceExecutionSchema(schema, execution, catalogue = []) {
|
|
5429
|
+
const fields = new Map(schema.fields.map((field) => [field.name, field]));
|
|
5430
|
+
for (const [name, capability] of Object.entries(execution?.fields ?? {})) {
|
|
5431
|
+
const definition = catalogue.find((definition2) => name === `@computed/${definition2.id}`);
|
|
5432
|
+
const revision = execution?.resolvedRevisions[name] ?? execution?.resolvedRevisions[name.slice(10)];
|
|
5433
|
+
const current = !!revision && (!definition || definition.revision === revision);
|
|
5434
|
+
fields.set(name, {
|
|
5435
|
+
...fields.get(name),
|
|
5436
|
+
name,
|
|
5437
|
+
label: definition?.label ?? fields.get(name)?.label ?? name,
|
|
5438
|
+
type: capability.type,
|
|
5439
|
+
source: { kind: "backend" },
|
|
5440
|
+
select: { enabled: current && capability.select },
|
|
5441
|
+
filter: { enabled: current && capability.filter === true },
|
|
5442
|
+
sort: { enabled: current && capability.sort },
|
|
5443
|
+
aggregate: { measure: current && capability.measure, groupable: current && capability.group }
|
|
5444
|
+
});
|
|
5445
|
+
}
|
|
5446
|
+
return { ...schema, fields: [...fields.values()] };
|
|
5447
|
+
}
|
|
5448
|
+
function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 = {}) {
|
|
5449
|
+
const now = evaluationTime(options2), q = normalizeQueryState(query);
|
|
5450
|
+
const sourceSchema = sourceExecutionSchema(baseSchema, execution);
|
|
5451
|
+
const source = q.grouping?.source ?? withoutGrouping(q);
|
|
5452
|
+
const diagnostics = [];
|
|
5453
|
+
if ((query.aggregations?.length ?? 0) !== (q.aggregations?.length ?? 0)) diagnostics.push("The query exceeds its metric budget or contains malformed metric state.");
|
|
5454
|
+
if (capabilities.version !== 3 || execution && execution.profile !== capabilities.profile) diagnostics.push("The negotiated query profile is unavailable.");
|
|
5455
|
+
let resultSchema = sourceSchema;
|
|
5456
|
+
if (q.grouping) {
|
|
5457
|
+
if (!capabilities.groupedRows) diagnostics.push("This server does not support grouped rows.");
|
|
5458
|
+
try {
|
|
5459
|
+
const model = createGroupingModel(sourceSchema);
|
|
5460
|
+
model.validate(q.grouping);
|
|
5461
|
+
resultSchema = model.resultSchema(q.grouping.keys, q.grouping.summaries);
|
|
5462
|
+
} catch (error) {
|
|
5463
|
+
diagnostics.push(error instanceof Error ? error.message : String(error));
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
const sourceRequest = toServerQuery(withoutGrouping(source), sourceSchema);
|
|
5467
|
+
for (const term of source.where) for (const predicate of predicatesOf(term)) if (["matches_regex", "not_matches_regex"].includes(predicate.op) && !capabilities.regexSourceFilters)
|
|
5468
|
+
diagnostics.push("This server profile does not support isolated regex source filtering.");
|
|
5469
|
+
const result = toServerQuery(withoutGrouping(q), resultSchema);
|
|
5470
|
+
if (sourceRequest.where.length !== source.where.length || result.where.length !== q.where.length) diagnostics.push("The query contains unsupported source or result predicates.");
|
|
5471
|
+
if (result.orderBy.length !== q.orderBy.length) diagnostics.push("The query contains unsupported result ordering.");
|
|
5472
|
+
if (q.grouping) {
|
|
5473
|
+
for (const term of q.grouping.source.where) for (const predicate of predicatesOf(term))
|
|
5474
|
+
if (predicate.field.startsWith("@computed/") && !capabilities.computedSourceFilters) diagnostics.push("This server does not support computed source predicates.");
|
|
5475
|
+
}
|
|
5476
|
+
const metrics = toMetricQuery(withoutGrouping(q), resultSchema, execution).metrics.map((metric) => {
|
|
5477
|
+
const dateGrouping = q.aggregations?.find((clause) => clause.id === metric.id)?.dateGrouping;
|
|
5478
|
+
return dateGrouping ? { ...metric, dateGrouping } : metric;
|
|
5479
|
+
});
|
|
5480
|
+
for (const metric of metrics) {
|
|
5481
|
+
diagnostics.push(...validateMetric(metric, resultSchema).map((issue) => issue.message));
|
|
5482
|
+
if (metric.dateGrouping && !capabilities.calendarGrouping) diagnostics.push("This server does not support calendar grouping.");
|
|
5483
|
+
}
|
|
5484
|
+
const revisions = /* @__PURE__ */ Object.create(null);
|
|
5485
|
+
for (const [id, revision] of Object.entries(execution?.resolvedRevisions ?? {})) {
|
|
5486
|
+
if (!revision || id.length > 256 || revision.length > 256) diagnostics.push("Invalid computed revision envelope.");
|
|
5487
|
+
else revisions[id.startsWith("@computed/") ? id.slice(10) : id] = revision;
|
|
5488
|
+
}
|
|
5489
|
+
return {
|
|
5490
|
+
version: 3,
|
|
5491
|
+
profile: capabilities.profile,
|
|
5492
|
+
now,
|
|
5493
|
+
expectedRevisions: revisions,
|
|
5494
|
+
...execution?.planToken ? { planToken: execution.planToken } : {},
|
|
5495
|
+
...execution?.snapshot !== void 0 ? { snapshot: execution.snapshot } : {},
|
|
5496
|
+
source: sourceRequest,
|
|
5497
|
+
result,
|
|
5498
|
+
...q.grouping ? { grouping: {
|
|
5499
|
+
keys: q.grouping.keys,
|
|
5500
|
+
summaries: q.grouping.summaries,
|
|
5501
|
+
representative: q.grouping.representative,
|
|
5502
|
+
...q.grouping.diagnostics ? { diagnostics: q.grouping.diagnostics } : {}
|
|
5503
|
+
} } : {},
|
|
5504
|
+
rows: options2.rows !== false,
|
|
5505
|
+
metrics,
|
|
5506
|
+
diagnostics: [...new Set(diagnostics)]
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
function validateQueryResultV3(result, request, schema) {
|
|
5510
|
+
if (!result || result.version !== 3 || result.now !== request.now || !Number.isSafeInteger(result.total) || result.total < 0 || !Number.isSafeInteger(result.sourceTotal) || result.sourceTotal < 0 || !Array.isArray(result.rows) || !Array.isArray(result.metrics) || result.execution?.profile !== request.profile || (result.execution.planToken ?? "") !== (request.planToken ?? "") || (result.execution.snapshot ?? "") !== (request.snapshot ?? "") || result.schema?.idField !== schema.idField)
|
|
5511
|
+
throw Error("Invalid or mismatched query execution response.");
|
|
5512
|
+
const expected = new Map(schema.fields.map((field) => [field.name, field.type]));
|
|
5513
|
+
if (!Array.isArray(result.schema.fields) || result.schema.fields.some((field) => expected.get(field.name) !== field.type) || request.rows && request.result.select.some((name) => !result.schema.fields.some((field) => field.name === name))) throw Error("The server returned a different projected schema.");
|
|
5514
|
+
for (const [id, revision] of Object.entries(request.expectedRevisions)) if (result.execution.resolvedRevisions[id] !== revision)
|
|
5515
|
+
throw Error("Computed definitions changed. Refresh the server execution handshake.");
|
|
5516
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5517
|
+
for (const row of result.rows) {
|
|
5518
|
+
const id = row[schema.idField];
|
|
5519
|
+
if (typeof id !== "string" && typeof id !== "number" || seen.has(id)) throw Error("The server returned invalid or duplicate group identities.");
|
|
5520
|
+
seen.add(id);
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
3637
5523
|
export {
|
|
3638
5524
|
AGG_OPS_BY_TYPE,
|
|
3639
5525
|
AGG_OPS_NEEDING_FIELD,
|
|
@@ -3641,17 +5527,29 @@ export {
|
|
|
3641
5527
|
EMPTY_QUERY,
|
|
3642
5528
|
FORMULA_FUNCTIONS,
|
|
3643
5529
|
FormulaError,
|
|
5530
|
+
GROUP_ID_FIELD,
|
|
5531
|
+
ITEM_DETAILS_COLUMN,
|
|
3644
5532
|
MAX_AGGREGATIONS,
|
|
3645
5533
|
MAX_GROUP_BY_FIELDS,
|
|
3646
5534
|
MAX_ORDER_BY_TERMS,
|
|
5535
|
+
MAX_PROPERTIES,
|
|
3647
5536
|
MAX_QUERY_LIMIT,
|
|
3648
5537
|
MAX_QUERY_OFFSET,
|
|
5538
|
+
MAX_QUERY_STATE_BYTES,
|
|
3649
5539
|
MAX_QUERY_TOKEN_LENGTH,
|
|
3650
5540
|
MAX_RELATIVE_TIME_MS,
|
|
5541
|
+
MAX_REPRESENTATIVE_SORTS,
|
|
5542
|
+
MAX_RESULT_GROUPS,
|
|
3651
5543
|
MAX_SELECT_COLUMNS,
|
|
5544
|
+
MAX_SUMMARY_COLUMNS,
|
|
3652
5545
|
MAX_WHERE_CLAUSES,
|
|
5546
|
+
METRIC_DATE_INTERVALS,
|
|
5547
|
+
METRIC_FUNCTIONS,
|
|
3653
5548
|
NULLARY_OPS,
|
|
3654
5549
|
OPS_BY_TYPE,
|
|
5550
|
+
OP_LABEL,
|
|
5551
|
+
QueryDecodeError,
|
|
5552
|
+
QuerySnapshotHttpError,
|
|
3655
5553
|
aggOpAllowedForType,
|
|
3656
5554
|
aggOpNeedsField,
|
|
3657
5555
|
aggOpsForField,
|
|
@@ -3663,8 +5561,13 @@ export {
|
|
|
3663
5561
|
compileFormula,
|
|
3664
5562
|
compileMetric,
|
|
3665
5563
|
computedFieldName,
|
|
5564
|
+
createGroupingModel,
|
|
5565
|
+
createPropertyView,
|
|
5566
|
+
createQueryLinkService,
|
|
3666
5567
|
createSetFilter,
|
|
3667
5568
|
decodeQuery,
|
|
5569
|
+
decodeQueryStrict,
|
|
5570
|
+
discoverFieldRelationships,
|
|
3668
5571
|
encodeQuery,
|
|
3669
5572
|
evaluateMetricReductions,
|
|
3670
5573
|
evaluateMetrics,
|
|
@@ -3675,6 +5578,7 @@ export {
|
|
|
3675
5578
|
histogramCounts,
|
|
3676
5579
|
histogramEdges,
|
|
3677
5580
|
httpComputedColumnStore,
|
|
5581
|
+
httpQuerySnapshotStore,
|
|
3678
5582
|
indexFields,
|
|
3679
5583
|
isComputedCellError,
|
|
3680
5584
|
isComputedField,
|
|
@@ -3686,38 +5590,62 @@ export {
|
|
|
3686
5590
|
isPushdownFilter,
|
|
3687
5591
|
isSelectable,
|
|
3688
5592
|
isSortable,
|
|
5593
|
+
itemPageFields,
|
|
5594
|
+
jsonPropertyViewStorage,
|
|
3689
5595
|
loadSchema,
|
|
5596
|
+
localPropertyViewStorage,
|
|
5597
|
+
localQuerySnapshotCache,
|
|
3690
5598
|
localStorageAdapter,
|
|
3691
5599
|
mapSetFilterValues,
|
|
3692
5600
|
matchesClause,
|
|
3693
5601
|
memoryComputedColumnStore,
|
|
5602
|
+
memoryPropertyViewStorage,
|
|
5603
|
+
memoryQuerySnapshotCache,
|
|
3694
5604
|
memoryStorageAdapter,
|
|
3695
5605
|
metricAggregatePlans,
|
|
3696
5606
|
metricComputationKey,
|
|
5607
|
+
metricDateFloor,
|
|
5608
|
+
metricDateNext,
|
|
5609
|
+
metricDateRange,
|
|
5610
|
+
metricDateTime,
|
|
3697
5611
|
metricDependencies,
|
|
3698
5612
|
metricExpression,
|
|
3699
5613
|
metricPlans,
|
|
3700
5614
|
negateClause,
|
|
5615
|
+
normalizePropertyFormat,
|
|
5616
|
+
normalizePropertyView,
|
|
3701
5617
|
normalizeQueryState,
|
|
5618
|
+
objectReferenceKey,
|
|
3702
5619
|
opAllowedForType,
|
|
3703
5620
|
opPairsForField,
|
|
3704
5621
|
opsForField,
|
|
3705
5622
|
parseRelativeDuration,
|
|
3706
5623
|
percentile,
|
|
3707
5624
|
predicatesOf,
|
|
5625
|
+
prepareGrouping,
|
|
3708
5626
|
queriesEqual,
|
|
3709
5627
|
readFieldValue,
|
|
5628
|
+
readObjectReference,
|
|
3710
5629
|
readSetFilter,
|
|
3711
5630
|
reduceMetricValues,
|
|
5631
|
+
renderItemPageTemplate,
|
|
3712
5632
|
resolveFieldName,
|
|
3713
5633
|
selectMetricShownRows,
|
|
3714
5634
|
selectedFields,
|
|
5635
|
+
sourceDependencies,
|
|
5636
|
+
sourceExecutionSchema,
|
|
3715
5637
|
stripSetFilter,
|
|
3716
5638
|
toAggregationQuery,
|
|
3717
5639
|
toMetricQuery,
|
|
5640
|
+
toQueryRequestV3,
|
|
3718
5641
|
toServerQuery,
|
|
3719
5642
|
toServerQueryV2,
|
|
3720
5643
|
validateComputedColumn,
|
|
3721
|
-
|
|
5644
|
+
validateDateGrouping,
|
|
5645
|
+
validateItemPageTemplate,
|
|
5646
|
+
validateMetric,
|
|
5647
|
+
validateQueryResultV3,
|
|
5648
|
+
validateQuerySnapshot,
|
|
5649
|
+
withoutGrouping
|
|
3722
5650
|
};
|
|
3723
5651
|
//# sourceMappingURL=index.js.map
|