measurr 0.2.0 → 0.4.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 +118 -31
- package/dist/analytics.d.ts +20 -7
- package/dist/dataset.d.ts +61 -33
- package/dist/guidance.d.ts +1 -1
- package/dist/{index-dj3369zz.js → index-4rmjas9h.js} +165 -44
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/period.d.ts +12 -3
- package/dist/plan.d.ts +6 -4
- package/dist/query.d.ts +40 -30
- package/dist/testing.d.ts +9 -4
- package/dist/testing.js +35 -22
- package/package.json +1 -1
- package/src/analytics.ts +230 -46
- package/src/dataset.ts +153 -65
- package/src/guidance.ts +1 -1
- package/src/index.ts +5 -2
- package/src/period.ts +10 -4
- package/src/plan.ts +40 -21
- package/src/query.ts +75 -26
- package/src/testing.ts +59 -33
|
@@ -8,7 +8,9 @@ import { z } from "zod";
|
|
|
8
8
|
|
|
9
9
|
// src/period.ts
|
|
10
10
|
var MAX_PERIOD_DAYS = 731;
|
|
11
|
-
var DEFAULT_PERIOD = {
|
|
11
|
+
var DEFAULT_PERIOD = {
|
|
12
|
+
last: { days: 30 }
|
|
13
|
+
};
|
|
12
14
|
var DAY_MS = 86400000;
|
|
13
15
|
var formatterCache = new Map;
|
|
14
16
|
function formatter(timezone) {
|
|
@@ -165,8 +167,18 @@ var periodSchema = z.union([
|
|
|
165
167
|
})
|
|
166
168
|
}),
|
|
167
169
|
z.object({ from: isoDate, to: isoDate.describe("Last day, inclusive.") }),
|
|
168
|
-
z.object({ preset: z.enum(PERIOD_PRESETS) })
|
|
169
|
-
|
|
170
|
+
z.object({ preset: z.enum(PERIOD_PRESETS) }),
|
|
171
|
+
z.object({ all: z.literal(true) }).describe("Every row, with no time filter.")
|
|
172
|
+
]);
|
|
173
|
+
function describePeriod(period) {
|
|
174
|
+
if ("all" in period)
|
|
175
|
+
return "all time (no time filter)";
|
|
176
|
+
if ("last" in period)
|
|
177
|
+
return `the last ${period.last.days} days`;
|
|
178
|
+
if ("from" in period)
|
|
179
|
+
return `${period.from} to ${period.to}`;
|
|
180
|
+
return period.preset;
|
|
181
|
+
}
|
|
170
182
|
function describeList(entries) {
|
|
171
183
|
return entries.map(([key, entry]) => `${key}: ${entry.label}${entry.description ? ` (${entry.description})` : ""}`).join(" · ");
|
|
172
184
|
}
|
|
@@ -247,11 +259,11 @@ function datasetQuerySchema(dataset) {
|
|
|
247
259
|
...firstFilter && {
|
|
248
260
|
filters: z.array(z.discriminatedUnion("dimension", [firstFilter, ...otherFilters])).optional().describe("Every filter must match.")
|
|
249
261
|
},
|
|
250
|
-
period: periodSchema.nullish(),
|
|
262
|
+
period: periodSchema.describe(`Which days to count, in the organization timezone. Defaults to ${describePeriod(dataset.defaultPeriod)}. A range covers at most two years.`).nullish(),
|
|
251
263
|
...timeKeys && {
|
|
252
264
|
time: z.enum(timeKeys).optional().describe(`Which time field the period applies to; time dimensions name their own. Defaults to ${dataset.defaultTime}. ${describeList(Object.entries(dataset.time))}`)
|
|
253
265
|
},
|
|
254
|
-
compareToPrevious: z.boolean().optional().describe("Also return the previous period of the same length."),
|
|
266
|
+
compareToPrevious: z.boolean().optional().describe("Also return the previous period of the same length. Not for all-time periods."),
|
|
255
267
|
sort: z.object({
|
|
256
268
|
by: z.enum(sortKeys),
|
|
257
269
|
direction: z.enum(["asc", "desc"]).optional()
|
|
@@ -268,6 +280,14 @@ function datasetQuerySchema(dataset) {
|
|
|
268
280
|
message: `Sort by one of the asked measures or grouped dimensions: ${returned.join(", ")}`
|
|
269
281
|
});
|
|
270
282
|
}
|
|
283
|
+
const period = query.period ?? dataset.defaultPeriod;
|
|
284
|
+
if (query.compareToPrevious && "all" in period) {
|
|
285
|
+
context.addIssue({
|
|
286
|
+
code: "custom",
|
|
287
|
+
path: ["compareToPrevious"],
|
|
288
|
+
message: "An all-time period has no previous period to compare with"
|
|
289
|
+
});
|
|
290
|
+
}
|
|
271
291
|
});
|
|
272
292
|
}
|
|
273
293
|
function buildQuerySchema(datasets) {
|
|
@@ -276,11 +296,15 @@ function buildQuerySchema(datasets) {
|
|
|
276
296
|
throw new Error("querySchema needs at least one dataset");
|
|
277
297
|
return z.discriminatedUnion("dataset", [first, ...rest]);
|
|
278
298
|
}
|
|
279
|
-
var
|
|
299
|
+
var rangeOutput = z.object({
|
|
280
300
|
from: z.string(),
|
|
281
301
|
to: z.string(),
|
|
282
302
|
timezone: z.string()
|
|
283
303
|
});
|
|
304
|
+
var periodOutput = z.union([
|
|
305
|
+
rangeOutput,
|
|
306
|
+
z.object({ all: z.literal(true), timezone: z.string() })
|
|
307
|
+
]);
|
|
284
308
|
var measureValue = z.number().nullable();
|
|
285
309
|
var dimensionValue = z.object({
|
|
286
310
|
key: z.union([z.string(), z.number()]).nullable(),
|
|
@@ -291,7 +315,7 @@ function resultSchemaOf(row, totals) {
|
|
|
291
315
|
period: periodOutput,
|
|
292
316
|
rows: z.array(row),
|
|
293
317
|
totals,
|
|
294
|
-
previous: z.object({ period:
|
|
318
|
+
previous: z.object({ period: rangeOutput, rows: z.array(row), totals }).optional(),
|
|
295
319
|
notes: z.array(z.string()),
|
|
296
320
|
truncated: z.boolean()
|
|
297
321
|
});
|
|
@@ -317,7 +341,7 @@ function normalizeQuery(dataset, parsed) {
|
|
|
317
341
|
measures: [...new Set(parsed.measures)],
|
|
318
342
|
groupBy: [...new Set(parsed.groupBy ?? [])],
|
|
319
343
|
filters,
|
|
320
|
-
period: parsed.period ??
|
|
344
|
+
period: parsed.period ?? dataset.defaultPeriod,
|
|
321
345
|
time: parsed.time ?? dataset.defaultTime,
|
|
322
346
|
compareToPrevious: parsed.compareToPrevious ?? false,
|
|
323
347
|
sort: parsed.sort ? { by: parsed.sort.by, direction: parsed.sort.direction ?? "desc" } : null,
|
|
@@ -411,14 +435,25 @@ function dimensionNodes(dataset, key, timezone) {
|
|
|
411
435
|
}
|
|
412
436
|
}
|
|
413
437
|
function baseWhere(dataset, tenant, period) {
|
|
414
|
-
const where =
|
|
415
|
-
|
|
438
|
+
const where = [];
|
|
439
|
+
if (tenant !== allTenants2) {
|
|
440
|
+
if (!dataset.tenantColumn) {
|
|
441
|
+
throw new Error(`Dataset ${dataset.key} has no tenant column; only allTenants may query it`);
|
|
442
|
+
}
|
|
443
|
+
const ids = typeof tenant === "object" ? [...new Set(tenant)] : [tenant];
|
|
444
|
+
const [only] = ids;
|
|
445
|
+
where.push(ids.length === 1 && only !== undefined ? {
|
|
416
446
|
kind: "compare",
|
|
417
447
|
op: "=",
|
|
418
448
|
left: dataset.tenantColumn,
|
|
419
|
-
right: param(
|
|
420
|
-
}
|
|
421
|
-
|
|
449
|
+
right: param(only)
|
|
450
|
+
} : {
|
|
451
|
+
kind: "in",
|
|
452
|
+
item: dataset.tenantColumn,
|
|
453
|
+
values: ids.map(param),
|
|
454
|
+
negated: false
|
|
455
|
+
});
|
|
456
|
+
}
|
|
422
457
|
if (dataset.scope)
|
|
423
458
|
where.push(dataset.scope);
|
|
424
459
|
if (period) {
|
|
@@ -520,7 +555,7 @@ function planQuery(dataset, query, context, grouped) {
|
|
|
520
555
|
});
|
|
521
556
|
}
|
|
522
557
|
const where = [
|
|
523
|
-
...baseWhere(dataset, context.tenant, {
|
|
558
|
+
...baseWhere(dataset, context.tenant, context.period && {
|
|
524
559
|
time: query.time,
|
|
525
560
|
start: context.period.start,
|
|
526
561
|
end: context.period.end
|
|
@@ -702,6 +737,7 @@ function createAnalytics2(options) {
|
|
|
702
737
|
const sourceNames = Object.keys(options.sources);
|
|
703
738
|
const byKey = new Map;
|
|
704
739
|
const sourceOf = new Map;
|
|
740
|
+
const restrictedOf = new Map;
|
|
705
741
|
for (const dataset of datasets) {
|
|
706
742
|
if (byKey.has(dataset.key))
|
|
707
743
|
throw new Error(`Two datasets use the key ${dataset.key}`);
|
|
@@ -709,6 +745,11 @@ function createAnalytics2(options) {
|
|
|
709
745
|
const source = resolveSource(dataset, options.sources, sourceNames);
|
|
710
746
|
sourceOf.set(dataset.key, source);
|
|
711
747
|
compileEverything(dataset, source.dialect);
|
|
748
|
+
const restricted = restrictedFields(dataset);
|
|
749
|
+
if (restricted.length > 0 && !options.authorizeField) {
|
|
750
|
+
throw new Error(`Dataset ${dataset.key} marks fields with access, but createAnalytics has no authorizeField`);
|
|
751
|
+
}
|
|
752
|
+
restrictedOf.set(dataset.key, restricted);
|
|
712
753
|
}
|
|
713
754
|
const now = options.now ?? (() => new Date);
|
|
714
755
|
const querySchemas = new Map;
|
|
@@ -725,9 +766,17 @@ function createAnalytics2(options) {
|
|
|
725
766
|
throw new Error(`No source for ${dataset.key}`);
|
|
726
767
|
return source;
|
|
727
768
|
};
|
|
728
|
-
const querySchemaFor = (
|
|
729
|
-
const
|
|
730
|
-
|
|
769
|
+
const querySchemaFor = (selected) => {
|
|
770
|
+
const keyOf = (dataset) => {
|
|
771
|
+
if (byKey.get(dataset.key) === dataset)
|
|
772
|
+
return dataset.key;
|
|
773
|
+
const fields = [
|
|
774
|
+
...Object.keys(dataset.measures),
|
|
775
|
+
...Object.keys(dataset.dimensions)
|
|
776
|
+
];
|
|
777
|
+
return `${dataset.key}(${fields.join(" ")})`;
|
|
778
|
+
};
|
|
779
|
+
const selectionKey = selected.map(keyOf).sort().join(",");
|
|
731
780
|
let schema = querySchemas.get(selectionKey);
|
|
732
781
|
if (!schema) {
|
|
733
782
|
schema = buildQuerySchema(selected);
|
|
@@ -743,49 +792,79 @@ function createAnalytics2(options) {
|
|
|
743
792
|
}
|
|
744
793
|
return schema;
|
|
745
794
|
};
|
|
795
|
+
const deniedFields = async (dataset, ctx) => {
|
|
796
|
+
const restricted = restrictedOf.get(dataset.key) ?? [];
|
|
797
|
+
const authorizeField = options.authorizeField;
|
|
798
|
+
if (restricted.length === 0 || !authorizeField)
|
|
799
|
+
return restricted;
|
|
800
|
+
const mayUse = async (field) => {
|
|
801
|
+
const answers = await Promise.all(field.access.map((access) => authorizeField(dataset, field.key, access, ctx)));
|
|
802
|
+
return answers.every(Boolean);
|
|
803
|
+
};
|
|
804
|
+
const allowed = await Promise.all(restricted.map(mayUse));
|
|
805
|
+
return restricted.filter((_, index) => !allowed[index]);
|
|
806
|
+
};
|
|
746
807
|
const allowedDatasets = async (ctx) => {
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
808
|
+
const everyTenant = options.tenant(ctx) === allTenants2;
|
|
809
|
+
const views = await Promise.all(datasets.map(async (dataset) => {
|
|
810
|
+
if (!dataset.tenantColumn && !everyTenant)
|
|
811
|
+
return null;
|
|
812
|
+
if (options.authorize && !await options.authorize(dataset, ctx))
|
|
813
|
+
return null;
|
|
814
|
+
const view = withoutFields(dataset, await deniedFields(dataset, ctx));
|
|
815
|
+
return Object.keys(view.measures).length > 0 ? view : null;
|
|
816
|
+
}));
|
|
817
|
+
return views.filter((view) => view !== null);
|
|
752
818
|
};
|
|
753
819
|
const querySchemaForCtx = async (ctx) => {
|
|
754
820
|
const allowed = await allowedDatasets(ctx);
|
|
755
821
|
if (allowed.length === 0) {
|
|
756
822
|
throw new AnalyticsError2("forbidden", "No dataset is allowed");
|
|
757
823
|
}
|
|
758
|
-
return querySchemaFor(allowed
|
|
824
|
+
return querySchemaFor(allowed);
|
|
759
825
|
};
|
|
760
826
|
const prepare = async (input, ctx) => {
|
|
761
|
-
const parsed = querySchemaFor(
|
|
827
|
+
const parsed = querySchemaFor(datasets).safeParse(input);
|
|
762
828
|
if (!parsed.success) {
|
|
763
829
|
throw new AnalyticsError2("invalid_query", `Invalid analytics query: ${z2.prettifyError(parsed.error)}`, parsed.error.issues);
|
|
764
830
|
}
|
|
765
|
-
const
|
|
831
|
+
const parsedQuery = parsed.data;
|
|
832
|
+
const dataset = datasetOf(parsedQuery.dataset);
|
|
766
833
|
if (options.authorize && !await options.authorize(dataset, ctx)) {
|
|
767
834
|
throw new AnalyticsError2("forbidden", `Not allowed to query ${dataset.key}`);
|
|
768
835
|
}
|
|
836
|
+
const dimensionsUsed = [
|
|
837
|
+
...parsedQuery.groupBy ?? [],
|
|
838
|
+
...(parsedQuery.filters ?? []).map((filter) => filter.dimension)
|
|
839
|
+
];
|
|
840
|
+
const refused = (await deniedFields(dataset, ctx)).filter((field) => field.kind === "measure" ? parsedQuery.measures.includes(field.key) : dimensionsUsed.includes(field.key));
|
|
841
|
+
if (refused.length > 0) {
|
|
842
|
+
const names = refused.map((field) => `${field.kind} ${field.key}`);
|
|
843
|
+
throw new AnalyticsError2("forbidden", `Not allowed to use ${names.join(", ")} of ${dataset.key}`);
|
|
844
|
+
}
|
|
769
845
|
const tenant = options.tenant(ctx);
|
|
770
846
|
if (!isTenant(tenant)) {
|
|
771
847
|
throw new AnalyticsError2("missing_tenant", "Refusing to query without a tenant");
|
|
772
848
|
}
|
|
849
|
+
if (!dataset.tenantColumn && tenant !== allTenants2) {
|
|
850
|
+
throw new AnalyticsError2("forbidden", `${dataset.key} has no tenant column; only a caller allowed to see every tenant may query it`);
|
|
851
|
+
}
|
|
773
852
|
const timezone = await options.timezone?.(ctx) || "UTC";
|
|
774
853
|
if (!isValidTimezone(timezone)) {
|
|
775
854
|
throw new AnalyticsError2("invalid_timezone", `Unknown timezone ${timezone}`);
|
|
776
855
|
}
|
|
777
|
-
const query = normalizeQuery(dataset,
|
|
856
|
+
const query = normalizeQuery(dataset, parsedQuery);
|
|
778
857
|
const at = now();
|
|
779
858
|
let period;
|
|
780
859
|
try {
|
|
781
|
-
period = resolvePeriod(query.period, timezone, at);
|
|
860
|
+
period = "all" in query.period ? null : resolvePeriod(query.period, timezone, at);
|
|
782
861
|
} catch (error) {
|
|
783
862
|
if (error instanceof PeriodError) {
|
|
784
863
|
throw new AnalyticsError2("invalid_period", error.message);
|
|
785
864
|
}
|
|
786
865
|
throw error;
|
|
787
866
|
}
|
|
788
|
-
const previous = query.compareToPrevious ? previousPeriod(period) : null;
|
|
867
|
+
const previous = query.compareToPrevious && period ? previousPeriod(period) : null;
|
|
789
868
|
return { dataset, query, tenant, timezone, period, previous, now: at };
|
|
790
869
|
};
|
|
791
870
|
const runQuery = async (input, ctx) => {
|
|
@@ -809,24 +888,22 @@ function createAnalytics2(options) {
|
|
|
809
888
|
const { rows, truncated } = shapeRows(dataset, query, rawRows, dialect);
|
|
810
889
|
const totalsRow = (rawTotals ?? rawRows)[0] ?? {};
|
|
811
890
|
const totals = shapeMeasures(dataset, query.measures, totalsRow, dialect);
|
|
812
|
-
|
|
813
|
-
return { period: periodOut, rows, totals, truncated };
|
|
891
|
+
return { rows, totals, truncated };
|
|
814
892
|
};
|
|
893
|
+
const range = ({ from, to }) => ({ from, to, timezone });
|
|
815
894
|
const [current, before] = await Promise.all([
|
|
816
895
|
runPeriod(period),
|
|
817
|
-
previous
|
|
896
|
+
previous && runPeriod(previous).then(({ rows, totals }) => ({
|
|
897
|
+
period: range(previous),
|
|
898
|
+
rows,
|
|
899
|
+
totals
|
|
900
|
+
}))
|
|
818
901
|
]);
|
|
819
902
|
return {
|
|
820
|
-
period:
|
|
903
|
+
period: period ? range(period) : { all: true, timezone },
|
|
821
904
|
rows: current.rows,
|
|
822
905
|
totals: current.totals,
|
|
823
|
-
...before && {
|
|
824
|
-
previous: {
|
|
825
|
-
period: before.period,
|
|
826
|
-
rows: before.rows,
|
|
827
|
-
totals: before.totals
|
|
828
|
-
}
|
|
829
|
-
},
|
|
906
|
+
...before && { previous: before },
|
|
830
907
|
notes: resultNotes(dataset, query),
|
|
831
908
|
truncated: current.truncated
|
|
832
909
|
};
|
|
@@ -882,13 +959,16 @@ function createAnalytics2(options) {
|
|
|
882
959
|
datasets: options.datasets,
|
|
883
960
|
query,
|
|
884
961
|
explain,
|
|
885
|
-
querySchema: (schemaOptions) => schemaOptions && ("ctx" in schemaOptions) ? querySchemaForCtx(schemaOptions.ctx) : querySchemaFor(schemaOptions?.datasets),
|
|
962
|
+
querySchema: (schemaOptions) => schemaOptions && ("ctx" in schemaOptions) ? querySchemaForCtx(schemaOptions.ctx) : querySchemaFor(schemaOptions?.datasets?.map(datasetOf) ?? datasets),
|
|
886
963
|
resultSchema: (key) => resultSchemaFor(key),
|
|
887
964
|
listDatasets: async (ctx) => (await allowedDatasets(ctx)).map((dataset) => catalogEntry(dataset))
|
|
888
965
|
};
|
|
889
966
|
}
|
|
890
967
|
function isTenant(value) {
|
|
891
|
-
return value === allTenants2 ||
|
|
968
|
+
return value === allTenants2 || isTenantId(value) || Array.isArray(value) && value.every(isTenantId);
|
|
969
|
+
}
|
|
970
|
+
function isTenantId(value) {
|
|
971
|
+
return typeof value === "string" && value !== "" || typeof value === "number" && Number.isFinite(value);
|
|
892
972
|
}
|
|
893
973
|
function periodStatements(dataset, query, context) {
|
|
894
974
|
const grouped = query.groupBy.length > 0;
|
|
@@ -910,7 +990,7 @@ function resolveSource(dataset, sources, sourceNames) {
|
|
|
910
990
|
function compileEverything(dataset, dialect) {
|
|
911
991
|
const now = new Date(0);
|
|
912
992
|
const context = {
|
|
913
|
-
tenant: "tenant",
|
|
993
|
+
tenant: dataset.tenantColumn ? "tenant" : allTenants2,
|
|
914
994
|
timezone: "UTC",
|
|
915
995
|
period: resolvePeriod({ last: { days: 1 } }, "UTC", now),
|
|
916
996
|
now,
|
|
@@ -976,6 +1056,38 @@ function decodeKey2(value, dialect) {
|
|
|
976
1056
|
return value;
|
|
977
1057
|
return dialect.decodeNumber(value) ?? String(value);
|
|
978
1058
|
}
|
|
1059
|
+
function restrictedFields(dataset) {
|
|
1060
|
+
const measures = dataset.measures;
|
|
1061
|
+
const accessOf = (key) => {
|
|
1062
|
+
const entry = measures[key];
|
|
1063
|
+
if (!entry)
|
|
1064
|
+
return [];
|
|
1065
|
+
const own = entry.access === undefined ? [] : [entry.access];
|
|
1066
|
+
return entry.kind === "ratio" ? [...own, ...accessOf(entry.numerator), ...accessOf(entry.denominator)] : own;
|
|
1067
|
+
};
|
|
1068
|
+
return [
|
|
1069
|
+
...Object.keys(measures).map((key) => ({
|
|
1070
|
+
kind: "measure",
|
|
1071
|
+
key,
|
|
1072
|
+
access: [...new Set(accessOf(key))]
|
|
1073
|
+
})),
|
|
1074
|
+
...Object.entries(dataset.dimensions).map(([key, entry]) => ({
|
|
1075
|
+
kind: "dimension",
|
|
1076
|
+
key,
|
|
1077
|
+
access: entry.access === undefined ? [] : [entry.access]
|
|
1078
|
+
}))
|
|
1079
|
+
].filter((field) => field.access.length > 0);
|
|
1080
|
+
}
|
|
1081
|
+
function withoutFields(dataset, fields) {
|
|
1082
|
+
if (fields.length === 0)
|
|
1083
|
+
return dataset;
|
|
1084
|
+
const keep = (kind, entries) => Object.fromEntries(Object.entries(entries).filter(([key]) => !fields.some((field) => field.kind === kind && field.key === key)));
|
|
1085
|
+
return {
|
|
1086
|
+
...dataset,
|
|
1087
|
+
measures: keep("measure", dataset.measures),
|
|
1088
|
+
dimensions: keep("dimension", dataset.dimensions)
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
979
1091
|
function catalogEntry(dataset) {
|
|
980
1092
|
return {
|
|
981
1093
|
key: dataset.key,
|
|
@@ -1013,6 +1125,7 @@ function aggregate(result, fn, arg, options) {
|
|
|
1013
1125
|
result,
|
|
1014
1126
|
label: options.label,
|
|
1015
1127
|
description: options.description,
|
|
1128
|
+
access: options.access,
|
|
1016
1129
|
node: {
|
|
1017
1130
|
kind: "aggregate",
|
|
1018
1131
|
fn,
|
|
@@ -1046,6 +1159,7 @@ var measure2 = {
|
|
|
1046
1159
|
result: "ratio",
|
|
1047
1160
|
label: options.label,
|
|
1048
1161
|
description: options.description,
|
|
1162
|
+
access: options.access,
|
|
1049
1163
|
numerator: options.numerator.node,
|
|
1050
1164
|
denominator: options.denominator?.node ?? null
|
|
1051
1165
|
};
|
|
@@ -1056,6 +1170,7 @@ var measure2 = {
|
|
|
1056
1170
|
result: "ratio",
|
|
1057
1171
|
label: options.label,
|
|
1058
1172
|
description: options.description,
|
|
1173
|
+
access: options.access,
|
|
1059
1174
|
numerator,
|
|
1060
1175
|
denominator
|
|
1061
1176
|
};
|
|
@@ -1065,6 +1180,7 @@ function base(options) {
|
|
|
1065
1180
|
return {
|
|
1066
1181
|
label: options.label,
|
|
1067
1182
|
description: options.description,
|
|
1183
|
+
access: options.access,
|
|
1068
1184
|
groupable: options.filterOnly !== true,
|
|
1069
1185
|
filterable: options.groupOnly !== true
|
|
1070
1186
|
};
|
|
@@ -1163,6 +1279,9 @@ function defineDataset2(definition) {
|
|
|
1163
1279
|
if (!firstTime) {
|
|
1164
1280
|
throw new Error(`Dataset ${definition.key}: declare at least one time field`);
|
|
1165
1281
|
}
|
|
1282
|
+
if (definition.tenantColumn === undefined) {
|
|
1283
|
+
throw new Error(`Dataset ${definition.key}: declare tenantColumn, or null for a dataset without tenants`);
|
|
1284
|
+
}
|
|
1166
1285
|
const time = Object.fromEntries(timeEntries.map(([key, field]) => [
|
|
1167
1286
|
key,
|
|
1168
1287
|
{ column: field.column.node, label: field.label }
|
|
@@ -1173,10 +1292,11 @@ function defineDataset2(definition) {
|
|
|
1173
1292
|
description: definition.description,
|
|
1174
1293
|
from: definition.from.ref,
|
|
1175
1294
|
source: definition.source,
|
|
1176
|
-
tenantColumn: definition.tenantColumn
|
|
1295
|
+
tenantColumn: definition.tenantColumn?.node ?? null,
|
|
1177
1296
|
scope: definition.scope?.node ?? null,
|
|
1178
1297
|
time,
|
|
1179
1298
|
defaultTime: definition.defaultTime ?? firstTime[0],
|
|
1299
|
+
defaultPeriod: definition.defaultPeriod ?? DEFAULT_PERIOD,
|
|
1180
1300
|
measures: definition.measures,
|
|
1181
1301
|
dimensions: definition.dimensions
|
|
1182
1302
|
};
|
|
@@ -1196,7 +1316,8 @@ function validateDataset(dataset) {
|
|
|
1196
1316
|
}
|
|
1197
1317
|
});
|
|
1198
1318
|
};
|
|
1199
|
-
|
|
1319
|
+
if (dataset.tenantColumn)
|
|
1320
|
+
check(dataset.tenantColumn, "tenantColumn");
|
|
1200
1321
|
if (dataset.scope)
|
|
1201
1322
|
check(dataset.scope, "scope");
|
|
1202
1323
|
for (const [key, field] of Object.entries(dataset.time)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { type Analytics, AnalyticsError, type AnalyticsErrorCode, type AnalyticsErrorDetails, type AnalyticsErrorOf, type AnalyticsOptions, type AnalyticsQuery, type AnalyticsQueryEvent, type AnalyticsResult, type AnalyticsSource, createAnalytics, type DatasetCatalogEntry, isAnalyticsError } from './analytics.js';
|
|
1
|
+
export { type AccessKey, type Analytics, 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';
|
|
2
2
|
export { CompileError, type CompiledStatement, type Dialect, type JoinClause, type SelectItem, type SelectStatement, type SqlRenderer, decodeNumber, renderStatement } from './compile.js';
|
|
3
3
|
export { type AnyDataset, type AnyDimension, type AnyMeasure, type Dataset, type DimensionValue, defineDataset, dimension, type MeasureValue, measure, type Ratio } 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 } from './plan.js';
|
|
8
|
-
export { AnalyticsResultSchema, type DatasetFilter, type DatasetQuery, type QueryResult, type ResultOf } from './query.js';
|
|
7
|
+
export { allTenants, type Tenant, type TenantId } from './plan.js';
|
|
8
|
+
export { AnalyticsResultSchema, type DatasetFilter, type DatasetQuery, type QueryResult, type ResultOf, type ResultPeriod } from './query.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
measure2,
|
|
8
8
|
dimension2,
|
|
9
9
|
defineDataset2
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-4rmjas9h.js";
|
|
11
11
|
import {
|
|
12
12
|
CompileError2,
|
|
13
13
|
decodeNumber2,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
// src/guidance.ts
|
|
40
40
|
var analyticsToolGuidance = `- Answer every "how many", "how much", "per week" or "split by" question with the analytics tool. Never count by hand from lists or pages.
|
|
41
41
|
- Pick the dataset whose description matches the question, the fewest measures that answer it and at most two groupings.
|
|
42
|
-
- Say which period and filters the numbers cover, using the dates in the result ("1-30 September").
|
|
42
|
+
- Say which period and filters the numbers cover, using the dates in the result ("1-30 September", or "all time").
|
|
43
43
|
- Compare with the previous period (compareToPrevious) when the change is the point, such as "is it going up".
|
|
44
44
|
- When the result has notes (overlapping rows, such as a record counted once per tag), say so in a few words.
|
|
45
45
|
- Ask one short question only when the period is genuinely ambiguous ("this season") and someone can answer; otherwise take the natural reading and state it.`;
|
package/dist/period.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* database compares plain timestamps and can use its time index.
|
|
5
5
|
*/
|
|
6
6
|
export type PeriodPreset = 'today' | 'yesterday' | 'thisWeek' | 'lastWeek' | 'thisMonth' | 'lastMonth' | 'thisYear';
|
|
7
|
-
|
|
7
|
+
/** A range of days: what `resolvePeriod` turns into instants. */
|
|
8
|
+
export type PeriodRange = {
|
|
8
9
|
last: {
|
|
9
10
|
days: number;
|
|
10
11
|
};
|
|
@@ -14,6 +15,10 @@ export type PeriodInput = {
|
|
|
14
15
|
} | {
|
|
15
16
|
preset: PeriodPreset;
|
|
16
17
|
};
|
|
18
|
+
/** A range of days, or `{ all: true }` for no time filter. */
|
|
19
|
+
export type PeriodInput = PeriodRange | {
|
|
20
|
+
all: true;
|
|
21
|
+
};
|
|
17
22
|
export type ResolvedPeriod = {
|
|
18
23
|
/** First local day, `YYYY-MM-DD`. */
|
|
19
24
|
from: string;
|
|
@@ -27,7 +32,11 @@ export type ResolvedPeriod = {
|
|
|
27
32
|
days: number;
|
|
28
33
|
};
|
|
29
34
|
export declare const MAX_PERIOD_DAYS = 731;
|
|
30
|
-
export declare const DEFAULT_PERIOD:
|
|
35
|
+
export declare const DEFAULT_PERIOD: {
|
|
36
|
+
readonly last: {
|
|
37
|
+
readonly days: 30;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
31
40
|
/** True for an IANA name the runtime knows. Names are also inlined in SQL, so be strict. */
|
|
32
41
|
export declare function isValidTimezone(timezone: string): boolean;
|
|
33
42
|
/** The local calendar date of `instant` in `timezone`, as `YYYY-MM-DD`. */
|
|
@@ -39,6 +48,6 @@ export declare class PeriodError extends Error {
|
|
|
39
48
|
readonly name = "PeriodError";
|
|
40
49
|
}
|
|
41
50
|
/** Resolves a period input to local days and instants in `timezone`. */
|
|
42
|
-
export declare function resolvePeriod(input:
|
|
51
|
+
export declare function resolvePeriod(input: PeriodRange, timezone: string, now: Date): ResolvedPeriod;
|
|
43
52
|
/** The period of equal length that ends the day before `period` starts. */
|
|
44
53
|
export declare function previousPeriod(period: ResolvedPeriod): ResolvedPeriod;
|
package/dist/plan.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Dialect, SelectStatement } from './compile.js';
|
|
2
2
|
import type { AnyDataset } from './dataset.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { PeriodInput, ResolvedPeriod } from './period.js';
|
|
4
4
|
/** A parsed query with every default filled in and duplicates removed. */
|
|
5
5
|
export type NormalizedQuery = {
|
|
6
6
|
dataset: string;
|
|
@@ -50,13 +50,15 @@ export declare function normalizeQuery(dataset: AnyDataset, parsed: ParsedQuery)
|
|
|
50
50
|
* symbol, so no query input (JSON, a model's tool call) can ever carry it.
|
|
51
51
|
*/
|
|
52
52
|
export declare const allTenants: unique symbol;
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
export type TenantId = string | number;
|
|
54
|
+
/** One tenant's id, a list of them (an empty list matches no rows), or `allTenants`. */
|
|
55
|
+
export type Tenant = TenantId | readonly TenantId[] | typeof allTenants;
|
|
55
56
|
/** What the planner needs besides the query: values from the hooks and resolved labels. */
|
|
56
57
|
export type PlanContext = {
|
|
57
58
|
tenant: Tenant;
|
|
58
59
|
timezone: string;
|
|
59
|
-
period:
|
|
60
|
+
/** Null for an all-time period: no time filter. */
|
|
61
|
+
period: ResolvedPeriod | null;
|
|
60
62
|
/** The moment the query runs. */
|
|
61
63
|
now: Date;
|
|
62
64
|
/** Keys for filters on open dimensions, after label resolution, by dimension. */
|
package/dist/query.d.ts
CHANGED
|
@@ -80,14 +80,30 @@ export type ResultRow<M, Dm, MeasureKey extends string, GroupKey extends string>
|
|
|
80
80
|
export type ResultTotals<M, MeasureKey extends string> = Simplify<{
|
|
81
81
|
[Key in MeasureKey & keyof M]: MeasureValue<M[Key]>;
|
|
82
82
|
}>;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
/** The days a result covers: local days `from` to `to` (inclusive), or all time. */
|
|
84
|
+
export type ResultPeriod = {
|
|
85
|
+
from: string;
|
|
86
|
+
to: string;
|
|
87
|
+
timezone: string;
|
|
88
|
+
} | {
|
|
89
|
+
all: true;
|
|
90
|
+
timezone: string;
|
|
91
|
+
};
|
|
92
|
+
/** The period query `Q` names; null or none means the dataset's `Default`. */
|
|
93
|
+
type QueryPeriod<Q, Default> = Q extends unknown ? 'period' extends keyof Q ? OrDefault<Q['period' & keyof Q], Default> : Default : never;
|
|
94
|
+
type OrDefault<P, Default> = P extends null | undefined ? Default : P;
|
|
95
|
+
type PeriodShape<P> = P extends {
|
|
96
|
+
all: true;
|
|
97
|
+
} ? Extract<ResultPeriod, {
|
|
98
|
+
all: true;
|
|
99
|
+
}> : Exclude<ResultPeriod, {
|
|
100
|
+
all: true;
|
|
101
|
+
}>;
|
|
102
|
+
export type QueryResult<Row, Totals, Period extends ResultPeriod = ResultPeriod> = {
|
|
103
|
+
period: Period;
|
|
89
104
|
rows: Row[];
|
|
90
105
|
totals: Totals;
|
|
106
|
+
/** Only for a range: an all-time period has no previous period. */
|
|
91
107
|
previous?: {
|
|
92
108
|
period: {
|
|
93
109
|
from: string;
|
|
@@ -101,28 +117,10 @@ export type QueryResult<Row, Totals> = {
|
|
|
101
117
|
truncated: boolean;
|
|
102
118
|
};
|
|
103
119
|
/** The typed result of query `Q` against the datasets `DS`. */
|
|
104
|
-
export type ResultOf<DS, Q> = DS extends Dataset<infer K, infer M, infer Dm, string> ? Q extends {
|
|
120
|
+
export type ResultOf<DS, Q> = DS extends Dataset<infer K, infer M, infer Dm, string, infer P> ? Q extends {
|
|
105
121
|
dataset: K;
|
|
106
122
|
measures: readonly (infer Mk extends string)[];
|
|
107
|
-
} ? QueryResult<ResultRow<M, Dm, Mk, GroupKeys<Q>>, ResultTotals<M, Mk
|
|
108
|
-
export declare const periodSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
109
|
-
last: z.ZodObject<{
|
|
110
|
-
days: z.ZodNumber;
|
|
111
|
-
}, z.core.$strip>;
|
|
112
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
113
|
-
from: z.ZodISODate;
|
|
114
|
-
to: z.ZodISODate;
|
|
115
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
116
|
-
preset: z.ZodEnum<{
|
|
117
|
-
lastMonth: "lastMonth";
|
|
118
|
-
lastWeek: "lastWeek";
|
|
119
|
-
thisMonth: "thisMonth";
|
|
120
|
-
thisWeek: "thisWeek";
|
|
121
|
-
thisYear: "thisYear";
|
|
122
|
-
today: "today";
|
|
123
|
-
yesterday: "yesterday";
|
|
124
|
-
}>;
|
|
125
|
-
}, z.core.$strip>]>;
|
|
123
|
+
} ? QueryResult<ResultRow<M, Dm, Mk, GroupKeys<Q>>, ResultTotals<M, Mk>, PeriodShape<QueryPeriod<Q, P>>> : never : never;
|
|
126
124
|
/** The input schema branch for one dataset. */
|
|
127
125
|
export declare function datasetQuerySchema(dataset: AnyDataset): z.ZodObject<{
|
|
128
126
|
dataset: z.ZodLiteral<string>;
|
|
@@ -250,6 +248,8 @@ export declare function datasetQuerySchema(dataset: AnyDataset): z.ZodObject<{
|
|
|
250
248
|
today: "today";
|
|
251
249
|
yesterday: "yesterday";
|
|
252
250
|
}>;
|
|
251
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
252
|
+
all: z.ZodLiteral<true>;
|
|
253
253
|
}, z.core.$strip>]>>>;
|
|
254
254
|
time?: z.ZodOptional<z.ZodEnum<{
|
|
255
255
|
[x: string]: string;
|
|
@@ -393,6 +393,8 @@ export declare function buildQuerySchema(datasets: readonly AnyDataset[]): z.Zod
|
|
|
393
393
|
today: "today";
|
|
394
394
|
yesterday: "yesterday";
|
|
395
395
|
}>;
|
|
396
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
397
|
+
all: z.ZodLiteral<true>;
|
|
396
398
|
}, z.core.$strip>]>>>;
|
|
397
399
|
time?: z.ZodOptional<z.ZodEnum<{
|
|
398
400
|
[x: string]: string;
|
|
@@ -534,6 +536,8 @@ export declare function buildQuerySchema(datasets: readonly AnyDataset[]): z.Zod
|
|
|
534
536
|
today: "today";
|
|
535
537
|
yesterday: "yesterday";
|
|
536
538
|
}>;
|
|
539
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
540
|
+
all: z.ZodLiteral<true>;
|
|
537
541
|
}, z.core.$strip>]>>>;
|
|
538
542
|
time?: z.ZodOptional<z.ZodEnum<{
|
|
539
543
|
[x: string]: string;
|
|
@@ -552,11 +556,14 @@ export declare function buildQuerySchema(datasets: readonly AnyDataset[]): z.Zod
|
|
|
552
556
|
}, z.core.$strip>[]], "dataset">;
|
|
553
557
|
/** The result schema for one dataset. Row fields exist only for grouped dimensions. */
|
|
554
558
|
export declare function datasetResultSchema(dataset: AnyDataset): z.ZodObject<{
|
|
555
|
-
period: z.ZodObject<{
|
|
559
|
+
period: z.ZodUnion<readonly [z.ZodObject<{
|
|
556
560
|
from: z.ZodString;
|
|
557
561
|
to: z.ZodString;
|
|
558
562
|
timezone: z.ZodString;
|
|
559
|
-
}, z.core.$strip
|
|
563
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
564
|
+
all: z.ZodLiteral<true>;
|
|
565
|
+
timezone: z.ZodString;
|
|
566
|
+
}, z.core.$strip>]>;
|
|
560
567
|
rows: z.ZodArray<z.ZodObject<{
|
|
561
568
|
[x: string]: z.ZodOptional<z.ZodNullable<z.ZodNumber>> | z.ZodOptional<z.ZodObject<{
|
|
562
569
|
key: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
@@ -591,11 +598,14 @@ export declare function datasetResultSchema(dataset: AnyDataset): z.ZodObject<{
|
|
|
591
598
|
* For contracts that must not depend on the dataset list, such as an LLM tool's output.
|
|
592
599
|
*/
|
|
593
600
|
export declare const AnalyticsResultSchema: z.ZodObject<{
|
|
594
|
-
period: z.ZodObject<{
|
|
601
|
+
period: z.ZodUnion<readonly [z.ZodObject<{
|
|
595
602
|
from: z.ZodString;
|
|
596
603
|
to: z.ZodString;
|
|
597
604
|
timezone: z.ZodString;
|
|
598
|
-
}, z.core.$strip
|
|
605
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
606
|
+
all: z.ZodLiteral<true>;
|
|
607
|
+
timezone: z.ZodString;
|
|
608
|
+
}, z.core.$strip>]>;
|
|
599
609
|
rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
600
610
|
key: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
601
611
|
label: z.ZodString;
|