measurr 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -8
- package/dist/analytics.d.ts +61 -3
- package/dist/dataset.d.ts +47 -4
- package/dist/dialects/clickhouse.d.ts +26 -0
- package/dist/dialects/clickhouse.js +101 -0
- package/dist/handler.js +8 -46
- package/dist/{index-g255jb1s.js → index-115syy01.js} +1 -1
- package/dist/{index-zgb1e89d.js → index-g0ah4ehn.js} +122 -15
- package/dist/index-psw9s799.js +46 -0
- package/dist/{index-g2mjqx4f.js → index-rgqsrd72.js} +24 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/metrics.d.ts +152 -0
- package/dist/metrics.js +167 -0
- package/dist/plan.d.ts +7 -1
- package/dist/query.d.ts +2 -2
- package/dist/shapes.d.ts +146 -1
- package/dist/shapes.js +84 -1
- package/dist/testing.js +5 -4
- package/package.json +15 -2
- package/src/analytics.ts +191 -17
- package/src/dataset.ts +82 -5
- package/src/dialects/clickhouse.ts +147 -0
- package/src/index.ts +14 -3
- package/src/metrics.ts +402 -0
- package/src/plan.ts +48 -7
- package/src/query.ts +17 -3
- package/src/shapes.ts +276 -1
- package/src/testing.ts +7 -2
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
PeriodError,
|
|
5
5
|
resolvePeriod,
|
|
6
6
|
previousPeriod
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-115syy01.js";
|
|
8
8
|
|
|
9
9
|
// src/query.ts
|
|
10
10
|
import { z } from "zod";
|
|
@@ -108,7 +108,8 @@ function datasetQuerySchema(dataset) {
|
|
|
108
108
|
throw new Error(`Dataset ${dataset.key} has no measures`);
|
|
109
109
|
const groupKeys = nonEmpty(groupable.map(([key]) => key));
|
|
110
110
|
const timeKeys = nonEmpty(Object.keys(dataset.time));
|
|
111
|
-
const
|
|
111
|
+
const sqlMeasureKeys = measures.filter(([, entry]) => entry.kind !== "resolved").map(([key]) => key);
|
|
112
|
+
const sortKeys = nonEmpty([...sqlMeasureKeys, ...groupKeys ?? []]) ?? measureKeys;
|
|
112
113
|
const notes = groupable.filter(([, dim]) => dim.kind === "manyToMany" && dim.note).map(([key, dim]) => `${key}: ${dim.kind === "manyToMany" ? dim.note : ""}`);
|
|
113
114
|
const [firstFilter, ...otherFilters] = filterable.map(([key, dim]) => filterSchema(key, dim));
|
|
114
115
|
return z.object({
|
|
@@ -142,6 +143,14 @@ function datasetQuerySchema(dataset) {
|
|
|
142
143
|
message: `Sort by one of the asked measures or grouped dimensions: ${returned.join(", ")}`
|
|
143
144
|
});
|
|
144
145
|
}
|
|
146
|
+
const sortMeasure = by === undefined ? undefined : dataset.measures[by];
|
|
147
|
+
if (sortMeasure?.kind === "resolved") {
|
|
148
|
+
context.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
path: ["sort", "by"],
|
|
151
|
+
message: `${by} is computed after the query runs and cannot be sorted by`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
145
154
|
const period = query.period ?? dataset.defaultPeriod;
|
|
146
155
|
if (query.compareToPrevious && "all" in period) {
|
|
147
156
|
context.addIssue({
|
|
@@ -256,8 +265,23 @@ function measureNode(dataset, key) {
|
|
|
256
265
|
left: measureNode(dataset, entry.numerator),
|
|
257
266
|
right: measureNode(dataset, entry.denominator)
|
|
258
267
|
};
|
|
268
|
+
case "resolved":
|
|
269
|
+
throw new Error(`Dataset ${dataset.key}: measure ${key} is resolved by the app, not the database`);
|
|
259
270
|
}
|
|
260
271
|
}
|
|
272
|
+
function sqlMeasures(dataset, keys) {
|
|
273
|
+
const result = new Set;
|
|
274
|
+
for (const key of keys) {
|
|
275
|
+
const entry = measureOf(dataset, key);
|
|
276
|
+
if (entry.kind === "resolved") {
|
|
277
|
+
for (const used of entry.uses)
|
|
278
|
+
result.add(used);
|
|
279
|
+
} else {
|
|
280
|
+
result.add(key);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return [...result];
|
|
284
|
+
}
|
|
261
285
|
function dimensionNodes(dataset, key, timezone) {
|
|
262
286
|
const entry = dimensionOf(dataset, key);
|
|
263
287
|
switch (entry.kind) {
|
|
@@ -410,12 +434,19 @@ function planQuery(dataset, query, context, grouped) {
|
|
|
410
434
|
select.push({ alias: columnAlias.groupLabel(key), node: nodes.label });
|
|
411
435
|
}
|
|
412
436
|
}
|
|
413
|
-
|
|
437
|
+
const measureKeys = sqlMeasures(dataset, query.measures);
|
|
438
|
+
for (const key of measureKeys) {
|
|
414
439
|
select.push({
|
|
415
440
|
alias: columnAlias.measure(key),
|
|
416
441
|
node: measureNode(dataset, key)
|
|
417
442
|
});
|
|
418
443
|
}
|
|
444
|
+
if (select.length === 0) {
|
|
445
|
+
select.push({
|
|
446
|
+
alias: "row_count",
|
|
447
|
+
node: { kind: "aggregate", fn: "count", arg: null, where: null }
|
|
448
|
+
});
|
|
449
|
+
}
|
|
419
450
|
const where = [
|
|
420
451
|
...baseWhere(dataset, context.tenant, context.period && {
|
|
421
452
|
time: query.time,
|
|
@@ -442,7 +473,10 @@ function orderBy(dataset, query, select) {
|
|
|
442
473
|
const kind = dimensionOf(dataset, key).kind;
|
|
443
474
|
return kind === "timeBucket" || kind === "timePart";
|
|
444
475
|
});
|
|
445
|
-
const sort = query.sort ?? (timeKey ? { by: timeKey, direction: "asc" } : {
|
|
476
|
+
const sort = query.sort ?? (timeKey ? { by: timeKey, direction: "asc" } : {
|
|
477
|
+
by: sqlMeasures(dataset, query.measures)[0] ?? "",
|
|
478
|
+
direction: "desc"
|
|
479
|
+
});
|
|
446
480
|
const sortIndex = sort.by in dataset.measures ? indexOf(columnAlias.measure(sort.by)) : indexOf(columnAlias.groupKey(sort.by));
|
|
447
481
|
if (sortIndex >= 0)
|
|
448
482
|
order.push({ index: sortIndex, direction: sort.direction });
|
|
@@ -546,8 +580,11 @@ function labelOf(dimension, key, rawLabel) {
|
|
|
546
580
|
function shapeMeasures(dataset, measures, raw, dialect) {
|
|
547
581
|
const values = {};
|
|
548
582
|
for (const key of measures) {
|
|
583
|
+
const entry = measureOf(dataset, key);
|
|
584
|
+
if (entry.kind === "resolved")
|
|
585
|
+
continue;
|
|
549
586
|
const value = dialect.decodeNumber(raw[columnAlias.measure(key)]);
|
|
550
|
-
values[key] =
|
|
587
|
+
values[key] = entry.result === "count" ? value ?? 0 : value;
|
|
551
588
|
}
|
|
552
589
|
return values;
|
|
553
590
|
}
|
|
@@ -566,7 +603,7 @@ function shapeRows(dataset, query, rawRows, dialect) {
|
|
|
566
603
|
}
|
|
567
604
|
return { ...row, ...shapeMeasures(dataset, query.measures, raw, dialect) };
|
|
568
605
|
});
|
|
569
|
-
return { rows, truncated };
|
|
606
|
+
return { rows, kept, truncated };
|
|
570
607
|
}
|
|
571
608
|
function resultNotes(dataset, query) {
|
|
572
609
|
const notes = [];
|
|
@@ -613,6 +650,25 @@ function createAnalytics2(options) {
|
|
|
613
650
|
}
|
|
614
651
|
restrictedOf.set(dataset.key, restricted);
|
|
615
652
|
}
|
|
653
|
+
const resolvers = options.resolvers ?? {};
|
|
654
|
+
for (const [datasetKey, entries] of Object.entries(resolvers)) {
|
|
655
|
+
const dataset = byKey.get(datasetKey);
|
|
656
|
+
if (!dataset)
|
|
657
|
+
throw new Error(`Resolvers name unknown dataset ${datasetKey}`);
|
|
658
|
+
for (const key of Object.keys(entries ?? {})) {
|
|
659
|
+
const entry = dataset.measures[key];
|
|
660
|
+
if (entry?.kind !== "resolved") {
|
|
661
|
+
throw new Error(`Resolvers name ${datasetKey}.${key}, which is not a resolved measure`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const dataset of datasets) {
|
|
666
|
+
for (const [key, entry] of Object.entries(dataset.measures)) {
|
|
667
|
+
if (entry.kind === "resolved" && !resolvers[dataset.key]?.[key]) {
|
|
668
|
+
throw new Error(`Dataset ${dataset.key} resolves ${key} in the app, but createAnalytics has no resolver for it`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
616
672
|
const now = options.now ?? (() => new Date);
|
|
617
673
|
const querySchemas = new Map;
|
|
618
674
|
const resultSchemas = new Map;
|
|
@@ -685,6 +741,13 @@ function createAnalytics2(options) {
|
|
|
685
741
|
}
|
|
686
742
|
return querySchemaFor(allowed);
|
|
687
743
|
};
|
|
744
|
+
const timezoneOf = async (ctx) => {
|
|
745
|
+
const timezone = await options.timezone?.(ctx) || "UTC";
|
|
746
|
+
if (!isValidTimezone(timezone)) {
|
|
747
|
+
throw new AnalyticsError2("invalid_timezone", `Unknown timezone ${timezone}`);
|
|
748
|
+
}
|
|
749
|
+
return timezone;
|
|
750
|
+
};
|
|
688
751
|
const prepare = async (input, ctx) => {
|
|
689
752
|
const parsed = querySchemaFor(datasets).safeParse(input);
|
|
690
753
|
if (!parsed.success) {
|
|
@@ -711,10 +774,7 @@ function createAnalytics2(options) {
|
|
|
711
774
|
if (!dataset.tenantColumn && tenant !== allTenants2) {
|
|
712
775
|
throw new AnalyticsError2("forbidden", `${dataset.key} has no tenant column; only a caller allowed to see every tenant may query it`);
|
|
713
776
|
}
|
|
714
|
-
const timezone = await
|
|
715
|
-
if (!isValidTimezone(timezone)) {
|
|
716
|
-
throw new AnalyticsError2("invalid_timezone", `Unknown timezone ${timezone}`);
|
|
717
|
-
}
|
|
777
|
+
const timezone = await timezoneOf(ctx);
|
|
718
778
|
const query = normalizeQuery(dataset, parsedQuery);
|
|
719
779
|
const at = now();
|
|
720
780
|
let period;
|
|
@@ -729,12 +789,13 @@ function createAnalytics2(options) {
|
|
|
729
789
|
const previous = query.compareToPrevious && period ? previousPeriod(period) : null;
|
|
730
790
|
return { dataset, query, tenant, timezone, period, previous, now: at };
|
|
731
791
|
};
|
|
732
|
-
const runQuery = async (prepared) => {
|
|
792
|
+
const runQuery = async (prepared, ctx) => {
|
|
733
793
|
const { dataset, query, tenant, timezone, period, previous, now } = prepared;
|
|
734
794
|
const source = sourceFor(dataset);
|
|
735
795
|
const { dialect } = source;
|
|
736
796
|
const run = (statement) => source.execute(dialect.compile(statement));
|
|
737
797
|
const resolvedValues = await resolveOpenValues(dataset, query, tenant, now, run, dialect);
|
|
798
|
+
const range = ({ from, to }) => ({ from, to, timezone });
|
|
738
799
|
const runPeriod = async (target) => {
|
|
739
800
|
const statements = periodStatements(dataset, query, {
|
|
740
801
|
tenant,
|
|
@@ -747,12 +808,46 @@ function createAnalytics2(options) {
|
|
|
747
808
|
run(statements.rows),
|
|
748
809
|
statements.totals ? run(statements.totals) : null
|
|
749
810
|
]);
|
|
750
|
-
const { rows, truncated } = shapeRows(dataset, query, rawRows, dialect);
|
|
811
|
+
const { rows, kept, truncated } = shapeRows(dataset, query, rawRows, dialect);
|
|
751
812
|
const totalsRow = (rawTotals ?? rawRows)[0] ?? {};
|
|
752
813
|
const totals = shapeMeasures(dataset, query.measures, totalsRow, dialect);
|
|
814
|
+
for (const key of query.measures) {
|
|
815
|
+
const entry = dataset.measures[key];
|
|
816
|
+
if (entry?.kind !== "resolved")
|
|
817
|
+
continue;
|
|
818
|
+
const used = (raw) => shapeMeasures(dataset, entry.uses, raw, dialect);
|
|
819
|
+
const resolver = resolvers[dataset.key]?.[key];
|
|
820
|
+
if (!resolver)
|
|
821
|
+
throw new Error(`No resolver for ${dataset.key}.${key}`);
|
|
822
|
+
const output = await resolver({
|
|
823
|
+
rows: kept.map((raw, index) => ({
|
|
824
|
+
dimensions: Object.fromEntries(query.groupBy.map((group) => [group, rows[index]?.[group]])),
|
|
825
|
+
values: used(raw)
|
|
826
|
+
})),
|
|
827
|
+
totals: used(totalsRow),
|
|
828
|
+
groupBy: query.groupBy,
|
|
829
|
+
filters: query.filters.map((filter) => {
|
|
830
|
+
const resolved = resolvedValues.get(filter.dimension);
|
|
831
|
+
return resolved && "values" in filter ? { ...filter, values: [...resolved] } : filter;
|
|
832
|
+
}),
|
|
833
|
+
period: target ? range(target) : { all: true, timezone },
|
|
834
|
+
tenant,
|
|
835
|
+
ctx
|
|
836
|
+
});
|
|
837
|
+
if (output.rows.length !== rows.length) {
|
|
838
|
+
throw new Error(`The resolver for ${dataset.key}.${key} returned ${output.rows.length} values for ${rows.length} rows`);
|
|
839
|
+
}
|
|
840
|
+
const value = (raw) => {
|
|
841
|
+
const finite = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
|
|
842
|
+
return entry.result === "count" ? finite ?? 0 : finite;
|
|
843
|
+
};
|
|
844
|
+
rows.forEach((row, index) => {
|
|
845
|
+
row[key] = value(output.rows[index]);
|
|
846
|
+
});
|
|
847
|
+
totals[key] = value(output.totals);
|
|
848
|
+
}
|
|
753
849
|
return { rows, totals, truncated };
|
|
754
850
|
};
|
|
755
|
-
const range = ({ from, to }) => ({ from, to, timezone });
|
|
756
851
|
const [current, before] = await Promise.all([
|
|
757
852
|
runPeriod(period),
|
|
758
853
|
previous && runPeriod(previous).then(({ rows, totals }) => ({
|
|
@@ -810,7 +905,7 @@ function createAnalytics2(options) {
|
|
|
810
905
|
if (key)
|
|
811
906
|
cache = result ? "hit" : "miss";
|
|
812
907
|
if (!result) {
|
|
813
|
-
result = await runQuery(prepared);
|
|
908
|
+
result = await runQuery(prepared, ctx);
|
|
814
909
|
if (key)
|
|
815
910
|
store(key, result, entry);
|
|
816
911
|
}
|
|
@@ -858,6 +953,7 @@ function createAnalytics2(options) {
|
|
|
858
953
|
explain,
|
|
859
954
|
querySchema: (schemaOptions) => schemaOptions && ("ctx" in schemaOptions) ? querySchemaForCtx(schemaOptions.ctx) : querySchemaFor(schemaOptions?.datasets?.map(datasetOf) ?? datasets),
|
|
860
955
|
resultSchema: (key) => resultSchemaFor(key),
|
|
956
|
+
timezone: timezoneOf,
|
|
861
957
|
listDatasets: async (ctx) => (await allowedDatasets(ctx)).map((dataset) => catalogEntry(dataset))
|
|
862
958
|
};
|
|
863
959
|
}
|
|
@@ -978,7 +1074,18 @@ function restrictedFields(dataset) {
|
|
|
978
1074
|
if (!entry)
|
|
979
1075
|
return [];
|
|
980
1076
|
const own = entry.access === undefined ? [] : [entry.access];
|
|
981
|
-
|
|
1077
|
+
switch (entry.kind) {
|
|
1078
|
+
case "ratio":
|
|
1079
|
+
return [
|
|
1080
|
+
...own,
|
|
1081
|
+
...accessOf(entry.numerator),
|
|
1082
|
+
...accessOf(entry.denominator)
|
|
1083
|
+
];
|
|
1084
|
+
case "resolved":
|
|
1085
|
+
return [...own, ...entry.uses.flatMap(accessOf)];
|
|
1086
|
+
default:
|
|
1087
|
+
return own;
|
|
1088
|
+
}
|
|
982
1089
|
};
|
|
983
1090
|
return [
|
|
984
1091
|
...Object.keys(measures).map((key) => ({
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnalyticsError2
|
|
3
|
+
} from "./index-g0ah4ehn.js";
|
|
4
|
+
|
|
5
|
+
// src/handler.ts
|
|
6
|
+
async function queryBatch2(analytics, queries, ctx) {
|
|
7
|
+
const results = {};
|
|
8
|
+
const errors = {};
|
|
9
|
+
for await (const chunk of streamBatch2(analytics, queries, ctx)) {
|
|
10
|
+
if ("error" in chunk)
|
|
11
|
+
errors[chunk.key] = chunk.error;
|
|
12
|
+
else
|
|
13
|
+
results[chunk.key] = chunk.result;
|
|
14
|
+
}
|
|
15
|
+
return { results, errors };
|
|
16
|
+
}
|
|
17
|
+
async function* streamBatch2(analytics, queries, ctx) {
|
|
18
|
+
const pending = new Map(Object.entries(queries).map(([key, query]) => {
|
|
19
|
+
const settled = analytics.query(query, ctx).then((result) => ({ key, result }), (error) => ({ key, error: batchError(error) }));
|
|
20
|
+
return [key, settled];
|
|
21
|
+
}));
|
|
22
|
+
while (pending.size > 0) {
|
|
23
|
+
const chunk = await Promise.race(pending.values());
|
|
24
|
+
pending.delete(chunk.key);
|
|
25
|
+
yield chunk;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function batchError(error) {
|
|
29
|
+
return error instanceof AnalyticsError2 ? { code: error.code, message: error.message } : { code: "internal", message: "The query failed" };
|
|
30
|
+
}
|
|
31
|
+
function toHttpError2(error) {
|
|
32
|
+
if (error.code === "forbidden") {
|
|
33
|
+
return { status: 403, code: "FORBIDDEN", message: error.message };
|
|
34
|
+
}
|
|
35
|
+
if (error.code === "missing_tenant") {
|
|
36
|
+
return { status: 401, code: "UNAUTHORIZED", message: error.message };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: 400,
|
|
40
|
+
code: "BAD_REQUEST",
|
|
41
|
+
message: error.message,
|
|
42
|
+
data: { code: error.code, details: error.details }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { queryBatch2, streamBatch2, toHttpError2 };
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./index-e6k30sfp.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_PERIOD
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-115syy01.js";
|
|
8
8
|
|
|
9
9
|
// src/dataset.ts
|
|
10
10
|
function aggregate(result, fn, arg, options) {
|
|
@@ -62,8 +62,19 @@ var measure2 = {
|
|
|
62
62
|
numerator,
|
|
63
63
|
denominator
|
|
64
64
|
};
|
|
65
|
-
}
|
|
65
|
+
},
|
|
66
|
+
resolved
|
|
66
67
|
};
|
|
68
|
+
function resolved(options) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "resolved",
|
|
71
|
+
result: options.result,
|
|
72
|
+
label: options.label,
|
|
73
|
+
description: options.description,
|
|
74
|
+
access: options.access,
|
|
75
|
+
uses: options.uses ?? []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
67
78
|
function base(options) {
|
|
68
79
|
return {
|
|
69
80
|
label: options.label,
|
|
@@ -222,6 +233,15 @@ function validateDataset(dataset) {
|
|
|
222
233
|
}
|
|
223
234
|
if (entry.kind === "ratio")
|
|
224
235
|
checkRatio(measures, key, [], fail);
|
|
236
|
+
if (entry.kind === "resolved") {
|
|
237
|
+
for (const used of entry.uses) {
|
|
238
|
+
const usedEntry = measures[used];
|
|
239
|
+
if (!usedEntry)
|
|
240
|
+
fail(`measure ${key} uses unknown measure ${used}`);
|
|
241
|
+
if (usedEntry?.kind === "resolved")
|
|
242
|
+
fail(`measure ${key} uses ${used}, which is resolved too`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
225
245
|
}
|
|
226
246
|
const dimensions = dataset.dimensions;
|
|
227
247
|
for (const [key, entry] of Object.entries(dimensions)) {
|
|
@@ -266,6 +286,8 @@ function checkRatio(measures, key, path, fail) {
|
|
|
266
286
|
const entry = measures[key];
|
|
267
287
|
if (!entry)
|
|
268
288
|
fail(`ratio measure ${path.at(-1)} names unknown measure ${key}`);
|
|
289
|
+
if (entry?.kind === "resolved")
|
|
290
|
+
fail(`ratio measure ${path.at(-1)} names ${key}, which is resolved`);
|
|
269
291
|
if (entry?.kind !== "ratio")
|
|
270
292
|
return;
|
|
271
293
|
checkRatio(measures, entry.numerator, [...path, key], fail);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { type AccessKey, type Analytics, type AnalyticsCache, AnalyticsError, type AnalyticsErrorCode, type AnalyticsErrorDetails, type AnalyticsErrorOf, type AnalyticsOptions, type AnalyticsQuery, type AnalyticsQueryEvent, type AnalyticsResult, type AnalyticsSource, createAnalytics, type DatasetCatalogEntry, type FieldKey, isAnalyticsError } from './analytics.js';
|
|
1
|
+
export { type AccessKey, type Analytics, type AnalyticsCache, AnalyticsError, type AnalyticsErrorCode, type AnalyticsErrorDetails, type AnalyticsErrorOf, type AnalyticsOptions, type AnalyticsQuery, type AnalyticsQueryEvent, type AnalyticsResolvers, type AnalyticsResult, type AnalyticsSource, createAnalytics, type DatasetCatalogEntry, type FieldKey, isAnalyticsError, type MeasureResolver, type ResolvedKey, type ResolverInput, type ResolverOutput } from './analytics.js';
|
|
2
2
|
export { CompileError, type CompiledStatement, type Dialect, type JoinClause, type SelectItem, type SelectStatement, type SqlRenderer, decodeNumber, renderStatement } from './compile.js';
|
|
3
|
-
export { type AnyDataset, type AnyDimension, type AnyMeasure, type Dataset, type DimensionValue, defineDataset, dimension, type MeasureValue, measure, type Ratio } from './dataset.js';
|
|
3
|
+
export { type AnyDataset, type AnyDimension, type AnyMeasure, type Dataset, type DimensionValue, defineDataset, dimension, type MeasureValue, measure, type Ratio, type ResolvedMeasure } from './dataset.js';
|
|
4
4
|
export { and, caseWhen, coalesce, type ColumnKey, eq, Expr, type ExprNode, exists, gt, gte, inList, isNotNull, isNull, literal, lt, lte, minutesAgo, ne, not, or, type SqlType, Table, type TableRef, table, toText } from './expr.js';
|
|
5
5
|
export { analyticsToolGuidance } from './guidance.js';
|
|
6
6
|
export type { PeriodInput, PeriodPreset } from './period.js';
|
|
7
|
-
export { allTenants, type Tenant, type TenantId } from './plan.js';
|
|
7
|
+
export { allTenants, type NormalizedFilter, type Tenant, type TenantId } from './plan.js';
|
|
8
8
|
export { AnalyticsResultSchema, type DatasetFilter, type DatasetQuery, type QueryResult, type ResultOf, type ResultPeriod } from './query.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
AnalyticsError2,
|
|
5
5
|
isAnalyticsError2,
|
|
6
6
|
createAnalytics2
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-g0ah4ehn.js";
|
|
8
8
|
import {
|
|
9
9
|
CompileError2,
|
|
10
10
|
decodeNumber2,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
measure2,
|
|
15
15
|
dimension2,
|
|
16
16
|
defineDataset2
|
|
17
|
-
} from "./index-
|
|
17
|
+
} from "./index-rgqsrd72.js";
|
|
18
18
|
import {
|
|
19
19
|
Expr2,
|
|
20
20
|
Table2,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named dashboard metrics: one query over a dataset and the shape it is drawn in. The
|
|
3
|
+
* registry runs a batch of them for a caller and yields each as it finishes; the period, the
|
|
4
|
+
* previous period and the tenant scope come from the request and `ctx`, never from a metric.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { Analytics } from './analytics.js';
|
|
8
|
+
import type { AnyDataset } from './dataset.js';
|
|
9
|
+
import type { DatasetQuery, QueryResult, ResultOf } from './query.js';
|
|
10
|
+
import type { MetricOutput, OutputForKind, OutputKind, TimeGranularity } from './shapes.js';
|
|
11
|
+
/** The buckets a time series is grouped by. */
|
|
12
|
+
export type MetricBucket = 'day' | 'week' | 'month';
|
|
13
|
+
/** What a metric's `query` and `shape` are run with. */
|
|
14
|
+
export type MetricRun<Filters> = {
|
|
15
|
+
/** The metric's own filters, parsed with its `filterSchema`. */
|
|
16
|
+
filters: Filters;
|
|
17
|
+
/** The time-bucket dimension to group a series by: the granularity, as near as it gets. */
|
|
18
|
+
bucket: MetricBucket;
|
|
19
|
+
granularity: TimeGranularity;
|
|
20
|
+
};
|
|
21
|
+
/** A result as `shape` receives it: one period, without `previous`. */
|
|
22
|
+
export type MetricQueryResult<DS, Q> = ResultOf<DS, Q> extends QueryResult<infer Row, infer Totals, infer _Period> ? Omit<QueryResult<Row, Totals>, 'previous'> : never;
|
|
23
|
+
export type MetricDefinition<DS extends AnyDataset, Key extends string, Kind extends OutputKind, Filters, Q extends DatasetQuery<DS>, Output extends OutputForKind<Kind>, Role extends string = string> = {
|
|
24
|
+
key: Key;
|
|
25
|
+
kind: Kind;
|
|
26
|
+
description: string;
|
|
27
|
+
/** For `canRun`; the registry does not read it. */
|
|
28
|
+
allowedRoles?: readonly Role[] | undefined;
|
|
29
|
+
/** False for a snapshot of now: it counts all time and has no previous period. */
|
|
30
|
+
supportsTimeRange: boolean;
|
|
31
|
+
filterSchema: z.ZodType<Filters>;
|
|
32
|
+
/** The query, without `period` or `compareToPrevious`: the registry sets those. */
|
|
33
|
+
query: (run: MetricRun<Filters>) => Q;
|
|
34
|
+
/** Called once for the period and once for the previous period. */
|
|
35
|
+
shape: (result: MetricQueryResult<DS, Q>, run: MetricRun<Filters>) => Output;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* `defineMetric` for metrics over the datasets `DS` (a union), with `allowedRoles` typed as
|
|
39
|
+
* `Role` and the app's own fields `Extra` (catalog metadata, say), which the registry carries
|
|
40
|
+
* along but does not read. Types only, so metric modules need not import the analytics
|
|
41
|
+
* instance:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* export const defineMetric = metricDefiner<(typeof datasets)[number], Role>()
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function metricDefiner<DS extends AnyDataset, Role extends string = string, Extra extends object = Record<never, never>>(): <const Key extends string, const Kind extends OutputKind, Filters, const Q extends DatasetQuery<DS>, Output extends OutputForKind<Kind>>(definition: MetricDefinition<DS, Key, Kind, Filters, Q, Output, Role> & Extra) => MetricDefinition<DS, Key, Kind, Filters, Q, Output, Role> & Extra;
|
|
48
|
+
/** Any metric, as the registry holds them. */
|
|
49
|
+
export type AnyMetric = {
|
|
50
|
+
key: string;
|
|
51
|
+
kind: OutputKind;
|
|
52
|
+
description: string;
|
|
53
|
+
allowedRoles?: readonly string[] | undefined;
|
|
54
|
+
supportsTimeRange: boolean;
|
|
55
|
+
filterSchema: z.ZodType<any>;
|
|
56
|
+
query: (run: MetricRun<any>) => DatasetQuery<AnyDataset>;
|
|
57
|
+
shape: (result: any, run: MetricRun<any>) => MetricOutput;
|
|
58
|
+
};
|
|
59
|
+
export type MetricKeyOf<R extends {
|
|
60
|
+
metrics: readonly AnyMetric[];
|
|
61
|
+
}> = R['metrics'][number]['key'];
|
|
62
|
+
export type MetricOf<R extends {
|
|
63
|
+
metrics: readonly AnyMetric[];
|
|
64
|
+
}, K extends MetricKeyOf<R>> = Extract<R['metrics'][number], {
|
|
65
|
+
key: K;
|
|
66
|
+
}>;
|
|
67
|
+
export type MetricOutputOf<R extends {
|
|
68
|
+
metrics: readonly AnyMetric[];
|
|
69
|
+
}, K extends MetricKeyOf<R>> = ReturnType<MetricOf<R, K>['shape']>;
|
|
70
|
+
export type MetricFiltersOf<R extends {
|
|
71
|
+
metrics: readonly AnyMetric[];
|
|
72
|
+
}, K extends MetricKeyOf<R>> = z.infer<MetricOf<R, K>['filterSchema']>;
|
|
73
|
+
export type MetricKeysOfKind<R extends {
|
|
74
|
+
metrics: readonly AnyMetric[];
|
|
75
|
+
}, Kind extends OutputKind> = Extract<R['metrics'][number], {
|
|
76
|
+
kind: Kind;
|
|
77
|
+
}>['key'];
|
|
78
|
+
/** One metric's numbers: the period, and the previous period on the same buckets. */
|
|
79
|
+
export type MetricResult<Output = MetricOutput> = {
|
|
80
|
+
current: Output;
|
|
81
|
+
previous?: Output;
|
|
82
|
+
supportsTimeRange: boolean;
|
|
83
|
+
};
|
|
84
|
+
/** One metric's result or error, as each finishes. */
|
|
85
|
+
export type MetricChunk<M extends AnyMetric = AnyMetric> = M extends AnyMetric ? {
|
|
86
|
+
key: M['key'];
|
|
87
|
+
/** The request's name for it; the key unless one request asks for a metric twice. */
|
|
88
|
+
requestKey: string;
|
|
89
|
+
/** Whether this is the last chunk of the request. */
|
|
90
|
+
done: boolean;
|
|
91
|
+
} & ({
|
|
92
|
+
result: MetricResult<ReturnType<M['shape']>>;
|
|
93
|
+
error?: undefined;
|
|
94
|
+
} | {
|
|
95
|
+
error: string;
|
|
96
|
+
result?: undefined;
|
|
97
|
+
}) : never;
|
|
98
|
+
export type MetricsRequest<Key extends string = string> = {
|
|
99
|
+
metrics: readonly {
|
|
100
|
+
key: Key;
|
|
101
|
+
requestKey?: string | undefined;
|
|
102
|
+
filters?: unknown;
|
|
103
|
+
}[];
|
|
104
|
+
/** Instants; the registry counts the local days they fall on in `analytics.timezone(ctx)`. */
|
|
105
|
+
from?: Date | undefined;
|
|
106
|
+
to?: Date | undefined;
|
|
107
|
+
/** Inferred from the range when omitted: day up to 31 days, week up to 90, else month. */
|
|
108
|
+
granularity?: TimeGranularity | undefined;
|
|
109
|
+
compareToPrevious?: boolean | undefined;
|
|
110
|
+
};
|
|
111
|
+
export type MetricRegistry<M extends readonly AnyMetric[], Ctx> = {
|
|
112
|
+
readonly metrics: M;
|
|
113
|
+
/** The keys, for a request schema or a catalog. */
|
|
114
|
+
readonly keys: [M[number]['key'], ...M[number]['key'][]];
|
|
115
|
+
/** The request schema; `.extend` it with the app's own fields. */
|
|
116
|
+
readonly requestSchema: z.ZodObject<{
|
|
117
|
+
metrics: z.ZodArray<z.ZodObject<{
|
|
118
|
+
key: z.ZodEnum<{
|
|
119
|
+
[K in M[number]['key']]: K;
|
|
120
|
+
}>;
|
|
121
|
+
requestKey: z.ZodOptional<z.ZodString>;
|
|
122
|
+
filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
123
|
+
}>>;
|
|
124
|
+
from: z.ZodOptional<z.ZodDate>;
|
|
125
|
+
to: z.ZodOptional<z.ZodDate>;
|
|
126
|
+
granularity: z.ZodOptional<z.ZodEnum<{
|
|
127
|
+
[G in TimeGranularity]: G;
|
|
128
|
+
}>>;
|
|
129
|
+
compareToPrevious: z.ZodDefault<z.ZodBoolean>;
|
|
130
|
+
}>;
|
|
131
|
+
get<K extends M[number]['key']>(key: K): Extract<M[number], {
|
|
132
|
+
key: K;
|
|
133
|
+
}>;
|
|
134
|
+
/** The metrics `canRun` allows for `ctx`. */
|
|
135
|
+
available(ctx: Ctx): Promise<M[number][]>;
|
|
136
|
+
/**
|
|
137
|
+
* Runs the requested metrics in parallel and yields each as it finishes. A metric `ctx` may
|
|
138
|
+
* not run, or whose filters do not parse, fails alone. A metric that supports a time range
|
|
139
|
+
* counts `from` to `to` (all time without them) and compares with the previous period when
|
|
140
|
+
* asked; a snapshot counts all time.
|
|
141
|
+
*/
|
|
142
|
+
run(request: MetricsRequest<M[number]['key']>, ctx: Ctx): AsyncGenerator<MetricChunk<M[number]>, void, undefined>;
|
|
143
|
+
};
|
|
144
|
+
export type MetricRegistryOptions<DS extends readonly AnyDataset[], M extends readonly AnyMetric[], Ctx> = {
|
|
145
|
+
analytics: Analytics<DS, Ctx>;
|
|
146
|
+
metrics: M;
|
|
147
|
+
/** Whether `ctx` may run `metric`. Every metric runs when omitted. */
|
|
148
|
+
canRun?: ((metric: M[number], ctx: Ctx) => boolean | Promise<boolean>) | undefined;
|
|
149
|
+
/** Hears a `shape` that throws; the metric fails with "The metric could not be drawn". */
|
|
150
|
+
onShapeError?: ((error: unknown, metric: M[number], ctx: Ctx) => unknown) | undefined;
|
|
151
|
+
};
|
|
152
|
+
export declare function createMetricRegistry<DS extends readonly AnyDataset[], const M extends readonly AnyMetric[], Ctx>(options: MetricRegistryOptions<DS, M, Ctx>): MetricRegistry<M, Ctx>;
|