metrickit 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/cache-redis.d.ts +10 -0
- package/dist/cache-redis.js +24 -0
- package/dist/cache-utils.d.ts +5 -0
- package/dist/cache.d.ts +18 -0
- package/dist/catalog.d.ts +12 -0
- package/dist/define-metric.d.ts +37 -0
- package/dist/engine.d.ts +200 -0
- package/dist/filters/default-metadata.d.ts +28 -0
- package/dist/filters/index.d.ts +3 -0
- package/dist/filters/parse.d.ts +7 -0
- package/dist/filters/types.d.ts +19 -0
- package/dist/frontend/catalog.d.ts +24 -0
- package/dist/frontend/dashboard.d.ts +12 -0
- package/dist/frontend/format.d.ts +10 -0
- package/dist/frontend/index.d.ts +10 -0
- package/dist/frontend/markers.d.ts +19 -0
- package/dist/frontend/renderers.d.ts +14 -0
- package/dist/frontend/requests.d.ts +17 -0
- package/dist/frontend/stream-state.d.ts +14 -0
- package/dist/frontend/time.d.ts +5 -0
- package/dist/frontend/transport.d.ts +19 -0
- package/dist/frontend/types.d.ts +108 -0
- package/dist/frontend.d.ts +1 -0
- package/dist/frontend.js +752 -0
- package/dist/helpers/clickhouse.d.ts +27 -0
- package/dist/helpers/distribution.d.ts +15 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/metric-type.d.ts +21 -0
- package/dist/helpers/pivot.d.ts +15 -0
- package/dist/helpers/prisma.d.ts +20 -0
- package/dist/helpers/timeseries.d.ts +6 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +668 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1322 -0
- package/dist/orpc.d.ts +36 -0
- package/dist/orpc.js +1157 -0
- package/dist/registry.d.ts +269 -0
- package/dist/run-metrics.d.ts +14 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/inputs.d.ts +19 -0
- package/dist/schemas/metric-type.d.ts +7 -0
- package/dist/schemas/output.d.ts +842 -0
- package/dist/schemas/time.d.ts +24 -0
- package/dist/time.d.ts +6 -0
- package/dist/type-guards.d.ts +7 -0
- package/package.json +91 -0
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
// src/time.ts
|
|
2
|
+
function getPreviousPeriod(range) {
|
|
3
|
+
if (!range.from || !range.to)
|
|
4
|
+
return null;
|
|
5
|
+
const durationMs = range.to.getTime() - range.from.getTime();
|
|
6
|
+
return {
|
|
7
|
+
from: new Date(range.from.getTime() - durationMs),
|
|
8
|
+
to: new Date(range.from.getTime())
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function getBucketStart(date, granularity) {
|
|
12
|
+
const d = new Date(date);
|
|
13
|
+
switch (granularity) {
|
|
14
|
+
case "hour":
|
|
15
|
+
d.setUTCMinutes(0, 0, 0);
|
|
16
|
+
break;
|
|
17
|
+
case "day":
|
|
18
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
19
|
+
break;
|
|
20
|
+
case "week": {
|
|
21
|
+
const day = d.getUTCDay();
|
|
22
|
+
const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1);
|
|
23
|
+
d.setUTCDate(diff);
|
|
24
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case "month":
|
|
28
|
+
d.setUTCDate(1);
|
|
29
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
30
|
+
break;
|
|
31
|
+
case "quarter": {
|
|
32
|
+
const month = d.getUTCMonth();
|
|
33
|
+
const quarterStart = month - month % 3;
|
|
34
|
+
d.setUTCMonth(quarterStart, 1);
|
|
35
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "year":
|
|
39
|
+
d.setUTCMonth(0, 1);
|
|
40
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
function normalizeUserDate(date) {
|
|
46
|
+
const d = new Date(date);
|
|
47
|
+
const hours = d.getUTCHours();
|
|
48
|
+
if (hours >= 20) {
|
|
49
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
50
|
+
}
|
|
51
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
52
|
+
return d;
|
|
53
|
+
}
|
|
54
|
+
function generateBuckets(from, to, granularity) {
|
|
55
|
+
const buckets = [];
|
|
56
|
+
const normalizedFrom = granularity === "hour" ? from : normalizeUserDate(from);
|
|
57
|
+
const normalizedTo = granularity === "hour" ? to : normalizeUserDate(to);
|
|
58
|
+
let current = getBucketStart(normalizedFrom, granularity);
|
|
59
|
+
while (current <= normalizedTo) {
|
|
60
|
+
buckets.push(new Date(current));
|
|
61
|
+
current = getNextBucket(current, granularity);
|
|
62
|
+
}
|
|
63
|
+
return buckets;
|
|
64
|
+
}
|
|
65
|
+
function getNextBucket(date, granularity) {
|
|
66
|
+
const d = new Date(date);
|
|
67
|
+
switch (granularity) {
|
|
68
|
+
case "hour":
|
|
69
|
+
d.setUTCHours(d.getUTCHours() + 1);
|
|
70
|
+
break;
|
|
71
|
+
case "day":
|
|
72
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
73
|
+
break;
|
|
74
|
+
case "week":
|
|
75
|
+
d.setUTCDate(d.getUTCDate() + 7);
|
|
76
|
+
break;
|
|
77
|
+
case "month":
|
|
78
|
+
d.setUTCMonth(d.getUTCMonth() + 1);
|
|
79
|
+
break;
|
|
80
|
+
case "quarter":
|
|
81
|
+
d.setUTCMonth(d.getUTCMonth() + 3);
|
|
82
|
+
break;
|
|
83
|
+
case "year":
|
|
84
|
+
d.setUTCFullYear(d.getUTCFullYear() + 1);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
return d;
|
|
88
|
+
}
|
|
89
|
+
function inferGranularity(range) {
|
|
90
|
+
if (!range.from || !range.to)
|
|
91
|
+
return "day";
|
|
92
|
+
const diffMs = range.to.getTime() - range.from.getTime();
|
|
93
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
94
|
+
if (diffDays <= 2)
|
|
95
|
+
return "hour";
|
|
96
|
+
if (diffDays <= 31)
|
|
97
|
+
return "day";
|
|
98
|
+
if (diffDays <= 90)
|
|
99
|
+
return "week";
|
|
100
|
+
if (diffDays <= 365)
|
|
101
|
+
return "month";
|
|
102
|
+
if (diffDays <= 730)
|
|
103
|
+
return "quarter";
|
|
104
|
+
return "year";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/helpers/clickhouse.ts
|
|
108
|
+
var CLICKHOUSE_GRANULARITY_FUNCTIONS = {
|
|
109
|
+
hour: "toStartOfHour",
|
|
110
|
+
day: "toStartOfDay",
|
|
111
|
+
week: "toStartOfWeek",
|
|
112
|
+
month: "toStartOfMonth",
|
|
113
|
+
quarter: "toStartOfQuarter",
|
|
114
|
+
year: "toStartOfYear"
|
|
115
|
+
};
|
|
116
|
+
function getClickhouseGranularity(granularity) {
|
|
117
|
+
return CLICKHOUSE_GRANULARITY_FUNCTIONS[granularity];
|
|
118
|
+
}
|
|
119
|
+
function buildClickhouseBucketExpr(columnName, granularity) {
|
|
120
|
+
const fn = CLICKHOUSE_GRANULARITY_FUNCTIONS[granularity];
|
|
121
|
+
if (granularity === "week") {
|
|
122
|
+
return `${fn}(${columnName}, 1)`;
|
|
123
|
+
}
|
|
124
|
+
return `${fn}(${columnName})`;
|
|
125
|
+
}
|
|
126
|
+
function toClickhouseDatetime(date, boundary) {
|
|
127
|
+
let d = date;
|
|
128
|
+
if (boundary === "start") {
|
|
129
|
+
d = normalizeUserDate(date);
|
|
130
|
+
} else if (boundary === "end") {
|
|
131
|
+
d = new Date(date.getTime() + 24 * 60 * 60 * 1000 - 1000);
|
|
132
|
+
}
|
|
133
|
+
return d.toISOString().slice(0, 19).replace("T", " ");
|
|
134
|
+
}
|
|
135
|
+
function buildClickhouseTimeParams(filters) {
|
|
136
|
+
return {
|
|
137
|
+
...filters.from ? { from: toClickhouseDatetime(filters.from) } : {},
|
|
138
|
+
...filters.to ? { to: toClickhouseDatetime(filters.to) } : {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function buildClickhouseTimeWhere(columnName, options) {
|
|
142
|
+
const parts = [];
|
|
143
|
+
if (options.from) {
|
|
144
|
+
parts.push(`${columnName} >= {from:DateTime}`);
|
|
145
|
+
}
|
|
146
|
+
if (options.to) {
|
|
147
|
+
parts.push(`${columnName} <= {to:DateTime}`);
|
|
148
|
+
}
|
|
149
|
+
return parts.join(" AND ");
|
|
150
|
+
}
|
|
151
|
+
function parseClickhouseBucketToTimestamp(bucket) {
|
|
152
|
+
const isoString = `${bucket.replace(" ", "T")}Z`;
|
|
153
|
+
return new Date(isoString).getTime();
|
|
154
|
+
}
|
|
155
|
+
// src/helpers/prisma.ts
|
|
156
|
+
function buildTimeRangeWhere(filters) {
|
|
157
|
+
if (!filters.from && !filters.to) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...filters.from ? { gte: filters.from } : {},
|
|
162
|
+
...filters.to ? { lte: filters.to } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function buildTimeRangeWhereObject(columnName, filters) {
|
|
166
|
+
const timeRange = buildTimeRangeWhere(filters);
|
|
167
|
+
if (!timeRange) {
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
return { [columnName]: timeRange };
|
|
171
|
+
}
|
|
172
|
+
function buildScopedTimeWhere(idColumn, timeColumn, filters) {
|
|
173
|
+
const idWhere = filters.scopeIds?.length ? { [idColumn]: { in: filters.scopeIds } } : filters.scopeId ? { [idColumn]: filters.scopeId } : {};
|
|
174
|
+
return {
|
|
175
|
+
...idWhere,
|
|
176
|
+
...buildTimeRangeWhereObject(timeColumn, filters)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// src/helpers/distribution.ts
|
|
180
|
+
function calculateDistribution(segments, options) {
|
|
181
|
+
const total = segments.reduce((sum, s) => sum + s.value, 0);
|
|
182
|
+
return {
|
|
183
|
+
kind: "distribution",
|
|
184
|
+
total,
|
|
185
|
+
segments: segments.map((s) => ({
|
|
186
|
+
key: s.key,
|
|
187
|
+
label: s.label,
|
|
188
|
+
value: s.value,
|
|
189
|
+
percent: total > 0 ? s.value / total * 100 : 0
|
|
190
|
+
})),
|
|
191
|
+
chartType: options?.chartType
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function calculateDistributionFromGroups(groups, keyField, labelMap = {}, options) {
|
|
195
|
+
return calculateDistribution(groups.map((g) => ({
|
|
196
|
+
key: g[keyField],
|
|
197
|
+
label: labelMap[g[keyField]] ?? g[keyField],
|
|
198
|
+
value: g._count._all
|
|
199
|
+
})), options);
|
|
200
|
+
}
|
|
201
|
+
// src/helpers/timeseries.ts
|
|
202
|
+
function mapBucketsToPoints(buckets, rows, valueExtractor) {
|
|
203
|
+
const dataMap = new Map;
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
if (row.bucket) {
|
|
206
|
+
const ts = parseClickhouseBucketToTimestamp(row.bucket);
|
|
207
|
+
dataMap.set(ts, row);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return buckets.map((ts) => ({
|
|
211
|
+
ts,
|
|
212
|
+
value: dataMap.has(ts.getTime()) ? valueExtractor(dataMap.get(ts.getTime())) : 0
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
function createEmptyPoints(from, to, granularity) {
|
|
216
|
+
const buckets = generateBuckets(from, to, granularity);
|
|
217
|
+
return buckets.map((ts) => ({ ts, value: 0 }));
|
|
218
|
+
}
|
|
219
|
+
// src/helpers/metric-type.ts
|
|
220
|
+
import { typedSwitch } from "typedswitch";
|
|
221
|
+
function resolveMetricType(metric, defaultMetric) {
|
|
222
|
+
return metric ?? defaultMetric;
|
|
223
|
+
}
|
|
224
|
+
function calculateMetricModeValue(args) {
|
|
225
|
+
const value = typedSwitch(resolveMetricType(args.metric, args.defaultMetric), {
|
|
226
|
+
TOTAL: () => args.total,
|
|
227
|
+
AVG: () => args.averageDivisor > 0 ? args.total / args.averageDivisor : 0,
|
|
228
|
+
PER_BUCKET: () => args.bucketCount && args.bucketCount > 0 ? args.total / args.bucketCount : 0
|
|
229
|
+
});
|
|
230
|
+
return args.postProcess ? args.postProcess(value) : value;
|
|
231
|
+
}
|
|
232
|
+
function getMetricModeLabel(args) {
|
|
233
|
+
return args.labels[resolveMetricType(args.metric, args.defaultMetric)];
|
|
234
|
+
}
|
|
235
|
+
function getBucketCountForMetricMode(args) {
|
|
236
|
+
if (resolveMetricType(args.metric, args.defaultMetric) !== "PER_BUCKET") {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
return Math.max(1, generateBuckets(args.from, args.to, inferGranularity({ from: args.from, to: args.to })).length);
|
|
240
|
+
}
|
|
241
|
+
// src/schemas/time.ts
|
|
242
|
+
import { z } from "zod";
|
|
243
|
+
var TimeGranularitySchema = z.enum([
|
|
244
|
+
"hour",
|
|
245
|
+
"day",
|
|
246
|
+
"week",
|
|
247
|
+
"month",
|
|
248
|
+
"quarter",
|
|
249
|
+
"year"
|
|
250
|
+
]);
|
|
251
|
+
var TimeRangeSchema = z.object({
|
|
252
|
+
from: z.date().optional(),
|
|
253
|
+
to: z.date().optional()
|
|
254
|
+
});
|
|
255
|
+
var RequiredTimeRangeSchema = z.object({
|
|
256
|
+
from: z.date(),
|
|
257
|
+
to: z.date()
|
|
258
|
+
});
|
|
259
|
+
var CompareSchema = z.object({
|
|
260
|
+
compareToPrevious: z.boolean().optional().default(false)
|
|
261
|
+
});
|
|
262
|
+
// src/schemas/output.ts
|
|
263
|
+
import { z as z2 } from "zod";
|
|
264
|
+
var OutputKindSchema = z2.enum([
|
|
265
|
+
"kpi",
|
|
266
|
+
"timeseries",
|
|
267
|
+
"distribution",
|
|
268
|
+
"table",
|
|
269
|
+
"leaderboard",
|
|
270
|
+
"pivot"
|
|
271
|
+
]);
|
|
272
|
+
var MetricUnitSchema = z2.enum([
|
|
273
|
+
"DKK",
|
|
274
|
+
"EUR",
|
|
275
|
+
"USD",
|
|
276
|
+
"GBP",
|
|
277
|
+
"SEK",
|
|
278
|
+
"NOK",
|
|
279
|
+
"PERCENTAGE"
|
|
280
|
+
]);
|
|
281
|
+
var KpiOutputSchema = z2.object({
|
|
282
|
+
kind: z2.literal("kpi"),
|
|
283
|
+
value: z2.number(),
|
|
284
|
+
label: z2.string().optional(),
|
|
285
|
+
unit: MetricUnitSchema.optional(),
|
|
286
|
+
prefix: z2.string().optional(),
|
|
287
|
+
suffix: z2.string().optional(),
|
|
288
|
+
trend: z2.enum(["up", "down", "flat"]).optional()
|
|
289
|
+
});
|
|
290
|
+
function defineKpiOutput(output) {
|
|
291
|
+
return {
|
|
292
|
+
kind: "kpi",
|
|
293
|
+
...output
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
var TimeSeriesPointSchema = z2.object({
|
|
297
|
+
ts: z2.date(),
|
|
298
|
+
value: z2.number(),
|
|
299
|
+
label: z2.string().optional()
|
|
300
|
+
});
|
|
301
|
+
var TimeSeriesSeriesSchema = z2.object({
|
|
302
|
+
key: z2.string(),
|
|
303
|
+
label: z2.string().optional(),
|
|
304
|
+
points: z2.array(TimeSeriesPointSchema),
|
|
305
|
+
chartType: z2.enum(["line", "bar"]).optional(),
|
|
306
|
+
axis: z2.enum(["left", "right"]).optional(),
|
|
307
|
+
meta: z2.record(z2.string(), z2.unknown()).optional()
|
|
308
|
+
});
|
|
309
|
+
var TimeSeriesOutputSchema = z2.object({
|
|
310
|
+
kind: z2.literal("timeseries"),
|
|
311
|
+
granularity: TimeGranularitySchema,
|
|
312
|
+
series: z2.array(TimeSeriesSeriesSchema)
|
|
313
|
+
});
|
|
314
|
+
function defineTimeSeriesOutput(output) {
|
|
315
|
+
if (Array.isArray(output.series)) {
|
|
316
|
+
return {
|
|
317
|
+
kind: "timeseries",
|
|
318
|
+
...output,
|
|
319
|
+
series: [...output.series]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
kind: "timeseries",
|
|
324
|
+
...output,
|
|
325
|
+
series: Object.entries(output.series).map(([key, series]) => ({
|
|
326
|
+
key,
|
|
327
|
+
...series
|
|
328
|
+
}))
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
var DistributionSegmentSchema = z2.object({
|
|
332
|
+
key: z2.string(),
|
|
333
|
+
label: z2.string(),
|
|
334
|
+
value: z2.number(),
|
|
335
|
+
percent: z2.number().optional()
|
|
336
|
+
});
|
|
337
|
+
var DistributionChartTypeSchema = z2.enum([
|
|
338
|
+
"bar",
|
|
339
|
+
"donut",
|
|
340
|
+
"pie",
|
|
341
|
+
"funnel"
|
|
342
|
+
]);
|
|
343
|
+
var DistributionOutputSchema = z2.object({
|
|
344
|
+
kind: z2.literal("distribution"),
|
|
345
|
+
total: z2.number(),
|
|
346
|
+
segments: z2.array(DistributionSegmentSchema),
|
|
347
|
+
chartType: DistributionChartTypeSchema.optional()
|
|
348
|
+
});
|
|
349
|
+
function defineDistributionOutput(output) {
|
|
350
|
+
return {
|
|
351
|
+
kind: "distribution",
|
|
352
|
+
...output,
|
|
353
|
+
segments: [...output.segments]
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
var TableColumnSchema = z2.object({
|
|
357
|
+
key: z2.string(),
|
|
358
|
+
label: z2.string(),
|
|
359
|
+
type: z2.enum(["string", "number", "date", "boolean"]).optional(),
|
|
360
|
+
nullable: z2.boolean().optional()
|
|
361
|
+
});
|
|
362
|
+
var TableOutputSchema = z2.object({
|
|
363
|
+
kind: z2.literal("table"),
|
|
364
|
+
columns: z2.array(TableColumnSchema),
|
|
365
|
+
rows: z2.array(z2.record(z2.string(), z2.unknown())),
|
|
366
|
+
total: z2.number().optional()
|
|
367
|
+
});
|
|
368
|
+
function defineTableOutput(output) {
|
|
369
|
+
return {
|
|
370
|
+
kind: "table",
|
|
371
|
+
...output,
|
|
372
|
+
columns: [...output.columns],
|
|
373
|
+
rows: [...output.rows]
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
var PivotDimensionSchema = z2.object({
|
|
377
|
+
key: z2.string(),
|
|
378
|
+
label: z2.string()
|
|
379
|
+
});
|
|
380
|
+
var PivotTotalsSchema = z2.object({
|
|
381
|
+
rowTotals: z2.array(z2.number()).optional(),
|
|
382
|
+
columnTotals: z2.array(z2.number()).optional(),
|
|
383
|
+
grandTotal: z2.number().optional()
|
|
384
|
+
});
|
|
385
|
+
function addGrandTotalMismatchIssue(ctx, totalsName, totals, grandTotal) {
|
|
386
|
+
const totalsSum = totals?.reduce((total, value) => total + value, 0);
|
|
387
|
+
if (totalsSum === undefined)
|
|
388
|
+
return;
|
|
389
|
+
if (Math.abs(totalsSum - grandTotal) <= Number.EPSILON)
|
|
390
|
+
return;
|
|
391
|
+
ctx.addIssue({
|
|
392
|
+
code: z2.ZodIssueCode.custom,
|
|
393
|
+
path: ["totals", "grandTotal"],
|
|
394
|
+
message: `grandTotal must equal the sum of ${totalsName}`
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function clonePivotTotals(totals) {
|
|
398
|
+
if (!totals)
|
|
399
|
+
return;
|
|
400
|
+
return {
|
|
401
|
+
rowTotals: totals.rowTotals ? [...totals.rowTotals] : undefined,
|
|
402
|
+
columnTotals: totals.columnTotals ? [...totals.columnTotals] : undefined,
|
|
403
|
+
grandTotal: totals.grandTotal
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function clonePivotCellTooltips(cellTooltips) {
|
|
407
|
+
if (!cellTooltips)
|
|
408
|
+
return;
|
|
409
|
+
return cellTooltips.map((row) => [...row]);
|
|
410
|
+
}
|
|
411
|
+
var PivotOutputSchema = z2.object({
|
|
412
|
+
kind: z2.literal("pivot"),
|
|
413
|
+
rowDimension: PivotDimensionSchema,
|
|
414
|
+
columnDimension: PivotDimensionSchema,
|
|
415
|
+
rows: z2.array(z2.string()),
|
|
416
|
+
columns: z2.array(z2.string()),
|
|
417
|
+
values: z2.array(z2.array(z2.number())),
|
|
418
|
+
cellTooltips: z2.array(z2.array(z2.string())).optional(),
|
|
419
|
+
totals: PivotTotalsSchema.optional()
|
|
420
|
+
}).superRefine((pivot, ctx) => {
|
|
421
|
+
const rowCount = pivot.rows.length;
|
|
422
|
+
const columnCount = pivot.columns.length;
|
|
423
|
+
if (pivot.values.length !== rowCount) {
|
|
424
|
+
ctx.addIssue({
|
|
425
|
+
code: z2.ZodIssueCode.custom,
|
|
426
|
+
path: ["values"],
|
|
427
|
+
message: `Pivot values must have ${rowCount} rows`
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
for (const [rowIndex, row] of pivot.values.entries()) {
|
|
431
|
+
if (row.length !== columnCount) {
|
|
432
|
+
ctx.addIssue({
|
|
433
|
+
code: z2.ZodIssueCode.custom,
|
|
434
|
+
path: ["values", rowIndex],
|
|
435
|
+
message: `Pivot row ${rowIndex} must have ${columnCount} columns`
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (pivot.cellTooltips !== undefined) {
|
|
440
|
+
if (pivot.cellTooltips.length !== rowCount) {
|
|
441
|
+
ctx.addIssue({
|
|
442
|
+
code: z2.ZodIssueCode.custom,
|
|
443
|
+
path: ["cellTooltips"],
|
|
444
|
+
message: `cellTooltips must have ${rowCount} rows`
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
for (const [rowIndex, row] of pivot.cellTooltips.entries()) {
|
|
448
|
+
if (row.length !== columnCount) {
|
|
449
|
+
ctx.addIssue({
|
|
450
|
+
code: z2.ZodIssueCode.custom,
|
|
451
|
+
path: ["cellTooltips", rowIndex],
|
|
452
|
+
message: `cellTooltips row ${rowIndex} must have ${columnCount} columns`
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (pivot.totals?.rowTotals && pivot.totals.rowTotals.length !== rowCount) {
|
|
458
|
+
ctx.addIssue({
|
|
459
|
+
code: z2.ZodIssueCode.custom,
|
|
460
|
+
path: ["totals", "rowTotals"],
|
|
461
|
+
message: `rowTotals must have ${rowCount} entries`
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (pivot.totals?.columnTotals && pivot.totals.columnTotals.length !== columnCount) {
|
|
465
|
+
ctx.addIssue({
|
|
466
|
+
code: z2.ZodIssueCode.custom,
|
|
467
|
+
path: ["totals", "columnTotals"],
|
|
468
|
+
message: `columnTotals must have ${columnCount} entries`
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (pivot.totals?.grandTotal !== undefined) {
|
|
472
|
+
addGrandTotalMismatchIssue(ctx, "rowTotals", pivot.totals.rowTotals, pivot.totals.grandTotal);
|
|
473
|
+
addGrandTotalMismatchIssue(ctx, "columnTotals", pivot.totals.columnTotals, pivot.totals.grandTotal);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
function definePivotOutput(output) {
|
|
477
|
+
return {
|
|
478
|
+
kind: "pivot",
|
|
479
|
+
...output,
|
|
480
|
+
rows: [...output.rows],
|
|
481
|
+
columns: [...output.columns],
|
|
482
|
+
values: output.values.map((row) => [...row]),
|
|
483
|
+
cellTooltips: clonePivotCellTooltips(output.cellTooltips),
|
|
484
|
+
totals: clonePivotTotals(output.totals)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
var LeaderboardItemSchema = z2.object({
|
|
488
|
+
rank: z2.number(),
|
|
489
|
+
id: z2.string(),
|
|
490
|
+
label: z2.string(),
|
|
491
|
+
value: z2.number(),
|
|
492
|
+
meta: z2.record(z2.string(), z2.unknown()).optional()
|
|
493
|
+
});
|
|
494
|
+
var LeaderboardOutputSchema = z2.object({
|
|
495
|
+
kind: z2.literal("leaderboard"),
|
|
496
|
+
items: z2.array(LeaderboardItemSchema),
|
|
497
|
+
total: z2.number().optional()
|
|
498
|
+
});
|
|
499
|
+
function defineLeaderboardOutput(output) {
|
|
500
|
+
return {
|
|
501
|
+
kind: "leaderboard",
|
|
502
|
+
...output,
|
|
503
|
+
items: [...output.items]
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function defineMetricOutput(kind, output) {
|
|
507
|
+
const handlers = {
|
|
508
|
+
kpi: () => defineKpiOutput(output),
|
|
509
|
+
timeseries: () => defineTimeSeriesOutput(output),
|
|
510
|
+
distribution: () => defineDistributionOutput(output),
|
|
511
|
+
table: () => defineTableOutput(output),
|
|
512
|
+
leaderboard: () => defineLeaderboardOutput(output),
|
|
513
|
+
pivot: () => definePivotOutput(output)
|
|
514
|
+
};
|
|
515
|
+
return handlers[kind]();
|
|
516
|
+
}
|
|
517
|
+
function validateOutput(kind, output) {
|
|
518
|
+
return OutputSchemaMap[kind].parse(output);
|
|
519
|
+
}
|
|
520
|
+
var MetricOutputSchema = z2.discriminatedUnion("kind", [
|
|
521
|
+
KpiOutputSchema,
|
|
522
|
+
TimeSeriesOutputSchema,
|
|
523
|
+
DistributionOutputSchema,
|
|
524
|
+
TableOutputSchema,
|
|
525
|
+
LeaderboardOutputSchema,
|
|
526
|
+
PivotOutputSchema
|
|
527
|
+
]);
|
|
528
|
+
var OutputSchemaMap = {
|
|
529
|
+
kpi: KpiOutputSchema,
|
|
530
|
+
timeseries: TimeSeriesOutputSchema,
|
|
531
|
+
distribution: DistributionOutputSchema,
|
|
532
|
+
table: TableOutputSchema,
|
|
533
|
+
leaderboard: LeaderboardOutputSchema,
|
|
534
|
+
pivot: PivotOutputSchema
|
|
535
|
+
};
|
|
536
|
+
var MetricExecutionCacheStatusSchema = z2.enum([
|
|
537
|
+
"hit",
|
|
538
|
+
"partialHit",
|
|
539
|
+
"miss",
|
|
540
|
+
"bypassed"
|
|
541
|
+
]);
|
|
542
|
+
var MetricExecutionSchema = z2.object({
|
|
543
|
+
cacheStatus: MetricExecutionCacheStatusSchema,
|
|
544
|
+
durationMs: z2.number().nonnegative(),
|
|
545
|
+
granularity: TimeGranularitySchema.optional()
|
|
546
|
+
});
|
|
547
|
+
var MetricResultSchema = z2.object({
|
|
548
|
+
current: MetricOutputSchema,
|
|
549
|
+
previous: MetricOutputSchema.optional(),
|
|
550
|
+
supportsTimeRange: z2.boolean(),
|
|
551
|
+
execution: MetricExecutionSchema.optional()
|
|
552
|
+
});
|
|
553
|
+
// src/schemas/inputs.ts
|
|
554
|
+
import { z as z3 } from "zod";
|
|
555
|
+
var BaseFiltersSchema = z3.object({
|
|
556
|
+
organizationIds: z3.array(z3.string()).optional()
|
|
557
|
+
}).merge(TimeRangeSchema);
|
|
558
|
+
var TimeSeriesFiltersSchema = z3.object({
|
|
559
|
+
organizationIds: z3.array(z3.string()).optional()
|
|
560
|
+
}).merge(RequiredTimeRangeSchema);
|
|
561
|
+
function extendBaseFilters(shape) {
|
|
562
|
+
const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
|
|
563
|
+
key,
|
|
564
|
+
schema.optional()
|
|
565
|
+
]));
|
|
566
|
+
return BaseFiltersSchema.extend(optionalShape);
|
|
567
|
+
}
|
|
568
|
+
function extendTimeSeriesFilters(shape) {
|
|
569
|
+
const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
|
|
570
|
+
key,
|
|
571
|
+
schema.optional()
|
|
572
|
+
]));
|
|
573
|
+
return TimeSeriesFiltersSchema.extend(optionalShape);
|
|
574
|
+
}
|
|
575
|
+
// src/schemas/metric-type.ts
|
|
576
|
+
import { z as z4 } from "zod";
|
|
577
|
+
var MetricTypeSchema = z4.enum(["TOTAL", "AVG", "PER_BUCKET"]);
|
|
578
|
+
// src/helpers/pivot.ts
|
|
579
|
+
function createPivotMatrix(rowCount, columnCount) {
|
|
580
|
+
return Array.from({ length: rowCount }, () => Array.from({ length: columnCount }, () => 0));
|
|
581
|
+
}
|
|
582
|
+
function addPivotCell(values, rowIndex, columnIndex, delta) {
|
|
583
|
+
const row = values[rowIndex];
|
|
584
|
+
if (!row)
|
|
585
|
+
return;
|
|
586
|
+
if (columnIndex < 0 || columnIndex >= row.length)
|
|
587
|
+
return;
|
|
588
|
+
row[columnIndex] = (row[columnIndex] ?? 0) + delta;
|
|
589
|
+
}
|
|
590
|
+
function computePivotTotals(values) {
|
|
591
|
+
const rowTotals = values.map((row) => row.reduce((acc, value) => acc + value, 0));
|
|
592
|
+
const columnCount = values[0]?.length ?? 0;
|
|
593
|
+
const columnTotals = Array.from({ length: columnCount }, (_, columnIndex) => values.reduce((acc, row) => acc + (row[columnIndex] ?? 0), 0));
|
|
594
|
+
const grandTotal = rowTotals.reduce((acc, value) => acc + value, 0);
|
|
595
|
+
return {
|
|
596
|
+
rowTotals,
|
|
597
|
+
columnTotals,
|
|
598
|
+
grandTotal
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function formatPivotBucketLabel(bucket, granularity) {
|
|
602
|
+
const year = bucket.getUTCFullYear();
|
|
603
|
+
const month = String(bucket.getUTCMonth() + 1).padStart(2, "0");
|
|
604
|
+
const day = String(bucket.getUTCDate()).padStart(2, "0");
|
|
605
|
+
const hour = String(bucket.getUTCHours()).padStart(2, "0");
|
|
606
|
+
switch (granularity) {
|
|
607
|
+
case "hour":
|
|
608
|
+
return `${year}-${month}-${day} ${hour}:00`;
|
|
609
|
+
case "day":
|
|
610
|
+
return `${year}-${month}-${day}`;
|
|
611
|
+
case "week": {
|
|
612
|
+
const { week, weekYear } = getIsoWeekAndYear(bucket);
|
|
613
|
+
return `Week ${week} ${weekYear}`;
|
|
614
|
+
}
|
|
615
|
+
case "month":
|
|
616
|
+
return `${year}-${month}`;
|
|
617
|
+
case "quarter": {
|
|
618
|
+
const quarter = Math.floor(bucket.getUTCMonth() / 3) + 1;
|
|
619
|
+
return `Q${quarter} ${year}`;
|
|
620
|
+
}
|
|
621
|
+
case "year":
|
|
622
|
+
return `${year}`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function getIsoWeekAndYear(date) {
|
|
626
|
+
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
627
|
+
const day = d.getUTCDay() || 7;
|
|
628
|
+
d.setUTCDate(d.getUTCDate() + 4 - day);
|
|
629
|
+
const weekYear = d.getUTCFullYear();
|
|
630
|
+
const yearStart = new Date(Date.UTC(weekYear, 0, 1));
|
|
631
|
+
const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
632
|
+
return { week, weekYear };
|
|
633
|
+
}
|
|
634
|
+
function buildPivotOutput(args) {
|
|
635
|
+
return definePivotOutput({
|
|
636
|
+
rowDimension: args.rowDimension,
|
|
637
|
+
columnDimension: args.columnDimension,
|
|
638
|
+
rows: args.rows,
|
|
639
|
+
columns: args.columns,
|
|
640
|
+
values: args.values,
|
|
641
|
+
cellTooltips: args.cellTooltips,
|
|
642
|
+
totals: args.totals ?? computePivotTotals(args.values)
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
export {
|
|
646
|
+
toClickhouseDatetime,
|
|
647
|
+
resolveMetricType,
|
|
648
|
+
parseClickhouseBucketToTimestamp,
|
|
649
|
+
mapBucketsToPoints,
|
|
650
|
+
getMetricModeLabel,
|
|
651
|
+
getClickhouseGranularity,
|
|
652
|
+
getBucketCountForMetricMode,
|
|
653
|
+
formatPivotBucketLabel,
|
|
654
|
+
createPivotMatrix,
|
|
655
|
+
createEmptyPoints,
|
|
656
|
+
computePivotTotals,
|
|
657
|
+
calculateMetricModeValue,
|
|
658
|
+
calculateDistributionFromGroups,
|
|
659
|
+
calculateDistribution,
|
|
660
|
+
buildTimeRangeWhereObject,
|
|
661
|
+
buildTimeRangeWhere,
|
|
662
|
+
buildScopedTimeWhere,
|
|
663
|
+
buildPivotOutput,
|
|
664
|
+
buildClickhouseTimeWhere,
|
|
665
|
+
buildClickhouseTimeParams,
|
|
666
|
+
buildClickhouseBucketExpr,
|
|
667
|
+
addPivotCell
|
|
668
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createMetricsEngine, type MetricKindSchemaMap, type MetricsEngineConfig } from './engine.ts';
|
|
2
|
+
export { createRegistry, getMetric, type AnyRegistry, type InferBaseFilters, type InferRegistryMetrics, type InferAvailableMetricKey, type GetMetricByKey, type MetricOutputFor, type MetricFiltersFor, type MetricKindFor, type MetricSeriesKeysFor, type MetricRequestFiltersFor, type MetricRequestFor, type MetricResultChunkFor, type MetricsExecutionResult, type MetricsRequestFor, type IsValidMetricKey, type MetricKeysOfKind } from './registry.ts';
|
|
3
|
+
export { defineKpiMetric, defineTimeSeriesMetric, defineDistributionMetric, defineTableMetric, defineLeaderboardMetric, definePivotMetric, getOutputSchema, validateMetricOutput, defineMetricWithSchema, type MetricDefinition, type MetricDefinitionInput, type MetricFilterFieldMetadataMap, type AnyMetricDefinition, type MetricKey, type MetricOutput as MetricOutputFromDef } from './define-metric.ts';
|
|
4
|
+
export { runMetrics, runMetricsStream, type MetricsRequest, type MetricsResult, type MetricResultChunk, type RunMetricsOptions } from './run-metrics.ts';
|
|
5
|
+
export { noopCacheAdapter, parseCache, type CacheAdapter } from './cache.ts';
|
|
6
|
+
export { redisCacheAdapter, type RedisLikeClient, type RedisPipeline } from './cache-redis.ts';
|
|
7
|
+
export * from './schemas/index.ts';
|
|
8
|
+
export { getPreviousPeriod, getBucketStart, normalizeUserDate, generateBuckets, inferGranularity } from './time.ts';
|
|
9
|
+
export { isKpi, isTimeSeries, isDistribution, isTable, isLeaderboard, isPivot } from './type-guards.ts';
|
|
10
|
+
export { defineMetricCatalogMetadata, type MetricCatalogMetadata, type MetricCatalogFreshness, type MetricCatalogSource } from './catalog.ts';
|
|
11
|
+
export * from './filters/index.ts';
|