@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.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/analytics-service.ts
|
|
2
|
+
import { percentScaleOf } from "@objectstack/spec/data";
|
|
2
3
|
import { createLogger, bucketKeyToCalendarRange, zonedDateStartToUtcMs } from "@objectstack/core";
|
|
3
4
|
|
|
4
5
|
// src/cube-registry.ts
|
|
@@ -1708,6 +1709,7 @@ function compileDataset(dataset, resolver) {
|
|
|
1708
1709
|
}
|
|
1709
1710
|
|
|
1710
1711
|
// src/dataset-executor.ts
|
|
1712
|
+
import { emptyGroupValueFor } from "@objectstack/spec/data";
|
|
1711
1713
|
import { filterTokenContextFrom, resolveFilterTokens } from "@objectstack/core";
|
|
1712
1714
|
function resolveSelectionTokens(compiled, selection, context) {
|
|
1713
1715
|
const tokenCtx = filterTokenContextFrom(context, /* @__PURE__ */ new Date());
|
|
@@ -1729,6 +1731,12 @@ function combineFilters(a, b) {
|
|
|
1729
1731
|
if (a && b) return { $and: [a, b] };
|
|
1730
1732
|
return a ?? b;
|
|
1731
1733
|
}
|
|
1734
|
+
function splitMeasuresByFilter(measures, measureFilters) {
|
|
1735
|
+
const unfiltered = [];
|
|
1736
|
+
const filtered = [];
|
|
1737
|
+
for (const m of measures) (measureFilters[m] ? filtered : unfiltered).push(m);
|
|
1738
|
+
return { unfiltered, filtered };
|
|
1739
|
+
}
|
|
1732
1740
|
function evaluateDerivedMeasures(rows, derived) {
|
|
1733
1741
|
if (derived.length === 0) return rows;
|
|
1734
1742
|
return rows.map((row) => {
|
|
@@ -1739,6 +1747,14 @@ function evaluateDerivedMeasures(rows, derived) {
|
|
|
1739
1747
|
return out;
|
|
1740
1748
|
});
|
|
1741
1749
|
}
|
|
1750
|
+
function fillEmptyGroups(rows, columnAggregates) {
|
|
1751
|
+
for (const [column, aggregate2] of Object.entries(columnAggregates)) {
|
|
1752
|
+
const empty = emptyGroupValueFor(aggregate2);
|
|
1753
|
+
if (empty === void 0) continue;
|
|
1754
|
+
for (const row of rows) if (row[column] == null) row[column] = empty;
|
|
1755
|
+
}
|
|
1756
|
+
return rows;
|
|
1757
|
+
}
|
|
1742
1758
|
function num(v) {
|
|
1743
1759
|
if (v == null) return null;
|
|
1744
1760
|
const n = typeof v === "number" ? v : Number(v);
|
|
@@ -1917,11 +1933,7 @@ var DatasetExecutor = class {
|
|
|
1917
1933
|
for (const d of selectedDerived) {
|
|
1918
1934
|
for (const dep of d.of) baseMeasures.add(dep);
|
|
1919
1935
|
}
|
|
1920
|
-
const unfiltered =
|
|
1921
|
-
const filtered = [];
|
|
1922
|
-
for (const m of baseMeasures) {
|
|
1923
|
-
(compiled.measureFilters[m] ? filtered : unfiltered).push(m);
|
|
1924
|
-
}
|
|
1936
|
+
const { unfiltered, filtered } = splitMeasuresByFilter(baseMeasures, compiled.measureFilters);
|
|
1925
1937
|
const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);
|
|
1926
1938
|
const dimensions = selection.dimensions ?? [];
|
|
1927
1939
|
const order = resolveOrdering(selection, dimensions, this.timeDimensionsOf(compiled, dimensions));
|
|
@@ -1932,6 +1944,76 @@ var DatasetExecutor = class {
|
|
|
1932
1944
|
const pushDownKeys = /* @__PURE__ */ new Set([...dimensions, ...unfiltered]);
|
|
1933
1945
|
const canPushDownWindow = singleQuery && labelOrderKeys.length === 0 && Object.keys(order ?? {}).every((k) => pushDownKeys.has(k));
|
|
1934
1946
|
const windowQuery = canPushDownWindow ? { order, limit: selection.limit, offset: selection.offset } : void 0;
|
|
1947
|
+
const result = await this.runMeasurePass(compiled, selection, {
|
|
1948
|
+
measures: [...baseMeasures],
|
|
1949
|
+
dimensions,
|
|
1950
|
+
baseFilter,
|
|
1951
|
+
window: windowQuery,
|
|
1952
|
+
context
|
|
1953
|
+
});
|
|
1954
|
+
if (selection.compareTo) {
|
|
1955
|
+
const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);
|
|
1956
|
+
result.rows = mergeByDimensions(
|
|
1957
|
+
result.rows,
|
|
1958
|
+
compareRows,
|
|
1959
|
+
dimensions,
|
|
1960
|
+
[...baseMeasures].map((m) => `${m}__compare`)
|
|
1961
|
+
);
|
|
1962
|
+
for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: "number" });
|
|
1963
|
+
}
|
|
1964
|
+
const fillColumns = {};
|
|
1965
|
+
for (const m of baseMeasures) {
|
|
1966
|
+
const aggregate2 = compiled.cube.measures?.[m]?.type;
|
|
1967
|
+
fillColumns[m] = aggregate2;
|
|
1968
|
+
if (selection.compareTo) fillColumns[`${m}__compare`] = aggregate2;
|
|
1969
|
+
}
|
|
1970
|
+
fillEmptyGroups(result.rows, fillColumns);
|
|
1971
|
+
result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);
|
|
1972
|
+
for (const d of selectedDerived) result.fields.push({ name: d.name, type: "number" });
|
|
1973
|
+
let sortKeys;
|
|
1974
|
+
for (const key of labelOrderKeys) {
|
|
1975
|
+
const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];
|
|
1976
|
+
if (values.length === 0) continue;
|
|
1977
|
+
const labels = await this.orderLabels.resolveLabels(key, values);
|
|
1978
|
+
if (labels && labels.size > 0) (sortKeys ?? (sortKeys = {}))[key] = labels;
|
|
1979
|
+
}
|
|
1980
|
+
result.rows = applyOrdering(result.rows, order, sortKeys);
|
|
1981
|
+
result.rows = applyWindow(result.rows, selection.limit, selection.offset);
|
|
1982
|
+
return result;
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Run ONE grouped pass over a set of base measures, honouring each measure's
|
|
1986
|
+
* own scoped `filter`: the unfiltered measures in a single query, plus one
|
|
1987
|
+
* supplementary query per filter-scoped measure, merged back by dimension key.
|
|
1988
|
+
*
|
|
1989
|
+
* **This is the executor's only implementation of "how a measure filter is
|
|
1990
|
+
* applied", and every window goes through it** — the current period, each
|
|
1991
|
+
* `totals` subset (which re-enters via `executeSelection`), and the
|
|
1992
|
+
* `compareTo` window. Before #4820 the comparison window had its own,
|
|
1993
|
+
* simpler answer: one shifted query over all base measures with only the
|
|
1994
|
+
* base filter, so `compiled.measureFilters` was never read on that path.
|
|
1995
|
+
* `won_count` counted won deals and `won_count__compare` counted every deal,
|
|
1996
|
+
* under one label, in adjacent columns. Only measures carrying a filter were
|
|
1997
|
+
* wrong — which is what made it survive: the unfiltered ones next to them
|
|
1998
|
+
* compared correctly.
|
|
1999
|
+
*
|
|
2000
|
+
* The caller supplies the `selection` this pass queries under, which is how
|
|
2001
|
+
* the comparison window differs at all: same measures, same dimensions, same
|
|
2002
|
+
* filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else
|
|
2003
|
+
* about the two passes may drift, because anything that does becomes a
|
|
2004
|
+
* discrepancy between two columns the reader is invited to subtract.
|
|
2005
|
+
*
|
|
2006
|
+
* Cost: one extra query per filter-scoped measure when `compareTo` is set.
|
|
2007
|
+
* The alternative — declaring the discrepancy in the response — is not one,
|
|
2008
|
+
* since the two columns exist to be directly comparable.
|
|
2009
|
+
*
|
|
2010
|
+
* @param window - Ordering/window to push into the SQL. Only ever set for a
|
|
2011
|
+
* selection the caller proved is a single self-sufficient query; a pass
|
|
2012
|
+
* that fans out must return its whole grid for the merge.
|
|
2013
|
+
*/
|
|
2014
|
+
async runMeasurePass(compiled, selection, opts) {
|
|
2015
|
+
const { measures, dimensions, baseFilter, window, context } = opts;
|
|
2016
|
+
const { unfiltered, filtered } = splitMeasuresByFilter(measures, compiled.measureFilters);
|
|
1935
2017
|
let result;
|
|
1936
2018
|
if (unfiltered.length > 0 || filtered.length === 0) {
|
|
1937
2019
|
result = await this.service.query(this.buildQuery(compiled, {
|
|
@@ -1940,7 +2022,7 @@ var DatasetExecutor = class {
|
|
|
1940
2022
|
where: baseFilter,
|
|
1941
2023
|
selection,
|
|
1942
2024
|
contextTimezone: context?.timezone,
|
|
1943
|
-
window
|
|
2025
|
+
window
|
|
1944
2026
|
}), context);
|
|
1945
2027
|
} else {
|
|
1946
2028
|
result = { rows: [], fields: [] };
|
|
@@ -1957,27 +2039,6 @@ var DatasetExecutor = class {
|
|
|
1957
2039
|
result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);
|
|
1958
2040
|
result.fields.push({ name: m, type: "number" });
|
|
1959
2041
|
}
|
|
1960
|
-
if (selection.compareTo) {
|
|
1961
|
-
const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);
|
|
1962
|
-
result.rows = mergeByDimensions(
|
|
1963
|
-
result.rows,
|
|
1964
|
-
compareRows,
|
|
1965
|
-
dimensions,
|
|
1966
|
-
[...baseMeasures].map((m) => `${m}__compare`)
|
|
1967
|
-
);
|
|
1968
|
-
for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: "number" });
|
|
1969
|
-
}
|
|
1970
|
-
result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);
|
|
1971
|
-
for (const d of selectedDerived) result.fields.push({ name: d.name, type: "number" });
|
|
1972
|
-
let sortKeys;
|
|
1973
|
-
for (const key of labelOrderKeys) {
|
|
1974
|
-
const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];
|
|
1975
|
-
if (values.length === 0) continue;
|
|
1976
|
-
const labels = await this.orderLabels.resolveLabels(key, values);
|
|
1977
|
-
if (labels && labels.size > 0) (sortKeys ?? (sortKeys = {}))[key] = labels;
|
|
1978
|
-
}
|
|
1979
|
-
result.rows = applyOrdering(result.rows, order, sortKeys);
|
|
1980
|
-
result.rows = applyWindow(result.rows, selection.limit, selection.offset);
|
|
1981
2042
|
return result;
|
|
1982
2043
|
}
|
|
1983
2044
|
/**
|
|
@@ -2042,13 +2103,11 @@ var DatasetExecutor = class {
|
|
|
2042
2103
|
const shiftedTd = (selection.timeDimensions ?? []).map(
|
|
2043
2104
|
(t) => t.dimension === cmp.dimension ? { ...t, dateRange: shifted } : t
|
|
2044
2105
|
);
|
|
2045
|
-
const sub = await this.
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
contextTimezone: context?.timezone
|
|
2051
|
-
}), context);
|
|
2106
|
+
const sub = await this.runMeasurePass(
|
|
2107
|
+
compiled,
|
|
2108
|
+
{ ...selection, timeDimensions: shiftedTd },
|
|
2109
|
+
{ measures, dimensions, baseFilter, context }
|
|
2110
|
+
);
|
|
2052
2111
|
return sub.rows.map((row) => {
|
|
2053
2112
|
const out = {};
|
|
2054
2113
|
for (const dim of dimensions) out[dim] = row[dim];
|
|
@@ -2418,6 +2477,7 @@ function isMissingSourceError(err) {
|
|
|
2418
2477
|
msg.includes("not registered") || // framework: object not in registry
|
|
2419
2478
|
msg.includes("unknown object") || msg.includes("is not a registered object");
|
|
2420
2479
|
}
|
|
2480
|
+
var BARE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/i;
|
|
2421
2481
|
var DEFAULT_CAPABILITIES = {
|
|
2422
2482
|
nativeSql: false,
|
|
2423
2483
|
objectqlAggregate: false,
|
|
@@ -2436,10 +2496,11 @@ var AnalyticsService = class {
|
|
|
2436
2496
|
}
|
|
2437
2497
|
this.readScopeProvider = config.getReadScope;
|
|
2438
2498
|
this.relationshipResolver = config.relationshipResolver;
|
|
2439
|
-
this.
|
|
2499
|
+
this.sourceFieldMeta = config.sourceFieldMeta;
|
|
2440
2500
|
this.labelResolver = config.labelResolver;
|
|
2441
2501
|
this.draftRowsResolver = config.draftRowsResolver;
|
|
2442
2502
|
this.isRegisteredObject = config.isRegisteredObject;
|
|
2503
|
+
this.getObjectFieldNames = config.getObjectFieldNames;
|
|
2443
2504
|
if (config.datasets) {
|
|
2444
2505
|
for (const ds of config.datasets) {
|
|
2445
2506
|
try {
|
|
@@ -2657,7 +2718,7 @@ var AnalyticsService = class {
|
|
|
2657
2718
|
if (!d.field || d.type !== "date") continue;
|
|
2658
2719
|
const granularity = resolveDimensionGranularity(selection, d.name, d.dateGranularity);
|
|
2659
2720
|
if (!granularity) continue;
|
|
2660
|
-
const ftype = this.
|
|
2721
|
+
const ftype = this.sourceFieldMeta?.(dataset.object, d.field)?.type;
|
|
2661
2722
|
if (ftype === "datetime") rangeDims.push({ d, granularity, instant: true });
|
|
2662
2723
|
else if (ftype === "date") rangeDims.push({ d, granularity, instant: false });
|
|
2663
2724
|
else if (rangeTz === "UTC") rangeDims.push({ d, granularity, instant: false });
|
|
@@ -2706,14 +2767,17 @@ var AnalyticsService = class {
|
|
|
2706
2767
|
if (f.format == null && m.format) f.format = m.format;
|
|
2707
2768
|
const fc = f;
|
|
2708
2769
|
const mc = m;
|
|
2770
|
+
const meta = m.field ? this.sourceFieldMeta?.(dataset.object, m.field) : void 0;
|
|
2709
2771
|
if (fc.currency == null) {
|
|
2710
|
-
const meta = m.field ? this.measureCurrency?.(dataset.object, m.field) : void 0;
|
|
2711
2772
|
const monetary = !!mc.currency || meta?.type === "currency";
|
|
2712
2773
|
if (monetary) {
|
|
2713
2774
|
const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency;
|
|
2714
2775
|
if (resolved) fc.currency = resolved;
|
|
2715
2776
|
}
|
|
2716
2777
|
}
|
|
2778
|
+
if (f.percentScale == null) {
|
|
2779
|
+
f.percentScale = m.derived?.op === "ratio" ? "fraction" : percentScaleOf(meta);
|
|
2780
|
+
}
|
|
2717
2781
|
}
|
|
2718
2782
|
}
|
|
2719
2783
|
if (result.fields?.length && selectedDims.length) {
|
|
@@ -2779,6 +2843,7 @@ var AnalyticsService = class {
|
|
|
2779
2843
|
if (!cube) {
|
|
2780
2844
|
this.assertInferableCube(name);
|
|
2781
2845
|
cube = this.inferCubeFromQuery(query);
|
|
2846
|
+
this.assertMeasureFields(query, cube, Object.keys(cube.measures));
|
|
2782
2847
|
this.cubeRegistry.register(cube);
|
|
2783
2848
|
const isScalarMetric = (query.dimensions?.length ?? 0) === 0 && (query.timeDimensions?.length ?? 0) === 0;
|
|
2784
2849
|
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.`;
|
|
@@ -2798,10 +2863,82 @@ var AnalyticsService = class {
|
|
|
2798
2863
|
...cube,
|
|
2799
2864
|
measures: { ...cube.measures, ...extraMeasures }
|
|
2800
2865
|
};
|
|
2866
|
+
this.assertMeasureFields(query, augmented, Object.keys(cube.measures));
|
|
2801
2867
|
this.cubeRegistry.register(augmented);
|
|
2802
2868
|
this.logger.debug(
|
|
2803
2869
|
`[Analytics] Augmented cube "${name}" with inferred measures: ${Object.keys(extraMeasures).join(",")}`
|
|
2804
2870
|
);
|
|
2871
|
+
} else {
|
|
2872
|
+
this.assertMeasureFields(query, cube, Object.keys(cube.measures));
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* [#4437] Reject a measure whose SOURCE FIELD the backing object does not
|
|
2877
|
+
* have, BEFORE the strategy compiles it into SQL.
|
|
2878
|
+
*
|
|
2879
|
+
* `inferMeasure` maps a suffix convention onto a field name and has no way to
|
|
2880
|
+
* know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the
|
|
2881
|
+
* driver threw `no such column`, and the caller got
|
|
2882
|
+
* `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver
|
|
2883
|
+
* error class on the wire, and nothing actionable, for what is a plain typo.
|
|
2884
|
+
* The DATA route has refused the same mistake with a `400 INVALID_FIELD`
|
|
2885
|
+
* naming the field since #4315/#4254; this is the analytics half of that
|
|
2886
|
+
* answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/
|
|
2887
|
+
* `param`) so one mistake has one shape across both routes.
|
|
2888
|
+
*
|
|
2889
|
+
* What it checks, and what it deliberately does not:
|
|
2890
|
+
*
|
|
2891
|
+
* - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose
|
|
2892
|
+
* `sql` is a real SQL expression has no field list to check against.
|
|
2893
|
+
* - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.
|
|
2894
|
+
* Absent hook / unknown object → stand down (see the config field's doc).
|
|
2895
|
+
* - Only measures whose source is a BARE COLUMN. `count(*)` has no source
|
|
2896
|
+
* field, and a dotted reference (`account.industry`) resolves through a
|
|
2897
|
+
* join whose target this check cannot see — both pass through untouched.
|
|
2898
|
+
* - `id` / `created_at` / `updated_at` are admitted unconditionally, matching
|
|
2899
|
+
* the data path's `resolveQueryFields`: they are engine-assigned rather than
|
|
2900
|
+
* declared, and a gate stricter than the engine it guards would reject
|
|
2901
|
+
* queries that used to work.
|
|
2902
|
+
*/
|
|
2903
|
+
assertMeasureFields(query, cube, declaredMeasures) {
|
|
2904
|
+
const probe = this.getObjectFieldNames;
|
|
2905
|
+
if (!probe) return;
|
|
2906
|
+
const measures = query.measures ?? [];
|
|
2907
|
+
if (measures.length === 0) return;
|
|
2908
|
+
const object = typeof cube.sql === "string" ? cube.sql.trim() : "";
|
|
2909
|
+
if (!object || !BARE_IDENTIFIER.test(object)) return;
|
|
2910
|
+
const fieldNames = probe(object);
|
|
2911
|
+
if (!fieldNames || fieldNames.length === 0) return;
|
|
2912
|
+
const known = /* @__PURE__ */ new Set([...fieldNames, "id", "created_at", "updated_at"]);
|
|
2913
|
+
const stripPrefix = (m) => m.includes(".") ? m.split(".").slice(1).join(".") : m;
|
|
2914
|
+
const sourceFieldOf = (measure) => {
|
|
2915
|
+
const metric = cube.measures[stripPrefix(measure)];
|
|
2916
|
+
if (!metric) return null;
|
|
2917
|
+
if (metric.type === "count" && (metric.sql === "*" || metric.sql == null)) return null;
|
|
2918
|
+
const source = typeof metric.sql === "string" ? metric.sql.trim() : "";
|
|
2919
|
+
if (!source || source === "*" || !BARE_IDENTIFIER.test(source)) return null;
|
|
2920
|
+
return source;
|
|
2921
|
+
};
|
|
2922
|
+
const invalid = /* @__PURE__ */ new Set();
|
|
2923
|
+
for (const measure of measures) {
|
|
2924
|
+
const source = sourceFieldOf(measure);
|
|
2925
|
+
if (source && !known.has(source)) invalid.add(stripPrefix(measure));
|
|
2926
|
+
}
|
|
2927
|
+
if (invalid.size === 0) return;
|
|
2928
|
+
const usable = declaredMeasures.filter((m) => !invalid.has(m));
|
|
2929
|
+
for (const measure of measures) {
|
|
2930
|
+
const source = sourceFieldOf(measure);
|
|
2931
|
+
if (!source || known.has(source)) continue;
|
|
2932
|
+
const err = new Error(
|
|
2933
|
+
`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(", ")}.`
|
|
2934
|
+
);
|
|
2935
|
+
err.code = "INVALID_FIELD";
|
|
2936
|
+
err.status = 400;
|
|
2937
|
+
err.field = source;
|
|
2938
|
+
err.object = object;
|
|
2939
|
+
err.param = "measures";
|
|
2940
|
+
err.measure = measure;
|
|
2941
|
+
throw err;
|
|
2805
2942
|
}
|
|
2806
2943
|
}
|
|
2807
2944
|
/**
|
|
@@ -2955,6 +3092,14 @@ var AnalyticsServicePlugin = class {
|
|
|
2955
3092
|
this.version = "1.0.0";
|
|
2956
3093
|
this.type = "standard";
|
|
2957
3094
|
this.dependencies = [];
|
|
3095
|
+
/**
|
|
3096
|
+
* init() probes the `data` engine ObjectQLPlugin provides for the
|
|
3097
|
+
* auto-bridge — order-if-present so the probe verdict is deterministic
|
|
3098
|
+
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
|
|
3099
|
+
* degrades on purpose (per-query lazy resolution / explicit
|
|
3100
|
+
* `executeAggregate`).
|
|
3101
|
+
*/
|
|
3102
|
+
this.optionalDependencies = ["com.objectstack.engine.objectql"];
|
|
2958
3103
|
this.options = options;
|
|
2959
3104
|
}
|
|
2960
3105
|
async init(ctx) {
|
|
@@ -3178,10 +3323,12 @@ var AnalyticsServicePlugin = class {
|
|
|
3178
3323
|
coerceTemporalFilterColumn,
|
|
3179
3324
|
relationshipResolver,
|
|
3180
3325
|
labelResolver,
|
|
3181
|
-
//
|
|
3182
|
-
|
|
3326
|
+
// Source-field metadata behind the display chains on result columns:
|
|
3327
|
+
// ADR-0053 currency (`currencyConfig.defaultCurrency`) and percent scale
|
|
3328
|
+
// (`max`, which is what marks whole-percent storage — objectui#3136).
|
|
3329
|
+
sourceFieldMeta: (object, field) => {
|
|
3183
3330
|
const f = dataEngine()?.getObject?.(object)?.fields?.[field];
|
|
3184
|
-
return f ? { type: f.type, defaultCurrency: f.currencyConfig?.defaultCurrency } : void 0;
|
|
3331
|
+
return f ? { type: f.type, max: f.max, defaultCurrency: f.currencyConfig?.defaultCurrency } : void 0;
|
|
3185
3332
|
},
|
|
3186
3333
|
// ADR-0062 D6 — a federated object carries an `external` block (ADR-0015).
|
|
3187
3334
|
// Reported so NativeSQLStrategy declines it (its hand-compiled FROM would
|
|
@@ -3204,6 +3351,18 @@ var AnalyticsServicePlugin = class {
|
|
|
3204
3351
|
if (!engine) return true;
|
|
3205
3352
|
return engine.getObject?.(name) != null;
|
|
3206
3353
|
},
|
|
3354
|
+
// [#4437] Field names for the measure source-field gate. Read from the
|
|
3355
|
+
// SAME schema registry `isRegisteredObject` above consults (and the data
|
|
3356
|
+
// path's #4315 gate reads), so "which fields exist" has one answer across
|
|
3357
|
+
// /data and /analytics. `undefined` — no engine, unknown object, or an
|
|
3358
|
+
// object with no field map (an external datasource whose columns are not
|
|
3359
|
+
// mirrored locally) — means "cannot answer", and the gate stands down.
|
|
3360
|
+
getObjectFieldNames: (objectName) => {
|
|
3361
|
+
const fields = dataEngine()?.getObject?.(objectName)?.fields;
|
|
3362
|
+
if (!fields || typeof fields !== "object") return void 0;
|
|
3363
|
+
const names = Object.keys(fields);
|
|
3364
|
+
return names.length > 0 ? names : void 0;
|
|
3365
|
+
},
|
|
3207
3366
|
draftRowsResolver
|
|
3208
3367
|
};
|
|
3209
3368
|
if (autoBridgedReadScope && securityPresentAtInit) {
|
|
@@ -3259,6 +3418,7 @@ export {
|
|
|
3259
3418
|
compileScopedFilterToSql,
|
|
3260
3419
|
createOrderLabelResolver,
|
|
3261
3420
|
evaluateDerivedMeasures,
|
|
3421
|
+
fillEmptyGroups,
|
|
3262
3422
|
mergeByDimensions,
|
|
3263
3423
|
pickDisplayField,
|
|
3264
3424
|
resolveDimensionLabels,
|