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.
Files changed (50) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +379 -0
  4. package/dist/cache-redis.d.ts +10 -0
  5. package/dist/cache-redis.js +24 -0
  6. package/dist/cache-utils.d.ts +5 -0
  7. package/dist/cache.d.ts +18 -0
  8. package/dist/catalog.d.ts +12 -0
  9. package/dist/define-metric.d.ts +37 -0
  10. package/dist/engine.d.ts +200 -0
  11. package/dist/filters/default-metadata.d.ts +28 -0
  12. package/dist/filters/index.d.ts +3 -0
  13. package/dist/filters/parse.d.ts +7 -0
  14. package/dist/filters/types.d.ts +19 -0
  15. package/dist/frontend/catalog.d.ts +24 -0
  16. package/dist/frontend/dashboard.d.ts +12 -0
  17. package/dist/frontend/format.d.ts +10 -0
  18. package/dist/frontend/index.d.ts +10 -0
  19. package/dist/frontend/markers.d.ts +19 -0
  20. package/dist/frontend/renderers.d.ts +14 -0
  21. package/dist/frontend/requests.d.ts +17 -0
  22. package/dist/frontend/stream-state.d.ts +14 -0
  23. package/dist/frontend/time.d.ts +5 -0
  24. package/dist/frontend/transport.d.ts +19 -0
  25. package/dist/frontend/types.d.ts +108 -0
  26. package/dist/frontend.d.ts +1 -0
  27. package/dist/frontend.js +752 -0
  28. package/dist/helpers/clickhouse.d.ts +27 -0
  29. package/dist/helpers/distribution.d.ts +15 -0
  30. package/dist/helpers/index.d.ts +6 -0
  31. package/dist/helpers/metric-type.d.ts +21 -0
  32. package/dist/helpers/pivot.d.ts +15 -0
  33. package/dist/helpers/prisma.d.ts +20 -0
  34. package/dist/helpers/timeseries.d.ts +6 -0
  35. package/dist/helpers.d.ts +1 -0
  36. package/dist/helpers.js +668 -0
  37. package/dist/index.d.ts +11 -0
  38. package/dist/index.js +1322 -0
  39. package/dist/orpc.d.ts +36 -0
  40. package/dist/orpc.js +1157 -0
  41. package/dist/registry.d.ts +269 -0
  42. package/dist/run-metrics.d.ts +14 -0
  43. package/dist/schemas/index.d.ts +4 -0
  44. package/dist/schemas/inputs.d.ts +19 -0
  45. package/dist/schemas/metric-type.d.ts +7 -0
  46. package/dist/schemas/output.d.ts +842 -0
  47. package/dist/schemas/time.d.ts +24 -0
  48. package/dist/time.d.ts +6 -0
  49. package/dist/type-guards.d.ts +7 -0
  50. package/package.json +91 -0
package/dist/orpc.js ADDED
@@ -0,0 +1,1157 @@
1
+ // src/schemas/time.ts
2
+ import { z } from "zod";
3
+ var TimeGranularitySchema = z.enum([
4
+ "hour",
5
+ "day",
6
+ "week",
7
+ "month",
8
+ "quarter",
9
+ "year"
10
+ ]);
11
+ var TimeRangeSchema = z.object({
12
+ from: z.date().optional(),
13
+ to: z.date().optional()
14
+ });
15
+ var RequiredTimeRangeSchema = z.object({
16
+ from: z.date(),
17
+ to: z.date()
18
+ });
19
+ var CompareSchema = z.object({
20
+ compareToPrevious: z.boolean().optional().default(false)
21
+ });
22
+ // src/schemas/output.ts
23
+ import { z as z2 } from "zod";
24
+ var OutputKindSchema = z2.enum([
25
+ "kpi",
26
+ "timeseries",
27
+ "distribution",
28
+ "table",
29
+ "leaderboard",
30
+ "pivot"
31
+ ]);
32
+ var MetricUnitSchema = z2.enum([
33
+ "DKK",
34
+ "EUR",
35
+ "USD",
36
+ "GBP",
37
+ "SEK",
38
+ "NOK",
39
+ "PERCENTAGE"
40
+ ]);
41
+ var KpiOutputSchema = z2.object({
42
+ kind: z2.literal("kpi"),
43
+ value: z2.number(),
44
+ label: z2.string().optional(),
45
+ unit: MetricUnitSchema.optional(),
46
+ prefix: z2.string().optional(),
47
+ suffix: z2.string().optional(),
48
+ trend: z2.enum(["up", "down", "flat"]).optional()
49
+ });
50
+ function defineKpiOutput(output) {
51
+ return {
52
+ kind: "kpi",
53
+ ...output
54
+ };
55
+ }
56
+ var TimeSeriesPointSchema = z2.object({
57
+ ts: z2.date(),
58
+ value: z2.number(),
59
+ label: z2.string().optional()
60
+ });
61
+ var TimeSeriesSeriesSchema = z2.object({
62
+ key: z2.string(),
63
+ label: z2.string().optional(),
64
+ points: z2.array(TimeSeriesPointSchema),
65
+ chartType: z2.enum(["line", "bar"]).optional(),
66
+ axis: z2.enum(["left", "right"]).optional(),
67
+ meta: z2.record(z2.string(), z2.unknown()).optional()
68
+ });
69
+ var TimeSeriesOutputSchema = z2.object({
70
+ kind: z2.literal("timeseries"),
71
+ granularity: TimeGranularitySchema,
72
+ series: z2.array(TimeSeriesSeriesSchema)
73
+ });
74
+ function defineTimeSeriesOutput(output) {
75
+ if (Array.isArray(output.series)) {
76
+ return {
77
+ kind: "timeseries",
78
+ ...output,
79
+ series: [...output.series]
80
+ };
81
+ }
82
+ return {
83
+ kind: "timeseries",
84
+ ...output,
85
+ series: Object.entries(output.series).map(([key, series]) => ({
86
+ key,
87
+ ...series
88
+ }))
89
+ };
90
+ }
91
+ var DistributionSegmentSchema = z2.object({
92
+ key: z2.string(),
93
+ label: z2.string(),
94
+ value: z2.number(),
95
+ percent: z2.number().optional()
96
+ });
97
+ var DistributionChartTypeSchema = z2.enum([
98
+ "bar",
99
+ "donut",
100
+ "pie",
101
+ "funnel"
102
+ ]);
103
+ var DistributionOutputSchema = z2.object({
104
+ kind: z2.literal("distribution"),
105
+ total: z2.number(),
106
+ segments: z2.array(DistributionSegmentSchema),
107
+ chartType: DistributionChartTypeSchema.optional()
108
+ });
109
+ function defineDistributionOutput(output) {
110
+ return {
111
+ kind: "distribution",
112
+ ...output,
113
+ segments: [...output.segments]
114
+ };
115
+ }
116
+ var TableColumnSchema = z2.object({
117
+ key: z2.string(),
118
+ label: z2.string(),
119
+ type: z2.enum(["string", "number", "date", "boolean"]).optional(),
120
+ nullable: z2.boolean().optional()
121
+ });
122
+ var TableOutputSchema = z2.object({
123
+ kind: z2.literal("table"),
124
+ columns: z2.array(TableColumnSchema),
125
+ rows: z2.array(z2.record(z2.string(), z2.unknown())),
126
+ total: z2.number().optional()
127
+ });
128
+ function defineTableOutput(output) {
129
+ return {
130
+ kind: "table",
131
+ ...output,
132
+ columns: [...output.columns],
133
+ rows: [...output.rows]
134
+ };
135
+ }
136
+ var PivotDimensionSchema = z2.object({
137
+ key: z2.string(),
138
+ label: z2.string()
139
+ });
140
+ var PivotTotalsSchema = z2.object({
141
+ rowTotals: z2.array(z2.number()).optional(),
142
+ columnTotals: z2.array(z2.number()).optional(),
143
+ grandTotal: z2.number().optional()
144
+ });
145
+ function addGrandTotalMismatchIssue(ctx, totalsName, totals, grandTotal) {
146
+ const totalsSum = totals?.reduce((total, value) => total + value, 0);
147
+ if (totalsSum === undefined)
148
+ return;
149
+ if (Math.abs(totalsSum - grandTotal) <= Number.EPSILON)
150
+ return;
151
+ ctx.addIssue({
152
+ code: z2.ZodIssueCode.custom,
153
+ path: ["totals", "grandTotal"],
154
+ message: `grandTotal must equal the sum of ${totalsName}`
155
+ });
156
+ }
157
+ function clonePivotTotals(totals) {
158
+ if (!totals)
159
+ return;
160
+ return {
161
+ rowTotals: totals.rowTotals ? [...totals.rowTotals] : undefined,
162
+ columnTotals: totals.columnTotals ? [...totals.columnTotals] : undefined,
163
+ grandTotal: totals.grandTotal
164
+ };
165
+ }
166
+ function clonePivotCellTooltips(cellTooltips) {
167
+ if (!cellTooltips)
168
+ return;
169
+ return cellTooltips.map((row) => [...row]);
170
+ }
171
+ var PivotOutputSchema = z2.object({
172
+ kind: z2.literal("pivot"),
173
+ rowDimension: PivotDimensionSchema,
174
+ columnDimension: PivotDimensionSchema,
175
+ rows: z2.array(z2.string()),
176
+ columns: z2.array(z2.string()),
177
+ values: z2.array(z2.array(z2.number())),
178
+ cellTooltips: z2.array(z2.array(z2.string())).optional(),
179
+ totals: PivotTotalsSchema.optional()
180
+ }).superRefine((pivot, ctx) => {
181
+ const rowCount = pivot.rows.length;
182
+ const columnCount = pivot.columns.length;
183
+ if (pivot.values.length !== rowCount) {
184
+ ctx.addIssue({
185
+ code: z2.ZodIssueCode.custom,
186
+ path: ["values"],
187
+ message: `Pivot values must have ${rowCount} rows`
188
+ });
189
+ }
190
+ for (const [rowIndex, row] of pivot.values.entries()) {
191
+ if (row.length !== columnCount) {
192
+ ctx.addIssue({
193
+ code: z2.ZodIssueCode.custom,
194
+ path: ["values", rowIndex],
195
+ message: `Pivot row ${rowIndex} must have ${columnCount} columns`
196
+ });
197
+ }
198
+ }
199
+ if (pivot.cellTooltips !== undefined) {
200
+ if (pivot.cellTooltips.length !== rowCount) {
201
+ ctx.addIssue({
202
+ code: z2.ZodIssueCode.custom,
203
+ path: ["cellTooltips"],
204
+ message: `cellTooltips must have ${rowCount} rows`
205
+ });
206
+ }
207
+ for (const [rowIndex, row] of pivot.cellTooltips.entries()) {
208
+ if (row.length !== columnCount) {
209
+ ctx.addIssue({
210
+ code: z2.ZodIssueCode.custom,
211
+ path: ["cellTooltips", rowIndex],
212
+ message: `cellTooltips row ${rowIndex} must have ${columnCount} columns`
213
+ });
214
+ }
215
+ }
216
+ }
217
+ if (pivot.totals?.rowTotals && pivot.totals.rowTotals.length !== rowCount) {
218
+ ctx.addIssue({
219
+ code: z2.ZodIssueCode.custom,
220
+ path: ["totals", "rowTotals"],
221
+ message: `rowTotals must have ${rowCount} entries`
222
+ });
223
+ }
224
+ if (pivot.totals?.columnTotals && pivot.totals.columnTotals.length !== columnCount) {
225
+ ctx.addIssue({
226
+ code: z2.ZodIssueCode.custom,
227
+ path: ["totals", "columnTotals"],
228
+ message: `columnTotals must have ${columnCount} entries`
229
+ });
230
+ }
231
+ if (pivot.totals?.grandTotal !== undefined) {
232
+ addGrandTotalMismatchIssue(ctx, "rowTotals", pivot.totals.rowTotals, pivot.totals.grandTotal);
233
+ addGrandTotalMismatchIssue(ctx, "columnTotals", pivot.totals.columnTotals, pivot.totals.grandTotal);
234
+ }
235
+ });
236
+ function definePivotOutput(output) {
237
+ return {
238
+ kind: "pivot",
239
+ ...output,
240
+ rows: [...output.rows],
241
+ columns: [...output.columns],
242
+ values: output.values.map((row) => [...row]),
243
+ cellTooltips: clonePivotCellTooltips(output.cellTooltips),
244
+ totals: clonePivotTotals(output.totals)
245
+ };
246
+ }
247
+ var LeaderboardItemSchema = z2.object({
248
+ rank: z2.number(),
249
+ id: z2.string(),
250
+ label: z2.string(),
251
+ value: z2.number(),
252
+ meta: z2.record(z2.string(), z2.unknown()).optional()
253
+ });
254
+ var LeaderboardOutputSchema = z2.object({
255
+ kind: z2.literal("leaderboard"),
256
+ items: z2.array(LeaderboardItemSchema),
257
+ total: z2.number().optional()
258
+ });
259
+ function defineLeaderboardOutput(output) {
260
+ return {
261
+ kind: "leaderboard",
262
+ ...output,
263
+ items: [...output.items]
264
+ };
265
+ }
266
+ function defineMetricOutput(kind, output) {
267
+ const handlers = {
268
+ kpi: () => defineKpiOutput(output),
269
+ timeseries: () => defineTimeSeriesOutput(output),
270
+ distribution: () => defineDistributionOutput(output),
271
+ table: () => defineTableOutput(output),
272
+ leaderboard: () => defineLeaderboardOutput(output),
273
+ pivot: () => definePivotOutput(output)
274
+ };
275
+ return handlers[kind]();
276
+ }
277
+ function validateOutput(kind, output) {
278
+ return OutputSchemaMap[kind].parse(output);
279
+ }
280
+ var MetricOutputSchema = z2.discriminatedUnion("kind", [
281
+ KpiOutputSchema,
282
+ TimeSeriesOutputSchema,
283
+ DistributionOutputSchema,
284
+ TableOutputSchema,
285
+ LeaderboardOutputSchema,
286
+ PivotOutputSchema
287
+ ]);
288
+ var OutputSchemaMap = {
289
+ kpi: KpiOutputSchema,
290
+ timeseries: TimeSeriesOutputSchema,
291
+ distribution: DistributionOutputSchema,
292
+ table: TableOutputSchema,
293
+ leaderboard: LeaderboardOutputSchema,
294
+ pivot: PivotOutputSchema
295
+ };
296
+ var MetricExecutionCacheStatusSchema = z2.enum([
297
+ "hit",
298
+ "partialHit",
299
+ "miss",
300
+ "bypassed"
301
+ ]);
302
+ var MetricExecutionSchema = z2.object({
303
+ cacheStatus: MetricExecutionCacheStatusSchema,
304
+ durationMs: z2.number().nonnegative(),
305
+ granularity: TimeGranularitySchema.optional()
306
+ });
307
+ var MetricResultSchema = z2.object({
308
+ current: MetricOutputSchema,
309
+ previous: MetricOutputSchema.optional(),
310
+ supportsTimeRange: z2.boolean(),
311
+ execution: MetricExecutionSchema.optional()
312
+ });
313
+ // src/schemas/inputs.ts
314
+ import { z as z3 } from "zod";
315
+ var BaseFiltersSchema = z3.object({
316
+ organizationIds: z3.array(z3.string()).optional()
317
+ }).merge(TimeRangeSchema);
318
+ var TimeSeriesFiltersSchema = z3.object({
319
+ organizationIds: z3.array(z3.string()).optional()
320
+ }).merge(RequiredTimeRangeSchema);
321
+ function extendBaseFilters(shape) {
322
+ const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
323
+ key,
324
+ schema.optional()
325
+ ]));
326
+ return BaseFiltersSchema.extend(optionalShape);
327
+ }
328
+ function extendTimeSeriesFilters(shape) {
329
+ const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
330
+ key,
331
+ schema.optional()
332
+ ]));
333
+ return TimeSeriesFiltersSchema.extend(optionalShape);
334
+ }
335
+ // src/schemas/metric-type.ts
336
+ import { z as z4 } from "zod";
337
+ var MetricTypeSchema = z4.enum(["TOTAL", "AVG", "PER_BUCKET"]);
338
+ // src/define-metric.ts
339
+ function defineMetricWithSchema(kind, outputSchema, def) {
340
+ return {
341
+ kind,
342
+ outputSchema,
343
+ ...def
344
+ };
345
+ }
346
+ function defineKpiMetric(def) {
347
+ return defineMetricWithSchema("kpi", KpiOutputSchema, def);
348
+ }
349
+ function defineTimeSeriesMetric(def) {
350
+ return defineMetricWithSchema("timeseries", TimeSeriesOutputSchema, def);
351
+ }
352
+ function defineDistributionMetric(def) {
353
+ return defineMetricWithSchema("distribution", DistributionOutputSchema, def);
354
+ }
355
+ function defineTableMetric(def) {
356
+ return defineMetricWithSchema("table", TableOutputSchema, def);
357
+ }
358
+ function defineLeaderboardMetric(def) {
359
+ return defineMetricWithSchema("leaderboard", LeaderboardOutputSchema, def);
360
+ }
361
+ function definePivotMetric(def) {
362
+ return defineMetricWithSchema("pivot", PivotOutputSchema, def);
363
+ }
364
+ function getOutputSchema(metric) {
365
+ return metric.outputSchema;
366
+ }
367
+ function validateMetricOutput(metric, output2) {
368
+ return getOutputSchema(metric).parse(output2);
369
+ }
370
+
371
+ // src/cache.ts
372
+ var noopCacheAdapter = {
373
+ async mget(keys) {
374
+ return keys.map(() => null);
375
+ },
376
+ async mset() {}
377
+ };
378
+ function parseCache(cached, schema) {
379
+ if (!cached)
380
+ return null;
381
+ try {
382
+ const parsed = JSON.parse(cached);
383
+ const result = schema.safeParse(parsed);
384
+ if (result.success) {
385
+ return result.data;
386
+ }
387
+ return null;
388
+ } catch {
389
+ return null;
390
+ }
391
+ }
392
+
393
+ // src/cache-utils.ts
394
+ function stableHash(obj) {
395
+ const stable = JSON.stringify(obj, Object.keys(obj).sort());
396
+ let h = 2166136261;
397
+ for (let i = 0;i < stable.length; i++) {
398
+ h ^= stable.charCodeAt(i);
399
+ h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
400
+ }
401
+ return (h >>> 0).toString(16);
402
+ }
403
+ function normalizeForCache(obj) {
404
+ const result = {};
405
+ for (const [key, value] of Object.entries(obj)) {
406
+ if (value === undefined)
407
+ continue;
408
+ if (value instanceof Date) {
409
+ result[key] = value.toISOString();
410
+ } else if (Array.isArray(value)) {
411
+ result[key] = value.map((v) => v instanceof Date ? v.toISOString() : v);
412
+ } else if (typeof value === "object" && value !== null) {
413
+ const nested = normalizeForCache(value);
414
+ for (const [nk, nv] of Object.entries(nested)) {
415
+ result[`${key}.${nk}`] = nv;
416
+ }
417
+ } else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
418
+ result[key] = value;
419
+ }
420
+ }
421
+ return result;
422
+ }
423
+ function getCacheKey(metricKey, filters, period = "current", granularity) {
424
+ const normalized = normalizeForCache({
425
+ ...filters,
426
+ granularity
427
+ });
428
+ const hash = stableHash(normalized);
429
+ return `metrics:${metricKey}:${period}:${hash}`;
430
+ }
431
+
432
+ // src/time.ts
433
+ function getPreviousPeriod(range) {
434
+ if (!range.from || !range.to)
435
+ return null;
436
+ const durationMs = range.to.getTime() - range.from.getTime();
437
+ return {
438
+ from: new Date(range.from.getTime() - durationMs),
439
+ to: new Date(range.from.getTime())
440
+ };
441
+ }
442
+ function getBucketStart(date, granularity) {
443
+ const d = new Date(date);
444
+ switch (granularity) {
445
+ case "hour":
446
+ d.setUTCMinutes(0, 0, 0);
447
+ break;
448
+ case "day":
449
+ d.setUTCHours(0, 0, 0, 0);
450
+ break;
451
+ case "week": {
452
+ const day = d.getUTCDay();
453
+ const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1);
454
+ d.setUTCDate(diff);
455
+ d.setUTCHours(0, 0, 0, 0);
456
+ break;
457
+ }
458
+ case "month":
459
+ d.setUTCDate(1);
460
+ d.setUTCHours(0, 0, 0, 0);
461
+ break;
462
+ case "quarter": {
463
+ const month = d.getUTCMonth();
464
+ const quarterStart = month - month % 3;
465
+ d.setUTCMonth(quarterStart, 1);
466
+ d.setUTCHours(0, 0, 0, 0);
467
+ break;
468
+ }
469
+ case "year":
470
+ d.setUTCMonth(0, 1);
471
+ d.setUTCHours(0, 0, 0, 0);
472
+ break;
473
+ }
474
+ return d;
475
+ }
476
+ function normalizeUserDate(date) {
477
+ const d = new Date(date);
478
+ const hours = d.getUTCHours();
479
+ if (hours >= 20) {
480
+ d.setUTCDate(d.getUTCDate() + 1);
481
+ }
482
+ d.setUTCHours(0, 0, 0, 0);
483
+ return d;
484
+ }
485
+ function generateBuckets(from, to, granularity) {
486
+ const buckets = [];
487
+ const normalizedFrom = granularity === "hour" ? from : normalizeUserDate(from);
488
+ const normalizedTo = granularity === "hour" ? to : normalizeUserDate(to);
489
+ let current = getBucketStart(normalizedFrom, granularity);
490
+ while (current <= normalizedTo) {
491
+ buckets.push(new Date(current));
492
+ current = getNextBucket(current, granularity);
493
+ }
494
+ return buckets;
495
+ }
496
+ function getNextBucket(date, granularity) {
497
+ const d = new Date(date);
498
+ switch (granularity) {
499
+ case "hour":
500
+ d.setUTCHours(d.getUTCHours() + 1);
501
+ break;
502
+ case "day":
503
+ d.setUTCDate(d.getUTCDate() + 1);
504
+ break;
505
+ case "week":
506
+ d.setUTCDate(d.getUTCDate() + 7);
507
+ break;
508
+ case "month":
509
+ d.setUTCMonth(d.getUTCMonth() + 1);
510
+ break;
511
+ case "quarter":
512
+ d.setUTCMonth(d.getUTCMonth() + 3);
513
+ break;
514
+ case "year":
515
+ d.setUTCFullYear(d.getUTCFullYear() + 1);
516
+ break;
517
+ }
518
+ return d;
519
+ }
520
+ function inferGranularity(range) {
521
+ if (!range.from || !range.to)
522
+ return "day";
523
+ const diffMs = range.to.getTime() - range.from.getTime();
524
+ const diffDays = diffMs / (1000 * 60 * 60 * 24);
525
+ if (diffDays <= 2)
526
+ return "hour";
527
+ if (diffDays <= 31)
528
+ return "day";
529
+ if (diffDays <= 90)
530
+ return "week";
531
+ if (diffDays <= 365)
532
+ return "month";
533
+ if (diffDays <= 730)
534
+ return "quarter";
535
+ return "year";
536
+ }
537
+
538
+ // src/run-metrics.ts
539
+ function shiftTimeSeriesTimestamps(output2, offsetMs) {
540
+ if (!output2 || typeof output2 !== "object" || !("kind" in output2) || output2.kind !== "timeseries") {
541
+ return output2;
542
+ }
543
+ const timeseriesOutput = output2;
544
+ return {
545
+ ...timeseriesOutput,
546
+ series: timeseriesOutput.series.map((s) => ({
547
+ ...s,
548
+ points: s.points.map((p) => ({
549
+ ...p,
550
+ ts: new Date(p.ts.getTime() + offsetMs)
551
+ }))
552
+ }))
553
+ };
554
+ }
555
+ async function runSingleMetric(ctx) {
556
+ const { metric, filters, granularity, compareToPrevious, resolverCtx } = ctx;
557
+ const current = await metric.resolve({
558
+ filters: metric.filterSchema.parse(filters),
559
+ ctx: { ...resolverCtx, granularity }
560
+ });
561
+ let previous;
562
+ if (compareToPrevious && metric.supportsTimeRange && filters.from && filters.to) {
563
+ const prevRange = getPreviousPeriod({ from: filters.from, to: filters.to });
564
+ if (prevRange) {
565
+ const prevFilters = {
566
+ ...filters,
567
+ from: prevRange.from,
568
+ to: prevRange.to
569
+ };
570
+ const prevResult = await metric.resolve({
571
+ filters: metric.filterSchema.parse(prevFilters),
572
+ ctx: { ...resolverCtx, granularity }
573
+ });
574
+ const periodOffset = filters.from.getTime() - prevRange.from.getTime();
575
+ previous = shiftTimeSeriesTimestamps(prevResult, periodOffset);
576
+ }
577
+ }
578
+ return { current, previous, supportsTimeRange: metric.supportsTimeRange };
579
+ }
580
+ async function prepareMetrics(options) {
581
+ const {
582
+ registry,
583
+ createContext,
584
+ cache = noopCacheAdapter,
585
+ hasAccess
586
+ } = options;
587
+ const request = registry.MetricsRequestSchema.parse(options.request);
588
+ const disableCache = request.disableCache ?? false;
589
+ const userCtx = await createContext();
590
+ const granularity = request.granularity ?? inferGranularity({ from: request.from, to: request.to });
591
+ const resolverCtx = { ...userCtx, granularity };
592
+ const cacheRequests = [];
593
+ const validMetrics = [];
594
+ const errors = {};
595
+ for (const metricReq of request.metrics) {
596
+ const metric = registry.getMetricByKey(metricReq.key);
597
+ const requestKey = metricReq.requestKey ?? metricReq.key;
598
+ if (!metric) {
599
+ errors[requestKey] = `Unknown metric: ${metricReq.key}`;
600
+ continue;
601
+ }
602
+ if (hasAccess && !hasAccess(metric)) {
603
+ errors[requestKey] = "Access denied";
604
+ continue;
605
+ }
606
+ let parsedMetricFilters;
607
+ try {
608
+ const parsedMetricRequest = registry.parseMetricRequestInput(metricReq, {
609
+ from: request.from,
610
+ to: request.to
611
+ });
612
+ parsedMetricFilters = parsedMetricRequest.filters;
613
+ validMetrics.push({
614
+ metric: parsedMetricRequest.metric,
615
+ filters: parsedMetricFilters,
616
+ requestKey: parsedMetricRequest.requestKey
617
+ });
618
+ } catch (err) {
619
+ errors[requestKey] = err instanceof Error ? err.message : "Invalid metric filters";
620
+ continue;
621
+ }
622
+ const filters = parsedMetricFilters;
623
+ const ttl = metric.cacheTtl ?? 600;
624
+ if (!disableCache && ttl > 0) {
625
+ cacheRequests.push({
626
+ key: getCacheKey(metric.key, filters, "current", granularity),
627
+ metric,
628
+ filters
629
+ });
630
+ if (request.compareToPrevious && metric.supportsTimeRange) {
631
+ const prevRange = getPreviousPeriod({
632
+ from: filters.from,
633
+ to: filters.to
634
+ });
635
+ if (prevRange) {
636
+ cacheRequests.push({
637
+ key: getCacheKey(metric.key, { ...filters, ...prevRange }, "previous", granularity),
638
+ metric,
639
+ filters: { ...filters, ...prevRange }
640
+ });
641
+ }
642
+ }
643
+ }
644
+ }
645
+ const cacheKeys = cacheRequests.map((r) => r.key);
646
+ const cachedValues = cacheKeys.length > 0 ? await cache.mget(cacheKeys) : [];
647
+ const cacheMap = new Map;
648
+ for (let i = 0;i < cacheRequests.length; i++) {
649
+ const req = cacheRequests[i];
650
+ const cached = cachedValues[i];
651
+ if (req && cached) {
652
+ const parsed = parseCache(cached, getOutputSchema(req.metric));
653
+ if (parsed) {
654
+ cacheMap.set(req.key, parsed);
655
+ }
656
+ }
657
+ }
658
+ return {
659
+ validMetrics,
660
+ errors,
661
+ cacheMap,
662
+ granularity,
663
+ resolverCtx,
664
+ compareToPrevious: request.compareToPrevious ?? false,
665
+ disableCache,
666
+ cache
667
+ };
668
+ }
669
+ async function executeMetric(validatedMetric, prepared) {
670
+ const { metric, filters, requestKey } = validatedMetric;
671
+ const {
672
+ cacheMap,
673
+ granularity,
674
+ resolverCtx,
675
+ compareToPrevious,
676
+ disableCache,
677
+ cache
678
+ } = prepared;
679
+ const startedAt = Date.now();
680
+ try {
681
+ const cachedCurrent = cacheMap.get(getCacheKey(metric.key, filters, "current", granularity));
682
+ const prevRange = getPreviousPeriod({ from: filters.from, to: filters.to });
683
+ const cachedPrevious = prevRange ? cacheMap.get(getCacheKey(metric.key, { ...filters, ...prevRange }, "previous", granularity)) : undefined;
684
+ if (cachedCurrent) {
685
+ let previous = cachedPrevious;
686
+ let usedLivePreviousFetch = false;
687
+ if (!previous && compareToPrevious && metric.supportsTimeRange && filters.from && filters.to) {
688
+ usedLivePreviousFetch = true;
689
+ const partialResult = await runSingleMetric({
690
+ metric,
691
+ filters,
692
+ granularity,
693
+ compareToPrevious: true,
694
+ resolverCtx
695
+ });
696
+ previous = partialResult.previous;
697
+ if (previous && prevRange) {
698
+ const ttl2 = metric.cacheTtl ?? 600;
699
+ if (!disableCache && ttl2 > 0) {
700
+ cache.mset([
701
+ {
702
+ key: getCacheKey(metric.key, { ...filters, ...prevRange }, "previous", granularity),
703
+ value: JSON.stringify(previous),
704
+ ttl: ttl2
705
+ }
706
+ ]).catch((error) => console.error(error));
707
+ }
708
+ }
709
+ }
710
+ return {
711
+ key: metric.key,
712
+ requestKey,
713
+ result: {
714
+ current: cachedCurrent,
715
+ previous,
716
+ supportsTimeRange: metric.supportsTimeRange,
717
+ execution: {
718
+ cacheStatus: usedLivePreviousFetch ? "partialHit" : "hit",
719
+ durationMs: Date.now() - startedAt,
720
+ granularity
721
+ }
722
+ }
723
+ };
724
+ }
725
+ const result = await runSingleMetric({
726
+ metric,
727
+ filters,
728
+ granularity,
729
+ compareToPrevious,
730
+ resolverCtx
731
+ });
732
+ const ttl = metric.cacheTtl ?? 600;
733
+ if (!disableCache && ttl > 0) {
734
+ const cacheEntries = [
735
+ {
736
+ key: getCacheKey(metric.key, filters, "current", granularity),
737
+ value: JSON.stringify(result.current),
738
+ ttl
739
+ }
740
+ ];
741
+ if (result.previous) {
742
+ const prevRange2 = getPreviousPeriod({
743
+ from: filters.from,
744
+ to: filters.to
745
+ });
746
+ if (prevRange2) {
747
+ cacheEntries.push({
748
+ key: getCacheKey(metric.key, { ...filters, ...prevRange2 }, "previous", granularity),
749
+ value: JSON.stringify(result.previous),
750
+ ttl
751
+ });
752
+ }
753
+ }
754
+ cache.mset(cacheEntries).catch((error) => console.error(error));
755
+ }
756
+ return {
757
+ key: metric.key,
758
+ requestKey,
759
+ result: {
760
+ ...result,
761
+ execution: {
762
+ cacheStatus: disableCache ? "bypassed" : "miss",
763
+ durationMs: Date.now() - startedAt,
764
+ granularity
765
+ }
766
+ }
767
+ };
768
+ } catch (err) {
769
+ return {
770
+ key: metric.key,
771
+ requestKey,
772
+ error: err instanceof Error ? err.message : "Unknown error"
773
+ };
774
+ }
775
+ }
776
+ async function runMetrics(options) {
777
+ const prepared = await prepareMetrics(options);
778
+ const { validMetrics, errors } = prepared;
779
+ const results = {};
780
+ const outcomes = await Promise.all(validMetrics.map((vm) => executeMetric(vm, prepared)));
781
+ for (const outcome of outcomes) {
782
+ if (outcome.error) {
783
+ errors[outcome.requestKey ?? outcome.key] = outcome.error;
784
+ } else if (outcome.result) {
785
+ results[outcome.requestKey ?? outcome.key] = outcome.result;
786
+ }
787
+ }
788
+ return {
789
+ metrics: results,
790
+ errors
791
+ };
792
+ }
793
+ async function* runMetricsStream(options) {
794
+ const prepared = await prepareMetrics(options);
795
+ const { validMetrics, errors } = prepared;
796
+ const totalMetrics = validMetrics.length + Object.keys(errors).length;
797
+ let yieldedCount = 0;
798
+ for (const key of Object.keys(errors)) {
799
+ yieldedCount++;
800
+ yield {
801
+ key,
802
+ requestKey: key,
803
+ error: errors[key],
804
+ done: yieldedCount === totalMetrics
805
+ };
806
+ }
807
+ if (validMetrics.length === 0) {
808
+ return;
809
+ }
810
+ const pending = new Map;
811
+ for (const vm of validMetrics) {
812
+ const promise = executeMetric(vm, prepared);
813
+ pending.set(promise, vm.requestKey);
814
+ }
815
+ while (pending.size > 0) {
816
+ const settled = await Promise.race(Array.from(pending.keys()).map((p) => p.then((result) => ({ promise: p, result }))));
817
+ pending.delete(settled.promise);
818
+ yieldedCount++;
819
+ yield {
820
+ key: settled.result.key,
821
+ requestKey: settled.result.requestKey,
822
+ result: settled.result.result,
823
+ error: settled.result.error,
824
+ done: yieldedCount === totalMetrics
825
+ };
826
+ }
827
+ }
828
+
829
+ // src/filters/types.ts
830
+ function defineMetricFilterFieldMetadata(metadata) {
831
+ return metadata;
832
+ }
833
+ // src/filters/parse.ts
834
+ function getDef(zodType) {
835
+ return zodType._def ?? {};
836
+ }
837
+ function getTypeName(zodType) {
838
+ const def = getDef(zodType);
839
+ if (def.typeName)
840
+ return def.typeName;
841
+ if (typeof def.type === "string") {
842
+ const v4ToV3Map = {
843
+ string: "ZodString",
844
+ number: "ZodNumber",
845
+ boolean: "ZodBoolean",
846
+ date: "ZodDate",
847
+ enum: "ZodEnum",
848
+ array: "ZodArray",
849
+ object: "ZodObject",
850
+ optional: "ZodOptional",
851
+ nullable: "ZodNullable",
852
+ default: "ZodDefault"
853
+ };
854
+ return v4ToV3Map[def.type] || def.type;
855
+ }
856
+ return;
857
+ }
858
+ function getEnumValues(zodType) {
859
+ const def = getDef(zodType);
860
+ const options = zodType.options;
861
+ if (Array.isArray(options)) {
862
+ return options.map(String);
863
+ }
864
+ if (def.entries && typeof def.entries === "object") {
865
+ return Object.values(def.entries).map(String);
866
+ }
867
+ if (def.values && typeof def.values === "object") {
868
+ return Object.values(def.values).map(String);
869
+ }
870
+ return [];
871
+ }
872
+ function getArrayElementType(zodType) {
873
+ const def = getDef(zodType);
874
+ if (def.element)
875
+ return def.element;
876
+ if (def.type && typeof def.type !== "string") {
877
+ return def.type;
878
+ }
879
+ return;
880
+ }
881
+ function getShape(zodType) {
882
+ return zodType.shape || {};
883
+ }
884
+ function unwrapZodType(zodType) {
885
+ const def = getDef(zodType);
886
+ const typeName = getTypeName(zodType);
887
+ const wrapperTypes = ["ZodOptional", "ZodNullable", "ZodDefault"];
888
+ if (typeName && wrapperTypes.includes(typeName) && def.innerType) {
889
+ return unwrapZodType(def.innerType);
890
+ }
891
+ return zodType;
892
+ }
893
+ function isMinMaxRangeObject(zodType) {
894
+ const shape = getShape(zodType);
895
+ const keys = Object.keys(shape);
896
+ const validKeys = ["min", "max"];
897
+ const hasOnlyValidKeys = keys.every((key) => validKeys.includes(key));
898
+ if (!hasOnlyValidKeys || keys.length === 0)
899
+ return false;
900
+ for (const key of keys) {
901
+ const fieldType = shape[key];
902
+ if (!fieldType)
903
+ return false;
904
+ const unwrapped = unwrapZodType(fieldType);
905
+ const typeName = getTypeName(unwrapped);
906
+ if (typeName !== "ZodNumber")
907
+ return false;
908
+ }
909
+ return true;
910
+ }
911
+ function determineTypeFromFilterObject(obj) {
912
+ const shape = getShape(obj);
913
+ for (const value of Object.values(shape)) {
914
+ const opValue = unwrapZodType(value);
915
+ const typeName = getTypeName(opValue);
916
+ switch (typeName) {
917
+ case "ZodNumber":
918
+ return "number";
919
+ case "ZodBoolean":
920
+ return "boolean";
921
+ case "ZodDate":
922
+ return "date";
923
+ case "ZodEnum":
924
+ case "ZodNativeEnum":
925
+ return "option";
926
+ case "ZodArray": {
927
+ const elementType = getArrayElementType(opValue);
928
+ const elementTypeName = elementType ? getTypeName(elementType) : null;
929
+ if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
930
+ return "multiOption";
931
+ }
932
+ if (elementTypeName === "ZodNumber")
933
+ return "number";
934
+ break;
935
+ }
936
+ }
937
+ }
938
+ return "text";
939
+ }
940
+ function determineFieldType(zodType) {
941
+ const unwrapped = unwrapZodType(zodType);
942
+ const typeName = getTypeName(unwrapped);
943
+ if (typeName === "ZodObject") {
944
+ if (isMinMaxRangeObject(unwrapped))
945
+ return "numberRange";
946
+ return determineTypeFromFilterObject(unwrapped);
947
+ }
948
+ switch (typeName) {
949
+ case "ZodString":
950
+ return "text";
951
+ case "ZodNumber":
952
+ return "number";
953
+ case "ZodBoolean":
954
+ return "boolean";
955
+ case "ZodDate":
956
+ return "date";
957
+ case "ZodEnum":
958
+ case "ZodNativeEnum":
959
+ return "option";
960
+ case "ZodArray": {
961
+ const elementType = getArrayElementType(unwrapped);
962
+ const elementTypeName = elementType ? getTypeName(elementType) : null;
963
+ if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
964
+ return "multiOption";
965
+ }
966
+ return "text";
967
+ }
968
+ default:
969
+ return "text";
970
+ }
971
+ }
972
+ function getDescription(zodType) {
973
+ const def = getDef(zodType);
974
+ if (def?.description)
975
+ return def.description;
976
+ const unwrapped = unwrapZodType(zodType);
977
+ const unwrappedDef = getDef(unwrapped);
978
+ if (unwrappedDef?.description)
979
+ return unwrappedDef.description;
980
+ if (getTypeName(unwrapped) === "ZodObject") {
981
+ const shape = getShape(unwrapped);
982
+ for (const value of Object.values(shape)) {
983
+ const opDef = getDef(value);
984
+ if (opDef?.description)
985
+ return opDef.description;
986
+ }
987
+ }
988
+ return;
989
+ }
990
+ function formatEnumValue(value) {
991
+ const label = value.toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (str) => str.toUpperCase());
992
+ return { label, value };
993
+ }
994
+ function formatFieldDisplayName(fieldId) {
995
+ return fieldId.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (str) => str.toUpperCase());
996
+ }
997
+ function extractEnumValues(zodType) {
998
+ const unwrapped = unwrapZodType(zodType);
999
+ const typeName = getTypeName(unwrapped);
1000
+ if (typeName === "ZodEnum" || typeName === "ZodNativeEnum") {
1001
+ const values = getEnumValues(unwrapped);
1002
+ return values.map(formatEnumValue);
1003
+ }
1004
+ if (typeName === "ZodArray") {
1005
+ const elementType = getArrayElementType(unwrapped);
1006
+ if (elementType) {
1007
+ const elementTypeName = getTypeName(elementType);
1008
+ if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
1009
+ const values = getEnumValues(elementType);
1010
+ return values.map(formatEnumValue);
1011
+ }
1012
+ }
1013
+ }
1014
+ if (typeName === "ZodObject") {
1015
+ const shape = getShape(unwrapped);
1016
+ for (const value of Object.values(shape)) {
1017
+ const opValue = unwrapZodType(value);
1018
+ const opTypeName = getTypeName(opValue);
1019
+ if (opTypeName === "ZodEnum" || opTypeName === "ZodNativeEnum") {
1020
+ const values = getEnumValues(opValue);
1021
+ return values.map(formatEnumValue);
1022
+ }
1023
+ if (opTypeName === "ZodArray") {
1024
+ const elementType = getArrayElementType(opValue);
1025
+ if (elementType) {
1026
+ const elementTypeName = getTypeName(elementType);
1027
+ if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
1028
+ const values = getEnumValues(elementType);
1029
+ return values.map(formatEnumValue);
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ return [];
1036
+ }
1037
+ function parseMetricFilterSchema(schema, options) {
1038
+ const excludeFields = new Set(options?.excludeFields ?? []);
1039
+ const includeFields = options?.includeFields;
1040
+ const fieldMetadata = options?.fieldMetadata ?? {};
1041
+ const fields = [];
1042
+ const unwrapped = unwrapZodType(schema);
1043
+ if (getTypeName(unwrapped) !== "ZodObject")
1044
+ return [];
1045
+ const shape = getShape(unwrapped);
1046
+ for (const [key, zodType] of Object.entries(shape)) {
1047
+ if (excludeFields.has(key))
1048
+ continue;
1049
+ if (includeFields && !includeFields.includes(key))
1050
+ continue;
1051
+ const metadata = fieldMetadata[key];
1052
+ const fieldType = metadata?.type ?? determineFieldType(zodType);
1053
+ const description = metadata?.description ?? getDescription(zodType);
1054
+ const enumOptions = metadata?.options ?? extractEnumValues(zodType);
1055
+ fields.push({
1056
+ id: key,
1057
+ displayName: metadata?.displayName ?? formatFieldDisplayName(key),
1058
+ type: fieldType,
1059
+ description,
1060
+ options: enumOptions.length > 0 ? enumOptions : undefined,
1061
+ defaultOperator: metadata?.defaultOperator
1062
+ });
1063
+ }
1064
+ return fields;
1065
+ }
1066
+ // src/filters/default-metadata.ts
1067
+ var COMMON_METRIC_FILTER_FIELD_METADATA = defineMetricFilterFieldMetadata({
1068
+ metric: {
1069
+ displayName: "Metric",
1070
+ description: "Choose how the metric should be aggregated.",
1071
+ defaultOperator: "is"
1072
+ },
1073
+ groupBy: {
1074
+ displayName: "Group By",
1075
+ description: "Split the metric into multiple groups.",
1076
+ defaultOperator: "is"
1077
+ },
1078
+ limit: {
1079
+ displayName: "Limit",
1080
+ description: "Maximum number of rows to return."
1081
+ },
1082
+ view: {
1083
+ displayName: "View",
1084
+ description: "Choose which interpretation of the metric to render.",
1085
+ defaultOperator: "is"
1086
+ },
1087
+ chartType: {
1088
+ displayName: "Chart Type",
1089
+ description: "Choose the preferred visualization for this metric.",
1090
+ defaultOperator: "is"
1091
+ }
1092
+ });
1093
+ function mergeMetricFilterFieldMetadata(...metadataEntries) {
1094
+ return Object.assign({}, ...metadataEntries);
1095
+ }
1096
+ // src/orpc.ts
1097
+ function createRunMetricsProcedure(options) {
1098
+ return async (input, orpcCtx) => {
1099
+ const result = await runMetrics({
1100
+ registry: options.registry,
1101
+ request: input,
1102
+ createContext: () => options.createContext(orpcCtx),
1103
+ cache: options.cache,
1104
+ hasAccess: options.hasAccess ? (metric) => options.hasAccess(metric, orpcCtx) : undefined
1105
+ });
1106
+ return {
1107
+ ...result,
1108
+ hasErrors: Object.keys(result.errors).length > 0
1109
+ };
1110
+ };
1111
+ }
1112
+ function createRunMetricsStreamProcedure(options) {
1113
+ return async function* (input, orpcCtx) {
1114
+ yield* runMetricsStream({
1115
+ registry: options.registry,
1116
+ request: input,
1117
+ createContext: () => options.createContext(orpcCtx),
1118
+ cache: options.cache,
1119
+ hasAccess: options.hasAccess ? (metric) => options.hasAccess(metric, orpcCtx) : undefined
1120
+ });
1121
+ };
1122
+ }
1123
+ function toAvailableMetric(metric) {
1124
+ const fieldMetadata = mergeMetricFilterFieldMetadata(COMMON_METRIC_FILTER_FIELD_METADATA, metric.filterFieldMetadata);
1125
+ return {
1126
+ key: metric.key,
1127
+ kind: metric.kind,
1128
+ displayName: metric.catalog?.displayName ?? metric.key,
1129
+ description: metric.description,
1130
+ supportsTimeRange: metric.supportsTimeRange,
1131
+ catalog: metric.catalog,
1132
+ filters: parseMetricFilterSchema(metric.filterSchema, {
1133
+ excludeFields: ["organizationIds", "from", "to"],
1134
+ fieldMetadata
1135
+ })
1136
+ };
1137
+ }
1138
+ function createGetAvailableMetricsProcedure(options) {
1139
+ return (orpcCtx) => {
1140
+ const accessibleMetrics = options.hasAccess ? options.registry.metrics.filter((metric) => options.hasAccess(metric, orpcCtx)) : options.registry.metrics;
1141
+ const metrics = accessibleMetrics.map((metric) => toAvailableMetric(metric));
1142
+ return { metrics, total: metrics.length };
1143
+ };
1144
+ }
1145
+ function createMetricsRouter(options) {
1146
+ return {
1147
+ runMetrics: createRunMetricsProcedure(options),
1148
+ runMetricsStream: createRunMetricsStreamProcedure(options),
1149
+ getAvailableMetrics: createGetAvailableMetricsProcedure(options)
1150
+ };
1151
+ }
1152
+ export {
1153
+ createRunMetricsStreamProcedure,
1154
+ createRunMetricsProcedure,
1155
+ createMetricsRouter,
1156
+ createGetAvailableMetricsProcedure
1157
+ };