@objectstack/service-analytics 17.0.0-rc.1 → 17.0.0-rc.2
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 +437 -0
- package/dist/index.cjs +201 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -12
- package/dist/index.d.ts +154 -12
- package/dist/index.js +200 -40
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
compileScopedFilterToSql: () => compileScopedFilterToSql,
|
|
32
32
|
createOrderLabelResolver: () => createOrderLabelResolver,
|
|
33
33
|
evaluateDerivedMeasures: () => evaluateDerivedMeasures,
|
|
34
|
+
fillEmptyGroups: () => fillEmptyGroups,
|
|
34
35
|
mergeByDimensions: () => mergeByDimensions,
|
|
35
36
|
pickDisplayField: () => pickDisplayField,
|
|
36
37
|
resolveDimensionLabels: () => resolveDimensionLabels,
|
|
@@ -40,6 +41,7 @@ __export(index_exports, {
|
|
|
40
41
|
module.exports = __toCommonJS(index_exports);
|
|
41
42
|
|
|
42
43
|
// src/analytics-service.ts
|
|
44
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
43
45
|
var import_core5 = require("@objectstack/core");
|
|
44
46
|
|
|
45
47
|
// src/cube-registry.ts
|
|
@@ -1749,6 +1751,7 @@ function compileDataset(dataset, resolver) {
|
|
|
1749
1751
|
}
|
|
1750
1752
|
|
|
1751
1753
|
// src/dataset-executor.ts
|
|
1754
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
1752
1755
|
var import_core3 = require("@objectstack/core");
|
|
1753
1756
|
function resolveSelectionTokens(compiled, selection, context) {
|
|
1754
1757
|
const tokenCtx = (0, import_core3.filterTokenContextFrom)(context, /* @__PURE__ */ new Date());
|
|
@@ -1770,6 +1773,12 @@ function combineFilters(a, b) {
|
|
|
1770
1773
|
if (a && b) return { $and: [a, b] };
|
|
1771
1774
|
return a ?? b;
|
|
1772
1775
|
}
|
|
1776
|
+
function splitMeasuresByFilter(measures, measureFilters) {
|
|
1777
|
+
const unfiltered = [];
|
|
1778
|
+
const filtered = [];
|
|
1779
|
+
for (const m of measures) (measureFilters[m] ? filtered : unfiltered).push(m);
|
|
1780
|
+
return { unfiltered, filtered };
|
|
1781
|
+
}
|
|
1773
1782
|
function evaluateDerivedMeasures(rows, derived) {
|
|
1774
1783
|
if (derived.length === 0) return rows;
|
|
1775
1784
|
return rows.map((row) => {
|
|
@@ -1780,6 +1789,14 @@ function evaluateDerivedMeasures(rows, derived) {
|
|
|
1780
1789
|
return out;
|
|
1781
1790
|
});
|
|
1782
1791
|
}
|
|
1792
|
+
function fillEmptyGroups(rows, columnAggregates) {
|
|
1793
|
+
for (const [column, aggregate2] of Object.entries(columnAggregates)) {
|
|
1794
|
+
const empty = (0, import_data2.emptyGroupValueFor)(aggregate2);
|
|
1795
|
+
if (empty === void 0) continue;
|
|
1796
|
+
for (const row of rows) if (row[column] == null) row[column] = empty;
|
|
1797
|
+
}
|
|
1798
|
+
return rows;
|
|
1799
|
+
}
|
|
1783
1800
|
function num(v) {
|
|
1784
1801
|
if (v == null) return null;
|
|
1785
1802
|
const n = typeof v === "number" ? v : Number(v);
|
|
@@ -1958,11 +1975,7 @@ var DatasetExecutor = class {
|
|
|
1958
1975
|
for (const d of selectedDerived) {
|
|
1959
1976
|
for (const dep of d.of) baseMeasures.add(dep);
|
|
1960
1977
|
}
|
|
1961
|
-
const unfiltered =
|
|
1962
|
-
const filtered = [];
|
|
1963
|
-
for (const m of baseMeasures) {
|
|
1964
|
-
(compiled.measureFilters[m] ? filtered : unfiltered).push(m);
|
|
1965
|
-
}
|
|
1978
|
+
const { unfiltered, filtered } = splitMeasuresByFilter(baseMeasures, compiled.measureFilters);
|
|
1966
1979
|
const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);
|
|
1967
1980
|
const dimensions = selection.dimensions ?? [];
|
|
1968
1981
|
const order = resolveOrdering(selection, dimensions, this.timeDimensionsOf(compiled, dimensions));
|
|
@@ -1973,6 +1986,76 @@ var DatasetExecutor = class {
|
|
|
1973
1986
|
const pushDownKeys = /* @__PURE__ */ new Set([...dimensions, ...unfiltered]);
|
|
1974
1987
|
const canPushDownWindow = singleQuery && labelOrderKeys.length === 0 && Object.keys(order ?? {}).every((k) => pushDownKeys.has(k));
|
|
1975
1988
|
const windowQuery = canPushDownWindow ? { order, limit: selection.limit, offset: selection.offset } : void 0;
|
|
1989
|
+
const result = await this.runMeasurePass(compiled, selection, {
|
|
1990
|
+
measures: [...baseMeasures],
|
|
1991
|
+
dimensions,
|
|
1992
|
+
baseFilter,
|
|
1993
|
+
window: windowQuery,
|
|
1994
|
+
context
|
|
1995
|
+
});
|
|
1996
|
+
if (selection.compareTo) {
|
|
1997
|
+
const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);
|
|
1998
|
+
result.rows = mergeByDimensions(
|
|
1999
|
+
result.rows,
|
|
2000
|
+
compareRows,
|
|
2001
|
+
dimensions,
|
|
2002
|
+
[...baseMeasures].map((m) => `${m}__compare`)
|
|
2003
|
+
);
|
|
2004
|
+
for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: "number" });
|
|
2005
|
+
}
|
|
2006
|
+
const fillColumns = {};
|
|
2007
|
+
for (const m of baseMeasures) {
|
|
2008
|
+
const aggregate2 = compiled.cube.measures?.[m]?.type;
|
|
2009
|
+
fillColumns[m] = aggregate2;
|
|
2010
|
+
if (selection.compareTo) fillColumns[`${m}__compare`] = aggregate2;
|
|
2011
|
+
}
|
|
2012
|
+
fillEmptyGroups(result.rows, fillColumns);
|
|
2013
|
+
result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);
|
|
2014
|
+
for (const d of selectedDerived) result.fields.push({ name: d.name, type: "number" });
|
|
2015
|
+
let sortKeys;
|
|
2016
|
+
for (const key of labelOrderKeys) {
|
|
2017
|
+
const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];
|
|
2018
|
+
if (values.length === 0) continue;
|
|
2019
|
+
const labels = await this.orderLabels.resolveLabels(key, values);
|
|
2020
|
+
if (labels && labels.size > 0) (sortKeys ?? (sortKeys = {}))[key] = labels;
|
|
2021
|
+
}
|
|
2022
|
+
result.rows = applyOrdering(result.rows, order, sortKeys);
|
|
2023
|
+
result.rows = applyWindow(result.rows, selection.limit, selection.offset);
|
|
2024
|
+
return result;
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Run ONE grouped pass over a set of base measures, honouring each measure's
|
|
2028
|
+
* own scoped `filter`: the unfiltered measures in a single query, plus one
|
|
2029
|
+
* supplementary query per filter-scoped measure, merged back by dimension key.
|
|
2030
|
+
*
|
|
2031
|
+
* **This is the executor's only implementation of "how a measure filter is
|
|
2032
|
+
* applied", and every window goes through it** — the current period, each
|
|
2033
|
+
* `totals` subset (which re-enters via `executeSelection`), and the
|
|
2034
|
+
* `compareTo` window. Before #4820 the comparison window had its own,
|
|
2035
|
+
* simpler answer: one shifted query over all base measures with only the
|
|
2036
|
+
* base filter, so `compiled.measureFilters` was never read on that path.
|
|
2037
|
+
* `won_count` counted won deals and `won_count__compare` counted every deal,
|
|
2038
|
+
* under one label, in adjacent columns. Only measures carrying a filter were
|
|
2039
|
+
* wrong — which is what made it survive: the unfiltered ones next to them
|
|
2040
|
+
* compared correctly.
|
|
2041
|
+
*
|
|
2042
|
+
* The caller supplies the `selection` this pass queries under, which is how
|
|
2043
|
+
* the comparison window differs at all: same measures, same dimensions, same
|
|
2044
|
+
* filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else
|
|
2045
|
+
* about the two passes may drift, because anything that does becomes a
|
|
2046
|
+
* discrepancy between two columns the reader is invited to subtract.
|
|
2047
|
+
*
|
|
2048
|
+
* Cost: one extra query per filter-scoped measure when `compareTo` is set.
|
|
2049
|
+
* The alternative — declaring the discrepancy in the response — is not one,
|
|
2050
|
+
* since the two columns exist to be directly comparable.
|
|
2051
|
+
*
|
|
2052
|
+
* @param window - Ordering/window to push into the SQL. Only ever set for a
|
|
2053
|
+
* selection the caller proved is a single self-sufficient query; a pass
|
|
2054
|
+
* that fans out must return its whole grid for the merge.
|
|
2055
|
+
*/
|
|
2056
|
+
async runMeasurePass(compiled, selection, opts) {
|
|
2057
|
+
const { measures, dimensions, baseFilter, window, context } = opts;
|
|
2058
|
+
const { unfiltered, filtered } = splitMeasuresByFilter(measures, compiled.measureFilters);
|
|
1976
2059
|
let result;
|
|
1977
2060
|
if (unfiltered.length > 0 || filtered.length === 0) {
|
|
1978
2061
|
result = await this.service.query(this.buildQuery(compiled, {
|
|
@@ -1981,7 +2064,7 @@ var DatasetExecutor = class {
|
|
|
1981
2064
|
where: baseFilter,
|
|
1982
2065
|
selection,
|
|
1983
2066
|
contextTimezone: context?.timezone,
|
|
1984
|
-
window
|
|
2067
|
+
window
|
|
1985
2068
|
}), context);
|
|
1986
2069
|
} else {
|
|
1987
2070
|
result = { rows: [], fields: [] };
|
|
@@ -1998,27 +2081,6 @@ var DatasetExecutor = class {
|
|
|
1998
2081
|
result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);
|
|
1999
2082
|
result.fields.push({ name: m, type: "number" });
|
|
2000
2083
|
}
|
|
2001
|
-
if (selection.compareTo) {
|
|
2002
|
-
const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);
|
|
2003
|
-
result.rows = mergeByDimensions(
|
|
2004
|
-
result.rows,
|
|
2005
|
-
compareRows,
|
|
2006
|
-
dimensions,
|
|
2007
|
-
[...baseMeasures].map((m) => `${m}__compare`)
|
|
2008
|
-
);
|
|
2009
|
-
for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: "number" });
|
|
2010
|
-
}
|
|
2011
|
-
result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);
|
|
2012
|
-
for (const d of selectedDerived) result.fields.push({ name: d.name, type: "number" });
|
|
2013
|
-
let sortKeys;
|
|
2014
|
-
for (const key of labelOrderKeys) {
|
|
2015
|
-
const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];
|
|
2016
|
-
if (values.length === 0) continue;
|
|
2017
|
-
const labels = await this.orderLabels.resolveLabels(key, values);
|
|
2018
|
-
if (labels && labels.size > 0) (sortKeys ?? (sortKeys = {}))[key] = labels;
|
|
2019
|
-
}
|
|
2020
|
-
result.rows = applyOrdering(result.rows, order, sortKeys);
|
|
2021
|
-
result.rows = applyWindow(result.rows, selection.limit, selection.offset);
|
|
2022
2084
|
return result;
|
|
2023
2085
|
}
|
|
2024
2086
|
/**
|
|
@@ -2083,13 +2145,11 @@ var DatasetExecutor = class {
|
|
|
2083
2145
|
const shiftedTd = (selection.timeDimensions ?? []).map(
|
|
2084
2146
|
(t) => t.dimension === cmp.dimension ? { ...t, dateRange: shifted } : t
|
|
2085
2147
|
);
|
|
2086
|
-
const sub = await this.
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
contextTimezone: context?.timezone
|
|
2092
|
-
}), context);
|
|
2148
|
+
const sub = await this.runMeasurePass(
|
|
2149
|
+
compiled,
|
|
2150
|
+
{ ...selection, timeDimensions: shiftedTd },
|
|
2151
|
+
{ measures, dimensions, baseFilter, context }
|
|
2152
|
+
);
|
|
2093
2153
|
return sub.rows.map((row) => {
|
|
2094
2154
|
const out = {};
|
|
2095
2155
|
for (const dim of dimensions) out[dim] = row[dim];
|
|
@@ -2459,6 +2519,7 @@ function isMissingSourceError(err) {
|
|
|
2459
2519
|
msg.includes("not registered") || // framework: object not in registry
|
|
2460
2520
|
msg.includes("unknown object") || msg.includes("is not a registered object");
|
|
2461
2521
|
}
|
|
2522
|
+
var BARE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/i;
|
|
2462
2523
|
var DEFAULT_CAPABILITIES = {
|
|
2463
2524
|
nativeSql: false,
|
|
2464
2525
|
objectqlAggregate: false,
|
|
@@ -2477,10 +2538,11 @@ var AnalyticsService = class {
|
|
|
2477
2538
|
}
|
|
2478
2539
|
this.readScopeProvider = config.getReadScope;
|
|
2479
2540
|
this.relationshipResolver = config.relationshipResolver;
|
|
2480
|
-
this.
|
|
2541
|
+
this.sourceFieldMeta = config.sourceFieldMeta;
|
|
2481
2542
|
this.labelResolver = config.labelResolver;
|
|
2482
2543
|
this.draftRowsResolver = config.draftRowsResolver;
|
|
2483
2544
|
this.isRegisteredObject = config.isRegisteredObject;
|
|
2545
|
+
this.getObjectFieldNames = config.getObjectFieldNames;
|
|
2484
2546
|
if (config.datasets) {
|
|
2485
2547
|
for (const ds of config.datasets) {
|
|
2486
2548
|
try {
|
|
@@ -2698,7 +2760,7 @@ var AnalyticsService = class {
|
|
|
2698
2760
|
if (!d.field || d.type !== "date") continue;
|
|
2699
2761
|
const granularity = resolveDimensionGranularity(selection, d.name, d.dateGranularity);
|
|
2700
2762
|
if (!granularity) continue;
|
|
2701
|
-
const ftype = this.
|
|
2763
|
+
const ftype = this.sourceFieldMeta?.(dataset.object, d.field)?.type;
|
|
2702
2764
|
if (ftype === "datetime") rangeDims.push({ d, granularity, instant: true });
|
|
2703
2765
|
else if (ftype === "date") rangeDims.push({ d, granularity, instant: false });
|
|
2704
2766
|
else if (rangeTz === "UTC") rangeDims.push({ d, granularity, instant: false });
|
|
@@ -2747,14 +2809,17 @@ var AnalyticsService = class {
|
|
|
2747
2809
|
if (f.format == null && m.format) f.format = m.format;
|
|
2748
2810
|
const fc = f;
|
|
2749
2811
|
const mc = m;
|
|
2812
|
+
const meta = m.field ? this.sourceFieldMeta?.(dataset.object, m.field) : void 0;
|
|
2750
2813
|
if (fc.currency == null) {
|
|
2751
|
-
const meta = m.field ? this.measureCurrency?.(dataset.object, m.field) : void 0;
|
|
2752
2814
|
const monetary = !!mc.currency || meta?.type === "currency";
|
|
2753
2815
|
if (monetary) {
|
|
2754
2816
|
const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency;
|
|
2755
2817
|
if (resolved) fc.currency = resolved;
|
|
2756
2818
|
}
|
|
2757
2819
|
}
|
|
2820
|
+
if (f.percentScale == null) {
|
|
2821
|
+
f.percentScale = m.derived?.op === "ratio" ? "fraction" : (0, import_data3.percentScaleOf)(meta);
|
|
2822
|
+
}
|
|
2758
2823
|
}
|
|
2759
2824
|
}
|
|
2760
2825
|
if (result.fields?.length && selectedDims.length) {
|
|
@@ -2820,6 +2885,7 @@ var AnalyticsService = class {
|
|
|
2820
2885
|
if (!cube) {
|
|
2821
2886
|
this.assertInferableCube(name);
|
|
2822
2887
|
cube = this.inferCubeFromQuery(query);
|
|
2888
|
+
this.assertMeasureFields(query, cube, Object.keys(cube.measures));
|
|
2823
2889
|
this.cubeRegistry.register(cube);
|
|
2824
2890
|
const isScalarMetric = (query.dimensions?.length ?? 0) === 0 && (query.timeDimensions?.length ?? 0) === 0;
|
|
2825
2891
|
const message = `[Analytics] No cube registered for "${name}"; auto-inferred a minimal cube (sql="${name}", measures=${Object.keys(cube.measures).join(",") || "(none)"}, dimensions=${Object.keys(cube.dimensions).join(",") || "(none)"}). Define an explicit Cube in your stack for full control.`;
|
|
@@ -2839,10 +2905,82 @@ var AnalyticsService = class {
|
|
|
2839
2905
|
...cube,
|
|
2840
2906
|
measures: { ...cube.measures, ...extraMeasures }
|
|
2841
2907
|
};
|
|
2908
|
+
this.assertMeasureFields(query, augmented, Object.keys(cube.measures));
|
|
2842
2909
|
this.cubeRegistry.register(augmented);
|
|
2843
2910
|
this.logger.debug(
|
|
2844
2911
|
`[Analytics] Augmented cube "${name}" with inferred measures: ${Object.keys(extraMeasures).join(",")}`
|
|
2845
2912
|
);
|
|
2913
|
+
} else {
|
|
2914
|
+
this.assertMeasureFields(query, cube, Object.keys(cube.measures));
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* [#4437] Reject a measure whose SOURCE FIELD the backing object does not
|
|
2919
|
+
* have, BEFORE the strategy compiles it into SQL.
|
|
2920
|
+
*
|
|
2921
|
+
* `inferMeasure` maps a suffix convention onto a field name and has no way to
|
|
2922
|
+
* know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the
|
|
2923
|
+
* driver threw `no such column`, and the caller got
|
|
2924
|
+
* `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver
|
|
2925
|
+
* error class on the wire, and nothing actionable, for what is a plain typo.
|
|
2926
|
+
* The DATA route has refused the same mistake with a `400 INVALID_FIELD`
|
|
2927
|
+
* naming the field since #4315/#4254; this is the analytics half of that
|
|
2928
|
+
* answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/
|
|
2929
|
+
* `param`) so one mistake has one shape across both routes.
|
|
2930
|
+
*
|
|
2931
|
+
* What it checks, and what it deliberately does not:
|
|
2932
|
+
*
|
|
2933
|
+
* - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose
|
|
2934
|
+
* `sql` is a real SQL expression has no field list to check against.
|
|
2935
|
+
* - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.
|
|
2936
|
+
* Absent hook / unknown object → stand down (see the config field's doc).
|
|
2937
|
+
* - Only measures whose source is a BARE COLUMN. `count(*)` has no source
|
|
2938
|
+
* field, and a dotted reference (`account.industry`) resolves through a
|
|
2939
|
+
* join whose target this check cannot see — both pass through untouched.
|
|
2940
|
+
* - `id` / `created_at` / `updated_at` are admitted unconditionally, matching
|
|
2941
|
+
* the data path's `resolveQueryFields`: they are engine-assigned rather than
|
|
2942
|
+
* declared, and a gate stricter than the engine it guards would reject
|
|
2943
|
+
* queries that used to work.
|
|
2944
|
+
*/
|
|
2945
|
+
assertMeasureFields(query, cube, declaredMeasures) {
|
|
2946
|
+
const probe = this.getObjectFieldNames;
|
|
2947
|
+
if (!probe) return;
|
|
2948
|
+
const measures = query.measures ?? [];
|
|
2949
|
+
if (measures.length === 0) return;
|
|
2950
|
+
const object = typeof cube.sql === "string" ? cube.sql.trim() : "";
|
|
2951
|
+
if (!object || !BARE_IDENTIFIER.test(object)) return;
|
|
2952
|
+
const fieldNames = probe(object);
|
|
2953
|
+
if (!fieldNames || fieldNames.length === 0) return;
|
|
2954
|
+
const known = /* @__PURE__ */ new Set([...fieldNames, "id", "created_at", "updated_at"]);
|
|
2955
|
+
const stripPrefix = (m) => m.includes(".") ? m.split(".").slice(1).join(".") : m;
|
|
2956
|
+
const sourceFieldOf = (measure) => {
|
|
2957
|
+
const metric = cube.measures[stripPrefix(measure)];
|
|
2958
|
+
if (!metric) return null;
|
|
2959
|
+
if (metric.type === "count" && (metric.sql === "*" || metric.sql == null)) return null;
|
|
2960
|
+
const source = typeof metric.sql === "string" ? metric.sql.trim() : "";
|
|
2961
|
+
if (!source || source === "*" || !BARE_IDENTIFIER.test(source)) return null;
|
|
2962
|
+
return source;
|
|
2963
|
+
};
|
|
2964
|
+
const invalid = /* @__PURE__ */ new Set();
|
|
2965
|
+
for (const measure of measures) {
|
|
2966
|
+
const source = sourceFieldOf(measure);
|
|
2967
|
+
if (source && !known.has(source)) invalid.add(stripPrefix(measure));
|
|
2968
|
+
}
|
|
2969
|
+
if (invalid.size === 0) return;
|
|
2970
|
+
const usable = declaredMeasures.filter((m) => !invalid.has(m));
|
|
2971
|
+
for (const measure of measures) {
|
|
2972
|
+
const source = sourceFieldOf(measure);
|
|
2973
|
+
if (!source || known.has(source)) continue;
|
|
2974
|
+
const err = new Error(
|
|
2975
|
+
`Measure '${measure}' on cube '${cube.name}' aggregates field '${source}', which object '${object}' does not have. Valid measures: ${usable.join(", ") || "(none)"}. Other measures are inferred from the object's OWN fields as '<field>_sum' / '_avg' / '_min' / '_max' / '_count_distinct', so check the spelling of '${source}' \u2014 known fields: ${[...fieldNames].sort().join(", ")}.`
|
|
2976
|
+
);
|
|
2977
|
+
err.code = "INVALID_FIELD";
|
|
2978
|
+
err.status = 400;
|
|
2979
|
+
err.field = source;
|
|
2980
|
+
err.object = object;
|
|
2981
|
+
err.param = "measures";
|
|
2982
|
+
err.measure = measure;
|
|
2983
|
+
throw err;
|
|
2846
2984
|
}
|
|
2847
2985
|
}
|
|
2848
2986
|
/**
|
|
@@ -2996,6 +3134,14 @@ var AnalyticsServicePlugin = class {
|
|
|
2996
3134
|
this.version = "1.0.0";
|
|
2997
3135
|
this.type = "standard";
|
|
2998
3136
|
this.dependencies = [];
|
|
3137
|
+
/**
|
|
3138
|
+
* init() probes the `data` engine ObjectQLPlugin provides for the
|
|
3139
|
+
* auto-bridge — order-if-present so the probe verdict is deterministic
|
|
3140
|
+
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
|
|
3141
|
+
* degrades on purpose (per-query lazy resolution / explicit
|
|
3142
|
+
* `executeAggregate`).
|
|
3143
|
+
*/
|
|
3144
|
+
this.optionalDependencies = ["com.objectstack.engine.objectql"];
|
|
2999
3145
|
this.options = options;
|
|
3000
3146
|
}
|
|
3001
3147
|
async init(ctx) {
|
|
@@ -3219,10 +3365,12 @@ var AnalyticsServicePlugin = class {
|
|
|
3219
3365
|
coerceTemporalFilterColumn,
|
|
3220
3366
|
relationshipResolver,
|
|
3221
3367
|
labelResolver,
|
|
3222
|
-
//
|
|
3223
|
-
|
|
3368
|
+
// Source-field metadata behind the display chains on result columns:
|
|
3369
|
+
// ADR-0053 currency (`currencyConfig.defaultCurrency`) and percent scale
|
|
3370
|
+
// (`max`, which is what marks whole-percent storage — objectui#3136).
|
|
3371
|
+
sourceFieldMeta: (object, field) => {
|
|
3224
3372
|
const f = dataEngine()?.getObject?.(object)?.fields?.[field];
|
|
3225
|
-
return f ? { type: f.type, defaultCurrency: f.currencyConfig?.defaultCurrency } : void 0;
|
|
3373
|
+
return f ? { type: f.type, max: f.max, defaultCurrency: f.currencyConfig?.defaultCurrency } : void 0;
|
|
3226
3374
|
},
|
|
3227
3375
|
// ADR-0062 D6 — a federated object carries an `external` block (ADR-0015).
|
|
3228
3376
|
// Reported so NativeSQLStrategy declines it (its hand-compiled FROM would
|
|
@@ -3245,6 +3393,18 @@ var AnalyticsServicePlugin = class {
|
|
|
3245
3393
|
if (!engine) return true;
|
|
3246
3394
|
return engine.getObject?.(name) != null;
|
|
3247
3395
|
},
|
|
3396
|
+
// [#4437] Field names for the measure source-field gate. Read from the
|
|
3397
|
+
// SAME schema registry `isRegisteredObject` above consults (and the data
|
|
3398
|
+
// path's #4315 gate reads), so "which fields exist" has one answer across
|
|
3399
|
+
// /data and /analytics. `undefined` — no engine, unknown object, or an
|
|
3400
|
+
// object with no field map (an external datasource whose columns are not
|
|
3401
|
+
// mirrored locally) — means "cannot answer", and the gate stands down.
|
|
3402
|
+
getObjectFieldNames: (objectName) => {
|
|
3403
|
+
const fields = dataEngine()?.getObject?.(objectName)?.fields;
|
|
3404
|
+
if (!fields || typeof fields !== "object") return void 0;
|
|
3405
|
+
const names = Object.keys(fields);
|
|
3406
|
+
return names.length > 0 ? names : void 0;
|
|
3407
|
+
},
|
|
3248
3408
|
draftRowsResolver
|
|
3249
3409
|
};
|
|
3250
3410
|
if (autoBridgedReadScope && securityPresentAtInit) {
|
|
@@ -3301,6 +3461,7 @@ var AnalyticsServicePlugin = class {
|
|
|
3301
3461
|
compileScopedFilterToSql,
|
|
3302
3462
|
createOrderLabelResolver,
|
|
3303
3463
|
evaluateDerivedMeasures,
|
|
3464
|
+
fillEmptyGroups,
|
|
3304
3465
|
mergeByDimensions,
|
|
3305
3466
|
pickDisplayField,
|
|
3306
3467
|
resolveDimensionLabels,
|