@objectstack/service-analytics 17.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +249 -0
- package/README.md +127 -328
- package/dist/index.cjs +128 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +99 -10
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -41,9 +41,9 @@ __export(index_exports, {
|
|
|
41
41
|
module.exports = __toCommonJS(index_exports);
|
|
42
42
|
|
|
43
43
|
// src/analytics-service.ts
|
|
44
|
-
var
|
|
44
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
45
45
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
46
|
-
var
|
|
46
|
+
var import_core6 = require("@objectstack/core");
|
|
47
47
|
var import_types = require("@objectstack/types");
|
|
48
48
|
|
|
49
49
|
// src/cube-registry.ts
|
|
@@ -162,21 +162,18 @@ var CubeRegistry = class {
|
|
|
162
162
|
};
|
|
163
163
|
|
|
164
164
|
// src/strategies/filter-normalizer.ts
|
|
165
|
-
var
|
|
165
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
166
166
|
var import_api = require("@objectstack/spec/api");
|
|
167
167
|
|
|
168
168
|
// src/comparand-shape.ts
|
|
169
|
+
var import_core = require("@objectstack/core");
|
|
170
|
+
var import_data = require("@objectstack/spec/data");
|
|
169
171
|
function isBindableComparand(value) {
|
|
170
|
-
if (value ===
|
|
171
|
-
|
|
172
|
-
if (kind === "string" || kind === "number" || kind === "bigint" || kind === "boolean") return true;
|
|
173
|
-
return value instanceof Date || ArrayBuffer.isView(value);
|
|
172
|
+
if (value === void 0) return true;
|
|
173
|
+
return (0, import_data.isAcceptedFilterComparand)(value) || ArrayBuffer.isView(value);
|
|
174
174
|
}
|
|
175
175
|
function isRenderableTextComparand(value) {
|
|
176
|
-
|
|
177
|
-
const kind = typeof value;
|
|
178
|
-
if (kind === "string" || kind === "number" || kind === "bigint" || kind === "boolean") return true;
|
|
179
|
-
return value instanceof Date;
|
|
176
|
+
return value === void 0 || (0, import_data.isAcceptedFilterComparand)(value);
|
|
180
177
|
}
|
|
181
178
|
function isFieldReference(value) {
|
|
182
179
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
@@ -212,6 +209,51 @@ function findIn(node, field) {
|
|
|
212
209
|
}
|
|
213
210
|
return null;
|
|
214
211
|
}
|
|
212
|
+
function findUninterpretableTemporalMember(filter, kindOf) {
|
|
213
|
+
return findUninterpretableIn(filter, "", kindOf);
|
|
214
|
+
}
|
|
215
|
+
function findUninterpretableIn(node, field, kindOf) {
|
|
216
|
+
if (!node || typeof node !== "object") return null;
|
|
217
|
+
if (Array.isArray(node)) {
|
|
218
|
+
for (const child of node) {
|
|
219
|
+
const hit = findUninterpretableIn(child, field, kindOf);
|
|
220
|
+
if (hit) return hit;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
if (node instanceof Date || ArrayBuffer.isView(node)) return null;
|
|
225
|
+
if (isFieldReference(node)) return null;
|
|
226
|
+
for (const [key, value] of Object.entries(node)) {
|
|
227
|
+
const scope = key.startsWith("$") ? field : key;
|
|
228
|
+
const kind = scope ? kindOf(scope) : null;
|
|
229
|
+
if (kind) {
|
|
230
|
+
const hit2 = judgeTemporalLiterals(value, scope, kind);
|
|
231
|
+
if (hit2) return hit2;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const hit = findUninterpretableIn(value, scope, kindOf);
|
|
235
|
+
if (hit) return hit;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function judgeTemporalLiterals(value, field, kind) {
|
|
240
|
+
if (Array.isArray(value)) {
|
|
241
|
+
for (const member of value) {
|
|
242
|
+
const hit = judgeTemporalLiterals(member, field, kind);
|
|
243
|
+
if (hit) return hit;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
if (value && typeof value === "object") {
|
|
248
|
+
if (value instanceof Date || ArrayBuffer.isView(value) || isFieldReference(value)) return null;
|
|
249
|
+
for (const nested of Object.values(value)) {
|
|
250
|
+
const hit = judgeTemporalLiterals(nested, field, kind);
|
|
251
|
+
if (hit) return hit;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return (0, import_core.isUninterpretableTemporalComparand)(kind, value) ? { field, kind, value } : null;
|
|
256
|
+
}
|
|
215
257
|
var TEXT_PATTERN_OPERATORS = /* @__PURE__ */ new Set([
|
|
216
258
|
"$contains",
|
|
217
259
|
"$notContains",
|
|
@@ -229,7 +271,7 @@ function shapePreview(value) {
|
|
|
229
271
|
}
|
|
230
272
|
}
|
|
231
273
|
function unrenderableTextComparandMessage(op, field, value) {
|
|
232
|
-
return `"${op}" on "${field}" matches against the TEXT of a pattern, but its comparand is ${Array.isArray(value) ? "an array" : "an object"} (${shapePreview(value)}). filter.zod.ts declares it a string (StringOperatorSchema);
|
|
274
|
+
return `"${op}" on "${field}" matches against the TEXT of a pattern, but its comparand is ${Array.isArray(value) ? "an array" : "an object"} (${shapePreview(value)}). filter.zod.ts declares it a string (StringOperatorSchema); ${import_data.ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} is accepted. Refusing rather than stringifying it: String({}) is "[object Object]", so the pattern that ran would be one nobody wrote \u2014 and a row storing that literal text matches it.`;
|
|
233
275
|
}
|
|
234
276
|
function fieldReferenceComparandMessage(op, field, ref, position) {
|
|
235
277
|
return `"${op}" on "${field}"${position ? ` (${position})` : ""} compares against the field reference { "$field": "${ref}" }, which this compiler does not lower into a column-to-column comparison. Refusing rather than binding it: the reference object used to become the BOUND VALUE of the comparison, so the emitted predicate compared "${field}" against the reference itself \u2014 a value no row can hold \u2014 and a read scope built from it answered the wrong row set with nothing to read. \u26A0\uFE0F This is NOT the platform declining the rule. @objectstack/spec declares this shape (FieldReferenceSchema), @objectstack/formula resolves it per record in memory, driver-sql / driver-sqlite-wasm compile it to a same-table column comparison for the six scalar operators since #5222, and since the 2026-08-12 ruling on #7598 the analytics native-SQL strategy DECLINES such a query so it routes to the ObjectQL engine path and runs there \u2014 the driver enforcing declared-only enumeration, the tenant-isolation ban and the comparison class with metadata it owns. What refuses here is this SQL lowering, whose only remaining caller is the /analytics/sql display echo; it has no faithful rendering of the predicate the engine path actually runs, and half-rendering one would describe a query that returns different rows. Run the query itself (/analytics/query) to get its rows (#7598).`;
|
|
@@ -238,7 +280,7 @@ function fieldReferenceBetweenBoundMessage(op, field, ref, index) {
|
|
|
238
280
|
return `"${op}" on "${field}" has the field reference { "$field": "${ref}" } at index ${index} of its [min, max] bounds. A range BOUND may not be a field reference on any backend: driver-sql and driver-sqlite-wasm refuse both endpoints (#5222), @objectstack/formula does not resolve a reference inside a list either \u2014 it orders the bounds against the raw reference object, which no value compares meaningfully to \u2014 and @objectstack/spec no longer declares the position at all (#7596 removed FieldReferenceSchema from the $between endpoint union, ADR-0049 declared = enforced). Refusing rather than lowering it: this compiler splits $between into its two bounds, so the reference would arrive at the driver under a "$gte" / "$lte" the author never wrote \u2014 a position the SQL drivers DO compile \u2014 and the range would quietly succeed here while the identical filter is refused everywhere else. Use a literal bound, or spell the comparison you meant as a scalar one ({ "${field}": { "$gte": { "$field": "${ref}" } } }), which IS served \u2014 on the ObjectQL engine path, where the driver enforces the #5222 rulings (#7598).`;
|
|
239
281
|
}
|
|
240
282
|
function unbindableListMemberMessage(op, field, value, index) {
|
|
241
|
-
return `"${op}" on "${field}" has a value at index ${index} of its list that cannot be bound as a SQL parameter: ${shapePreview(value)}. Every member of an $in/$nin/$between list is a comparand in its own right \u2014 use
|
|
283
|
+
return `"${op}" on "${field}" has a value at index ${index} of its list that cannot be bound as a SQL parameter: ${shapePreview(value)}. Every member of an $in/$nin/$between list is a comparand in its own right \u2014 use ${import_data.ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} (or a binary value). Refusing rather than binding it: the member can equal no stored value, so the list silently loses that entry (and a $nin loses the exclusion the caller wrote).`;
|
|
242
284
|
}
|
|
243
285
|
|
|
244
286
|
// src/strategies/filter-normalizer.ts
|
|
@@ -586,7 +628,7 @@ function nullSafeNegationOperand(node) {
|
|
|
586
628
|
}
|
|
587
629
|
function filterArrayNotLowerableError(where) {
|
|
588
630
|
return invalidFilterError(
|
|
589
|
-
`[analytics] received a 'where' array that is not a filter: ${JSON.stringify(where)}. A filter array is a comparison [field, operator, value], a logical node ["and"|"or", ...conditions], or a list of those \u2014 it is INPUT-ONLY sugar (spec 'FilterArray'), lowered to a FilterCondition by @objectstack/spec parseFilterAST() at every door, this one included (#5158/#5334). This value cannot be lowered, and an unapplied filter would have charted the UNFILTERED dataset. Recognised operators: ${[...
|
|
631
|
+
`[analytics] received a 'where' array that is not a filter: ${JSON.stringify(where)}. A filter array is a comparison [field, operator, value], a logical node ["and"|"or", ...conditions], or a list of those \u2014 it is INPUT-ONLY sugar (spec 'FilterArray'), lowered to a FilterCondition by @objectstack/spec parseFilterAST() at every door, this one included (#5158/#5334). This value cannot be lowered, and an unapplied filter would have charted the UNFILTERED dataset. Recognised operators: ${[...import_data2.VALID_AST_OPERATORS].sort().join(", ")}. Infix joins ([condA, "or", condB]) are NOT one of the shapes \u2014 write the prefix form ["or", condA, condB].`
|
|
590
632
|
);
|
|
591
633
|
}
|
|
592
634
|
function lowerAnalyticsWhere(query) {
|
|
@@ -595,8 +637,8 @@ function lowerAnalyticsWhere(query) {
|
|
|
595
637
|
if (!where || typeof where !== "object") return null;
|
|
596
638
|
if (Array.isArray(where)) {
|
|
597
639
|
if (where.length === 0) return null;
|
|
598
|
-
if (!(0,
|
|
599
|
-
const condition = (0,
|
|
640
|
+
if (!(0, import_data2.isFilterAST)(where)) throw filterArrayNotLowerableError(where);
|
|
641
|
+
const condition = (0, import_data2.parseFilterAST)(where);
|
|
600
642
|
if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
|
|
601
643
|
throw invalidFilterError(
|
|
602
644
|
`[analytics] filter array ${JSON.stringify(where)} passed isFilterAST() but parseFilterAST() lowered it to ${JSON.stringify(condition)}. Refusing rather than charting the dataset unfiltered (#5158/#5334).`
|
|
@@ -1019,7 +1061,7 @@ function invalidMemberError(message, meta) {
|
|
|
1019
1061
|
}
|
|
1020
1062
|
|
|
1021
1063
|
// src/strategies/native-sql-strategy.ts
|
|
1022
|
-
var
|
|
1064
|
+
var import_core2 = require("@objectstack/core");
|
|
1023
1065
|
var AGGREGATE_SQL = {
|
|
1024
1066
|
"count": () => "COUNT(*)",
|
|
1025
1067
|
"sum": (col) => `SUM(${col})`,
|
|
@@ -1051,9 +1093,51 @@ var NativeSQLStrategy = class {
|
|
|
1051
1093
|
}
|
|
1052
1094
|
}
|
|
1053
1095
|
if (this.carriesCrossFieldComparison(query, ctx)) return false;
|
|
1096
|
+
if (this.carriesUninterpretableTemporalComparand(query, ctx)) return false;
|
|
1054
1097
|
const caps = ctx.queryCapabilities(query.cube);
|
|
1055
1098
|
return caps.nativeSql && typeof ctx.executeRawSql === "function";
|
|
1056
1099
|
}
|
|
1100
|
+
/**
|
|
1101
|
+
* [#8690] Does the query's `where` compare a declared TIME dimension against
|
|
1102
|
+
* a value no temporal storage rule can read? See the ruling at
|
|
1103
|
+
* {@link canHandle}.
|
|
1104
|
+
*
|
|
1105
|
+
* The classification comes from the CUBE, the only metadata this package has:
|
|
1106
|
+
* a dimension declares `type: 'time'` (compiled from the dataset dimension's
|
|
1107
|
+
* `type: 'date'`), and {@link lookupMember} is the same resolution every other
|
|
1108
|
+
* member lookup in this strategy uses, so "the member the gate classified"
|
|
1109
|
+
* and "the member the compiler emits" cannot drift apart.
|
|
1110
|
+
*
|
|
1111
|
+
* A `time` dimension is read with the DATETIME rule — the permissive one of
|
|
1112
|
+
* the three. That is the right direction because this is a routing decision,
|
|
1113
|
+
* not a verdict: the engine door re-judges with the field's real declared
|
|
1114
|
+
* type and has the final say, so under-classifying an exotic spelling merely
|
|
1115
|
+
* leaves today's behaviour, while over-classifying would silently move a
|
|
1116
|
+
* working dashboard off the fast path. The comparands this card measured
|
|
1117
|
+
* (`last_30_days`, `not-a-date-at-all`) are unreadable under all three rules,
|
|
1118
|
+
* so the decline fires for them whichever backing type the dimension has.
|
|
1119
|
+
*
|
|
1120
|
+
* `lowerAnalyticsWhere` rather than `query.where` raw, so the authored ARRAY
|
|
1121
|
+
* sugar is seen after `parseFilterAST` has lowered it; a THROW from that
|
|
1122
|
+
* lowering is not this gate's to answer — the filter is malformed either way
|
|
1123
|
+
* and `normalizeAnalyticsFilterTree` refuses it a moment later with the
|
|
1124
|
+
* message and envelope it has always had.
|
|
1125
|
+
*/
|
|
1126
|
+
carriesUninterpretableTemporalComparand(query, ctx) {
|
|
1127
|
+
const cube = query.cube ? ctx.getCube(query.cube) : void 0;
|
|
1128
|
+
if (!cube) return false;
|
|
1129
|
+
let where = null;
|
|
1130
|
+
try {
|
|
1131
|
+
where = lowerAnalyticsWhere(query);
|
|
1132
|
+
} catch {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
if (!where) return false;
|
|
1136
|
+
return findUninterpretableTemporalMember(
|
|
1137
|
+
where,
|
|
1138
|
+
(member) => this.lookupMember(cube, member, "dimension")?.type === "time" ? "datetime" : null
|
|
1139
|
+
) !== null;
|
|
1140
|
+
}
|
|
1057
1141
|
/**
|
|
1058
1142
|
* [#7598] Does serving this query require the cross-field capability this
|
|
1059
1143
|
* strategy declines? See the ruling recorded at {@link canHandle}.
|
|
@@ -1176,7 +1260,7 @@ var NativeSQLStrategy = class {
|
|
|
1176
1260
|
if (range.length === 2) {
|
|
1177
1261
|
const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);
|
|
1178
1262
|
const column = this.temporalColumn(ctx, td2, colExpr);
|
|
1179
|
-
const nextDay = (0,
|
|
1263
|
+
const nextDay = (0, import_core2.nextUtcCalendarDay)(range[1]);
|
|
1180
1264
|
params.push(this.coerceTemporal(ctx, td2, range[0]));
|
|
1181
1265
|
const lower = `${column} >= $${params.length}`;
|
|
1182
1266
|
if (nextDay != null) {
|
|
@@ -1546,7 +1630,7 @@ var NativeSQLStrategy = class {
|
|
|
1546
1630
|
return `${rawCol} ${sqlOp} ${patternRef} ESCAPE $${params.length}`;
|
|
1547
1631
|
}
|
|
1548
1632
|
if (operator === "lte") {
|
|
1549
|
-
const nextDay = (0,
|
|
1633
|
+
const nextDay = (0, import_core2.nextUtcCalendarDay)(values[0]);
|
|
1550
1634
|
if (nextDay != null) {
|
|
1551
1635
|
params.push(this.coerceTemporal(ctx, target, nextDay));
|
|
1552
1636
|
return `${this.temporalColumn(ctx, target, rawCol)} < $${params.length}`;
|
|
@@ -1576,8 +1660,8 @@ var NativeSQLStrategy = class {
|
|
|
1576
1660
|
};
|
|
1577
1661
|
|
|
1578
1662
|
// src/strategies/objectql-strategy.ts
|
|
1579
|
-
var
|
|
1580
|
-
var
|
|
1663
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
1664
|
+
var import_core3 = require("@objectstack/core");
|
|
1581
1665
|
|
|
1582
1666
|
// src/strategies/cross-object-rebucket.ts
|
|
1583
1667
|
var RECOMBINABLE_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -1822,7 +1906,7 @@ var ObjectQLStrategy = class {
|
|
|
1822
1906
|
);
|
|
1823
1907
|
if (filterClause) whereParts.push(filterClause);
|
|
1824
1908
|
for (const { field, bounds } of this.dateRangeBounds(cube, query)) {
|
|
1825
|
-
const nextDay = (0,
|
|
1909
|
+
const nextDay = (0, import_core3.nextUtcCalendarDay)(bounds.$lte);
|
|
1826
1910
|
params.push(bounds.$gte, nextDay ?? bounds.$lte);
|
|
1827
1911
|
whereParts.push(
|
|
1828
1912
|
`(${field} >= $${params.length - 1} AND ${field} ${nextDay ? "<" : "<="} $${params.length})`
|
|
@@ -1873,11 +1957,11 @@ var ObjectQLStrategy = class {
|
|
|
1873
1957
|
* predicate. `$and` makes that structurally impossible.
|
|
1874
1958
|
*/
|
|
1875
1959
|
withReadScope(objectName, filter, ctx) {
|
|
1876
|
-
const userFilter = Object.keys(filter).length > 0 ? (0,
|
|
1960
|
+
const userFilter = Object.keys(filter).length > 0 ? (0, import_data3.markFilterSubtreeProvenance)(filter, "author") : void 0;
|
|
1877
1961
|
if (typeof ctx.getReadScope !== "function") return userFilter;
|
|
1878
1962
|
const scope = ctx.getReadScope(objectName);
|
|
1879
1963
|
if (scope === void 0 || scope === null) return userFilter;
|
|
1880
|
-
const scopeFilter = (0,
|
|
1964
|
+
const scopeFilter = (0, import_data3.markFilterSubtreeProvenance)(scope, "policy");
|
|
1881
1965
|
if (!userFilter) return scopeFilter;
|
|
1882
1966
|
return { $and: [userFilter, scopeFilter] };
|
|
1883
1967
|
}
|
|
@@ -2051,7 +2135,7 @@ var ObjectQLStrategy = class {
|
|
|
2051
2135
|
if (fkValues.length === 0 || typeof ctx.executeAggregate !== "function") return map;
|
|
2052
2136
|
const idFilter = { id: { $in: fkValues } };
|
|
2053
2137
|
const scope = typeof ctx.getReadScope === "function" ? ctx.getReadScope(refObject) : null;
|
|
2054
|
-
if (scope != null) (0,
|
|
2138
|
+
if (scope != null) (0, import_data3.markFilterSubtreeProvenance)(scope, "policy");
|
|
2055
2139
|
const filter = scope != null ? { $and: [idFilter, scope] } : idFilter;
|
|
2056
2140
|
const rows = await ctx.executeAggregate(refObject, {
|
|
2057
2141
|
groupBy: ["id", attr],
|
|
@@ -2543,10 +2627,10 @@ var ObjectQLStrategy = class {
|
|
|
2543
2627
|
};
|
|
2544
2628
|
|
|
2545
2629
|
// src/dataset-compiler.ts
|
|
2546
|
-
var
|
|
2630
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
2547
2631
|
var import_ui = require("@objectstack/spec/ui");
|
|
2548
2632
|
var UNSUPPORTED_AGGREGATES = /* @__PURE__ */ new Set();
|
|
2549
|
-
var SUPPORTED_AGGREGATES =
|
|
2633
|
+
var SUPPORTED_AGGREGATES = import_data4.AggregationFunction.options.filter((a) => !UNSUPPORTED_AGGREGATES.has(a));
|
|
2550
2634
|
function aggregateToMetricType(m) {
|
|
2551
2635
|
if (!m.aggregate) {
|
|
2552
2636
|
throw new Error(`[dataset-compiler] non-derived measure "${m.name}" has no aggregate`);
|
|
@@ -2708,11 +2792,11 @@ function compileDataset(dataset, resolver, options) {
|
|
|
2708
2792
|
}
|
|
2709
2793
|
|
|
2710
2794
|
// src/dataset-executor.ts
|
|
2711
|
-
var
|
|
2712
|
-
var
|
|
2795
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
2796
|
+
var import_core4 = require("@objectstack/core");
|
|
2713
2797
|
function resolveSelectionTokens(compiled, selection, context) {
|
|
2714
|
-
const tokenCtx = (0,
|
|
2715
|
-
const resolve = (v) => (0,
|
|
2798
|
+
const tokenCtx = (0, import_core4.filterTokenContextFrom)(context, /* @__PURE__ */ new Date());
|
|
2799
|
+
const resolve = (v) => (0, import_core4.resolveFilterTokens)(v, tokenCtx);
|
|
2716
2800
|
const filter = resolve(compiled.filter);
|
|
2717
2801
|
const measureFilters = resolve(compiled.measureFilters);
|
|
2718
2802
|
const runtimeFilter = resolve(selection.runtimeFilter);
|
|
@@ -2748,7 +2832,7 @@ function evaluateDerivedMeasures(rows, derived) {
|
|
|
2748
2832
|
}
|
|
2749
2833
|
function fillEmptyGroups(rows, columnAggregates) {
|
|
2750
2834
|
for (const [column, aggregate2] of Object.entries(columnAggregates)) {
|
|
2751
|
-
const empty = (0,
|
|
2835
|
+
const empty = (0, import_data5.emptyGroupValueFor)(aggregate2);
|
|
2752
2836
|
if (empty === void 0) continue;
|
|
2753
2837
|
for (const row of rows) if (row[column] == null) row[column] = empty;
|
|
2754
2838
|
}
|
|
@@ -2948,7 +3032,7 @@ function bucketKeyAtOrdinal(ordinal, granularity) {
|
|
|
2948
3032
|
}
|
|
2949
3033
|
function alignedCompareBucketKey(key, granularity, kind, currentRange, shiftedRange) {
|
|
2950
3034
|
if (typeof key !== "string" || key.length === 0) return null;
|
|
2951
|
-
const span = (0,
|
|
3035
|
+
const span = (0, import_core4.bucketKeyToCalendarRange)(key, granularity);
|
|
2952
3036
|
if (!span) return null;
|
|
2953
3037
|
const targetOrdinal = kind === "previousYear" ? bucketOrdinalOfDay(shiftYear(span.start, 1), granularity) : bucketOrdinalOfDay(span.start, granularity) + (bucketOrdinalOfDay(currentRange[0], granularity) - bucketOrdinalOfDay(shiftedRange[0], granularity));
|
|
2954
3038
|
const first = bucketOrdinalOfDay(currentRange[0], granularity);
|
|
@@ -3412,18 +3496,18 @@ function pickDisplayField(fields) {
|
|
|
3412
3496
|
}
|
|
3413
3497
|
|
|
3414
3498
|
// src/preview-evaluator.ts
|
|
3415
|
-
var
|
|
3499
|
+
var import_core5 = require("@objectstack/core");
|
|
3416
3500
|
function compare(a, b) {
|
|
3417
3501
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
3418
3502
|
if (a instanceof Date || b instanceof Date) {
|
|
3419
|
-
const ai = (0,
|
|
3420
|
-
const bi = (0,
|
|
3503
|
+
const ai = (0, import_core5.utcInstantMs)(a);
|
|
3504
|
+
const bi = (0, import_core5.utcInstantMs)(b);
|
|
3421
3505
|
if (ai !== null && bi !== null) return ai - bi;
|
|
3422
3506
|
}
|
|
3423
3507
|
return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
|
|
3424
3508
|
}
|
|
3425
3509
|
function lteBound(value, bound) {
|
|
3426
|
-
const nextDay = (0,
|
|
3510
|
+
const nextDay = (0, import_core5.nextUtcCalendarDay)(bound);
|
|
3427
3511
|
if (nextDay != null) return compare(value, nextDay) < 0;
|
|
3428
3512
|
return compare(value, bound) <= 0;
|
|
3429
3513
|
}
|
|
@@ -3481,7 +3565,7 @@ function matchesWhere(row, where) {
|
|
|
3481
3565
|
function bucketDate(value, granularity, timezone) {
|
|
3482
3566
|
const d = new Date(String(value));
|
|
3483
3567
|
if (Number.isNaN(d.getTime())) return null;
|
|
3484
|
-
const { year: y, month, day: dayNum } = (0,
|
|
3568
|
+
const { year: y, month, day: dayNum } = (0, import_core5.calendarPartsInTzOrUtc)(d, timezone);
|
|
3485
3569
|
const m = `${month}`.padStart(2, "0");
|
|
3486
3570
|
const day = `${dayNum}`.padStart(2, "0");
|
|
3487
3571
|
switch (granularity) {
|
|
@@ -3535,7 +3619,7 @@ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
|
|
|
3535
3619
|
const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
3536
3620
|
filtered = filtered.filter((r) => {
|
|
3537
3621
|
const v = String(r[field] ?? "");
|
|
3538
|
-
const nextDay = (0,
|
|
3622
|
+
const nextDay = (0, import_core5.nextUtcCalendarDay)(end);
|
|
3539
3623
|
const inUpper = nextDay != null ? v < nextDay : v <= `${end}~`;
|
|
3540
3624
|
return v >= String(start) && inUpper;
|
|
3541
3625
|
});
|
|
@@ -3669,7 +3753,7 @@ var AnalyticsService = class {
|
|
|
3669
3753
|
this.datasetRegistry = /* @__PURE__ */ new Map();
|
|
3670
3754
|
/** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */
|
|
3671
3755
|
this.warnedNoObjectRegistry = false;
|
|
3672
|
-
this.logger = config.logger || (0,
|
|
3756
|
+
this.logger = config.logger || (0, import_core6.createLogger)({ level: "info", format: "pretty" });
|
|
3673
3757
|
this.cubeRegistry = new CubeRegistry();
|
|
3674
3758
|
if (config.cubes) {
|
|
3675
3759
|
this.cubeRegistry.registerAll(config.cubes);
|
|
@@ -3683,7 +3767,7 @@ var AnalyticsService = class {
|
|
|
3683
3767
|
this.getObjectFieldNames = config.getObjectFieldNames;
|
|
3684
3768
|
this.getObjectDatasource = config.getObjectDatasource;
|
|
3685
3769
|
this.isExternalObject = config.isExternalObject;
|
|
3686
|
-
this.debugSql = config.debugSql ?? (0,
|
|
3770
|
+
this.debugSql = config.debugSql ?? (0, import_core6.getEnv)("NODE_ENV") === "development";
|
|
3687
3771
|
if (config.datasets) {
|
|
3688
3772
|
for (const ds of config.datasets) {
|
|
3689
3773
|
try {
|
|
@@ -3948,11 +4032,11 @@ var AnalyticsService = class {
|
|
|
3948
4032
|
else if (rangeTz === "UTC") rangeDims.push({ d, granularity, instant: false });
|
|
3949
4033
|
}
|
|
3950
4034
|
if (rangeDims.length && result.rows.length) {
|
|
3951
|
-
const bound = (ymd, instant) => instant ? new Date((0,
|
|
4035
|
+
const bound = (ymd, instant) => instant ? new Date((0, import_core6.zonedDateStartToUtcMs)(ymd, rangeTz)).toISOString() : ymd;
|
|
3952
4036
|
result.drillRanges = result.rows.map((row) => {
|
|
3953
4037
|
const ranges = {};
|
|
3954
4038
|
for (const { d, granularity, instant } of rangeDims) {
|
|
3955
|
-
const cal = (0,
|
|
4039
|
+
const cal = (0, import_core6.bucketKeyToCalendarRange)(row[d.name], granularity);
|
|
3956
4040
|
if (cal) {
|
|
3957
4041
|
ranges[d.name] = { field: d.field, gte: bound(cal.start, instant), lt: bound(cal.end, instant) };
|
|
3958
4042
|
}
|
|
@@ -4003,7 +4087,7 @@ var AnalyticsService = class {
|
|
|
4003
4087
|
}
|
|
4004
4088
|
}
|
|
4005
4089
|
if (f.percentScale == null) {
|
|
4006
|
-
f.percentScale = m.derived?.op === "ratio" ? "fraction" : (0,
|
|
4090
|
+
f.percentScale = m.derived?.op === "ratio" ? "fraction" : (0, import_data6.percentScaleOf)(meta);
|
|
4007
4091
|
}
|
|
4008
4092
|
}
|
|
4009
4093
|
}
|