@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.js
CHANGED
|
@@ -128,7 +128,8 @@ var MONGO_TO_CUBE_OP = {
|
|
|
128
128
|
$nin: "notIn",
|
|
129
129
|
$contains: "contains",
|
|
130
130
|
$notContains: "notContains",
|
|
131
|
-
$
|
|
131
|
+
$startsWith: "startsWith",
|
|
132
|
+
$endsWith: "endsWith"
|
|
132
133
|
};
|
|
133
134
|
function stringifyForCube(v) {
|
|
134
135
|
if (v == null) return "";
|
|
@@ -137,55 +138,102 @@ function stringifyForCube(v) {
|
|
|
137
138
|
if (typeof v === "object") return JSON.stringify(v);
|
|
138
139
|
return String(v);
|
|
139
140
|
}
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
function andOf(children) {
|
|
142
|
+
if (children.length === 0) return null;
|
|
143
|
+
if (children.length === 1) return children[0];
|
|
144
|
+
return { kind: "and", children };
|
|
145
|
+
}
|
|
146
|
+
function fieldLeaves(key, raw) {
|
|
147
|
+
const out = [];
|
|
148
|
+
const leaf = (operator, values) => {
|
|
149
|
+
out.push({ kind: "leaf", member: key, operator, values });
|
|
150
|
+
};
|
|
151
|
+
if (raw === null) {
|
|
152
|
+
leaf("notSet", []);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
if (typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
|
|
156
|
+
const wrapper = raw;
|
|
157
|
+
const opKeys = Object.keys(wrapper).filter((k) => k.startsWith("$"));
|
|
158
|
+
if (opKeys.length > 0) {
|
|
159
|
+
for (const opKey of opKeys) {
|
|
160
|
+
if (opKey === "$between") {
|
|
161
|
+
const v2 = wrapper[opKey];
|
|
162
|
+
if (!Array.isArray(v2) || v2.length !== 2) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`[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.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
leaf("gte", [stringifyForCube(v2[0])]);
|
|
168
|
+
leaf("lte", [stringifyForCube(v2[1])]);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (opKey === "$null" || opKey === "$exists") {
|
|
172
|
+
const isNull = opKey === "$null" ? wrapper[opKey] === true : wrapper[opKey] === false;
|
|
173
|
+
leaf(isNull ? "notSet" : "set", []);
|
|
174
|
+
continue;
|
|
147
175
|
}
|
|
176
|
+
const cubeOp = MONGO_TO_CUBE_OP[opKey];
|
|
177
|
+
if (!cubeOp) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`[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.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const v = wrapper[opKey];
|
|
183
|
+
leaf(cubeOp, Array.isArray(v) ? v.map(stringifyForCube) : [stringifyForCube(v)]);
|
|
148
184
|
}
|
|
149
|
-
|
|
185
|
+
return out;
|
|
150
186
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
out.push({ member: key, operator: "notSet", values: [] });
|
|
154
|
-
continue;
|
|
187
|
+
for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {
|
|
188
|
+
out.push(...fieldLeaves(`${key}.${nestedKey}`, nestedVal));
|
|
155
189
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(raw)) leaf("in", raw.map(stringifyForCube));
|
|
193
|
+
else leaf("equals", [stringifyForCube(raw)]);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function buildNode(cond) {
|
|
197
|
+
const children = [];
|
|
198
|
+
for (const [key, raw] of Object.entries(cond)) {
|
|
199
|
+
if (raw === void 0) continue;
|
|
200
|
+
if (key === "$and" || key === "$or") {
|
|
201
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`[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.`
|
|
204
|
+
);
|
|
171
205
|
}
|
|
206
|
+
const branches = raw.map((sub) => sub && typeof sub === "object" ? buildNode(sub) : null).filter((n) => n !== null);
|
|
207
|
+
if (branches.length === 0) continue;
|
|
208
|
+
if (key === "$and") children.push(...branches);
|
|
209
|
+
else children.push(branches.length === 1 ? branches[0] : { kind: "or", children: branches });
|
|
172
210
|
continue;
|
|
173
211
|
}
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
212
|
+
if (key === "$not") {
|
|
213
|
+
const inner = raw && typeof raw === "object" ? buildNode(raw) : null;
|
|
214
|
+
if (inner) children.push({ kind: "not", child: inner });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (key.startsWith("$")) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`[analytics] Unsupported top-level filter operator "${key}". Dropping it would silently widen the query to rows the filter excludes.`
|
|
220
|
+
);
|
|
178
221
|
}
|
|
222
|
+
children.push(...fieldLeaves(key, raw));
|
|
179
223
|
}
|
|
224
|
+
return andOf(children);
|
|
180
225
|
}
|
|
181
|
-
function
|
|
182
|
-
if (!query || typeof query !== "object") return
|
|
183
|
-
const out = [];
|
|
226
|
+
function normalizeAnalyticsFilterTree(query) {
|
|
227
|
+
if (!query || typeof query !== "object") return null;
|
|
184
228
|
const where = query.where;
|
|
185
|
-
if (where
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
229
|
+
if (!where || typeof where !== "object" || Array.isArray(where)) return null;
|
|
230
|
+
return buildNode(where);
|
|
231
|
+
}
|
|
232
|
+
function collectFilterLeaves(node) {
|
|
233
|
+
if (!node) return [];
|
|
234
|
+
if (node.kind === "leaf") return [{ member: node.member, operator: node.operator, values: node.values }];
|
|
235
|
+
if (node.kind === "not") return collectFilterLeaves(node.child);
|
|
236
|
+
return node.children.flatMap(collectFilterLeaves);
|
|
189
237
|
}
|
|
190
238
|
function recoverNumber(s) {
|
|
191
239
|
if (/^-?\d+(\.\d+)?$/.test(s)) {
|
|
@@ -317,6 +365,18 @@ function compileOperator(col, op, val, field, params) {
|
|
|
317
365
|
}
|
|
318
366
|
|
|
319
367
|
// src/strategies/native-sql-strategy.ts
|
|
368
|
+
import { nextUtcCalendarDay } from "@objectstack/core";
|
|
369
|
+
var AGGREGATE_SQL = {
|
|
370
|
+
"count": () => "COUNT(*)",
|
|
371
|
+
"sum": (col) => `SUM(${col})`,
|
|
372
|
+
"avg": (col) => `AVG(${col})`,
|
|
373
|
+
"min": (col) => `MIN(${col})`,
|
|
374
|
+
"max": (col) => `MAX(${col})`,
|
|
375
|
+
"count_distinct": (col) => `COUNT(DISTINCT ${col})`
|
|
376
|
+
};
|
|
377
|
+
var SUPPORTED_AGGREGATE_SQL_KEYS = Object.keys(AGGREGATE_SQL);
|
|
378
|
+
var EXPRESSION_METRIC_TYPES = /* @__PURE__ */ new Set(["number", "string", "boolean"]);
|
|
379
|
+
var IDENTIFIER_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
320
380
|
var NativeSQLStrategy = class {
|
|
321
381
|
constructor() {
|
|
322
382
|
this.name = "NativeSQLStrategy";
|
|
@@ -371,15 +431,15 @@ var NativeSQLStrategy = class {
|
|
|
371
431
|
}
|
|
372
432
|
}
|
|
373
433
|
const whereClauses = [];
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
434
|
+
const filterSql = this.compileFilterNode(
|
|
435
|
+
normalizeAnalyticsFilterTree(query),
|
|
436
|
+
cube,
|
|
437
|
+
tableName,
|
|
438
|
+
joins,
|
|
439
|
+
params,
|
|
440
|
+
ctx
|
|
441
|
+
);
|
|
442
|
+
if (filterSql) whereClauses.push(filterSql);
|
|
383
443
|
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
384
444
|
for (const td of query.timeDimensions) {
|
|
385
445
|
const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);
|
|
@@ -387,11 +447,17 @@ var NativeSQLStrategy = class {
|
|
|
387
447
|
const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
388
448
|
if (range.length === 2) {
|
|
389
449
|
const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
450
|
+
const column = this.temporalColumn(ctx, td2, colExpr);
|
|
451
|
+
const nextDay = nextUtcCalendarDay(range[1]);
|
|
452
|
+
params.push(this.coerceTemporal(ctx, td2, range[0]));
|
|
453
|
+
const lower = `${column} >= $${params.length}`;
|
|
454
|
+
if (nextDay != null) {
|
|
455
|
+
params.push(this.coerceTemporal(ctx, td2, nextDay));
|
|
456
|
+
whereClauses.push(`(${lower} AND ${column} < $${params.length})`);
|
|
457
|
+
} else {
|
|
458
|
+
params.push(this.coerceTemporal(ctx, td2, range[1]));
|
|
459
|
+
whereClauses.push(`(${lower} AND ${column} <= $${params.length})`);
|
|
460
|
+
}
|
|
395
461
|
}
|
|
396
462
|
}
|
|
397
463
|
}
|
|
@@ -489,6 +555,7 @@ var NativeSQLStrategy = class {
|
|
|
489
555
|
}
|
|
490
556
|
return rawSql;
|
|
491
557
|
}
|
|
558
|
+
if (!IDENTIFIER_PATH.test(rawSql)) return rawSql;
|
|
492
559
|
const segments = rawSql.split(".");
|
|
493
560
|
const column = segments[segments.length - 1];
|
|
494
561
|
const hops = segments.slice(0, -1);
|
|
@@ -548,24 +615,19 @@ var NativeSQLStrategy = class {
|
|
|
548
615
|
}
|
|
549
616
|
resolveMeasureSql(cube, member, parentTable, joins) {
|
|
550
617
|
const measure = this.lookupMember(cube, member, "measure");
|
|
551
|
-
if (!measure)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
case "sum":
|
|
557
|
-
return `SUM(${col})`;
|
|
558
|
-
case "avg":
|
|
559
|
-
return `AVG(${col})`;
|
|
560
|
-
case "min":
|
|
561
|
-
return `MIN(${col})`;
|
|
562
|
-
case "max":
|
|
563
|
-
return `MAX(${col})`;
|
|
564
|
-
case "count_distinct":
|
|
565
|
-
return `COUNT(DISTINCT ${col})`;
|
|
566
|
-
default:
|
|
567
|
-
return `COUNT(*)`;
|
|
618
|
+
if (!measure) {
|
|
619
|
+
const declared = Object.keys(cube.measures ?? {});
|
|
620
|
+
throw new Error(
|
|
621
|
+
`[native-sql-strategy] cube "${cube.name}" declares no measure "${member}"` + (declared.length ? ` (declared: ${declared.join(", ")})` : " (it declares none)")
|
|
622
|
+
);
|
|
568
623
|
}
|
|
624
|
+
const col = measure.sql === "*" ? "*" : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);
|
|
625
|
+
const wrap = AGGREGATE_SQL[measure.type];
|
|
626
|
+
if (wrap) return wrap(col);
|
|
627
|
+
if (EXPRESSION_METRIC_TYPES.has(measure.type)) return col;
|
|
628
|
+
throw new Error(
|
|
629
|
+
`[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(", ")}).`
|
|
630
|
+
);
|
|
569
631
|
}
|
|
570
632
|
resolveFieldSql(cube, member, parentTable, joins) {
|
|
571
633
|
const dim = this.lookupMember(cube, member, "dimension");
|
|
@@ -618,7 +680,53 @@ var NativeSQLStrategy = class {
|
|
|
618
680
|
}
|
|
619
681
|
return coerceFilterValueForSql(value);
|
|
620
682
|
}
|
|
621
|
-
|
|
683
|
+
/**
|
|
684
|
+
* The column side of {@link coerceTemporal}: normalise the reference so it
|
|
685
|
+
* reads in the storage form the comparand was coerced into.
|
|
686
|
+
*
|
|
687
|
+
* A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)
|
|
688
|
+
* and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's
|
|
689
|
+
* own `created_at`) at the SAME time, so coercing the value alone fixes one half
|
|
690
|
+
* and empties the other. That is #3912: a `dateRange: last_30_days` on
|
|
691
|
+
* `created_date` read 0 with 29 rows in range. Every other column and dialect
|
|
692
|
+
* gets its reference back verbatim.
|
|
693
|
+
*/
|
|
694
|
+
temporalColumn(ctx, target, col) {
|
|
695
|
+
if (typeof ctx.coerceTemporalFilterColumn !== "function") return col;
|
|
696
|
+
return ctx.coerceTemporalFilterColumn(target.object, target.field, col) || col;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Compile a normalized filter node into a boolean SQL expression, recursing
|
|
700
|
+
* through the combinators. `null` = no constraint.
|
|
701
|
+
*
|
|
702
|
+
* Leaves go through {@link buildFilterClause} exactly as they did when this
|
|
703
|
+
* was a flat loop, so the storage-form coercion and the calendar-day
|
|
704
|
+
* upper-bound rule (#3777) apply at every depth — including inside an `$or`,
|
|
705
|
+
* where a second, combinator-aware implementation would have been free to
|
|
706
|
+
* drift from the first.
|
|
707
|
+
*
|
|
708
|
+
* Parenthesisation is explicit rather than left to SQL's precedence: `AND`
|
|
709
|
+
* does bind tighter than `OR`, so `a AND b OR c` happens to be right, but
|
|
710
|
+
* being right by construction is what keeps a future edit from making it
|
|
711
|
+
* wrong.
|
|
712
|
+
*/
|
|
713
|
+
compileFilterNode(node, cube, parentTable, joins, params, ctx) {
|
|
714
|
+
if (!node) return null;
|
|
715
|
+
if (node.kind === "leaf") {
|
|
716
|
+
const colExpr = this.resolveFieldSql(cube, node.member, parentTable, joins);
|
|
717
|
+
const target = this.resolveStorageTarget(cube, node.member, parentTable);
|
|
718
|
+
return this.buildFilterClause(colExpr, node.operator, node.values, params, ctx, target);
|
|
719
|
+
}
|
|
720
|
+
if (node.kind === "not") {
|
|
721
|
+
const inner = this.compileFilterNode(node.child, cube, parentTable, joins, params, ctx);
|
|
722
|
+
return inner ? `NOT (${inner})` : null;
|
|
723
|
+
}
|
|
724
|
+
const parts = node.children.map((child) => this.compileFilterNode(child, cube, parentTable, joins, params, ctx)).filter((s) => !!s);
|
|
725
|
+
if (parts.length === 0) return null;
|
|
726
|
+
if (parts.length === 1) return parts[0];
|
|
727
|
+
return `(${parts.join(node.kind === "or" ? " OR " : " AND ")})`;
|
|
728
|
+
}
|
|
729
|
+
buildFilterClause(rawCol, operator, values, params, ctx, target) {
|
|
622
730
|
const opMap = {
|
|
623
731
|
equals: "=",
|
|
624
732
|
notEquals: "!=",
|
|
@@ -627,26 +735,42 @@ var NativeSQLStrategy = class {
|
|
|
627
735
|
lt: "<",
|
|
628
736
|
lte: "<=",
|
|
629
737
|
contains: "LIKE",
|
|
630
|
-
notContains: "NOT LIKE"
|
|
738
|
+
notContains: "NOT LIKE",
|
|
739
|
+
startsWith: "LIKE",
|
|
740
|
+
endsWith: "LIKE"
|
|
631
741
|
};
|
|
632
|
-
|
|
633
|
-
|
|
742
|
+
const likePattern = {
|
|
743
|
+
contains: (v) => `%${v}%`,
|
|
744
|
+
notContains: (v) => `%${v}%`,
|
|
745
|
+
startsWith: (v) => `${v}%`,
|
|
746
|
+
endsWith: (v) => `%${v}`
|
|
747
|
+
};
|
|
748
|
+
if (operator === "set") return `${rawCol} IS NOT NULL`;
|
|
749
|
+
if (operator === "notSet") return `${rawCol} IS NULL`;
|
|
634
750
|
if (operator === "in" || operator === "notIn") {
|
|
635
751
|
if (!values || values.length === 0) return null;
|
|
636
752
|
const placeholders = values.map((v) => {
|
|
637
753
|
params.push(this.coerceTemporal(ctx, target, v));
|
|
638
754
|
return `$${params.length}`;
|
|
639
755
|
}).join(", ");
|
|
640
|
-
return `${
|
|
756
|
+
return `${this.temporalColumn(ctx, target, rawCol)} ${operator === "in" ? "IN" : "NOT IN"} (${placeholders})`;
|
|
641
757
|
}
|
|
642
758
|
const sqlOp = opMap[operator];
|
|
643
759
|
if (!sqlOp || !values || values.length === 0) return null;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
params.
|
|
760
|
+
const pattern = likePattern[operator];
|
|
761
|
+
if (pattern) {
|
|
762
|
+
params.push(pattern(values[0]));
|
|
763
|
+
return `${rawCol} ${sqlOp} $${params.length}`;
|
|
764
|
+
}
|
|
765
|
+
if (operator === "lte") {
|
|
766
|
+
const nextDay = nextUtcCalendarDay(values[0]);
|
|
767
|
+
if (nextDay != null) {
|
|
768
|
+
params.push(this.coerceTemporal(ctx, target, nextDay));
|
|
769
|
+
return `${this.temporalColumn(ctx, target, rawCol)} < $${params.length}`;
|
|
770
|
+
}
|
|
648
771
|
}
|
|
649
|
-
|
|
772
|
+
params.push(this.coerceTemporal(ctx, target, values[0]));
|
|
773
|
+
return `${this.temporalColumn(ctx, target, rawCol)} ${sqlOp} $${params.length}`;
|
|
650
774
|
}
|
|
651
775
|
extractObjectName(cube) {
|
|
652
776
|
return cube.sql.trim();
|
|
@@ -668,6 +792,9 @@ var NativeSQLStrategy = class {
|
|
|
668
792
|
}
|
|
669
793
|
};
|
|
670
794
|
|
|
795
|
+
// src/strategies/objectql-strategy.ts
|
|
796
|
+
import { nextUtcCalendarDay as nextUtcCalendarDay2 } from "@objectstack/core";
|
|
797
|
+
|
|
671
798
|
// src/strategies/cross-object-rebucket.ts
|
|
672
799
|
var RECOMBINABLE_METHODS = /* @__PURE__ */ new Set([
|
|
673
800
|
"sum",
|
|
@@ -770,11 +897,7 @@ var ObjectQLStrategy = class {
|
|
|
770
897
|
}
|
|
771
898
|
const filter = {};
|
|
772
899
|
const conjuncts = [];
|
|
773
|
-
|
|
774
|
-
const fieldName = this.resolveFieldName(cube, f.member, "any");
|
|
775
|
-
const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(f.operator, f.values));
|
|
776
|
-
if (extra) conjuncts.push(extra);
|
|
777
|
-
}
|
|
900
|
+
this.applyFilterNode(normalizeAnalyticsFilterTree(query), cube, filter, conjuncts);
|
|
778
901
|
for (const { field, bounds } of this.dateRangeBounds(cube, query)) {
|
|
779
902
|
const extra = this.mergeFilterOperand(filter, field, bounds);
|
|
780
903
|
if (extra) conjuncts.push(extra);
|
|
@@ -806,11 +929,9 @@ var ObjectQLStrategy = class {
|
|
|
806
929
|
});
|
|
807
930
|
const mappedRows = rows.map((row) => {
|
|
808
931
|
const mapped = {};
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (shortName in row) mapped[dim] = row[shortName];
|
|
813
|
-
}
|
|
932
|
+
for (const dim of this.projectedDimensions(query)) {
|
|
933
|
+
const shortName = this.resolveFieldName(cube, dim, "dimension");
|
|
934
|
+
if (shortName in row) mapped[dim] = row[shortName];
|
|
814
935
|
}
|
|
815
936
|
if (query.measures) {
|
|
816
937
|
for (const m of query.measures) {
|
|
@@ -856,7 +977,7 @@ var ObjectQLStrategy = class {
|
|
|
856
977
|
}
|
|
857
978
|
const tableName = this.extractObjectName(cube);
|
|
858
979
|
const plan = this.planCrossObject(cube, query, Object.fromEntries(
|
|
859
|
-
|
|
980
|
+
collectFilterLeaves(normalizeAnalyticsFilterTree(query)).map((f) => [this.resolveFieldName(cube, f.member, "any"), true])
|
|
860
981
|
));
|
|
861
982
|
const crossByDim = new Map((plan?.crossDims ?? []).map((cd) => [cd.outputName, cd]));
|
|
862
983
|
const joinClauses = [];
|
|
@@ -893,18 +1014,18 @@ var ObjectQLStrategy = class {
|
|
|
893
1014
|
}
|
|
894
1015
|
}
|
|
895
1016
|
const whereParts = [];
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
);
|
|
903
|
-
if (clause) whereParts.push(clause);
|
|
904
|
-
}
|
|
1017
|
+
const filterClause = this.renderFilterNodeSql(
|
|
1018
|
+
normalizeAnalyticsFilterTree(query),
|
|
1019
|
+
cube,
|
|
1020
|
+
params
|
|
1021
|
+
);
|
|
1022
|
+
if (filterClause) whereParts.push(filterClause);
|
|
905
1023
|
for (const { field, bounds } of this.dateRangeBounds(cube, query)) {
|
|
906
|
-
|
|
907
|
-
|
|
1024
|
+
const nextDay = nextUtcCalendarDay2(bounds.$lte);
|
|
1025
|
+
params.push(bounds.$gte, nextDay ?? bounds.$lte);
|
|
1026
|
+
whereParts.push(
|
|
1027
|
+
`(${field} >= $${params.length - 1} AND ${field} ${nextDay ? "<" : "<="} $${params.length})`
|
|
1028
|
+
);
|
|
908
1029
|
}
|
|
909
1030
|
const scope = ctx.getReadScope?.(tableName);
|
|
910
1031
|
if (scope != null) {
|
|
@@ -1078,7 +1199,7 @@ var ObjectQLStrategy = class {
|
|
|
1078
1199
|
const merged = rebucketCrossObject(baseRows, baseDimFields, resolvedDims, measures);
|
|
1079
1200
|
const mappedRows = merged.map((row) => {
|
|
1080
1201
|
const out = {};
|
|
1081
|
-
for (const dim of query
|
|
1202
|
+
for (const dim of this.projectedDimensions(query)) {
|
|
1082
1203
|
if (crossByDim.has(dim)) {
|
|
1083
1204
|
if (dim in row) out[dim] = row[dim];
|
|
1084
1205
|
} else {
|
|
@@ -1086,11 +1207,6 @@ var ObjectQLStrategy = class {
|
|
|
1086
1207
|
if (field in row) out[dim] = row[field];
|
|
1087
1208
|
}
|
|
1088
1209
|
}
|
|
1089
|
-
for (const td of query.timeDimensions ?? []) {
|
|
1090
|
-
if (query.dimensions?.includes(td.dimension)) continue;
|
|
1091
|
-
const field = this.resolveFieldName(cube, td.dimension, "dimension");
|
|
1092
|
-
if (field in row) out[td.dimension] = row[field];
|
|
1093
|
-
}
|
|
1094
1210
|
for (const m of query.measures ?? []) {
|
|
1095
1211
|
if (m in row) out[m] = row[m];
|
|
1096
1212
|
}
|
|
@@ -1233,6 +1349,75 @@ var ObjectQLStrategy = class {
|
|
|
1233
1349
|
* are handed back for the caller to AND in separately, so the engine
|
|
1234
1350
|
* intersects them instead of the strategy picking a winner.
|
|
1235
1351
|
*/
|
|
1352
|
+
/**
|
|
1353
|
+
* Fold a normalized filter node into the engine filter being built.
|
|
1354
|
+
*
|
|
1355
|
+
* AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly
|
|
1356
|
+
* as the flat loop this replaced did — so a query without combinators still
|
|
1357
|
+
* produces byte-identical engine input. Anything structural (`$or`, `$not`,
|
|
1358
|
+
* a nested `$and` that cannot merge) becomes its own conjunct, which the
|
|
1359
|
+
* caller ANDs in. The engine speaks these combinators natively
|
|
1360
|
+
* (`FilterCondition` declares them and every driver compiles them), so this
|
|
1361
|
+
* path hands them over rather than lowering them.
|
|
1362
|
+
*/
|
|
1363
|
+
applyFilterNode(node, cube, filter, conjuncts) {
|
|
1364
|
+
if (!node) return;
|
|
1365
|
+
if (node.kind === "leaf") {
|
|
1366
|
+
const fieldName = this.resolveFieldName(cube, node.member, "any");
|
|
1367
|
+
const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(node.operator, node.values));
|
|
1368
|
+
if (extra) conjuncts.push(extra);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (node.kind === "and") {
|
|
1372
|
+
for (const child of node.children) this.applyFilterNode(child, cube, filter, conjuncts);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const rendered = this.filterNodeToCondition(node, cube);
|
|
1376
|
+
if (rendered) conjuncts.push(rendered);
|
|
1377
|
+
}
|
|
1378
|
+
/** A node as a standalone `FilterCondition` the engine can consume. */
|
|
1379
|
+
filterNodeToCondition(node, cube) {
|
|
1380
|
+
if (!node) return null;
|
|
1381
|
+
if (node.kind === "not") {
|
|
1382
|
+
const inner = this.filterNodeToCondition(node.child, cube);
|
|
1383
|
+
return inner ? { $not: inner } : null;
|
|
1384
|
+
}
|
|
1385
|
+
if (node.kind === "or") {
|
|
1386
|
+
const branches = node.children.map((child) => this.filterNodeToCondition(child, cube)).filter((c) => !!c);
|
|
1387
|
+
return branches.length > 0 ? { $or: branches } : null;
|
|
1388
|
+
}
|
|
1389
|
+
const filter = {};
|
|
1390
|
+
const conjuncts = [];
|
|
1391
|
+
this.applyFilterNode(node, cube, filter, conjuncts);
|
|
1392
|
+
if (conjuncts.length > 0) {
|
|
1393
|
+
filter.$and = [...Array.isArray(filter.$and) ? filter.$and : [], ...conjuncts];
|
|
1394
|
+
}
|
|
1395
|
+
return Object.keys(filter).length > 0 ? filter : null;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Render a normalized filter node as the display SQL `/analytics/sql`
|
|
1399
|
+
* echoes. Values still bind as `$n` placeholders — the echo travels to the
|
|
1400
|
+
* browser, so a comparand is never inlined.
|
|
1401
|
+
*/
|
|
1402
|
+
renderFilterNodeSql(node, cube, params) {
|
|
1403
|
+
if (!node) return null;
|
|
1404
|
+
if (node.kind === "leaf") {
|
|
1405
|
+
return this.buildFilterClauseSql(
|
|
1406
|
+
this.resolveFieldName(cube, node.member, "any"),
|
|
1407
|
+
node.operator,
|
|
1408
|
+
node.values,
|
|
1409
|
+
params
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
if (node.kind === "not") {
|
|
1413
|
+
const inner = this.renderFilterNodeSql(node.child, cube, params);
|
|
1414
|
+
return inner ? `NOT (${inner})` : null;
|
|
1415
|
+
}
|
|
1416
|
+
const parts = node.children.map((child) => this.renderFilterNodeSql(child, cube, params)).filter((s) => !!s);
|
|
1417
|
+
if (parts.length === 0) return null;
|
|
1418
|
+
if (parts.length === 1) return parts[0];
|
|
1419
|
+
return `(${parts.join(node.kind === "or" ? " OR " : " AND ")})`;
|
|
1420
|
+
}
|
|
1236
1421
|
mergeFilterOperand(filter, field, operand) {
|
|
1237
1422
|
const existing = filter[field];
|
|
1238
1423
|
if (existing === void 0) {
|
|
@@ -1256,9 +1441,13 @@ var ObjectQLStrategy = class {
|
|
|
1256
1441
|
* HERE on every driver — and "bucketed trend" is precisely the shape that also
|
|
1257
1442
|
* carries a range ("last 12 months", "this quarter").
|
|
1258
1443
|
*
|
|
1259
|
-
* Bounds are inclusive on both ends —
|
|
1260
|
-
* `
|
|
1261
|
-
*
|
|
1444
|
+
* Bounds are inclusive on both ends — logically "from day X through day Y".
|
|
1445
|
+
* The `$lte` end is left as the bare calendar day on purpose: the driver's
|
|
1446
|
+
* filter compiler owns the calendar-day → instant translation, compiling a
|
|
1447
|
+
* bare-day `$lte` on a `datetime` column into the half-open `< nextDay`
|
|
1448
|
+
* (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`
|
|
1449
|
+
* performs the same half-open translation itself because it binds into raw
|
|
1450
|
+
* SQL, so one dashboard reads the same on every driver.
|
|
1262
1451
|
*
|
|
1263
1452
|
* Comparands are coerced by the SAME helper the `where` path uses, so an
|
|
1264
1453
|
* epoch-ms bound recovers as a number and an ISO string stays a string. No
|
|
@@ -1318,24 +1507,60 @@ var ObjectQLStrategy = class {
|
|
|
1318
1507
|
return { $lte: v0 };
|
|
1319
1508
|
case "contains":
|
|
1320
1509
|
return { $regex: values[0] };
|
|
1510
|
+
// `notContains` had no arm and fell to the `default` below, which returns
|
|
1511
|
+
// a BARE VALUE — i.e. `{field: 'x'}`, an equality. "does not contain x"
|
|
1512
|
+
// was compiled as "equals x". These three pass through as the canonical
|
|
1513
|
+
// spec operators every driver implements directly, so an anchored match
|
|
1514
|
+
// stays anchored rather than depending on regex dialect (#4128).
|
|
1515
|
+
case "notContains":
|
|
1516
|
+
return { $notContains: values[0] };
|
|
1517
|
+
case "startsWith":
|
|
1518
|
+
return { $startsWith: values[0] };
|
|
1519
|
+
case "endsWith":
|
|
1520
|
+
return { $endsWith: values[0] };
|
|
1321
1521
|
case "in":
|
|
1322
1522
|
return { $in: all };
|
|
1323
1523
|
case "notIn":
|
|
1324
1524
|
return { $nin: all };
|
|
1325
1525
|
default:
|
|
1326
|
-
|
|
1526
|
+
throw new Error(
|
|
1527
|
+
`[analytics] ObjectQL strategy cannot express filter operator "${operator}". Treating it as an equality would silently query something the author did not ask for.`
|
|
1528
|
+
);
|
|
1327
1529
|
}
|
|
1328
1530
|
}
|
|
1329
1531
|
extractObjectName(cube) {
|
|
1330
1532
|
return cube.sql.trim();
|
|
1331
1533
|
}
|
|
1534
|
+
/**
|
|
1535
|
+
* The dimensions this query PROJECTS, in the order the result carries them:
|
|
1536
|
+
* every `dimensions` entry, then every granular `timeDimensions` entry that
|
|
1537
|
+
* is not already one of them.
|
|
1538
|
+
*
|
|
1539
|
+
* `timeDimensions` is not merely a filter carrier. An entry with a
|
|
1540
|
+
* `granularity` is GROUPED BY — see the `td.granularity` sites that build
|
|
1541
|
+
* groupBy here, in `generateSql` and in the cross-object path — so its
|
|
1542
|
+
* bucket is a COLUMN of the result; an entry without one only contributes a
|
|
1543
|
+
* `dateRange` predicate and must NOT be projected.
|
|
1544
|
+
*
|
|
1545
|
+
* Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly
|
|
1546
|
+
* that set. When they did not, a bucketed query returned rows carrying only
|
|
1547
|
+
* the measures and a `fields` list that never mentioned the bucket — a trend
|
|
1548
|
+
* chart got N values and no x-axis (#4033) — even though the SQL had
|
|
1549
|
+
* selected `date_trunc(…) AS "<dim>"` all along. One definition, every
|
|
1550
|
+
* consumer.
|
|
1551
|
+
*/
|
|
1552
|
+
projectedDimensions(query) {
|
|
1553
|
+
const out = [...query.dimensions ?? []];
|
|
1554
|
+
for (const td of query.timeDimensions ?? []) {
|
|
1555
|
+
if (td.granularity && !out.includes(td.dimension)) out.push(td.dimension);
|
|
1556
|
+
}
|
|
1557
|
+
return out;
|
|
1558
|
+
}
|
|
1332
1559
|
buildFieldMeta(query, cube) {
|
|
1333
1560
|
const fields = [];
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
fields.push({ name: dim, type: d?.type || "string" });
|
|
1338
|
-
}
|
|
1561
|
+
for (const dim of this.projectedDimensions(query)) {
|
|
1562
|
+
const d = this.lookupMember(cube, dim, "dimension");
|
|
1563
|
+
fields.push({ name: dim, type: d?.type || "string" });
|
|
1339
1564
|
}
|
|
1340
1565
|
if (query.measures) {
|
|
1341
1566
|
for (const m of query.measures) {
|
|
@@ -1347,14 +1572,16 @@ var ObjectQLStrategy = class {
|
|
|
1347
1572
|
};
|
|
1348
1573
|
|
|
1349
1574
|
// src/dataset-compiler.ts
|
|
1575
|
+
import { AggregationFunction } from "@objectstack/spec/data";
|
|
1350
1576
|
var UNSUPPORTED_AGGREGATES = /* @__PURE__ */ new Set(["array_agg", "string_agg"]);
|
|
1577
|
+
var SUPPORTED_AGGREGATES = AggregationFunction.options.filter((a) => !UNSUPPORTED_AGGREGATES.has(a));
|
|
1351
1578
|
function aggregateToMetricType(m) {
|
|
1352
1579
|
if (!m.aggregate) {
|
|
1353
1580
|
throw new Error(`[dataset-compiler] non-derived measure "${m.name}" has no aggregate`);
|
|
1354
1581
|
}
|
|
1355
1582
|
if (UNSUPPORTED_AGGREGATES.has(m.aggregate)) {
|
|
1356
1583
|
throw new Error(
|
|
1357
|
-
`[dataset-compiler] measure "${m.name}" uses aggregate "${m.aggregate}" which is not supported by the v1 dataset runtime (supported:
|
|
1584
|
+
`[dataset-compiler] measure "${m.name}" uses aggregate "${m.aggregate}" which is not supported by the v1 dataset runtime (supported: ${SUPPORTED_AGGREGATES.join(", ")}).`
|
|
1358
1585
|
);
|
|
1359
1586
|
}
|
|
1360
1587
|
return m.aggregate;
|
|
@@ -1581,7 +1808,7 @@ function applyWindow(rows, limit, offset) {
|
|
|
1581
1808
|
if (start === 0 && limit == null) return rows;
|
|
1582
1809
|
return rows.slice(start, limit != null ? start + limit : void 0);
|
|
1583
1810
|
}
|
|
1584
|
-
function resolveOrdering(selection, dimensions) {
|
|
1811
|
+
function resolveOrdering(selection, dimensions, timeDimensions = []) {
|
|
1585
1812
|
const order = selection.order;
|
|
1586
1813
|
if (order && Object.keys(order).length > 0) {
|
|
1587
1814
|
const selectable = /* @__PURE__ */ new Set([
|
|
@@ -1600,6 +1827,10 @@ function resolveOrdering(selection, dimensions) {
|
|
|
1600
1827
|
if ((selection.limit != null || selection.offset != null) && dimensions.length > 0) {
|
|
1601
1828
|
return Object.fromEntries(dimensions.map((d) => [d, "asc"]));
|
|
1602
1829
|
}
|
|
1830
|
+
const timeKeys = timeDimensions.filter((d) => dimensions.includes(d));
|
|
1831
|
+
if (timeKeys.length > 0) {
|
|
1832
|
+
return Object.fromEntries(timeKeys.map((d) => [d, "asc"]));
|
|
1833
|
+
}
|
|
1603
1834
|
return void 0;
|
|
1604
1835
|
}
|
|
1605
1836
|
function parseUTC(date) {
|
|
@@ -1693,7 +1924,7 @@ var DatasetExecutor = class {
|
|
|
1693
1924
|
}
|
|
1694
1925
|
const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);
|
|
1695
1926
|
const dimensions = selection.dimensions ?? [];
|
|
1696
|
-
const order = resolveOrdering(selection, dimensions);
|
|
1927
|
+
const order = resolveOrdering(selection, dimensions, this.timeDimensionsOf(compiled, dimensions));
|
|
1697
1928
|
const labelOrderKeys = this.orderLabels ? Object.keys(order ?? {}).filter(
|
|
1698
1929
|
(k) => dimensions.includes(k) && this.orderLabels.isLabelBearing(k)
|
|
1699
1930
|
) : [];
|
|
@@ -1749,6 +1980,19 @@ var DatasetExecutor = class {
|
|
|
1749
1980
|
result.rows = applyWindow(result.rows, selection.limit, selection.offset);
|
|
1750
1981
|
return result;
|
|
1751
1982
|
}
|
|
1983
|
+
/**
|
|
1984
|
+
* The selected dimensions the compiled cube types as `time`, in selection
|
|
1985
|
+
* order (#3916) — the axis {@link resolveOrdering} defaults to ascending.
|
|
1986
|
+
*
|
|
1987
|
+
* Membership is decided by the DIMENSION's declared type, not by whether the
|
|
1988
|
+
* selection happens to bucket it: a `date` dimension left ungranulated groups
|
|
1989
|
+
* raw timestamps, and those want chronological order every bit as much as
|
|
1990
|
+
* month buckets do. (Both sort correctly — `compareValues` compares Dates and
|
|
1991
|
+
* ISO strings chronologically, and bucket keys are minted sort-stable.)
|
|
1992
|
+
*/
|
|
1993
|
+
timeDimensionsOf(compiled, dimensions) {
|
|
1994
|
+
return dimensions.filter((d) => compiled.cube.dimensions[d]?.type === "time");
|
|
1995
|
+
}
|
|
1752
1996
|
buildQuery(compiled, opts) {
|
|
1753
1997
|
const q = {
|
|
1754
1998
|
cube: compiled.cube.name,
|
|
@@ -1990,11 +2234,21 @@ function pickDisplayField(fields) {
|
|
|
1990
2234
|
}
|
|
1991
2235
|
|
|
1992
2236
|
// src/preview-evaluator.ts
|
|
1993
|
-
import { calendarPartsInTzOrUtc } from "@objectstack/core";
|
|
2237
|
+
import { calendarPartsInTzOrUtc, nextUtcCalendarDay as nextUtcCalendarDay3, utcInstantMs } from "@objectstack/core";
|
|
1994
2238
|
function compare(a, b) {
|
|
1995
2239
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
2240
|
+
if (a instanceof Date || b instanceof Date) {
|
|
2241
|
+
const ai = utcInstantMs(a);
|
|
2242
|
+
const bi = utcInstantMs(b);
|
|
2243
|
+
if (ai !== null && bi !== null) return ai - bi;
|
|
2244
|
+
}
|
|
1996
2245
|
return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
|
|
1997
2246
|
}
|
|
2247
|
+
function lteBound(value, bound) {
|
|
2248
|
+
const nextDay = nextUtcCalendarDay3(bound);
|
|
2249
|
+
if (nextDay != null) return compare(value, nextDay) < 0;
|
|
2250
|
+
return compare(value, bound) <= 0;
|
|
2251
|
+
}
|
|
1998
2252
|
function matchOp(value, op, expected) {
|
|
1999
2253
|
switch (op) {
|
|
2000
2254
|
case "$eq":
|
|
@@ -2007,8 +2261,16 @@ function matchOp(value, op, expected) {
|
|
|
2007
2261
|
return value != null && compare(value, expected) >= 0;
|
|
2008
2262
|
case "$lt":
|
|
2009
2263
|
return value != null && compare(value, expected) < 0;
|
|
2010
|
-
case "$lte":
|
|
2011
|
-
|
|
2264
|
+
case "$lte": {
|
|
2265
|
+
if (value == null) return false;
|
|
2266
|
+
return lteBound(value, expected);
|
|
2267
|
+
}
|
|
2268
|
+
case "$between": {
|
|
2269
|
+
if (value == null || !Array.isArray(expected) || expected.length !== 2) return false;
|
|
2270
|
+
const [min, max] = expected;
|
|
2271
|
+
if (min == null || max == null) return false;
|
|
2272
|
+
return compare(value, min) >= 0 && lteBound(value, max);
|
|
2273
|
+
}
|
|
2012
2274
|
case "$in":
|
|
2013
2275
|
return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));
|
|
2014
2276
|
case "$nin":
|
|
@@ -2095,7 +2357,9 @@ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
|
|
|
2095
2357
|
const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
2096
2358
|
filtered = filtered.filter((r) => {
|
|
2097
2359
|
const v = String(r[field] ?? "");
|
|
2098
|
-
|
|
2360
|
+
const nextDay = nextUtcCalendarDay3(end);
|
|
2361
|
+
const inUpper = nextDay != null ? v < nextDay : v <= `${end}~`;
|
|
2362
|
+
return v >= String(start) && inUpper;
|
|
2099
2363
|
});
|
|
2100
2364
|
}
|
|
2101
2365
|
const dimensions = query.dimensions ?? [];
|
|
@@ -2195,6 +2459,7 @@ var AnalyticsService = class {
|
|
|
2195
2459
|
// fall back to any explicitly-configured provider for legacy cubes.
|
|
2196
2460
|
getAllowedRelationships: (cubeName) => this.datasetRegistry.get(cubeName)?.allowedRelationships ?? config.getAllowedRelationships?.(cubeName),
|
|
2197
2461
|
coerceTemporalFilterValue: config.coerceTemporalFilterValue,
|
|
2462
|
+
coerceTemporalFilterColumn: config.coerceTemporalFilterColumn,
|
|
2198
2463
|
isExternalObject: config.isExternalObject
|
|
2199
2464
|
};
|
|
2200
2465
|
const builtIn = [
|
|
@@ -2682,6 +2947,11 @@ var FallbackDelegateStrategy = class {
|
|
|
2682
2947
|
var AnalyticsServicePlugin = class {
|
|
2683
2948
|
constructor(options = {}) {
|
|
2684
2949
|
this.name = "com.objectstack.service-analytics";
|
|
2950
|
+
/**
|
|
2951
|
+
* Services init() registers on every path (ADR-0116, #4131) — lets the
|
|
2952
|
+
* kernel name this plugin when a consumer requires one before it inits.
|
|
2953
|
+
*/
|
|
2954
|
+
this.providesServices = ["analytics"];
|
|
2685
2955
|
this.version = "1.0.0";
|
|
2686
2956
|
this.type = "standard";
|
|
2687
2957
|
this.dependencies = [];
|
|
@@ -2884,6 +3154,17 @@ var AnalyticsServicePlugin = class {
|
|
|
2884
3154
|
}
|
|
2885
3155
|
return value;
|
|
2886
3156
|
};
|
|
3157
|
+
const coerceTemporalFilterColumn = (objectName, fieldName, columnSql) => {
|
|
3158
|
+
try {
|
|
3159
|
+
const svc = ctx.getService("data");
|
|
3160
|
+
const driver = svc?.getDriverForObject?.(objectName);
|
|
3161
|
+
if (driver && typeof driver.temporalFilterColumnSql === "function") {
|
|
3162
|
+
return driver.temporalFilterColumnSql(objectName, fieldName, columnSql);
|
|
3163
|
+
}
|
|
3164
|
+
} catch {
|
|
3165
|
+
}
|
|
3166
|
+
return columnSql;
|
|
3167
|
+
};
|
|
2887
3168
|
const config = {
|
|
2888
3169
|
cubes: this.options.cubes,
|
|
2889
3170
|
logger: ctx.logger,
|
|
@@ -2894,6 +3175,7 @@ var AnalyticsServicePlugin = class {
|
|
|
2894
3175
|
getReadScope,
|
|
2895
3176
|
getAllowedRelationships: this.options.getAllowedRelationships,
|
|
2896
3177
|
coerceTemporalFilterValue,
|
|
3178
|
+
coerceTemporalFilterColumn,
|
|
2897
3179
|
relationshipResolver,
|
|
2898
3180
|
labelResolver,
|
|
2899
3181
|
// ADR-0053 — source-field currency metadata for the measure currency chain.
|