@happyvertical/smrt-reports 0.57.6 → 0.57.8
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/AGENTS.md +10 -0
- package/dist/chunks/runtime-time-D7JZ1e4J.js +1052 -0
- package/dist/chunks/runtime-time-D7JZ1e4J.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -591
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +28 -4
- package/dist/playground.js +1 -1
- package/dist/playground.js.map +1 -1
- package/dist/recipes.d.ts +21 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/runtime-compiler.d.ts +7 -40
- package/dist/runtime-compiler.d.ts.map +1 -1
- package/dist/runtime-dimensions.d.ts +43 -0
- package/dist/runtime-dimensions.d.ts.map +1 -0
- package/dist/runtime-memory.d.ts +81 -0
- package/dist/runtime-memory.d.ts.map +1 -0
- package/dist/runtime-plan.d.ts +121 -0
- package/dist/runtime-plan.d.ts.map +1 -0
- package/dist/runtime-report.d.ts.map +1 -1
- package/dist/runtime-server.d.ts.map +1 -1
- package/dist/runtime-spec-hash.d.ts +11 -0
- package/dist/runtime-spec-hash.d.ts.map +1 -0
- package/dist/runtime-spec.d.ts +16 -4
- package/dist/runtime-spec.d.ts.map +1 -1
- package/dist/runtime-time.d.ts +41 -0
- package/dist/runtime-time.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +37 -12
- package/dist/spec.d.ts +25 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +2 -0
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/dist/svelte/ReportConfirmation.svelte +97 -24
- package/dist/svelte/ReportConfirmation.svelte.d.ts +20 -0
- package/dist/svelte/ReportConfirmation.svelte.d.ts.map +1 -1
- package/dist/svelte/ReportDetail.svelte +263 -12
- package/dist/svelte/ReportDetail.svelte.d.ts +10 -1
- package/dist/svelte/ReportDetail.svelte.d.ts.map +1 -1
- package/dist/svelte/ReportPicker.svelte +47 -14
- package/dist/svelte/ReportPicker.svelte.d.ts +3 -0
- package/dist/svelte/ReportPicker.svelte.d.ts.map +1 -1
- package/dist/svelte/ReportResult.svelte +216 -35
- package/dist/svelte/ReportResult.svelte.d.ts +8 -2
- package/dist/svelte/ReportResult.svelte.d.ts.map +1 -1
- package/dist/svelte/RuntimeReportWidgetOptions.svelte +9 -1
- package/dist/svelte/RuntimeReportWidgetOptions.svelte.d.ts.map +1 -1
- package/dist/svelte/SavedReportCard.svelte +242 -0
- package/dist/svelte/SavedReportCard.svelte.d.ts +30 -0
- package/dist/svelte/SavedReportCard.svelte.d.ts.map +1 -0
- package/dist/svelte/SavedReports.svelte +206 -33
- package/dist/svelte/SavedReports.svelte.d.ts +13 -0
- package/dist/svelte/SavedReports.svelte.d.ts.map +1 -1
- package/dist/svelte/SavedReportsWidget.svelte +59 -0
- package/dist/svelte/SavedReportsWidget.svelte.d.ts +19 -0
- package/dist/svelte/SavedReportsWidget.svelte.d.ts.map +1 -0
- package/dist/svelte/client.d.ts.map +1 -1
- package/dist/svelte/client.js +3 -0
- package/dist/svelte/describe.d.ts +94 -0
- package/dist/svelte/describe.d.ts.map +1 -0
- package/dist/svelte/describe.js +249 -0
- package/dist/svelte/index.d.ts +4 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -1
- package/dist/svelte/messages.d.ts +48 -0
- package/dist/svelte/messages.d.ts.map +1 -1
- package/dist/svelte/messages.js +59 -11
- package/dist/svelte/playground.d.ts +2 -2
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +1 -1
- package/dist/svelte/types.d.ts +2 -0
- package/dist/svelte/types.d.ts.map +1 -1
- package/package.json +13 -9
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
import { SmrtError } from "@happyvertical/smrt-core/smrt-error";
|
|
2
|
+
//#region src/runtime-spec.ts
|
|
3
|
+
var RUNTIME_REPORT_SPEC_VERSION = 1;
|
|
4
|
+
var RUNTIME_REPORT_LIMITS = Object.freeze({
|
|
5
|
+
maxTitleLength: 120,
|
|
6
|
+
maxDescriptionLength: 500,
|
|
7
|
+
maxDimensions: 4,
|
|
8
|
+
maxMeasures: 6,
|
|
9
|
+
maxFilters: 12,
|
|
10
|
+
maxHaving: 4,
|
|
11
|
+
maxSort: 3,
|
|
12
|
+
maxInValues: 100,
|
|
13
|
+
maxStringValueLength: 256,
|
|
14
|
+
defaultRowLimit: 100,
|
|
15
|
+
maxRowLimit: 1e3,
|
|
16
|
+
maxAliasLength: 40,
|
|
17
|
+
maxFieldNameLength: 64,
|
|
18
|
+
maxSourceIdLength: 120
|
|
19
|
+
});
|
|
20
|
+
var RUNTIME_REPORT_BUCKETS = [
|
|
21
|
+
"minute",
|
|
22
|
+
"hour",
|
|
23
|
+
"day",
|
|
24
|
+
"week",
|
|
25
|
+
"month",
|
|
26
|
+
"quarter",
|
|
27
|
+
"year"
|
|
28
|
+
];
|
|
29
|
+
var RUNTIME_REPORT_MEASURE_FNS = [
|
|
30
|
+
"count",
|
|
31
|
+
"countDistinct",
|
|
32
|
+
"sum",
|
|
33
|
+
"avg",
|
|
34
|
+
"min",
|
|
35
|
+
"max"
|
|
36
|
+
];
|
|
37
|
+
var RUNTIME_REPORT_FILTER_OPS = [
|
|
38
|
+
"eq",
|
|
39
|
+
"ne",
|
|
40
|
+
"gt",
|
|
41
|
+
"gte",
|
|
42
|
+
"lt",
|
|
43
|
+
"lte",
|
|
44
|
+
"in",
|
|
45
|
+
"notIn",
|
|
46
|
+
"contains",
|
|
47
|
+
"isNull",
|
|
48
|
+
"isNotNull"
|
|
49
|
+
];
|
|
50
|
+
var RUNTIME_REPORT_HAVING_OPS = [
|
|
51
|
+
"eq",
|
|
52
|
+
"ne",
|
|
53
|
+
"gt",
|
|
54
|
+
"gte",
|
|
55
|
+
"lt",
|
|
56
|
+
"lte"
|
|
57
|
+
];
|
|
58
|
+
var RUNTIME_REPORT_CHART_TYPES = [
|
|
59
|
+
"table",
|
|
60
|
+
"bar",
|
|
61
|
+
"line",
|
|
62
|
+
"pie",
|
|
63
|
+
"stat"
|
|
64
|
+
];
|
|
65
|
+
var RuntimeReportError = class extends SmrtError {
|
|
66
|
+
code;
|
|
67
|
+
path;
|
|
68
|
+
/**
|
|
69
|
+
* HTTP-style status so tool loops classify it: 4xx input problems are
|
|
70
|
+
* repairable by the caller, `tenant_required` and `confirmation_required`
|
|
71
|
+
* are denials, results that cannot be represented are server errors.
|
|
72
|
+
*/
|
|
73
|
+
status;
|
|
74
|
+
constructor(code, path, message) {
|
|
75
|
+
super(path ? `${path}: ${message}` : message, code, "validation");
|
|
76
|
+
this.name = "RuntimeReportError";
|
|
77
|
+
this.code = code;
|
|
78
|
+
this.path = path;
|
|
79
|
+
this.status = code === "tenant_required" || code === "confirmation_required" ? 403 : code === "invalid_result" ? 500 : 422;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function fail(path, message) {
|
|
83
|
+
throw new RuntimeReportError("invalid_spec", path, message);
|
|
84
|
+
}
|
|
85
|
+
var RESERVED_ALIASES = /* @__PURE__ */ new Set([
|
|
86
|
+
"all",
|
|
87
|
+
"and",
|
|
88
|
+
"as",
|
|
89
|
+
"asc",
|
|
90
|
+
"between",
|
|
91
|
+
"by",
|
|
92
|
+
"case",
|
|
93
|
+
"cast",
|
|
94
|
+
"check",
|
|
95
|
+
"column",
|
|
96
|
+
"constraint",
|
|
97
|
+
"create",
|
|
98
|
+
"cross",
|
|
99
|
+
"default",
|
|
100
|
+
"delete",
|
|
101
|
+
"desc",
|
|
102
|
+
"distinct",
|
|
103
|
+
"drop",
|
|
104
|
+
"else",
|
|
105
|
+
"end",
|
|
106
|
+
"except",
|
|
107
|
+
"exists",
|
|
108
|
+
"false",
|
|
109
|
+
"for",
|
|
110
|
+
"foreign",
|
|
111
|
+
"from",
|
|
112
|
+
"full",
|
|
113
|
+
"group",
|
|
114
|
+
"having",
|
|
115
|
+
"in",
|
|
116
|
+
"inner",
|
|
117
|
+
"insert",
|
|
118
|
+
"intersect",
|
|
119
|
+
"into",
|
|
120
|
+
"is",
|
|
121
|
+
"join",
|
|
122
|
+
"left",
|
|
123
|
+
"like",
|
|
124
|
+
"limit",
|
|
125
|
+
"natural",
|
|
126
|
+
"not",
|
|
127
|
+
"null",
|
|
128
|
+
"offset",
|
|
129
|
+
"on",
|
|
130
|
+
"or",
|
|
131
|
+
"order",
|
|
132
|
+
"outer",
|
|
133
|
+
"primary",
|
|
134
|
+
"references",
|
|
135
|
+
"right",
|
|
136
|
+
"select",
|
|
137
|
+
"set",
|
|
138
|
+
"table",
|
|
139
|
+
"then",
|
|
140
|
+
"to",
|
|
141
|
+
"true",
|
|
142
|
+
"union",
|
|
143
|
+
"unique",
|
|
144
|
+
"update",
|
|
145
|
+
"user",
|
|
146
|
+
"using",
|
|
147
|
+
"values",
|
|
148
|
+
"when",
|
|
149
|
+
"where",
|
|
150
|
+
"with"
|
|
151
|
+
]);
|
|
152
|
+
var ALIAS_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
153
|
+
var FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
154
|
+
var SOURCE_PATTERN = /^[A-Za-z@][A-Za-z0-9_@./:-]*$/;
|
|
155
|
+
function isRecord(value) {
|
|
156
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
157
|
+
}
|
|
158
|
+
function strictObject(value, path, allowed) {
|
|
159
|
+
if (!isRecord(value)) fail(path, "must be an object");
|
|
160
|
+
for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(`${path}.${key}`, "is not a known key");
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function strictArray(value, path, max, min = 0) {
|
|
164
|
+
if (value === void 0) return [];
|
|
165
|
+
if (!Array.isArray(value)) fail(path, "must be an array");
|
|
166
|
+
if (value.length > max) fail(path, `must have at most ${max} entries`);
|
|
167
|
+
if (value.length < min) fail(path, `must have at least ${min} entries`);
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
function text(value, path, max, options = {}) {
|
|
171
|
+
if (typeof value !== "string") {
|
|
172
|
+
if (options.optional && value === void 0) return "";
|
|
173
|
+
fail(path, "must be a string");
|
|
174
|
+
}
|
|
175
|
+
const trimmed = value.trim();
|
|
176
|
+
if (trimmed.length === 0 && !options.optional) fail(path, "must not be empty");
|
|
177
|
+
if (trimmed.length > max) fail(path, `must be at most ${max} characters`);
|
|
178
|
+
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(trimmed)) fail(path, "must not contain control characters");
|
|
179
|
+
return trimmed;
|
|
180
|
+
}
|
|
181
|
+
function oneOf(value, path, allowed) {
|
|
182
|
+
if (typeof value !== "string" || !allowed.includes(value)) fail(path, `must be one of: ${allowed.join(", ")}`);
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
function fieldName(value, path) {
|
|
186
|
+
if (typeof value !== "string") fail(path, "must be a string");
|
|
187
|
+
if (value.length === 0 || value.length > RUNTIME_REPORT_LIMITS.maxFieldNameLength || !FIELD_PATTERN.test(value)) fail(path, "must be a plain field name (letters, digits, underscore)");
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function alias(value, path) {
|
|
191
|
+
if (typeof value !== "string") fail(path, "must be a string");
|
|
192
|
+
if (value.length === 0 || value.length > RUNTIME_REPORT_LIMITS.maxAliasLength || !ALIAS_PATTERN.test(value)) fail(path, `must be a lowercase identifier of at most ${RUNTIME_REPORT_LIMITS.maxAliasLength} characters (a-z, 0-9, underscore)`);
|
|
193
|
+
if (RESERVED_ALIASES.has(value)) fail(path, `'${value}' is a reserved word`);
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function scalar(value, path) {
|
|
197
|
+
if (typeof value === "string") {
|
|
198
|
+
if (value.length > RUNTIME_REPORT_LIMITS.maxStringValueLength) fail(path, `must be at most ${RUNTIME_REPORT_LIMITS.maxStringValueLength} characters`);
|
|
199
|
+
if (value.includes("\0")) fail(path, "must not contain NUL");
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
if (typeof value === "number") {
|
|
203
|
+
if (!Number.isFinite(value)) fail(path, "must be a finite number");
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
if (typeof value === "boolean") return value;
|
|
207
|
+
return fail(path, "must be a string, number or boolean");
|
|
208
|
+
}
|
|
209
|
+
function snake(name) {
|
|
210
|
+
return name.replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
211
|
+
}
|
|
212
|
+
function parseDimension(value, path) {
|
|
213
|
+
const raw = strictObject(value, path, [
|
|
214
|
+
"field",
|
|
215
|
+
"as",
|
|
216
|
+
"bucket"
|
|
217
|
+
]);
|
|
218
|
+
const field = fieldName(raw.field, `${path}.field`);
|
|
219
|
+
const bucket = raw.bucket === void 0 ? void 0 : oneOf(raw.bucket, `${path}.bucket`, RUNTIME_REPORT_BUCKETS);
|
|
220
|
+
return {
|
|
221
|
+
field,
|
|
222
|
+
as: raw.as === void 0 ? alias(bucket ? `${snake(field)}_${bucket}` : snake(field), `${path}.as`) : alias(raw.as, `${path}.as`),
|
|
223
|
+
...bucket ? { bucket } : {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function parseMeasure(value, path) {
|
|
227
|
+
const raw = strictObject(value, path, [
|
|
228
|
+
"fn",
|
|
229
|
+
"field",
|
|
230
|
+
"as"
|
|
231
|
+
]);
|
|
232
|
+
const fn = oneOf(raw.fn, `${path}.fn`, RUNTIME_REPORT_MEASURE_FNS);
|
|
233
|
+
const field = raw.field === void 0 ? void 0 : fieldName(raw.field, `${path}.field`);
|
|
234
|
+
if (!field && fn !== "count") fail(`${path}.field`, `is required for ${fn}`);
|
|
235
|
+
const as = raw.as === void 0 ? alias(field ? `${snake(fn)}_${snake(field)}` : "row_count", `${path}.as`) : alias(raw.as, `${path}.as`);
|
|
236
|
+
return {
|
|
237
|
+
fn,
|
|
238
|
+
...field ? { field } : {},
|
|
239
|
+
as
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function parseFilter(value, path) {
|
|
243
|
+
const raw = strictObject(value, path, [
|
|
244
|
+
"field",
|
|
245
|
+
"op",
|
|
246
|
+
"value"
|
|
247
|
+
]);
|
|
248
|
+
const field = fieldName(raw.field, `${path}.field`);
|
|
249
|
+
const op = oneOf(raw.op, `${path}.op`, RUNTIME_REPORT_FILTER_OPS);
|
|
250
|
+
if (op === "isNull" || op === "isNotNull") {
|
|
251
|
+
if (raw.value !== void 0) fail(`${path}.value`, `must be omitted for ${op}`);
|
|
252
|
+
return {
|
|
253
|
+
field,
|
|
254
|
+
op
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (raw.value === void 0) fail(`${path}.value`, `is required for ${op}`);
|
|
258
|
+
if (op === "in" || op === "notIn") return {
|
|
259
|
+
field,
|
|
260
|
+
op,
|
|
261
|
+
value: strictArray(raw.value, `${path}.value`, RUNTIME_REPORT_LIMITS.maxInValues, 1).map((entry, index) => scalar(entry, `${path}.value[${index}]`))
|
|
262
|
+
};
|
|
263
|
+
if (Array.isArray(raw.value)) fail(`${path}.value`, `must be a scalar for ${op}`);
|
|
264
|
+
const parsed = scalar(raw.value, `${path}.value`);
|
|
265
|
+
if (op === "contains" && typeof parsed !== "string") fail(`${path}.value`, "must be a string for contains");
|
|
266
|
+
return {
|
|
267
|
+
field,
|
|
268
|
+
op,
|
|
269
|
+
value: parsed
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function parseHaving(value, path) {
|
|
273
|
+
const raw = strictObject(value, path, [
|
|
274
|
+
"measure",
|
|
275
|
+
"op",
|
|
276
|
+
"value"
|
|
277
|
+
]);
|
|
278
|
+
const measure = alias(raw.measure, `${path}.measure`);
|
|
279
|
+
const op = oneOf(raw.op, `${path}.op`, RUNTIME_REPORT_HAVING_OPS);
|
|
280
|
+
if (typeof raw.value !== "number" || !Number.isFinite(raw.value)) fail(`${path}.value`, "must be a finite number");
|
|
281
|
+
return {
|
|
282
|
+
measure,
|
|
283
|
+
op,
|
|
284
|
+
value: raw.value
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function parseSort(value, path) {
|
|
288
|
+
const raw = strictObject(value, path, ["by", "direction"]);
|
|
289
|
+
return {
|
|
290
|
+
by: alias(raw.by, `${path}.by`),
|
|
291
|
+
direction: raw.direction === void 0 ? "asc" : oneOf(raw.direction, `${path}.direction`, ["asc", "desc"])
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function parseRuntimeReportSpec(input) {
|
|
295
|
+
let candidate = input;
|
|
296
|
+
if (typeof input === "string") {
|
|
297
|
+
if (input.length > 64 * 1024) fail("", "spec JSON is too large");
|
|
298
|
+
try {
|
|
299
|
+
candidate = JSON.parse(input);
|
|
300
|
+
} catch {
|
|
301
|
+
fail("", "spec is not valid JSON");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const raw = strictObject(candidate, "spec", [
|
|
305
|
+
"version",
|
|
306
|
+
"title",
|
|
307
|
+
"description",
|
|
308
|
+
"source",
|
|
309
|
+
"dimensions",
|
|
310
|
+
"measures",
|
|
311
|
+
"filters",
|
|
312
|
+
"having",
|
|
313
|
+
"sort",
|
|
314
|
+
"limit",
|
|
315
|
+
"chart"
|
|
316
|
+
]);
|
|
317
|
+
if (raw.version !== void 0 && raw.version !== 1) fail("spec.version", `unsupported version (expected 1)`);
|
|
318
|
+
const title = text(raw.title, "spec.title", RUNTIME_REPORT_LIMITS.maxTitleLength);
|
|
319
|
+
const description = text(raw.description, "spec.description", RUNTIME_REPORT_LIMITS.maxDescriptionLength, { optional: true });
|
|
320
|
+
if (typeof raw.source !== "string" || raw.source.length === 0 || raw.source.length > RUNTIME_REPORT_LIMITS.maxSourceIdLength || !SOURCE_PATTERN.test(raw.source)) fail("spec.source", "must be a source id");
|
|
321
|
+
const dimensions = strictArray(raw.dimensions, "spec.dimensions", RUNTIME_REPORT_LIMITS.maxDimensions).map((entry, index) => parseDimension(entry, `spec.dimensions[${index}]`));
|
|
322
|
+
const measures = strictArray(raw.measures, "spec.measures", RUNTIME_REPORT_LIMITS.maxMeasures, 1).map((entry, index) => parseMeasure(entry, `spec.measures[${index}]`));
|
|
323
|
+
const filters = strictArray(raw.filters, "spec.filters", RUNTIME_REPORT_LIMITS.maxFilters).map((entry, index) => parseFilter(entry, `spec.filters[${index}]`));
|
|
324
|
+
const having = strictArray(raw.having, "spec.having", RUNTIME_REPORT_LIMITS.maxHaving).map((entry, index) => parseHaving(entry, `spec.having[${index}]`));
|
|
325
|
+
const sort = strictArray(raw.sort, "spec.sort", RUNTIME_REPORT_LIMITS.maxSort).map((entry, index) => parseSort(entry, `spec.sort[${index}]`));
|
|
326
|
+
const dimensionAliases = /* @__PURE__ */ new Set();
|
|
327
|
+
const measureAliases = /* @__PURE__ */ new Set();
|
|
328
|
+
const seen = /* @__PURE__ */ new Set();
|
|
329
|
+
for (const [index, dimension] of dimensions.entries()) {
|
|
330
|
+
if (seen.has(dimension.as)) fail(`spec.dimensions[${index}].as`, `duplicate alias '${dimension.as}'`);
|
|
331
|
+
seen.add(dimension.as);
|
|
332
|
+
dimensionAliases.add(dimension.as);
|
|
333
|
+
}
|
|
334
|
+
for (const [index, measure] of measures.entries()) {
|
|
335
|
+
if (seen.has(measure.as)) fail(`spec.measures[${index}].as`, `duplicate alias '${measure.as}'`);
|
|
336
|
+
seen.add(measure.as);
|
|
337
|
+
measureAliases.add(measure.as);
|
|
338
|
+
}
|
|
339
|
+
for (const [index, entry] of having.entries()) if (!measureAliases.has(entry.measure)) fail(`spec.having[${index}].measure`, "must be a declared measure alias");
|
|
340
|
+
for (const [index, entry] of sort.entries()) if (!seen.has(entry.by)) fail(`spec.sort[${index}].by`, "must be a declared dimension or measure alias");
|
|
341
|
+
let limit = RUNTIME_REPORT_LIMITS.defaultRowLimit;
|
|
342
|
+
if (raw.limit !== void 0) {
|
|
343
|
+
if (typeof raw.limit !== "number" || !Number.isSafeInteger(raw.limit) || raw.limit < 1 || raw.limit > RUNTIME_REPORT_LIMITS.maxRowLimit) fail("spec.limit", `must be an integer between 1 and ${RUNTIME_REPORT_LIMITS.maxRowLimit}`);
|
|
344
|
+
limit = raw.limit;
|
|
345
|
+
}
|
|
346
|
+
let chart = { type: "table" };
|
|
347
|
+
if (raw.chart !== void 0) {
|
|
348
|
+
const rawChart = strictObject(raw.chart, "spec.chart", [
|
|
349
|
+
"type",
|
|
350
|
+
"x",
|
|
351
|
+
"y"
|
|
352
|
+
]);
|
|
353
|
+
const type = oneOf(rawChart.type, "spec.chart.type", RUNTIME_REPORT_CHART_TYPES);
|
|
354
|
+
const x = rawChart.x === void 0 ? void 0 : alias(rawChart.x, "spec.chart.x");
|
|
355
|
+
if (x !== void 0 && !dimensionAliases.has(x)) fail("spec.chart.x", "must be a declared dimension alias");
|
|
356
|
+
const y = strictArray(rawChart.y, "spec.chart.y", RUNTIME_REPORT_LIMITS.maxMeasures).map((entry, index) => alias(entry, `spec.chart.y[${index}]`));
|
|
357
|
+
for (const [index, name] of y.entries()) if (!measureAliases.has(name)) fail(`spec.chart.y[${index}]`, "must be a declared measure alias");
|
|
358
|
+
chart = {
|
|
359
|
+
type,
|
|
360
|
+
...x ? { x } : {},
|
|
361
|
+
...y.length > 0 ? { y } : {}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
version: 1,
|
|
366
|
+
title,
|
|
367
|
+
...description ? { description } : {},
|
|
368
|
+
source: raw.source,
|
|
369
|
+
dimensions,
|
|
370
|
+
measures,
|
|
371
|
+
filters,
|
|
372
|
+
having,
|
|
373
|
+
sort,
|
|
374
|
+
limit,
|
|
375
|
+
chart
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function canonicalize(value) {
|
|
379
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
380
|
+
if (isRecord(value)) return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
function serializeRuntimeReportSpec(spec) {
|
|
384
|
+
return JSON.stringify(canonicalize(spec));
|
|
385
|
+
}
|
|
386
|
+
async function runtimeReportSpecDigest(spec) {
|
|
387
|
+
const subtle = globalThis.crypto?.subtle;
|
|
388
|
+
if (!subtle) throw new Error("runtimeReportSpecDigest needs Web Crypto (crypto.subtle), which browsers offer only in a secure context");
|
|
389
|
+
const bytes = new TextEncoder().encode(serializeRuntimeReportSpec(spec));
|
|
390
|
+
const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
391
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/runtime-plan.ts
|
|
395
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
396
|
+
function humanizeRuntimeReportName(name) {
|
|
397
|
+
return name.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/^./, (value) => value.toUpperCase());
|
|
398
|
+
}
|
|
399
|
+
function unknownRuntimeReportField(path, name) {
|
|
400
|
+
throw new RuntimeReportError("unknown_field", path, `'${name}' is not a field available on this source`);
|
|
401
|
+
}
|
|
402
|
+
function badOperation(path, message) {
|
|
403
|
+
throw new RuntimeReportError("invalid_operation", path, message);
|
|
404
|
+
}
|
|
405
|
+
function badValue(path, message) {
|
|
406
|
+
throw new RuntimeReportError("invalid_value", path, message);
|
|
407
|
+
}
|
|
408
|
+
var RUNTIME_REPORT_OPERATORS_BY_TYPE = {
|
|
409
|
+
string: [
|
|
410
|
+
"eq",
|
|
411
|
+
"ne",
|
|
412
|
+
"gt",
|
|
413
|
+
"gte",
|
|
414
|
+
"lt",
|
|
415
|
+
"lte",
|
|
416
|
+
"in",
|
|
417
|
+
"notIn",
|
|
418
|
+
"contains",
|
|
419
|
+
"isNull",
|
|
420
|
+
"isNotNull"
|
|
421
|
+
],
|
|
422
|
+
integer: [
|
|
423
|
+
"eq",
|
|
424
|
+
"ne",
|
|
425
|
+
"gt",
|
|
426
|
+
"gte",
|
|
427
|
+
"lt",
|
|
428
|
+
"lte",
|
|
429
|
+
"in",
|
|
430
|
+
"notIn",
|
|
431
|
+
"isNull",
|
|
432
|
+
"isNotNull"
|
|
433
|
+
],
|
|
434
|
+
decimal: [
|
|
435
|
+
"eq",
|
|
436
|
+
"ne",
|
|
437
|
+
"gt",
|
|
438
|
+
"gte",
|
|
439
|
+
"lt",
|
|
440
|
+
"lte",
|
|
441
|
+
"in",
|
|
442
|
+
"notIn",
|
|
443
|
+
"isNull",
|
|
444
|
+
"isNotNull"
|
|
445
|
+
],
|
|
446
|
+
datetime: [
|
|
447
|
+
"eq",
|
|
448
|
+
"ne",
|
|
449
|
+
"gt",
|
|
450
|
+
"gte",
|
|
451
|
+
"lt",
|
|
452
|
+
"lte",
|
|
453
|
+
"in",
|
|
454
|
+
"notIn",
|
|
455
|
+
"isNull",
|
|
456
|
+
"isNotNull"
|
|
457
|
+
],
|
|
458
|
+
boolean: [
|
|
459
|
+
"eq",
|
|
460
|
+
"ne",
|
|
461
|
+
"isNull",
|
|
462
|
+
"isNotNull"
|
|
463
|
+
],
|
|
464
|
+
id: [
|
|
465
|
+
"eq",
|
|
466
|
+
"ne",
|
|
467
|
+
"in",
|
|
468
|
+
"notIn",
|
|
469
|
+
"isNull",
|
|
470
|
+
"isNotNull"
|
|
471
|
+
]
|
|
472
|
+
};
|
|
473
|
+
function runtimeReportAggregatesFor(type) {
|
|
474
|
+
if (type === "integer" || type === "decimal") return [
|
|
475
|
+
"count",
|
|
476
|
+
"countDistinct",
|
|
477
|
+
"sum",
|
|
478
|
+
"avg",
|
|
479
|
+
"min",
|
|
480
|
+
"max"
|
|
481
|
+
];
|
|
482
|
+
if (type === "datetime") return [
|
|
483
|
+
"count",
|
|
484
|
+
"countDistinct",
|
|
485
|
+
"min",
|
|
486
|
+
"max"
|
|
487
|
+
];
|
|
488
|
+
return ["count", "countDistinct"];
|
|
489
|
+
}
|
|
490
|
+
function assertRuntimeReportFilterOperator(filter, type, path) {
|
|
491
|
+
if (!RUNTIME_REPORT_OPERATORS_BY_TYPE[type].includes(filter.op)) badOperation(`${path}.op`, `'${filter.op}' is not valid for ${type} fields`);
|
|
492
|
+
}
|
|
493
|
+
function coerceRuntimeReportValue(type, value, path) {
|
|
494
|
+
switch (type) {
|
|
495
|
+
case "string":
|
|
496
|
+
if (typeof value !== "string") badValue(path, "must be a string");
|
|
497
|
+
return value;
|
|
498
|
+
case "integer":
|
|
499
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) badValue(path, "must be a safe integer");
|
|
500
|
+
return value;
|
|
501
|
+
case "decimal":
|
|
502
|
+
if (typeof value !== "number" || !Number.isFinite(value)) badValue(path, "must be a finite number");
|
|
503
|
+
return value;
|
|
504
|
+
case "boolean":
|
|
505
|
+
if (typeof value !== "boolean") badValue(path, "must be true or false");
|
|
506
|
+
return value;
|
|
507
|
+
case "datetime": {
|
|
508
|
+
if (typeof value !== "string") badValue(path, "must be an ISO-8601 string");
|
|
509
|
+
const parsed = new Date(value);
|
|
510
|
+
if (Number.isNaN(parsed.getTime()) || !/^\d{4}-\d{2}-\d{2}/.test(value)) badValue(path, "must be an ISO-8601 date or timestamp");
|
|
511
|
+
return parsed;
|
|
512
|
+
}
|
|
513
|
+
case "id":
|
|
514
|
+
if (typeof value !== "string" || !UUID_PATTERN.test(value)) badValue(path, "must be a UUID");
|
|
515
|
+
return value.toLowerCase();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function resolveRuntimeReportFieldPolicy(source, knownNames) {
|
|
519
|
+
const policy = (key) => {
|
|
520
|
+
const value = source[key];
|
|
521
|
+
if (value === void 0) return void 0;
|
|
522
|
+
if (!Array.isArray(value) || Array.from(value).some((name) => typeof name !== "string" || !knownNames.has(name))) throw new RuntimeReportError("unsupported_source", `source.${key}`, `${key} must contain canonical field names declared on the source`);
|
|
523
|
+
return new Set(value);
|
|
524
|
+
};
|
|
525
|
+
const included = policy("fields");
|
|
526
|
+
const excluded = policy("excludeFields");
|
|
527
|
+
policy("dimensions");
|
|
528
|
+
if (source.timeField !== void 0 && (typeof source.timeField !== "string" || !knownNames.has(source.timeField))) throw new RuntimeReportError("unsupported_source", "source.timeField", "timeField must be a canonical field name declared on the source");
|
|
529
|
+
return {
|
|
530
|
+
...included ? { included } : {},
|
|
531
|
+
...excluded ? { excluded } : {}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function measureColumnType(fn, field) {
|
|
535
|
+
if (fn === "count" || fn === "countDistinct") return "integer";
|
|
536
|
+
if (fn === "avg") return "decimal";
|
|
537
|
+
return field?.type ?? "decimal";
|
|
538
|
+
}
|
|
539
|
+
function planRuntimeReportColumns(spec, fields) {
|
|
540
|
+
const lookup = (name, path) => {
|
|
541
|
+
const field = fields.get(name);
|
|
542
|
+
if (!field) unknownRuntimeReportField(path, name);
|
|
543
|
+
return field;
|
|
544
|
+
};
|
|
545
|
+
const columns = [];
|
|
546
|
+
for (const [index, dimension] of spec.dimensions.entries()) {
|
|
547
|
+
const path = `spec.dimensions[${index}]`;
|
|
548
|
+
const field = lookup(dimension.field, `${path}.field`);
|
|
549
|
+
if (dimension.bucket && field.type !== "datetime") badOperation(`${path}.bucket`, "buckets are only valid on datetime fields");
|
|
550
|
+
columns.push({
|
|
551
|
+
id: dimension.as,
|
|
552
|
+
label: humanizeRuntimeReportName(dimension.as),
|
|
553
|
+
role: "dimension",
|
|
554
|
+
type: dimension.bucket ? "datetime" : field.type,
|
|
555
|
+
field: field.name,
|
|
556
|
+
...dimension.bucket ? { bucket: dimension.bucket } : {},
|
|
557
|
+
...field.format && !dimension.bucket ? { format: field.format } : {}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
for (const [index, measure] of spec.measures.entries()) {
|
|
561
|
+
const path = `spec.measures[${index}]`;
|
|
562
|
+
const field = measure.field ? lookup(measure.field, `${path}.field`) : void 0;
|
|
563
|
+
if ((measure.fn === "sum" || measure.fn === "avg") && field?.type !== "integer" && field?.type !== "decimal") badOperation(`${path}.fn`, `${measure.fn} requires a numeric field`);
|
|
564
|
+
if ((measure.fn === "min" || measure.fn === "max") && field?.type !== "integer" && field?.type !== "decimal" && field?.type !== "datetime") badOperation(`${path}.fn`, `${measure.fn} requires a numeric or datetime field`);
|
|
565
|
+
const keepsFormat = field?.format && (measure.fn === "sum" || measure.fn === "min" || measure.fn === "max" || measure.fn === "avg");
|
|
566
|
+
columns.push({
|
|
567
|
+
id: measure.as,
|
|
568
|
+
label: humanizeRuntimeReportName(measure.as),
|
|
569
|
+
role: "measure",
|
|
570
|
+
type: measureColumnType(measure.fn, field),
|
|
571
|
+
...field ? { field: field.name } : {},
|
|
572
|
+
fn: measure.fn,
|
|
573
|
+
...keepsFormat ? { format: field?.format } : {}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return columns;
|
|
577
|
+
}
|
|
578
|
+
function normalizeRuntimeReportCell(column, value) {
|
|
579
|
+
if (value === null || value === void 0) return null;
|
|
580
|
+
if (value instanceof Date) return value.toISOString();
|
|
581
|
+
if (typeof value === "bigint") {
|
|
582
|
+
const asNumber = Number(value);
|
|
583
|
+
if (!Number.isSafeInteger(asNumber)) throw new RuntimeReportError("invalid_result", column.id, "integer result exceeds the JavaScript safe range");
|
|
584
|
+
return asNumber;
|
|
585
|
+
}
|
|
586
|
+
if (column.type === "boolean") {
|
|
587
|
+
if (value === 1 || value === "1" || value === "true" || value === true) return true;
|
|
588
|
+
if (value === 0 || value === "0" || value === "false" || value === false) return false;
|
|
589
|
+
}
|
|
590
|
+
if (column.type === "integer" || column.type === "decimal") {
|
|
591
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
592
|
+
if (!Number.isFinite(numeric)) throw new RuntimeReportError("invalid_result", column.id, "numeric result is not finite");
|
|
593
|
+
if (column.type === "integer" && !Number.isSafeInteger(numeric)) throw new RuntimeReportError("invalid_result", column.id, "integer result exceeds the JavaScript safe range");
|
|
594
|
+
return numeric;
|
|
595
|
+
}
|
|
596
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
597
|
+
return String(value);
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/runtime-dimensions.ts
|
|
601
|
+
var INTERNAL_NAME = /^(?:id|_.*)$|(?:Hash|Json|Token|Secret|Signature|Authority|Snapshot|Checksum|Cursor|Etag)$|^(?:external|historical)[A-Z]/;
|
|
602
|
+
var BOOKKEEPING_TIME = /^(?:created|updated|deleted|synced|archived)At$|^last[A-Z]\w*At$/;
|
|
603
|
+
var STATUS_NAME = /^(?:status|state)$|(?:Status|State)$/;
|
|
604
|
+
var KIND_NAME = /^(?:kind|type|category|method|channel|stage|priority|segment|tier|source)$|(?:Kind|Type|Category|Method|Channel|Stage|Priority|Segment|Tier)$/;
|
|
605
|
+
var PARTY_RELATION = /^(?:customer|vendor|supplier|client|member|employee|owner|assignee|product|location|project|account|contact)Id$/;
|
|
606
|
+
var MAX_CATEGORY_VALUES = 50;
|
|
607
|
+
function kindOf(field) {
|
|
608
|
+
if (field.type === "datetime") return field.name === "deletedAt" ? void 0 : "time";
|
|
609
|
+
if (INTERNAL_NAME.test(field.name)) return void 0;
|
|
610
|
+
if (field.type === "boolean") return "category";
|
|
611
|
+
if (field.type === "string" && field.enum && field.enum.length > 0 && field.enum.length <= MAX_CATEGORY_VALUES) return "category";
|
|
612
|
+
if (field.type === "id" && /Id$/.test(field.name)) return "relation";
|
|
613
|
+
}
|
|
614
|
+
function rank(field) {
|
|
615
|
+
if (field.type === "boolean") return 10;
|
|
616
|
+
if (field.type === "id") return PARTY_RELATION.test(field.name) ? 70 : 30;
|
|
617
|
+
if (STATUS_NAME.test(field.name)) return 100;
|
|
618
|
+
if (KIND_NAME.test(field.name)) return 80;
|
|
619
|
+
return 50;
|
|
620
|
+
}
|
|
621
|
+
function planRuntimeReportDimensions(fields, hints = {}) {
|
|
622
|
+
const all = [...fields.values()];
|
|
623
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const field of all) {
|
|
625
|
+
const kind = kindOf(field);
|
|
626
|
+
if (kind) kinds.set(field.name, kind);
|
|
627
|
+
}
|
|
628
|
+
const times = all.filter((field) => kinds.get(field.name) === "time");
|
|
629
|
+
const defaultTimeField = (hints.timeField && fields.get(hints.timeField)?.type === "datetime" ? hints.timeField : void 0) ?? times.find((field) => field.businessDate)?.name ?? times.find((field) => !BOOKKEEPING_TIME.test(field.name))?.name ?? times.find((field) => field.name === "createdAt")?.name ?? times[0]?.name;
|
|
630
|
+
const timeFields = [...defaultTimeField ? [defaultTimeField] : [], ...times.map((f) => f.name).filter((name) => name !== defaultTimeField)];
|
|
631
|
+
let dimensions;
|
|
632
|
+
if (hints.dimensions) {
|
|
633
|
+
dimensions = hints.dimensions.filter((name) => fields.has(name) && fields.get(name)?.type !== "datetime");
|
|
634
|
+
for (const name of dimensions) if (!kinds.has(name)) kinds.set(name, fields.get(name)?.type === "id" ? "relation" : "category");
|
|
635
|
+
for (const [name, kind] of [...kinds]) if (kind !== "time" && !dimensions.includes(name)) kinds.delete(name);
|
|
636
|
+
} else dimensions = all.filter((field) => {
|
|
637
|
+
const kind = kinds.get(field.name);
|
|
638
|
+
return kind === "category" || kind === "relation";
|
|
639
|
+
}).map((field, index) => ({
|
|
640
|
+
field,
|
|
641
|
+
index
|
|
642
|
+
})).sort((a, b) => rank(b.field) - rank(a.field) || a.index - b.index).map(({ field }) => field.name);
|
|
643
|
+
return {
|
|
644
|
+
kinds,
|
|
645
|
+
timeFields,
|
|
646
|
+
...defaultTimeField ? { defaultTimeField } : {},
|
|
647
|
+
dimensions
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function describeRuntimeReportSourceFields(fields, hints = {}) {
|
|
651
|
+
const plan = planRuntimeReportDimensions(fields, hints);
|
|
652
|
+
return {
|
|
653
|
+
fields: [...fields.values()].map((field) => {
|
|
654
|
+
const dimension = plan.kinds.get(field.name);
|
|
655
|
+
return {
|
|
656
|
+
name: field.name,
|
|
657
|
+
label: field.label,
|
|
658
|
+
type: field.type,
|
|
659
|
+
...field.format ? { format: field.format } : {},
|
|
660
|
+
...field.enum ? { enum: field.enum } : {},
|
|
661
|
+
...dimension ? { dimension } : {},
|
|
662
|
+
filterOperators: RUNTIME_REPORT_OPERATORS_BY_TYPE[field.type],
|
|
663
|
+
aggregates: runtimeReportAggregatesFor(field.type)
|
|
664
|
+
};
|
|
665
|
+
}),
|
|
666
|
+
timeFields: plan.timeFields,
|
|
667
|
+
...plan.defaultTimeField ? { defaultTimeField: plan.defaultTimeField } : {},
|
|
668
|
+
dimensions: plan.dimensions
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/runtime-memory.ts
|
|
673
|
+
var NON_NAMEABLE = /* @__PURE__ */ new Set(["tenantId", "tenant_id"]);
|
|
674
|
+
function resolveMemorySource(sources, id) {
|
|
675
|
+
const source = sources.find((candidate) => candidate.id === id);
|
|
676
|
+
if (!source) throw new RuntimeReportError("unknown_source", "spec.source", `'${id}' is not an available report source`);
|
|
677
|
+
return source;
|
|
678
|
+
}
|
|
679
|
+
function memoryFields(source) {
|
|
680
|
+
const { included, excluded } = resolveRuntimeReportFieldPolicy(source, new Set(source.schema.map((field) => field.name)));
|
|
681
|
+
const fields = /* @__PURE__ */ new Map();
|
|
682
|
+
for (const field of source.schema) {
|
|
683
|
+
if (field.name.startsWith("_") || NON_NAMEABLE.has(field.name) || excluded?.has(field.name) || included && !included.has(field.name) || fields.has(field.name)) continue;
|
|
684
|
+
fields.set(field.name, {
|
|
685
|
+
name: field.name,
|
|
686
|
+
type: field.type,
|
|
687
|
+
...field.format ? { format: field.format } : {},
|
|
688
|
+
...field.type === "string" && field.enum?.length ? { enum: field.enum } : {},
|
|
689
|
+
...field.type === "datetime" && field.format === "date" ? { businessDate: true } : {},
|
|
690
|
+
label: field.label ?? humanizeRuntimeReportName(field.name)
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return fields;
|
|
694
|
+
}
|
|
695
|
+
function describeRuntimeReportMemorySource(sources, sourceId) {
|
|
696
|
+
const source = resolveMemorySource(sources, sourceId);
|
|
697
|
+
return {
|
|
698
|
+
id: source.id,
|
|
699
|
+
label: source.label ?? humanizeRuntimeReportName(source.id),
|
|
700
|
+
...source.description ? { description: source.description } : {},
|
|
701
|
+
...describeRuntimeReportSourceFields(memoryFields(source), source)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function toDate(value) {
|
|
705
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
706
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
707
|
+
const parsed = new Date(value);
|
|
708
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
function readCell(row, field) {
|
|
713
|
+
const value = row[field.name];
|
|
714
|
+
if (value === null || value === void 0) return null;
|
|
715
|
+
switch (field.type) {
|
|
716
|
+
case "integer":
|
|
717
|
+
case "decimal": {
|
|
718
|
+
if (typeof value === "bigint") {
|
|
719
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) throw new RuntimeReportError("invalid_result", field.name, "integer value exceeds the JavaScript safe range");
|
|
720
|
+
return Number(value);
|
|
721
|
+
}
|
|
722
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
723
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
724
|
+
}
|
|
725
|
+
case "boolean":
|
|
726
|
+
if (value === true || value === 1 || value === "1" || value === "true") return true;
|
|
727
|
+
if (value === false || value === 0 || value === "0" || value === "false") return false;
|
|
728
|
+
return null;
|
|
729
|
+
case "datetime": return toDate(value);
|
|
730
|
+
default: return typeof value === "string" ? value : String(value);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function compareCells(a, b) {
|
|
734
|
+
const left = a instanceof Date ? a.getTime() : a;
|
|
735
|
+
const right = b instanceof Date ? b.getTime() : b;
|
|
736
|
+
if (typeof left === "boolean" || typeof right === "boolean") return Number(left) - Number(right);
|
|
737
|
+
if (left < right) return -1;
|
|
738
|
+
if (left > right) return 1;
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
function filterPredicate(filter, field, path) {
|
|
742
|
+
assertRuntimeReportFilterOperator(filter, field.type, path);
|
|
743
|
+
const read = (row) => readCell(row, field);
|
|
744
|
+
switch (filter.op) {
|
|
745
|
+
case "isNull": return (row) => read(row) === null;
|
|
746
|
+
case "isNotNull": return (row) => read(row) !== null;
|
|
747
|
+
case "in":
|
|
748
|
+
case "notIn": {
|
|
749
|
+
const values = filter.value.map((entry, index) => coerceRuntimeReportValue(field.type, entry, `${path}.value[${index}]`));
|
|
750
|
+
const member = (cell) => values.some((value) => compareCells(cell, value) === 0);
|
|
751
|
+
return filter.op === "in" ? (row) => {
|
|
752
|
+
const cell = read(row);
|
|
753
|
+
return cell !== null && member(cell);
|
|
754
|
+
} : (row) => {
|
|
755
|
+
const cell = read(row);
|
|
756
|
+
return cell !== null && !member(cell);
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
case "contains": {
|
|
760
|
+
const needle = coerceRuntimeReportValue(field.type, filter.value, `${path}.value`);
|
|
761
|
+
return (row) => {
|
|
762
|
+
const cell = read(row);
|
|
763
|
+
return typeof cell === "string" && cell.includes(needle);
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
default: {
|
|
767
|
+
const target = coerceRuntimeReportValue(field.type, filter.value, `${path}.value`);
|
|
768
|
+
const test = {
|
|
769
|
+
eq: (order) => order === 0,
|
|
770
|
+
ne: (order) => order !== 0,
|
|
771
|
+
gt: (order) => order > 0,
|
|
772
|
+
gte: (order) => order >= 0,
|
|
773
|
+
lt: (order) => order < 0,
|
|
774
|
+
lte: (order) => order <= 0
|
|
775
|
+
}[filter.op];
|
|
776
|
+
return (row) => {
|
|
777
|
+
const cell = read(row);
|
|
778
|
+
return cell !== null && test(compareCells(cell, target));
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function bucketStart(date, bucket) {
|
|
784
|
+
const y = date.getUTCFullYear();
|
|
785
|
+
const m = date.getUTCMonth();
|
|
786
|
+
const d = date.getUTCDate();
|
|
787
|
+
switch (bucket) {
|
|
788
|
+
case "minute": return new Date(Date.UTC(y, m, d, date.getUTCHours(), date.getUTCMinutes()));
|
|
789
|
+
case "hour": return new Date(Date.UTC(y, m, d, date.getUTCHours()));
|
|
790
|
+
case "day": return new Date(Date.UTC(y, m, d));
|
|
791
|
+
case "week": {
|
|
792
|
+
const daysSinceMonday = (date.getUTCDay() + 6) % 7;
|
|
793
|
+
return new Date(Date.UTC(y, m, d - daysSinceMonday));
|
|
794
|
+
}
|
|
795
|
+
case "month": return new Date(Date.UTC(y, m, 1));
|
|
796
|
+
case "quarter": return new Date(Date.UTC(y, Math.floor(m / 3) * 3, 1));
|
|
797
|
+
case "year": return new Date(Date.UTC(y, 0, 1));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function cellKey(cell) {
|
|
801
|
+
if (cell === null) return "n";
|
|
802
|
+
if (cell instanceof Date) return `d${cell.getTime()}`;
|
|
803
|
+
return `${typeof cell}:${String(cell)}`;
|
|
804
|
+
}
|
|
805
|
+
function aggregate(fn, cells, rowCount) {
|
|
806
|
+
if (cells === null) return rowCount;
|
|
807
|
+
const present = cells.filter((cell) => cell !== null);
|
|
808
|
+
switch (fn) {
|
|
809
|
+
case "count": return present.length;
|
|
810
|
+
case "countDistinct": return new Set(present.map(cellKey)).size;
|
|
811
|
+
case "sum":
|
|
812
|
+
if (present.length === 0) return null;
|
|
813
|
+
return present.reduce((total, value) => total + value, 0);
|
|
814
|
+
case "avg":
|
|
815
|
+
if (present.length === 0) return null;
|
|
816
|
+
return present.reduce((sum, value) => sum + value, 0) / present.length;
|
|
817
|
+
case "min":
|
|
818
|
+
case "max": {
|
|
819
|
+
if (present.length === 0) return null;
|
|
820
|
+
let best = present[0];
|
|
821
|
+
for (const cell of present.slice(1)) {
|
|
822
|
+
const order = compareCells(cell, best);
|
|
823
|
+
if (fn === "min" ? order < 0 : order > 0) best = cell;
|
|
824
|
+
}
|
|
825
|
+
return best;
|
|
826
|
+
}
|
|
827
|
+
default: return null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function havingHolds(entry, value) {
|
|
831
|
+
if (typeof value !== "number") return false;
|
|
832
|
+
switch (entry.op) {
|
|
833
|
+
case "eq": return value === entry.value;
|
|
834
|
+
case "ne": return value !== entry.value;
|
|
835
|
+
case "gt": return value > entry.value;
|
|
836
|
+
case "gte": return value >= entry.value;
|
|
837
|
+
case "lt": return value < entry.value;
|
|
838
|
+
case "lte": return value <= entry.value;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function compareForSort(a, b, direction) {
|
|
842
|
+
let order;
|
|
843
|
+
if (a === null && b === null) order = 0;
|
|
844
|
+
else if (a === null) order = -1;
|
|
845
|
+
else if (b === null) order = 1;
|
|
846
|
+
else order = compareCells(a, b);
|
|
847
|
+
return direction === "desc" ? -order : order;
|
|
848
|
+
}
|
|
849
|
+
async function runRuntimeReportInMemory(input, rows, options) {
|
|
850
|
+
const spec = parseRuntimeReportSpec(input);
|
|
851
|
+
const source = resolveMemorySource(options.sources, spec.source);
|
|
852
|
+
const fields = memoryFields(source);
|
|
853
|
+
const columns = planRuntimeReportColumns(spec, fields);
|
|
854
|
+
const lookup = (name, path) => {
|
|
855
|
+
const field = fields.get(name);
|
|
856
|
+
if (!field) unknownRuntimeReportField(path, name);
|
|
857
|
+
return field;
|
|
858
|
+
};
|
|
859
|
+
const predicates = [];
|
|
860
|
+
if (source.schema.some((field) => field.name === "deletedAt")) predicates.push((row) => row.deletedAt === null || row.deletedAt === void 0);
|
|
861
|
+
for (const [index, filter] of spec.filters.entries()) {
|
|
862
|
+
const path = `spec.filters[${index}]`;
|
|
863
|
+
predicates.push(filterPredicate(filter, lookup(filter.field, `${path}.field`), path));
|
|
864
|
+
}
|
|
865
|
+
const kept = rows.filter((row) => predicates.every((test) => test(row)));
|
|
866
|
+
const dimensionReaders = spec.dimensions.map((dimension) => {
|
|
867
|
+
const field = fields.get(dimension.field);
|
|
868
|
+
return (row) => {
|
|
869
|
+
const cell = readCell(row, field);
|
|
870
|
+
if (dimension.bucket && cell instanceof Date) return bucketStart(cell, dimension.bucket);
|
|
871
|
+
return cell;
|
|
872
|
+
};
|
|
873
|
+
});
|
|
874
|
+
const groups = /* @__PURE__ */ new Map();
|
|
875
|
+
if (spec.dimensions.length === 0) groups.set("", {
|
|
876
|
+
key: [],
|
|
877
|
+
rows: kept
|
|
878
|
+
});
|
|
879
|
+
else for (const row of kept) {
|
|
880
|
+
const key = dimensionReaders.map((read) => read(row));
|
|
881
|
+
const id = JSON.stringify(key.map(cellKey));
|
|
882
|
+
const group = groups.get(id);
|
|
883
|
+
if (group) group.rows.push(row);
|
|
884
|
+
else groups.set(id, {
|
|
885
|
+
key,
|
|
886
|
+
rows: [row]
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const measureFields = spec.measures.map((measure) => measure.field ? fields.get(measure.field) : null);
|
|
890
|
+
let results = [...groups.values()].map((group) => {
|
|
891
|
+
const values = {};
|
|
892
|
+
spec.dimensions.forEach((dimension, index) => {
|
|
893
|
+
values[dimension.as] = group.key[index];
|
|
894
|
+
});
|
|
895
|
+
spec.measures.forEach((measure, index) => {
|
|
896
|
+
const field = measureFields[index];
|
|
897
|
+
values[measure.as] = aggregate(measure.fn, field ? group.rows.map((row) => readCell(row, field)) : null, group.rows.length);
|
|
898
|
+
});
|
|
899
|
+
return values;
|
|
900
|
+
});
|
|
901
|
+
if (spec.having.length > 0) results = results.filter((values) => spec.having.every((entry) => havingHolds(entry, values[entry.measure])));
|
|
902
|
+
const sort = spec.sort.length > 0 ? spec.sort : spec.dimensions.length > 0 ? [{
|
|
903
|
+
by: spec.dimensions[0].as,
|
|
904
|
+
direction: "asc"
|
|
905
|
+
}] : [];
|
|
906
|
+
if (sort.length > 0) results = results.map((values, index) => ({
|
|
907
|
+
values,
|
|
908
|
+
index
|
|
909
|
+
})).sort((left, right) => {
|
|
910
|
+
for (const entry of sort) {
|
|
911
|
+
const order = compareForSort(left.values[entry.by], right.values[entry.by], entry.direction);
|
|
912
|
+
if (order !== 0) return order;
|
|
913
|
+
}
|
|
914
|
+
return left.index - right.index;
|
|
915
|
+
}).map(({ values }) => values);
|
|
916
|
+
const limit = Math.min(spec.limit, options.maxRows ?? RUNTIME_REPORT_LIMITS.maxRowLimit);
|
|
917
|
+
const truncated = results.length > limit;
|
|
918
|
+
const outRows = results.slice(0, limit).map((values) => {
|
|
919
|
+
const out = {};
|
|
920
|
+
for (const column of columns) out[column.id] = normalizeRuntimeReportCell(column, values[column.id]);
|
|
921
|
+
return out;
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
version: 1,
|
|
925
|
+
specHash: await runtimeReportSpecDigest(spec),
|
|
926
|
+
title: spec.title,
|
|
927
|
+
columns,
|
|
928
|
+
rows: outRows,
|
|
929
|
+
rowCount: outRows.length,
|
|
930
|
+
truncated,
|
|
931
|
+
ranAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
932
|
+
chart: spec.chart
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/runtime-time.ts
|
|
937
|
+
var DAY = 864e5;
|
|
938
|
+
var ymd = (time) => new Date(time).toISOString().slice(0, 10);
|
|
939
|
+
var MONTHS = [
|
|
940
|
+
"january",
|
|
941
|
+
"february",
|
|
942
|
+
"march",
|
|
943
|
+
"april",
|
|
944
|
+
"may",
|
|
945
|
+
"june",
|
|
946
|
+
"july",
|
|
947
|
+
"august",
|
|
948
|
+
"september",
|
|
949
|
+
"october",
|
|
950
|
+
"november",
|
|
951
|
+
"december"
|
|
952
|
+
];
|
|
953
|
+
var ORDINAL_QUARTERS = {
|
|
954
|
+
first: 1,
|
|
955
|
+
second: 2,
|
|
956
|
+
third: 3,
|
|
957
|
+
fourth: 4
|
|
958
|
+
};
|
|
959
|
+
var BUCKET_WORDS = [
|
|
960
|
+
["minute", /\b(?:minutely|(?:per|by|each|every|a) minute|minute by minute)\b/],
|
|
961
|
+
["hour", /\b(?:hourly|(?:per|by|each|every|an?) hour|hour by hour)\b/],
|
|
962
|
+
["day", /\b(?:daily|(?:per|by|each|every|a) day|day by day)\b/],
|
|
963
|
+
["week", /\b(?:weekly|(?:per|by|each|every|a) week|week by week|week over week)\b/],
|
|
964
|
+
["month", /\b(?:monthly|(?:per|by|each|every|a) month|month by month|month over month)\b/],
|
|
965
|
+
["quarter", /\b(?:quarterly|(?:per|by|each|every|a) quarter|quarter by quarter|quarter over quarter)\b/],
|
|
966
|
+
["year", /\b(?:yearly|annually|annual|(?:per|by|each|every|a) year|year by year|year over year)\b/]
|
|
967
|
+
];
|
|
968
|
+
var TREND_WORDS = /\b(?:over time|trends?|trending|timeline|history)\b/;
|
|
969
|
+
function runtimeReportBucketForSpan(days) {
|
|
970
|
+
if (days === null) return "month";
|
|
971
|
+
if (days <= 31) return "day";
|
|
972
|
+
if (days <= 92) return "week";
|
|
973
|
+
if (days <= 3 * 366) return "month";
|
|
974
|
+
return "quarter";
|
|
975
|
+
}
|
|
976
|
+
function span(range) {
|
|
977
|
+
return Math.round((Date.parse(`${range.to}T00:00:00Z`) - Date.parse(`${range.from}T00:00:00Z`)) / DAY);
|
|
978
|
+
}
|
|
979
|
+
function resolveRuntimeReportTimeRange(phrase, now) {
|
|
980
|
+
const text = phrase.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
|
|
981
|
+
const y = now.getUTCFullYear();
|
|
982
|
+
const m = now.getUTCMonth();
|
|
983
|
+
const today = Date.UTC(y, m, now.getUTCDate());
|
|
984
|
+
const tomorrow = today + DAY;
|
|
985
|
+
const monday = today - (now.getUTCDay() + 6) % 7 * DAY;
|
|
986
|
+
const quarter = Math.floor(m / 3) * 3;
|
|
987
|
+
const range = (from, to, label) => ({
|
|
988
|
+
from: ymd(from),
|
|
989
|
+
to: ymd(to),
|
|
990
|
+
label
|
|
991
|
+
});
|
|
992
|
+
const recent = /\b(?:last|past|previous|trailing)\s+(\d{1,4})\s+(day|week|month|year)s?\b/.exec(text);
|
|
993
|
+
if (recent) {
|
|
994
|
+
const n = Number(recent[1]);
|
|
995
|
+
const unit = recent[2];
|
|
996
|
+
if (n > 0) return range(unit === "day" ? tomorrow - n * DAY : unit === "week" ? tomorrow - n * 7 * DAY : unit === "month" ? Date.UTC(y, m - n, now.getUTCDate() + 1) : Date.UTC(y - n, m, now.getUTCDate() + 1), tomorrow, `last ${n} ${unit}${n === 1 ? "" : "s"}`);
|
|
997
|
+
}
|
|
998
|
+
const q = /\bq([1-4])(?:\s*(?:of\s*)?((?:19|20)\d{2}))?\b/.exec(text) ?? /\b(first|second|third|fourth) quarter(?: (?:of )?((?:19|20)\d{2}))?\b/.exec(text);
|
|
999
|
+
if (q) {
|
|
1000
|
+
const n = /^\d$/.test(q[1]) ? Number(q[1]) : ORDINAL_QUARTERS[q[1]];
|
|
1001
|
+
const startMonth = (n - 1) * 3;
|
|
1002
|
+
const year2 = q[2] ? Number(q[2]) : Date.UTC(y, startMonth, 1) <= today ? y : y - 1;
|
|
1003
|
+
return range(Date.UTC(year2, startMonth, 1), Date.UTC(year2, startMonth + 3, 1), `Q${n} ${year2}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (/\b(?:year to date|ytd)\b/.test(text)) return range(Date.UTC(y, 0, 1), tomorrow, "year to date");
|
|
1006
|
+
if (/\b(?:month to date|mtd)\b/.test(text)) return range(Date.UTC(y, m, 1), tomorrow, "month to date");
|
|
1007
|
+
if (/\btoday\b/.test(text)) return range(today, tomorrow, "today");
|
|
1008
|
+
if (/\byesterday\b/.test(text)) return range(today - DAY, today, "yesterday");
|
|
1009
|
+
const relative = /\b(this|current|last|previous|past) (week|month|quarter|year)\b/.exec(text);
|
|
1010
|
+
if (relative) {
|
|
1011
|
+
const last = relative[1] !== "this" && relative[1] !== "current";
|
|
1012
|
+
const unit = relative[2];
|
|
1013
|
+
const label = `${last ? "last" : "this"} ${unit}`;
|
|
1014
|
+
switch (unit) {
|
|
1015
|
+
case "week": return last ? range(monday - 7 * DAY, monday, label) : range(monday, monday + 7 * DAY, label);
|
|
1016
|
+
case "month": return last ? range(Date.UTC(y, m - 1, 1), Date.UTC(y, m, 1), label) : range(Date.UTC(y, m, 1), Date.UTC(y, m + 1, 1), label);
|
|
1017
|
+
case "quarter": return last ? range(Date.UTC(y, quarter - 3, 1), Date.UTC(y, quarter, 1), label) : range(Date.UTC(y, quarter, 1), Date.UTC(y, quarter + 3, 1), label);
|
|
1018
|
+
default: return last ? range(Date.UTC(y - 1, 0, 1), Date.UTC(y, 0, 1), label) : range(Date.UTC(y, 0, 1), Date.UTC(y + 1, 0, 1), label);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const month = new RegExp(`\\b(${MONTHS.join("|")})(?: ((?:19|20)\\d{2}))?\\b`).exec(text);
|
|
1022
|
+
if (month && (month[1] !== "may" || month[2])) {
|
|
1023
|
+
const match = month;
|
|
1024
|
+
const index = MONTHS.indexOf(match[1]);
|
|
1025
|
+
const year2 = match[2] ? Number(match[2]) : Date.UTC(y, index, 1) <= today ? y : y - 1;
|
|
1026
|
+
const name = match[1].charAt(0).toUpperCase() + match[1].slice(1);
|
|
1027
|
+
return range(Date.UTC(year2, index, 1), Date.UTC(year2, index + 1, 1), `${name} ${year2}`);
|
|
1028
|
+
}
|
|
1029
|
+
const year = /\b(?:in|for|during)\s+((?:19|20)\d{2})\b/.exec(text) ?? /^\s*((?:19|20)\d{2})\s*$/.exec(text);
|
|
1030
|
+
if (year) {
|
|
1031
|
+
const n = Number(year[1]);
|
|
1032
|
+
return range(Date.UTC(n, 0, 1), Date.UTC(n + 1, 0, 1), String(n));
|
|
1033
|
+
}
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
function parseRuntimeReportTime(text, now) {
|
|
1037
|
+
const words = text.toLowerCase();
|
|
1038
|
+
const range = resolveRuntimeReportTimeRange(words, now) ?? void 0;
|
|
1039
|
+
let bucket = BUCKET_WORDS.find(([, pattern]) => pattern.test(words))?.[0];
|
|
1040
|
+
const trend = TREND_WORDS.test(words);
|
|
1041
|
+
if (!bucket && trend) bucket = runtimeReportBucketForSpan(range ? span(range) : null);
|
|
1042
|
+
const mentionsTime = Boolean(bucket || range || trend) || /\b(?:date|dates|time|when|minute|hour|day|week|month|quarter|year)s?\b/.test(words);
|
|
1043
|
+
return {
|
|
1044
|
+
...bucket ? { bucket } : {},
|
|
1045
|
+
...range ? { range } : {},
|
|
1046
|
+
mentionsTime
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
export { runtimeReportSpecDigest as C, parseRuntimeReportSpec as S, RUNTIME_REPORT_HAVING_OPS as _, runRuntimeReportInMemory as a, RUNTIME_REPORT_SPEC_VERSION as b, coerceRuntimeReportValue as c, planRuntimeReportColumns as d, resolveRuntimeReportFieldPolicy as f, RUNTIME_REPORT_FILTER_OPS as g, RUNTIME_REPORT_CHART_TYPES as h, describeRuntimeReportMemorySource as i, humanizeRuntimeReportName as l, RUNTIME_REPORT_BUCKETS as m, resolveRuntimeReportTimeRange as n, describeRuntimeReportSourceFields as o, unknownRuntimeReportField as p, runtimeReportBucketForSpan as r, assertRuntimeReportFilterOperator as s, parseRuntimeReportTime as t, normalizeRuntimeReportCell as u, RUNTIME_REPORT_LIMITS as v, serializeRuntimeReportSpec as w, RuntimeReportError as x, RUNTIME_REPORT_MEASURE_FNS as y };
|
|
1051
|
+
|
|
1052
|
+
//# sourceMappingURL=runtime-time-D7JZ1e4J.js.map
|