measurr 0.4.0 → 0.6.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.
@@ -0,0 +1,303 @@
1
+ import {
2
+ rootOf,
3
+ directChildren
4
+ } from "./index-e6k30sfp.js";
5
+ import {
6
+ DEFAULT_PERIOD
7
+ } from "./index-115syy01.js";
8
+
9
+ // src/dataset.ts
10
+ function aggregate(result, fn, arg, options) {
11
+ return {
12
+ kind: "aggregate",
13
+ result,
14
+ label: options.label,
15
+ description: options.description,
16
+ access: options.access,
17
+ node: {
18
+ kind: "aggregate",
19
+ fn,
20
+ arg: arg?.node ?? null,
21
+ where: options.where?.node ?? null
22
+ }
23
+ };
24
+ }
25
+ var measure2 = {
26
+ count(options) {
27
+ return aggregate("count", "count", null, options);
28
+ },
29
+ countWhere(condition, options) {
30
+ return aggregate("count", "count", null, { ...options, where: condition });
31
+ },
32
+ countDistinct(expr, options) {
33
+ return aggregate("count", "countDistinct", expr, options);
34
+ },
35
+ sum(expr, options) {
36
+ return aggregate("number", "sum", expr, options);
37
+ },
38
+ avg(expr, options) {
39
+ return aggregate("number", "avg", expr, options);
40
+ },
41
+ median(expr, options) {
42
+ return aggregate("number", "median", expr, options);
43
+ },
44
+ share(options) {
45
+ return {
46
+ kind: "share",
47
+ result: "ratio",
48
+ label: options.label,
49
+ description: options.description,
50
+ access: options.access,
51
+ numerator: options.numerator.node,
52
+ denominator: options.denominator?.node ?? null
53
+ };
54
+ },
55
+ ratio(numerator, denominator, options) {
56
+ return {
57
+ kind: "ratio",
58
+ result: "ratio",
59
+ label: options.label,
60
+ description: options.description,
61
+ access: options.access,
62
+ numerator,
63
+ denominator
64
+ };
65
+ }
66
+ };
67
+ function base(options) {
68
+ return {
69
+ label: options.label,
70
+ description: options.description,
71
+ access: options.access,
72
+ groupable: options.filterOnly !== true,
73
+ filterable: options.groupOnly !== true
74
+ };
75
+ }
76
+ function withAccess(_options, dimension) {
77
+ return dimension;
78
+ }
79
+ var dimension2 = {
80
+ enum(expr, values, options) {
81
+ for (const value of Object.values(values)) {
82
+ if (!(value in options.labels)) {
83
+ throw new Error(`${options.label}: the value ${value} has no label`);
84
+ }
85
+ }
86
+ return withAccess(options, {
87
+ ...base(options),
88
+ category: "closed",
89
+ kind: "enum",
90
+ expr: expr.node,
91
+ values: options.labels,
92
+ empty: options.empty
93
+ });
94
+ },
95
+ computed(options) {
96
+ return withAccess(options, {
97
+ ...base(options),
98
+ category: "closed",
99
+ kind: "computed",
100
+ expr: options.expr.node,
101
+ values: options.values,
102
+ empty: options.empty
103
+ });
104
+ },
105
+ relation(options) {
106
+ return withAccess(options, {
107
+ ...base(options),
108
+ category: "open",
109
+ kind: "relation",
110
+ key: options.key.node,
111
+ name: options.name.node,
112
+ empty: options.empty
113
+ });
114
+ },
115
+ manyToMany(options) {
116
+ return withAccess(options, {
117
+ ...base(options),
118
+ category: "open",
119
+ kind: "manyToMany",
120
+ table: options.through.table.ref,
121
+ on: options.through.on.node,
122
+ key: options.through.key.node,
123
+ name: options.through.name.node,
124
+ empty: options.empty,
125
+ note: options.note
126
+ });
127
+ },
128
+ number(expr, options) {
129
+ return withAccess(options, {
130
+ ...base(options),
131
+ category: "numeric",
132
+ kind: "number",
133
+ expr: expr.node,
134
+ labelFor: options.labelFor
135
+ });
136
+ },
137
+ timeBucket(time, unit, options) {
138
+ return withAccess(options, {
139
+ ...base({
140
+ ...options,
141
+ label: options?.label ?? unit[0]?.toUpperCase() + unit.slice(1)
142
+ }),
143
+ category: "time",
144
+ kind: "timeBucket",
145
+ time,
146
+ unit,
147
+ labelFor: options?.labelFor
148
+ });
149
+ },
150
+ timePart(time, part, options) {
151
+ return withAccess(options, {
152
+ ...base({
153
+ ...options,
154
+ label: options?.label ?? (part === "isodow" ? "Weekday" : "Hour")
155
+ }),
156
+ category: "numeric",
157
+ kind: "timePart",
158
+ time,
159
+ part,
160
+ labelFor: options?.labelFor
161
+ });
162
+ }
163
+ };
164
+ function defineDataset2(definition) {
165
+ const timeEntries = Object.entries(definition.time);
166
+ const firstTime = timeEntries[0];
167
+ if (!firstTime) {
168
+ throw new Error(`Dataset ${definition.key}: declare at least one time field`);
169
+ }
170
+ if (definition.tenantColumn === undefined) {
171
+ throw new Error(`Dataset ${definition.key}: declare tenantColumn, or null for a dataset without tenants`);
172
+ }
173
+ const time = Object.fromEntries(timeEntries.map(([key, field]) => [
174
+ key,
175
+ { column: field.column.node, label: field.label }
176
+ ]));
177
+ const dataset = {
178
+ key: definition.key,
179
+ label: definition.label,
180
+ description: definition.description,
181
+ from: definition.from.ref,
182
+ source: definition.source,
183
+ tenantColumn: definition.tenantColumn?.node ?? null,
184
+ scope: definition.scope?.node ?? null,
185
+ time,
186
+ defaultTime: definition.defaultTime ?? firstTime[0],
187
+ defaultPeriod: definition.defaultPeriod ?? DEFAULT_PERIOD,
188
+ measures: definition.measures,
189
+ dimensions: definition.dimensions
190
+ };
191
+ return validateDataset(dataset);
192
+ }
193
+ function validateDataset(dataset) {
194
+ const fail = (message) => {
195
+ throw new Error(`Dataset ${dataset.key}: ${message}`);
196
+ };
197
+ const check = (node, where, extraRoots = []) => {
198
+ visitScoped(node, [dataset.from, ...extraRoots], (inner, roots) => {
199
+ if (inner.kind === "column") {
200
+ const root = rootOf(inner.table);
201
+ if (!roots.includes(root)) {
202
+ fail(`${where} reads ${inner.table.name}.${inner.column}, which is not joined to ${dataset.from.name}`);
203
+ }
204
+ }
205
+ });
206
+ };
207
+ if (dataset.tenantColumn)
208
+ check(dataset.tenantColumn, "tenantColumn");
209
+ if (dataset.scope)
210
+ check(dataset.scope, "scope");
211
+ for (const [key, field] of Object.entries(dataset.time)) {
212
+ check(field.column, `time field ${key}`);
213
+ }
214
+ const measures = dataset.measures;
215
+ for (const [key, entry] of Object.entries(measures)) {
216
+ if (entry.kind === "aggregate")
217
+ check(entry.node, `measure ${key}`);
218
+ if (entry.kind === "share") {
219
+ check(entry.numerator, `measure ${key}`);
220
+ if (entry.denominator)
221
+ check(entry.denominator, `measure ${key}`);
222
+ }
223
+ if (entry.kind === "ratio")
224
+ checkRatio(measures, key, [], fail);
225
+ }
226
+ const dimensions = dataset.dimensions;
227
+ for (const [key, entry] of Object.entries(dimensions)) {
228
+ const where = `dimension ${key}`;
229
+ switch (entry.kind) {
230
+ case "enum":
231
+ case "number":
232
+ check(entry.expr, where);
233
+ break;
234
+ case "computed":
235
+ check(entry.expr, where);
236
+ for (const value of plainCaseValues(entry.expr)) {
237
+ if (!(value in entry.values)) {
238
+ fail(`${where} can produce "${value}", which is missing from its values`);
239
+ }
240
+ }
241
+ break;
242
+ case "relation":
243
+ check(entry.key, where);
244
+ check(entry.name, where);
245
+ break;
246
+ case "manyToMany":
247
+ if (entry.table.join) {
248
+ fail(`${where}: through.table must be a standalone table(), not a join`);
249
+ }
250
+ check(entry.on, where, [entry.table]);
251
+ check(entry.key, where, [entry.table]);
252
+ check(entry.name, where, [entry.table]);
253
+ break;
254
+ case "timeBucket":
255
+ case "timePart":
256
+ if (!(entry.time in dataset.time))
257
+ fail(`${where} names unknown time field ${entry.time}`);
258
+ break;
259
+ }
260
+ }
261
+ return dataset;
262
+ }
263
+ function checkRatio(measures, key, path, fail) {
264
+ if (path.includes(key))
265
+ fail(`ratio measures form a cycle: ${[...path, key].join(" -> ")}`);
266
+ const entry = measures[key];
267
+ if (!entry)
268
+ fail(`ratio measure ${path.at(-1)} names unknown measure ${key}`);
269
+ if (entry?.kind !== "ratio")
270
+ return;
271
+ checkRatio(measures, entry.numerator, [...path, key], fail);
272
+ checkRatio(measures, entry.denominator, [...path, key], fail);
273
+ }
274
+ function visitScoped(node, roots, visit, seenJoins = new Set) {
275
+ visit(node, roots);
276
+ if (node.kind === "exists") {
277
+ visitScoped(node.where, [...roots, node.table], visit, seenJoins);
278
+ return;
279
+ }
280
+ if (node.kind === "column" && node.table.join && !seenJoins.has(node.table)) {
281
+ seenJoins.add(node.table);
282
+ visitScoped(node.table.join.on, roots, visit, seenJoins);
283
+ return;
284
+ }
285
+ for (const child of directChildren(node)) {
286
+ visitScoped(child, roots, visit, seenJoins);
287
+ }
288
+ }
289
+ function plainCaseValues(node) {
290
+ if (node.kind === "param" && typeof node.value === "string")
291
+ return [node.value];
292
+ if (node.kind === "case") {
293
+ return [
294
+ ...node.branches.flatMap((branch) => plainCaseValues(branch.result)),
295
+ ...node.otherwise ? plainCaseValues(node.otherwise) : []
296
+ ];
297
+ }
298
+ if (node.kind === "coalesce")
299
+ return node.items.flatMap(plainCaseValues);
300
+ return [];
301
+ }
302
+
303
+ export { measure2, dimension2, defineDataset2 };
@@ -0,0 +1,46 @@
1
+ import {
2
+ AnalyticsError2
3
+ } from "./index-z5xv3ffk.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 };