@objectstack/service-analytics 17.0.0-rc.0 → 17.0.0-rc.1
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/CHANGELOG.md +2656 -0
- package/dist/index.cjs +414 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +406 -124
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -40,7 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
module.exports = __toCommonJS(index_exports);
|
|
41
41
|
|
|
42
42
|
// src/analytics-service.ts
|
|
43
|
-
var
|
|
43
|
+
var import_core5 = require("@objectstack/core");
|
|
44
44
|
|
|
45
45
|
// src/cube-registry.ts
|
|
46
46
|
var CubeRegistry = class {
|
|
@@ -169,7 +169,8 @@ var MONGO_TO_CUBE_OP = {
|
|
|
169
169
|
$nin: "notIn",
|
|
170
170
|
$contains: "contains",
|
|
171
171
|
$notContains: "notContains",
|
|
172
|
-
$
|
|
172
|
+
$startsWith: "startsWith",
|
|
173
|
+
$endsWith: "endsWith"
|
|
173
174
|
};
|
|
174
175
|
function stringifyForCube(v) {
|
|
175
176
|
if (v == null) return "";
|
|
@@ -178,55 +179,102 @@ function stringifyForCube(v) {
|
|
|
178
179
|
if (typeof v === "object") return JSON.stringify(v);
|
|
179
180
|
return String(v);
|
|
180
181
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
function andOf(children) {
|
|
183
|
+
if (children.length === 0) return null;
|
|
184
|
+
if (children.length === 1) return children[0];
|
|
185
|
+
return { kind: "and", children };
|
|
186
|
+
}
|
|
187
|
+
function fieldLeaves(key, raw) {
|
|
188
|
+
const out = [];
|
|
189
|
+
const leaf = (operator, values) => {
|
|
190
|
+
out.push({ kind: "leaf", member: key, operator, values });
|
|
191
|
+
};
|
|
192
|
+
if (raw === null) {
|
|
193
|
+
leaf("notSet", []);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
if (typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
|
|
197
|
+
const wrapper = raw;
|
|
198
|
+
const opKeys = Object.keys(wrapper).filter((k) => k.startsWith("$"));
|
|
199
|
+
if (opKeys.length > 0) {
|
|
200
|
+
for (const opKey of opKeys) {
|
|
201
|
+
if (opKey === "$between") {
|
|
202
|
+
const v2 = wrapper[opKey];
|
|
203
|
+
if (!Array.isArray(v2) || v2.length !== 2) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`[analytics] "$between" on "${key}" needs a two-element [min, max] array, got ${JSON.stringify(v2)}. Dropping the predicate would silently widen the query to every row.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
leaf("gte", [stringifyForCube(v2[0])]);
|
|
209
|
+
leaf("lte", [stringifyForCube(v2[1])]);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (opKey === "$null" || opKey === "$exists") {
|
|
213
|
+
const isNull = opKey === "$null" ? wrapper[opKey] === true : wrapper[opKey] === false;
|
|
214
|
+
leaf(isNull ? "notSet" : "set", []);
|
|
215
|
+
continue;
|
|
188
216
|
}
|
|
217
|
+
const cubeOp = MONGO_TO_CUBE_OP[opKey];
|
|
218
|
+
if (!cubeOp) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`[analytics] Unsupported filter operator "${opKey}" on "${key}". Supported: ${Object.keys(MONGO_TO_CUBE_OP).join(", ")}, $between, $null, $exists, and the $and/$or/$not combinators. Dropping it would silently widen the query to rows the filter excludes.`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const v = wrapper[opKey];
|
|
224
|
+
leaf(cubeOp, Array.isArray(v) ? v.map(stringifyForCube) : [stringifyForCube(v)]);
|
|
189
225
|
}
|
|
190
|
-
|
|
226
|
+
return out;
|
|
191
227
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
out.push({ member: key, operator: "notSet", values: [] });
|
|
195
|
-
continue;
|
|
228
|
+
for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {
|
|
229
|
+
out.push(...fieldLeaves(`${key}.${nestedKey}`, nestedVal));
|
|
196
230
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
if (Array.isArray(raw)) leaf("in", raw.map(stringifyForCube));
|
|
234
|
+
else leaf("equals", [stringifyForCube(raw)]);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function buildNode(cond) {
|
|
238
|
+
const children = [];
|
|
239
|
+
for (const [key, raw] of Object.entries(cond)) {
|
|
240
|
+
if (raw === void 0) continue;
|
|
241
|
+
if (key === "$and" || key === "$or") {
|
|
242
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`[analytics] "${key}" requires a non-empty array. An empty combinator has no defensible reading \u2014 dropping it widens the query, and treating it as "match nothing" silently empties a chart.`
|
|
245
|
+
);
|
|
212
246
|
}
|
|
247
|
+
const branches = raw.map((sub) => sub && typeof sub === "object" ? buildNode(sub) : null).filter((n) => n !== null);
|
|
248
|
+
if (branches.length === 0) continue;
|
|
249
|
+
if (key === "$and") children.push(...branches);
|
|
250
|
+
else children.push(branches.length === 1 ? branches[0] : { kind: "or", children: branches });
|
|
213
251
|
continue;
|
|
214
252
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
253
|
+
if (key === "$not") {
|
|
254
|
+
const inner = raw && typeof raw === "object" ? buildNode(raw) : null;
|
|
255
|
+
if (inner) children.push({ kind: "not", child: inner });
|
|
256
|
+
continue;
|
|
219
257
|
}
|
|
258
|
+
if (key.startsWith("$")) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`[analytics] Unsupported top-level filter operator "${key}". Dropping it would silently widen the query to rows the filter excludes.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
children.push(...fieldLeaves(key, raw));
|
|
220
264
|
}
|
|
265
|
+
return andOf(children);
|
|
221
266
|
}
|
|
222
|
-
function
|
|
223
|
-
if (!query || typeof query !== "object") return
|
|
224
|
-
const out = [];
|
|
267
|
+
function normalizeAnalyticsFilterTree(query) {
|
|
268
|
+
if (!query || typeof query !== "object") return null;
|
|
225
269
|
const where = query.where;
|
|
226
|
-
if (where
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
270
|
+
if (!where || typeof where !== "object" || Array.isArray(where)) return null;
|
|
271
|
+
return buildNode(where);
|
|
272
|
+
}
|
|
273
|
+
function collectFilterLeaves(node) {
|
|
274
|
+
if (!node) return [];
|
|
275
|
+
if (node.kind === "leaf") return [{ member: node.member, operator: node.operator, values: node.values }];
|
|
276
|
+
if (node.kind === "not") return collectFilterLeaves(node.child);
|
|
277
|
+
return node.children.flatMap(collectFilterLeaves);
|
|
230
278
|
}
|
|
231
279
|
function recoverNumber(s) {
|
|
232
280
|
if (/^-?\d+(\.\d+)?$/.test(s)) {
|
|
@@ -358,6 +406,18 @@ function compileOperator(col, op, val, field, params) {
|
|
|
358
406
|
}
|
|
359
407
|
|
|
360
408
|
// src/strategies/native-sql-strategy.ts
|
|
409
|
+
var import_core = require("@objectstack/core");
|
|
410
|
+
var AGGREGATE_SQL = {
|
|
411
|
+
"count": () => "COUNT(*)",
|
|
412
|
+
"sum": (col) => `SUM(${col})`,
|
|
413
|
+
"avg": (col) => `AVG(${col})`,
|
|
414
|
+
"min": (col) => `MIN(${col})`,
|
|
415
|
+
"max": (col) => `MAX(${col})`,
|
|
416
|
+
"count_distinct": (col) => `COUNT(DISTINCT ${col})`
|
|
417
|
+
};
|
|
418
|
+
var SUPPORTED_AGGREGATE_SQL_KEYS = Object.keys(AGGREGATE_SQL);
|
|
419
|
+
var EXPRESSION_METRIC_TYPES = /* @__PURE__ */ new Set(["number", "string", "boolean"]);
|
|
420
|
+
var IDENTIFIER_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
361
421
|
var NativeSQLStrategy = class {
|
|
362
422
|
constructor() {
|
|
363
423
|
this.name = "NativeSQLStrategy";
|
|
@@ -412,15 +472,15 @@ var NativeSQLStrategy = class {
|
|
|
412
472
|
}
|
|
413
473
|
}
|
|
414
474
|
const whereClauses = [];
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
475
|
+
const filterSql = this.compileFilterNode(
|
|
476
|
+
normalizeAnalyticsFilterTree(query),
|
|
477
|
+
cube,
|
|
478
|
+
tableName,
|
|
479
|
+
joins,
|
|
480
|
+
params,
|
|
481
|
+
ctx
|
|
482
|
+
);
|
|
483
|
+
if (filterSql) whereClauses.push(filterSql);
|
|
424
484
|
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
425
485
|
for (const td of query.timeDimensions) {
|
|
426
486
|
const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);
|
|
@@ -428,11 +488,17 @@ var NativeSQLStrategy = class {
|
|
|
428
488
|
const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
429
489
|
if (range.length === 2) {
|
|
430
490
|
const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
491
|
+
const column = this.temporalColumn(ctx, td2, colExpr);
|
|
492
|
+
const nextDay = (0, import_core.nextUtcCalendarDay)(range[1]);
|
|
493
|
+
params.push(this.coerceTemporal(ctx, td2, range[0]));
|
|
494
|
+
const lower = `${column} >= $${params.length}`;
|
|
495
|
+
if (nextDay != null) {
|
|
496
|
+
params.push(this.coerceTemporal(ctx, td2, nextDay));
|
|
497
|
+
whereClauses.push(`(${lower} AND ${column} < $${params.length})`);
|
|
498
|
+
} else {
|
|
499
|
+
params.push(this.coerceTemporal(ctx, td2, range[1]));
|
|
500
|
+
whereClauses.push(`(${lower} AND ${column} <= $${params.length})`);
|
|
501
|
+
}
|
|
436
502
|
}
|
|
437
503
|
}
|
|
438
504
|
}
|
|
@@ -530,6 +596,7 @@ var NativeSQLStrategy = class {
|
|
|
530
596
|
}
|
|
531
597
|
return rawSql;
|
|
532
598
|
}
|
|
599
|
+
if (!IDENTIFIER_PATH.test(rawSql)) return rawSql;
|
|
533
600
|
const segments = rawSql.split(".");
|
|
534
601
|
const column = segments[segments.length - 1];
|
|
535
602
|
const hops = segments.slice(0, -1);
|
|
@@ -589,24 +656,19 @@ var NativeSQLStrategy = class {
|
|
|
589
656
|
}
|
|
590
657
|
resolveMeasureSql(cube, member, parentTable, joins) {
|
|
591
658
|
const measure = this.lookupMember(cube, member, "measure");
|
|
592
|
-
if (!measure)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
case "sum":
|
|
598
|
-
return `SUM(${col})`;
|
|
599
|
-
case "avg":
|
|
600
|
-
return `AVG(${col})`;
|
|
601
|
-
case "min":
|
|
602
|
-
return `MIN(${col})`;
|
|
603
|
-
case "max":
|
|
604
|
-
return `MAX(${col})`;
|
|
605
|
-
case "count_distinct":
|
|
606
|
-
return `COUNT(DISTINCT ${col})`;
|
|
607
|
-
default:
|
|
608
|
-
return `COUNT(*)`;
|
|
659
|
+
if (!measure) {
|
|
660
|
+
const declared = Object.keys(cube.measures ?? {});
|
|
661
|
+
throw new Error(
|
|
662
|
+
`[native-sql-strategy] cube "${cube.name}" declares no measure "${member}"` + (declared.length ? ` (declared: ${declared.join(", ")})` : " (it declares none)")
|
|
663
|
+
);
|
|
609
664
|
}
|
|
665
|
+
const col = measure.sql === "*" ? "*" : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);
|
|
666
|
+
const wrap = AGGREGATE_SQL[measure.type];
|
|
667
|
+
if (wrap) return wrap(col);
|
|
668
|
+
if (EXPRESSION_METRIC_TYPES.has(measure.type)) return col;
|
|
669
|
+
throw new Error(
|
|
670
|
+
`[native-sql-strategy] measure "${member}" on cube "${cube.name}" has unrecognised type "${measure.type}" \u2014 expected an aggregate (${SUPPORTED_AGGREGATE_SQL_KEYS.join(", ")}) or a custom-expression type (${[...EXPRESSION_METRIC_TYPES].join(", ")}).`
|
|
671
|
+
);
|
|
610
672
|
}
|
|
611
673
|
resolveFieldSql(cube, member, parentTable, joins) {
|
|
612
674
|
const dim = this.lookupMember(cube, member, "dimension");
|
|
@@ -659,7 +721,53 @@ var NativeSQLStrategy = class {
|
|
|
659
721
|
}
|
|
660
722
|
return coerceFilterValueForSql(value);
|
|
661
723
|
}
|
|
662
|
-
|
|
724
|
+
/**
|
|
725
|
+
* The column side of {@link coerceTemporal}: normalise the reference so it
|
|
726
|
+
* reads in the storage form the comparand was coerced into.
|
|
727
|
+
*
|
|
728
|
+
* A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)
|
|
729
|
+
* and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's
|
|
730
|
+
* own `created_at`) at the SAME time, so coercing the value alone fixes one half
|
|
731
|
+
* and empties the other. That is #3912: a `dateRange: last_30_days` on
|
|
732
|
+
* `created_date` read 0 with 29 rows in range. Every other column and dialect
|
|
733
|
+
* gets its reference back verbatim.
|
|
734
|
+
*/
|
|
735
|
+
temporalColumn(ctx, target, col) {
|
|
736
|
+
if (typeof ctx.coerceTemporalFilterColumn !== "function") return col;
|
|
737
|
+
return ctx.coerceTemporalFilterColumn(target.object, target.field, col) || col;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Compile a normalized filter node into a boolean SQL expression, recursing
|
|
741
|
+
* through the combinators. `null` = no constraint.
|
|
742
|
+
*
|
|
743
|
+
* Leaves go through {@link buildFilterClause} exactly as they did when this
|
|
744
|
+
* was a flat loop, so the storage-form coercion and the calendar-day
|
|
745
|
+
* upper-bound rule (#3777) apply at every depth — including inside an `$or`,
|
|
746
|
+
* where a second, combinator-aware implementation would have been free to
|
|
747
|
+
* drift from the first.
|
|
748
|
+
*
|
|
749
|
+
* Parenthesisation is explicit rather than left to SQL's precedence: `AND`
|
|
750
|
+
* does bind tighter than `OR`, so `a AND b OR c` happens to be right, but
|
|
751
|
+
* being right by construction is what keeps a future edit from making it
|
|
752
|
+
* wrong.
|
|
753
|
+
*/
|
|
754
|
+
compileFilterNode(node, cube, parentTable, joins, params, ctx) {
|
|
755
|
+
if (!node) return null;
|
|
756
|
+
if (node.kind === "leaf") {
|
|
757
|
+
const colExpr = this.resolveFieldSql(cube, node.member, parentTable, joins);
|
|
758
|
+
const target = this.resolveStorageTarget(cube, node.member, parentTable);
|
|
759
|
+
return this.buildFilterClause(colExpr, node.operator, node.values, params, ctx, target);
|
|
760
|
+
}
|
|
761
|
+
if (node.kind === "not") {
|
|
762
|
+
const inner = this.compileFilterNode(node.child, cube, parentTable, joins, params, ctx);
|
|
763
|
+
return inner ? `NOT (${inner})` : null;
|
|
764
|
+
}
|
|
765
|
+
const parts = node.children.map((child) => this.compileFilterNode(child, cube, parentTable, joins, params, ctx)).filter((s) => !!s);
|
|
766
|
+
if (parts.length === 0) return null;
|
|
767
|
+
if (parts.length === 1) return parts[0];
|
|
768
|
+
return `(${parts.join(node.kind === "or" ? " OR " : " AND ")})`;
|
|
769
|
+
}
|
|
770
|
+
buildFilterClause(rawCol, operator, values, params, ctx, target) {
|
|
663
771
|
const opMap = {
|
|
664
772
|
equals: "=",
|
|
665
773
|
notEquals: "!=",
|
|
@@ -668,26 +776,42 @@ var NativeSQLStrategy = class {
|
|
|
668
776
|
lt: "<",
|
|
669
777
|
lte: "<=",
|
|
670
778
|
contains: "LIKE",
|
|
671
|
-
notContains: "NOT LIKE"
|
|
779
|
+
notContains: "NOT LIKE",
|
|
780
|
+
startsWith: "LIKE",
|
|
781
|
+
endsWith: "LIKE"
|
|
672
782
|
};
|
|
673
|
-
|
|
674
|
-
|
|
783
|
+
const likePattern = {
|
|
784
|
+
contains: (v) => `%${v}%`,
|
|
785
|
+
notContains: (v) => `%${v}%`,
|
|
786
|
+
startsWith: (v) => `${v}%`,
|
|
787
|
+
endsWith: (v) => `%${v}`
|
|
788
|
+
};
|
|
789
|
+
if (operator === "set") return `${rawCol} IS NOT NULL`;
|
|
790
|
+
if (operator === "notSet") return `${rawCol} IS NULL`;
|
|
675
791
|
if (operator === "in" || operator === "notIn") {
|
|
676
792
|
if (!values || values.length === 0) return null;
|
|
677
793
|
const placeholders = values.map((v) => {
|
|
678
794
|
params.push(this.coerceTemporal(ctx, target, v));
|
|
679
795
|
return `$${params.length}`;
|
|
680
796
|
}).join(", ");
|
|
681
|
-
return `${
|
|
797
|
+
return `${this.temporalColumn(ctx, target, rawCol)} ${operator === "in" ? "IN" : "NOT IN"} (${placeholders})`;
|
|
682
798
|
}
|
|
683
799
|
const sqlOp = opMap[operator];
|
|
684
800
|
if (!sqlOp || !values || values.length === 0) return null;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
params.
|
|
801
|
+
const pattern = likePattern[operator];
|
|
802
|
+
if (pattern) {
|
|
803
|
+
params.push(pattern(values[0]));
|
|
804
|
+
return `${rawCol} ${sqlOp} $${params.length}`;
|
|
689
805
|
}
|
|
690
|
-
|
|
806
|
+
if (operator === "lte") {
|
|
807
|
+
const nextDay = (0, import_core.nextUtcCalendarDay)(values[0]);
|
|
808
|
+
if (nextDay != null) {
|
|
809
|
+
params.push(this.coerceTemporal(ctx, target, nextDay));
|
|
810
|
+
return `${this.temporalColumn(ctx, target, rawCol)} < $${params.length}`;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
params.push(this.coerceTemporal(ctx, target, values[0]));
|
|
814
|
+
return `${this.temporalColumn(ctx, target, rawCol)} ${sqlOp} $${params.length}`;
|
|
691
815
|
}
|
|
692
816
|
extractObjectName(cube) {
|
|
693
817
|
return cube.sql.trim();
|
|
@@ -709,6 +833,9 @@ var NativeSQLStrategy = class {
|
|
|
709
833
|
}
|
|
710
834
|
};
|
|
711
835
|
|
|
836
|
+
// src/strategies/objectql-strategy.ts
|
|
837
|
+
var import_core2 = require("@objectstack/core");
|
|
838
|
+
|
|
712
839
|
// src/strategies/cross-object-rebucket.ts
|
|
713
840
|
var RECOMBINABLE_METHODS = /* @__PURE__ */ new Set([
|
|
714
841
|
"sum",
|
|
@@ -811,11 +938,7 @@ var ObjectQLStrategy = class {
|
|
|
811
938
|
}
|
|
812
939
|
const filter = {};
|
|
813
940
|
const conjuncts = [];
|
|
814
|
-
|
|
815
|
-
const fieldName = this.resolveFieldName(cube, f.member, "any");
|
|
816
|
-
const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(f.operator, f.values));
|
|
817
|
-
if (extra) conjuncts.push(extra);
|
|
818
|
-
}
|
|
941
|
+
this.applyFilterNode(normalizeAnalyticsFilterTree(query), cube, filter, conjuncts);
|
|
819
942
|
for (const { field, bounds } of this.dateRangeBounds(cube, query)) {
|
|
820
943
|
const extra = this.mergeFilterOperand(filter, field, bounds);
|
|
821
944
|
if (extra) conjuncts.push(extra);
|
|
@@ -847,11 +970,9 @@ var ObjectQLStrategy = class {
|
|
|
847
970
|
});
|
|
848
971
|
const mappedRows = rows.map((row) => {
|
|
849
972
|
const mapped = {};
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (shortName in row) mapped[dim] = row[shortName];
|
|
854
|
-
}
|
|
973
|
+
for (const dim of this.projectedDimensions(query)) {
|
|
974
|
+
const shortName = this.resolveFieldName(cube, dim, "dimension");
|
|
975
|
+
if (shortName in row) mapped[dim] = row[shortName];
|
|
855
976
|
}
|
|
856
977
|
if (query.measures) {
|
|
857
978
|
for (const m of query.measures) {
|
|
@@ -897,7 +1018,7 @@ var ObjectQLStrategy = class {
|
|
|
897
1018
|
}
|
|
898
1019
|
const tableName = this.extractObjectName(cube);
|
|
899
1020
|
const plan = this.planCrossObject(cube, query, Object.fromEntries(
|
|
900
|
-
|
|
1021
|
+
collectFilterLeaves(normalizeAnalyticsFilterTree(query)).map((f) => [this.resolveFieldName(cube, f.member, "any"), true])
|
|
901
1022
|
));
|
|
902
1023
|
const crossByDim = new Map((plan?.crossDims ?? []).map((cd) => [cd.outputName, cd]));
|
|
903
1024
|
const joinClauses = [];
|
|
@@ -934,18 +1055,18 @@ var ObjectQLStrategy = class {
|
|
|
934
1055
|
}
|
|
935
1056
|
}
|
|
936
1057
|
const whereParts = [];
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
);
|
|
944
|
-
if (clause) whereParts.push(clause);
|
|
945
|
-
}
|
|
1058
|
+
const filterClause = this.renderFilterNodeSql(
|
|
1059
|
+
normalizeAnalyticsFilterTree(query),
|
|
1060
|
+
cube,
|
|
1061
|
+
params
|
|
1062
|
+
);
|
|
1063
|
+
if (filterClause) whereParts.push(filterClause);
|
|
946
1064
|
for (const { field, bounds } of this.dateRangeBounds(cube, query)) {
|
|
947
|
-
|
|
948
|
-
|
|
1065
|
+
const nextDay = (0, import_core2.nextUtcCalendarDay)(bounds.$lte);
|
|
1066
|
+
params.push(bounds.$gte, nextDay ?? bounds.$lte);
|
|
1067
|
+
whereParts.push(
|
|
1068
|
+
`(${field} >= $${params.length - 1} AND ${field} ${nextDay ? "<" : "<="} $${params.length})`
|
|
1069
|
+
);
|
|
949
1070
|
}
|
|
950
1071
|
const scope = ctx.getReadScope?.(tableName);
|
|
951
1072
|
if (scope != null) {
|
|
@@ -1119,7 +1240,7 @@ var ObjectQLStrategy = class {
|
|
|
1119
1240
|
const merged = rebucketCrossObject(baseRows, baseDimFields, resolvedDims, measures);
|
|
1120
1241
|
const mappedRows = merged.map((row) => {
|
|
1121
1242
|
const out = {};
|
|
1122
|
-
for (const dim of query
|
|
1243
|
+
for (const dim of this.projectedDimensions(query)) {
|
|
1123
1244
|
if (crossByDim.has(dim)) {
|
|
1124
1245
|
if (dim in row) out[dim] = row[dim];
|
|
1125
1246
|
} else {
|
|
@@ -1127,11 +1248,6 @@ var ObjectQLStrategy = class {
|
|
|
1127
1248
|
if (field in row) out[dim] = row[field];
|
|
1128
1249
|
}
|
|
1129
1250
|
}
|
|
1130
|
-
for (const td of query.timeDimensions ?? []) {
|
|
1131
|
-
if (query.dimensions?.includes(td.dimension)) continue;
|
|
1132
|
-
const field = this.resolveFieldName(cube, td.dimension, "dimension");
|
|
1133
|
-
if (field in row) out[td.dimension] = row[field];
|
|
1134
|
-
}
|
|
1135
1251
|
for (const m of query.measures ?? []) {
|
|
1136
1252
|
if (m in row) out[m] = row[m];
|
|
1137
1253
|
}
|
|
@@ -1274,6 +1390,75 @@ var ObjectQLStrategy = class {
|
|
|
1274
1390
|
* are handed back for the caller to AND in separately, so the engine
|
|
1275
1391
|
* intersects them instead of the strategy picking a winner.
|
|
1276
1392
|
*/
|
|
1393
|
+
/**
|
|
1394
|
+
* Fold a normalized filter node into the engine filter being built.
|
|
1395
|
+
*
|
|
1396
|
+
* AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly
|
|
1397
|
+
* as the flat loop this replaced did — so a query without combinators still
|
|
1398
|
+
* produces byte-identical engine input. Anything structural (`$or`, `$not`,
|
|
1399
|
+
* a nested `$and` that cannot merge) becomes its own conjunct, which the
|
|
1400
|
+
* caller ANDs in. The engine speaks these combinators natively
|
|
1401
|
+
* (`FilterCondition` declares them and every driver compiles them), so this
|
|
1402
|
+
* path hands them over rather than lowering them.
|
|
1403
|
+
*/
|
|
1404
|
+
applyFilterNode(node, cube, filter, conjuncts) {
|
|
1405
|
+
if (!node) return;
|
|
1406
|
+
if (node.kind === "leaf") {
|
|
1407
|
+
const fieldName = this.resolveFieldName(cube, node.member, "any");
|
|
1408
|
+
const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(node.operator, node.values));
|
|
1409
|
+
if (extra) conjuncts.push(extra);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (node.kind === "and") {
|
|
1413
|
+
for (const child of node.children) this.applyFilterNode(child, cube, filter, conjuncts);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const rendered = this.filterNodeToCondition(node, cube);
|
|
1417
|
+
if (rendered) conjuncts.push(rendered);
|
|
1418
|
+
}
|
|
1419
|
+
/** A node as a standalone `FilterCondition` the engine can consume. */
|
|
1420
|
+
filterNodeToCondition(node, cube) {
|
|
1421
|
+
if (!node) return null;
|
|
1422
|
+
if (node.kind === "not") {
|
|
1423
|
+
const inner = this.filterNodeToCondition(node.child, cube);
|
|
1424
|
+
return inner ? { $not: inner } : null;
|
|
1425
|
+
}
|
|
1426
|
+
if (node.kind === "or") {
|
|
1427
|
+
const branches = node.children.map((child) => this.filterNodeToCondition(child, cube)).filter((c) => !!c);
|
|
1428
|
+
return branches.length > 0 ? { $or: branches } : null;
|
|
1429
|
+
}
|
|
1430
|
+
const filter = {};
|
|
1431
|
+
const conjuncts = [];
|
|
1432
|
+
this.applyFilterNode(node, cube, filter, conjuncts);
|
|
1433
|
+
if (conjuncts.length > 0) {
|
|
1434
|
+
filter.$and = [...Array.isArray(filter.$and) ? filter.$and : [], ...conjuncts];
|
|
1435
|
+
}
|
|
1436
|
+
return Object.keys(filter).length > 0 ? filter : null;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Render a normalized filter node as the display SQL `/analytics/sql`
|
|
1440
|
+
* echoes. Values still bind as `$n` placeholders — the echo travels to the
|
|
1441
|
+
* browser, so a comparand is never inlined.
|
|
1442
|
+
*/
|
|
1443
|
+
renderFilterNodeSql(node, cube, params) {
|
|
1444
|
+
if (!node) return null;
|
|
1445
|
+
if (node.kind === "leaf") {
|
|
1446
|
+
return this.buildFilterClauseSql(
|
|
1447
|
+
this.resolveFieldName(cube, node.member, "any"),
|
|
1448
|
+
node.operator,
|
|
1449
|
+
node.values,
|
|
1450
|
+
params
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
if (node.kind === "not") {
|
|
1454
|
+
const inner = this.renderFilterNodeSql(node.child, cube, params);
|
|
1455
|
+
return inner ? `NOT (${inner})` : null;
|
|
1456
|
+
}
|
|
1457
|
+
const parts = node.children.map((child) => this.renderFilterNodeSql(child, cube, params)).filter((s) => !!s);
|
|
1458
|
+
if (parts.length === 0) return null;
|
|
1459
|
+
if (parts.length === 1) return parts[0];
|
|
1460
|
+
return `(${parts.join(node.kind === "or" ? " OR " : " AND ")})`;
|
|
1461
|
+
}
|
|
1277
1462
|
mergeFilterOperand(filter, field, operand) {
|
|
1278
1463
|
const existing = filter[field];
|
|
1279
1464
|
if (existing === void 0) {
|
|
@@ -1297,9 +1482,13 @@ var ObjectQLStrategy = class {
|
|
|
1297
1482
|
* HERE on every driver — and "bucketed trend" is precisely the shape that also
|
|
1298
1483
|
* carries a range ("last 12 months", "this quarter").
|
|
1299
1484
|
*
|
|
1300
|
-
* Bounds are inclusive on both ends —
|
|
1301
|
-
* `
|
|
1302
|
-
*
|
|
1485
|
+
* Bounds are inclusive on both ends — logically "from day X through day Y".
|
|
1486
|
+
* The `$lte` end is left as the bare calendar day on purpose: the driver's
|
|
1487
|
+
* filter compiler owns the calendar-day → instant translation, compiling a
|
|
1488
|
+
* bare-day `$lte` on a `datetime` column into the half-open `< nextDay`
|
|
1489
|
+
* (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`
|
|
1490
|
+
* performs the same half-open translation itself because it binds into raw
|
|
1491
|
+
* SQL, so one dashboard reads the same on every driver.
|
|
1303
1492
|
*
|
|
1304
1493
|
* Comparands are coerced by the SAME helper the `where` path uses, so an
|
|
1305
1494
|
* epoch-ms bound recovers as a number and an ISO string stays a string. No
|
|
@@ -1359,24 +1548,60 @@ var ObjectQLStrategy = class {
|
|
|
1359
1548
|
return { $lte: v0 };
|
|
1360
1549
|
case "contains":
|
|
1361
1550
|
return { $regex: values[0] };
|
|
1551
|
+
// `notContains` had no arm and fell to the `default` below, which returns
|
|
1552
|
+
// a BARE VALUE — i.e. `{field: 'x'}`, an equality. "does not contain x"
|
|
1553
|
+
// was compiled as "equals x". These three pass through as the canonical
|
|
1554
|
+
// spec operators every driver implements directly, so an anchored match
|
|
1555
|
+
// stays anchored rather than depending on regex dialect (#4128).
|
|
1556
|
+
case "notContains":
|
|
1557
|
+
return { $notContains: values[0] };
|
|
1558
|
+
case "startsWith":
|
|
1559
|
+
return { $startsWith: values[0] };
|
|
1560
|
+
case "endsWith":
|
|
1561
|
+
return { $endsWith: values[0] };
|
|
1362
1562
|
case "in":
|
|
1363
1563
|
return { $in: all };
|
|
1364
1564
|
case "notIn":
|
|
1365
1565
|
return { $nin: all };
|
|
1366
1566
|
default:
|
|
1367
|
-
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
`[analytics] ObjectQL strategy cannot express filter operator "${operator}". Treating it as an equality would silently query something the author did not ask for.`
|
|
1569
|
+
);
|
|
1368
1570
|
}
|
|
1369
1571
|
}
|
|
1370
1572
|
extractObjectName(cube) {
|
|
1371
1573
|
return cube.sql.trim();
|
|
1372
1574
|
}
|
|
1575
|
+
/**
|
|
1576
|
+
* The dimensions this query PROJECTS, in the order the result carries them:
|
|
1577
|
+
* every `dimensions` entry, then every granular `timeDimensions` entry that
|
|
1578
|
+
* is not already one of them.
|
|
1579
|
+
*
|
|
1580
|
+
* `timeDimensions` is not merely a filter carrier. An entry with a
|
|
1581
|
+
* `granularity` is GROUPED BY — see the `td.granularity` sites that build
|
|
1582
|
+
* groupBy here, in `generateSql` and in the cross-object path — so its
|
|
1583
|
+
* bucket is a COLUMN of the result; an entry without one only contributes a
|
|
1584
|
+
* `dateRange` predicate and must NOT be projected.
|
|
1585
|
+
*
|
|
1586
|
+
* Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly
|
|
1587
|
+
* that set. When they did not, a bucketed query returned rows carrying only
|
|
1588
|
+
* the measures and a `fields` list that never mentioned the bucket — a trend
|
|
1589
|
+
* chart got N values and no x-axis (#4033) — even though the SQL had
|
|
1590
|
+
* selected `date_trunc(…) AS "<dim>"` all along. One definition, every
|
|
1591
|
+
* consumer.
|
|
1592
|
+
*/
|
|
1593
|
+
projectedDimensions(query) {
|
|
1594
|
+
const out = [...query.dimensions ?? []];
|
|
1595
|
+
for (const td of query.timeDimensions ?? []) {
|
|
1596
|
+
if (td.granularity && !out.includes(td.dimension)) out.push(td.dimension);
|
|
1597
|
+
}
|
|
1598
|
+
return out;
|
|
1599
|
+
}
|
|
1373
1600
|
buildFieldMeta(query, cube) {
|
|
1374
1601
|
const fields = [];
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
fields.push({ name: dim, type: d?.type || "string" });
|
|
1379
|
-
}
|
|
1602
|
+
for (const dim of this.projectedDimensions(query)) {
|
|
1603
|
+
const d = this.lookupMember(cube, dim, "dimension");
|
|
1604
|
+
fields.push({ name: dim, type: d?.type || "string" });
|
|
1380
1605
|
}
|
|
1381
1606
|
if (query.measures) {
|
|
1382
1607
|
for (const m of query.measures) {
|
|
@@ -1388,14 +1613,16 @@ var ObjectQLStrategy = class {
|
|
|
1388
1613
|
};
|
|
1389
1614
|
|
|
1390
1615
|
// src/dataset-compiler.ts
|
|
1616
|
+
var import_data = require("@objectstack/spec/data");
|
|
1391
1617
|
var UNSUPPORTED_AGGREGATES = /* @__PURE__ */ new Set(["array_agg", "string_agg"]);
|
|
1618
|
+
var SUPPORTED_AGGREGATES = import_data.AggregationFunction.options.filter((a) => !UNSUPPORTED_AGGREGATES.has(a));
|
|
1392
1619
|
function aggregateToMetricType(m) {
|
|
1393
1620
|
if (!m.aggregate) {
|
|
1394
1621
|
throw new Error(`[dataset-compiler] non-derived measure "${m.name}" has no aggregate`);
|
|
1395
1622
|
}
|
|
1396
1623
|
if (UNSUPPORTED_AGGREGATES.has(m.aggregate)) {
|
|
1397
1624
|
throw new Error(
|
|
1398
|
-
`[dataset-compiler] measure "${m.name}" uses aggregate "${m.aggregate}" which is not supported by the v1 dataset runtime (supported:
|
|
1625
|
+
`[dataset-compiler] measure "${m.name}" uses aggregate "${m.aggregate}" which is not supported by the v1 dataset runtime (supported: ${SUPPORTED_AGGREGATES.join(", ")}).`
|
|
1399
1626
|
);
|
|
1400
1627
|
}
|
|
1401
1628
|
return m.aggregate;
|
|
@@ -1522,10 +1749,10 @@ function compileDataset(dataset, resolver) {
|
|
|
1522
1749
|
}
|
|
1523
1750
|
|
|
1524
1751
|
// src/dataset-executor.ts
|
|
1525
|
-
var
|
|
1752
|
+
var import_core3 = require("@objectstack/core");
|
|
1526
1753
|
function resolveSelectionTokens(compiled, selection, context) {
|
|
1527
|
-
const tokenCtx = (0,
|
|
1528
|
-
const resolve = (v) => (0,
|
|
1754
|
+
const tokenCtx = (0, import_core3.filterTokenContextFrom)(context, /* @__PURE__ */ new Date());
|
|
1755
|
+
const resolve = (v) => (0, import_core3.resolveFilterTokens)(v, tokenCtx);
|
|
1529
1756
|
const filter = resolve(compiled.filter);
|
|
1530
1757
|
const measureFilters = resolve(compiled.measureFilters);
|
|
1531
1758
|
const runtimeFilter = resolve(selection.runtimeFilter);
|
|
@@ -1622,7 +1849,7 @@ function applyWindow(rows, limit, offset) {
|
|
|
1622
1849
|
if (start === 0 && limit == null) return rows;
|
|
1623
1850
|
return rows.slice(start, limit != null ? start + limit : void 0);
|
|
1624
1851
|
}
|
|
1625
|
-
function resolveOrdering(selection, dimensions) {
|
|
1852
|
+
function resolveOrdering(selection, dimensions, timeDimensions = []) {
|
|
1626
1853
|
const order = selection.order;
|
|
1627
1854
|
if (order && Object.keys(order).length > 0) {
|
|
1628
1855
|
const selectable = /* @__PURE__ */ new Set([
|
|
@@ -1641,6 +1868,10 @@ function resolveOrdering(selection, dimensions) {
|
|
|
1641
1868
|
if ((selection.limit != null || selection.offset != null) && dimensions.length > 0) {
|
|
1642
1869
|
return Object.fromEntries(dimensions.map((d) => [d, "asc"]));
|
|
1643
1870
|
}
|
|
1871
|
+
const timeKeys = timeDimensions.filter((d) => dimensions.includes(d));
|
|
1872
|
+
if (timeKeys.length > 0) {
|
|
1873
|
+
return Object.fromEntries(timeKeys.map((d) => [d, "asc"]));
|
|
1874
|
+
}
|
|
1644
1875
|
return void 0;
|
|
1645
1876
|
}
|
|
1646
1877
|
function parseUTC(date) {
|
|
@@ -1734,7 +1965,7 @@ var DatasetExecutor = class {
|
|
|
1734
1965
|
}
|
|
1735
1966
|
const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);
|
|
1736
1967
|
const dimensions = selection.dimensions ?? [];
|
|
1737
|
-
const order = resolveOrdering(selection, dimensions);
|
|
1968
|
+
const order = resolveOrdering(selection, dimensions, this.timeDimensionsOf(compiled, dimensions));
|
|
1738
1969
|
const labelOrderKeys = this.orderLabels ? Object.keys(order ?? {}).filter(
|
|
1739
1970
|
(k) => dimensions.includes(k) && this.orderLabels.isLabelBearing(k)
|
|
1740
1971
|
) : [];
|
|
@@ -1790,6 +2021,19 @@ var DatasetExecutor = class {
|
|
|
1790
2021
|
result.rows = applyWindow(result.rows, selection.limit, selection.offset);
|
|
1791
2022
|
return result;
|
|
1792
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* The selected dimensions the compiled cube types as `time`, in selection
|
|
2026
|
+
* order (#3916) — the axis {@link resolveOrdering} defaults to ascending.
|
|
2027
|
+
*
|
|
2028
|
+
* Membership is decided by the DIMENSION's declared type, not by whether the
|
|
2029
|
+
* selection happens to bucket it: a `date` dimension left ungranulated groups
|
|
2030
|
+
* raw timestamps, and those want chronological order every bit as much as
|
|
2031
|
+
* month buckets do. (Both sort correctly — `compareValues` compares Dates and
|
|
2032
|
+
* ISO strings chronologically, and bucket keys are minted sort-stable.)
|
|
2033
|
+
*/
|
|
2034
|
+
timeDimensionsOf(compiled, dimensions) {
|
|
2035
|
+
return dimensions.filter((d) => compiled.cube.dimensions[d]?.type === "time");
|
|
2036
|
+
}
|
|
1793
2037
|
buildQuery(compiled, opts) {
|
|
1794
2038
|
const q = {
|
|
1795
2039
|
cube: compiled.cube.name,
|
|
@@ -2031,11 +2275,21 @@ function pickDisplayField(fields) {
|
|
|
2031
2275
|
}
|
|
2032
2276
|
|
|
2033
2277
|
// src/preview-evaluator.ts
|
|
2034
|
-
var
|
|
2278
|
+
var import_core4 = require("@objectstack/core");
|
|
2035
2279
|
function compare(a, b) {
|
|
2036
2280
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
2281
|
+
if (a instanceof Date || b instanceof Date) {
|
|
2282
|
+
const ai = (0, import_core4.utcInstantMs)(a);
|
|
2283
|
+
const bi = (0, import_core4.utcInstantMs)(b);
|
|
2284
|
+
if (ai !== null && bi !== null) return ai - bi;
|
|
2285
|
+
}
|
|
2037
2286
|
return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
|
|
2038
2287
|
}
|
|
2288
|
+
function lteBound(value, bound) {
|
|
2289
|
+
const nextDay = (0, import_core4.nextUtcCalendarDay)(bound);
|
|
2290
|
+
if (nextDay != null) return compare(value, nextDay) < 0;
|
|
2291
|
+
return compare(value, bound) <= 0;
|
|
2292
|
+
}
|
|
2039
2293
|
function matchOp(value, op, expected) {
|
|
2040
2294
|
switch (op) {
|
|
2041
2295
|
case "$eq":
|
|
@@ -2048,8 +2302,16 @@ function matchOp(value, op, expected) {
|
|
|
2048
2302
|
return value != null && compare(value, expected) >= 0;
|
|
2049
2303
|
case "$lt":
|
|
2050
2304
|
return value != null && compare(value, expected) < 0;
|
|
2051
|
-
case "$lte":
|
|
2052
|
-
|
|
2305
|
+
case "$lte": {
|
|
2306
|
+
if (value == null) return false;
|
|
2307
|
+
return lteBound(value, expected);
|
|
2308
|
+
}
|
|
2309
|
+
case "$between": {
|
|
2310
|
+
if (value == null || !Array.isArray(expected) || expected.length !== 2) return false;
|
|
2311
|
+
const [min, max] = expected;
|
|
2312
|
+
if (min == null || max == null) return false;
|
|
2313
|
+
return compare(value, min) >= 0 && lteBound(value, max);
|
|
2314
|
+
}
|
|
2053
2315
|
case "$in":
|
|
2054
2316
|
return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));
|
|
2055
2317
|
case "$nin":
|
|
@@ -2082,7 +2344,7 @@ function matchesWhere(row, where) {
|
|
|
2082
2344
|
function bucketDate(value, granularity, timezone) {
|
|
2083
2345
|
const d = new Date(String(value));
|
|
2084
2346
|
if (Number.isNaN(d.getTime())) return null;
|
|
2085
|
-
const { year: y, month, day: dayNum } = (0,
|
|
2347
|
+
const { year: y, month, day: dayNum } = (0, import_core4.calendarPartsInTzOrUtc)(d, timezone);
|
|
2086
2348
|
const m = `${month}`.padStart(2, "0");
|
|
2087
2349
|
const day = `${dayNum}`.padStart(2, "0");
|
|
2088
2350
|
switch (granularity) {
|
|
@@ -2136,7 +2398,9 @@ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
|
|
|
2136
2398
|
const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
2137
2399
|
filtered = filtered.filter((r) => {
|
|
2138
2400
|
const v = String(r[field] ?? "");
|
|
2139
|
-
|
|
2401
|
+
const nextDay = (0, import_core4.nextUtcCalendarDay)(end);
|
|
2402
|
+
const inUpper = nextDay != null ? v < nextDay : v <= `${end}~`;
|
|
2403
|
+
return v >= String(start) && inUpper;
|
|
2140
2404
|
});
|
|
2141
2405
|
}
|
|
2142
2406
|
const dimensions = query.dimensions ?? [];
|
|
@@ -2206,7 +2470,7 @@ var AnalyticsService = class {
|
|
|
2206
2470
|
this.datasetRegistry = /* @__PURE__ */ new Map();
|
|
2207
2471
|
/** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */
|
|
2208
2472
|
this.warnedNoObjectRegistry = false;
|
|
2209
|
-
this.logger = config.logger || (0,
|
|
2473
|
+
this.logger = config.logger || (0, import_core5.createLogger)({ level: "info", format: "pretty" });
|
|
2210
2474
|
this.cubeRegistry = new CubeRegistry();
|
|
2211
2475
|
if (config.cubes) {
|
|
2212
2476
|
this.cubeRegistry.registerAll(config.cubes);
|
|
@@ -2236,6 +2500,7 @@ var AnalyticsService = class {
|
|
|
2236
2500
|
// fall back to any explicitly-configured provider for legacy cubes.
|
|
2237
2501
|
getAllowedRelationships: (cubeName) => this.datasetRegistry.get(cubeName)?.allowedRelationships ?? config.getAllowedRelationships?.(cubeName),
|
|
2238
2502
|
coerceTemporalFilterValue: config.coerceTemporalFilterValue,
|
|
2503
|
+
coerceTemporalFilterColumn: config.coerceTemporalFilterColumn,
|
|
2239
2504
|
isExternalObject: config.isExternalObject
|
|
2240
2505
|
};
|
|
2241
2506
|
const builtIn = [
|
|
@@ -2439,11 +2704,11 @@ var AnalyticsService = class {
|
|
|
2439
2704
|
else if (rangeTz === "UTC") rangeDims.push({ d, granularity, instant: false });
|
|
2440
2705
|
}
|
|
2441
2706
|
if (rangeDims.length && result.rows.length) {
|
|
2442
|
-
const bound = (ymd, instant) => instant ? new Date((0,
|
|
2707
|
+
const bound = (ymd, instant) => instant ? new Date((0, import_core5.zonedDateStartToUtcMs)(ymd, rangeTz)).toISOString() : ymd;
|
|
2443
2708
|
result.drillRanges = result.rows.map((row) => {
|
|
2444
2709
|
const ranges = {};
|
|
2445
2710
|
for (const { d, granularity, instant } of rangeDims) {
|
|
2446
|
-
const cal = (0,
|
|
2711
|
+
const cal = (0, import_core5.bucketKeyToCalendarRange)(row[d.name], granularity);
|
|
2447
2712
|
if (cal) {
|
|
2448
2713
|
ranges[d.name] = { field: d.field, gte: bound(cal.start, instant), lt: bound(cal.end, instant) };
|
|
2449
2714
|
}
|
|
@@ -2723,6 +2988,11 @@ var FallbackDelegateStrategy = class {
|
|
|
2723
2988
|
var AnalyticsServicePlugin = class {
|
|
2724
2989
|
constructor(options = {}) {
|
|
2725
2990
|
this.name = "com.objectstack.service-analytics";
|
|
2991
|
+
/**
|
|
2992
|
+
* Services init() registers on every path (ADR-0116, #4131) — lets the
|
|
2993
|
+
* kernel name this plugin when a consumer requires one before it inits.
|
|
2994
|
+
*/
|
|
2995
|
+
this.providesServices = ["analytics"];
|
|
2726
2996
|
this.version = "1.0.0";
|
|
2727
2997
|
this.type = "standard";
|
|
2728
2998
|
this.dependencies = [];
|
|
@@ -2925,6 +3195,17 @@ var AnalyticsServicePlugin = class {
|
|
|
2925
3195
|
}
|
|
2926
3196
|
return value;
|
|
2927
3197
|
};
|
|
3198
|
+
const coerceTemporalFilterColumn = (objectName, fieldName, columnSql) => {
|
|
3199
|
+
try {
|
|
3200
|
+
const svc = ctx.getService("data");
|
|
3201
|
+
const driver = svc?.getDriverForObject?.(objectName);
|
|
3202
|
+
if (driver && typeof driver.temporalFilterColumnSql === "function") {
|
|
3203
|
+
return driver.temporalFilterColumnSql(objectName, fieldName, columnSql);
|
|
3204
|
+
}
|
|
3205
|
+
} catch {
|
|
3206
|
+
}
|
|
3207
|
+
return columnSql;
|
|
3208
|
+
};
|
|
2928
3209
|
const config = {
|
|
2929
3210
|
cubes: this.options.cubes,
|
|
2930
3211
|
logger: ctx.logger,
|
|
@@ -2935,6 +3216,7 @@ var AnalyticsServicePlugin = class {
|
|
|
2935
3216
|
getReadScope,
|
|
2936
3217
|
getAllowedRelationships: this.options.getAllowedRelationships,
|
|
2937
3218
|
coerceTemporalFilterValue,
|
|
3219
|
+
coerceTemporalFilterColumn,
|
|
2938
3220
|
relationshipResolver,
|
|
2939
3221
|
labelResolver,
|
|
2940
3222
|
// ADR-0053 — source-field currency metadata for the measure currency chain.
|