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.
@@ -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 = { last: { days: 30 } };
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
- ]).describe("Which days to count, in the organization timezone. Defaults to the last 30 days. At most two years.");
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 periodOutput = z.object({
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: periodOutput, rows: z.array(row), totals }).optional(),
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 ?? DEFAULT_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 = tenant === allTenants2 ? [] : [
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(tenant)
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 = (keys) => {
729
- const selected = keys ? keys.map(datasetOf) : datasets;
730
- const selectionKey = selected.map((dataset) => dataset.key).sort().join(",");
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 { authorize } = options;
748
- if (!authorize)
749
- return datasets;
750
- const allowed = await Promise.all(datasets.map((dataset) => authorize(dataset, ctx)));
751
- return datasets.filter((_, index) => allowed[index]);
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.map((dataset) => dataset.key));
824
+ return querySchemaFor(allowed);
759
825
  };
760
826
  const prepare = async (input, ctx) => {
761
- const parsed = querySchemaFor(undefined).safeParse(input);
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 dataset = datasetOf(parsed.data.dataset);
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, parsed.data);
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
- const periodOut = { from: target.from, to: target.to, timezone };
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 ? runPeriod(previous) : null
896
+ previous && runPeriod(previous).then(({ rows, totals }) => ({
897
+ period: range(previous),
898
+ rows,
899
+ totals
900
+ }))
818
901
  ]);
819
902
  return {
820
- period: current.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 || typeof value === "string" && value !== "" || typeof value === "number" && Number.isFinite(value);
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.node,
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
- check(dataset.tenantColumn, "tenantColumn");
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-dj3369zz.js";
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
- export type PeriodInput = {
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: PeriodInput;
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: PeriodInput, timezone: string, now: Date): ResolvedPeriod;
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 { type PeriodInput, type ResolvedPeriod } from './period.js';
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
- /** One tenant's id, or `allTenants`. */
54
- export type Tenant = string | number | typeof allTenants;
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: ResolvedPeriod;
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
- export type QueryResult<Row, Totals> = {
84
- period: {
85
- from: string;
86
- to: string;
87
- timezone: string;
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>> : never : never;
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;