@objectstack/service-analytics 9.11.0 → 10.2.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/dist/index.d.cts CHANGED
@@ -221,12 +221,31 @@ interface AnalyticsServiceConfig {
221
221
  * `StrategyContext.coerceTemporalFilterValue` for the full rationale.
222
222
  */
223
223
  coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;
224
+ /**
225
+ * ADR-0062 D6 — report whether an object is federated (external datasource).
226
+ * Threaded into the StrategyContext so `NativeSQLStrategy` declines external
227
+ * objects (which it would otherwise query against the wrong physical table),
228
+ * routing them to the driver-correct ObjectQL aggregate path instead. See
229
+ * `StrategyContext.isExternalObject`.
230
+ */
231
+ isExternalObject?: (objectName: string) => boolean;
224
232
  /**
225
233
  * ADR-0021 — optional object-graph resolver used when compiling datasets:
226
234
  * `(baseObject, relationshipName) => relatedObjectName | undefined`. When
227
235
  * provided, `queryDataset` validates that every declared `include` exists.
228
236
  */
229
237
  relationshipResolver?: RelationshipResolver;
238
+ /**
239
+ * ADR-0053 currency chain — resolve a measure's SOURCE FIELD currency
240
+ * metadata so a monetary measure that omits an explicit `currency` falls back
241
+ * to the field's declared currency, then the tenant default (`ctx.currency`).
242
+ * Returns the source field's `type` and (fixed-mode) `defaultCurrency`;
243
+ * `undefined` for an unknown field. Non-`currency` fields never get a code.
244
+ */
245
+ measureCurrency?: (object: string, field: string) => {
246
+ type?: string;
247
+ defaultCurrency?: string;
248
+ } | undefined;
230
249
  /** Pre-defined datasets to compile + register at construction (ADR-0021). */
231
250
  datasets?: Dataset[];
232
251
  /**
@@ -276,6 +295,7 @@ declare class AnalyticsService implements IAnalyticsService {
276
295
  private readonly datasetRegistry;
277
296
  /** Optional object-graph resolver used when compiling datasets. */
278
297
  private readonly relationshipResolver?;
298
+ private readonly measureCurrency?;
279
299
  /** Optional dimension display-label resolver (select options / lookup names). */
280
300
  private readonly labelResolver?;
281
301
  /** ADR-0037 P3: pending-seed row resolver for draft data preview. */
package/dist/index.d.ts CHANGED
@@ -221,12 +221,31 @@ interface AnalyticsServiceConfig {
221
221
  * `StrategyContext.coerceTemporalFilterValue` for the full rationale.
222
222
  */
223
223
  coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;
224
+ /**
225
+ * ADR-0062 D6 — report whether an object is federated (external datasource).
226
+ * Threaded into the StrategyContext so `NativeSQLStrategy` declines external
227
+ * objects (which it would otherwise query against the wrong physical table),
228
+ * routing them to the driver-correct ObjectQL aggregate path instead. See
229
+ * `StrategyContext.isExternalObject`.
230
+ */
231
+ isExternalObject?: (objectName: string) => boolean;
224
232
  /**
225
233
  * ADR-0021 — optional object-graph resolver used when compiling datasets:
226
234
  * `(baseObject, relationshipName) => relatedObjectName | undefined`. When
227
235
  * provided, `queryDataset` validates that every declared `include` exists.
228
236
  */
229
237
  relationshipResolver?: RelationshipResolver;
238
+ /**
239
+ * ADR-0053 currency chain — resolve a measure's SOURCE FIELD currency
240
+ * metadata so a monetary measure that omits an explicit `currency` falls back
241
+ * to the field's declared currency, then the tenant default (`ctx.currency`).
242
+ * Returns the source field's `type` and (fixed-mode) `defaultCurrency`;
243
+ * `undefined` for an unknown field. Non-`currency` fields never get a code.
244
+ */
245
+ measureCurrency?: (object: string, field: string) => {
246
+ type?: string;
247
+ defaultCurrency?: string;
248
+ } | undefined;
230
249
  /** Pre-defined datasets to compile + register at construction (ADR-0021). */
231
250
  datasets?: Dataset[];
232
251
  /**
@@ -276,6 +295,7 @@ declare class AnalyticsService implements IAnalyticsService {
276
295
  private readonly datasetRegistry;
277
296
  /** Optional object-graph resolver used when compiling datasets. */
278
297
  private readonly relationshipResolver?;
298
+ private readonly measureCurrency?;
279
299
  /** Optional dimension display-label resolver (select options / lookup names). */
280
300
  private readonly labelResolver?;
281
301
  /** ADR-0037 P3: pending-seed row resolver for draft data preview. */
package/dist/index.js CHANGED
@@ -320,6 +320,17 @@ var NativeSQLStrategy = class {
320
320
  canHandle(query, ctx) {
321
321
  if (!query.cube) return false;
322
322
  if (query.timeDimensions?.some((td) => !!td.granularity)) return false;
323
+ if (typeof ctx.isExternalObject === "function") {
324
+ const cube = ctx.getCube(query.cube);
325
+ if (cube) {
326
+ if (ctx.isExternalObject(this.extractObjectName(cube))) return false;
327
+ const joinTargets = cube.joins ? Object.values(cube.joins) : [];
328
+ for (const j of joinTargets) {
329
+ const joinedObject = j?.name;
330
+ if (joinedObject && ctx.isExternalObject(joinedObject)) return false;
331
+ }
332
+ }
333
+ }
323
334
  const caps = ctx.queryCapabilities(query.cube);
324
335
  return caps.nativeSql && typeof ctx.executeRawSql === "function";
325
336
  }
@@ -1456,6 +1467,7 @@ var AnalyticsService = class {
1456
1467
  }
1457
1468
  this.readScopeProvider = config.getReadScope;
1458
1469
  this.relationshipResolver = config.relationshipResolver;
1470
+ this.measureCurrency = config.measureCurrency;
1459
1471
  this.labelResolver = config.labelResolver;
1460
1472
  this.draftRowsResolver = config.draftRowsResolver;
1461
1473
  if (config.datasets) {
@@ -1476,7 +1488,8 @@ var AnalyticsService = class {
1476
1488
  // Prefer a compiled dataset's declared relationships (D-C join allowlist);
1477
1489
  // fall back to any explicitly-configured provider for legacy cubes.
1478
1490
  getAllowedRelationships: (cubeName) => this.datasetRegistry.get(cubeName)?.allowedRelationships ?? config.getAllowedRelationships?.(cubeName),
1479
- coerceTemporalFilterValue: config.coerceTemporalFilterValue
1491
+ coerceTemporalFilterValue: config.coerceTemporalFilterValue,
1492
+ isExternalObject: config.isExternalObject
1480
1493
  };
1481
1494
  const builtIn = [
1482
1495
  new NativeSQLStrategy(),
@@ -1633,8 +1646,21 @@ var AnalyticsService = class {
1633
1646
  }
1634
1647
  throw err;
1635
1648
  }
1636
- if (this.labelResolver && selection.dimensions?.length) {
1637
- const dims = selection.dimensions.map((name) => dataset.dimensions?.find((d) => d.name === name)).filter((d) => !!d?.field).map((d) => ({ name: d.name, field: d.field, type: d.type, dateGranularity: d.dateGranularity }));
1649
+ const selectedDims = (selection.dimensions ?? []).map((name) => dataset.dimensions?.find((d) => d.name === name)).filter((d) => !!d);
1650
+ const drillDims = selectedDims.filter((d) => !!d.field && d.type !== "date");
1651
+ if (drillDims.length && result.rows.length) {
1652
+ result.object = dataset.object;
1653
+ result.dimensionFields = Object.fromEntries(
1654
+ drillDims.map((d) => [d.name, d.field])
1655
+ );
1656
+ result.drillRawRows = result.rows.map((row) => {
1657
+ const raw = {};
1658
+ for (const d of drillDims) raw[d.name] = row[d.name];
1659
+ return raw;
1660
+ });
1661
+ }
1662
+ if (this.labelResolver && selectedDims.length) {
1663
+ const dims = selectedDims.filter((d) => !!d.field).map((d) => ({ name: d.name, field: d.field, type: d.type, dateGranularity: d.dateGranularity }));
1638
1664
  if (dims.length) {
1639
1665
  try {
1640
1666
  await resolveDimensionLabels(dataset.object, dims, result.rows, this.labelResolver);
@@ -1656,6 +1682,25 @@ var AnalyticsService = class {
1656
1682
  if (!m) continue;
1657
1683
  if (f.label == null && typeof m.label === "string") f.label = m.label;
1658
1684
  if (f.format == null && m.format) f.format = m.format;
1685
+ const fc = f;
1686
+ const mc = m;
1687
+ if (fc.currency == null) {
1688
+ const meta = m.field ? this.measureCurrency?.(dataset.object, m.field) : void 0;
1689
+ const monetary = !!mc.currency || meta?.type === "currency";
1690
+ if (monetary) {
1691
+ const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency;
1692
+ if (resolved) fc.currency = resolved;
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+ if (result.fields?.length && selectedDims.length) {
1698
+ const dimByName = new Map(selectedDims.map((d) => [d.name, d]));
1699
+ const dimByField = new Map(selectedDims.filter((d) => !!d.field).map((d) => [d.field, d]));
1700
+ for (const f of result.fields) {
1701
+ if (f.label != null) continue;
1702
+ const d = dimByName.get(f.name) ?? dimByField.get(f.name);
1703
+ if (d && typeof d.label === "string") f.label = d.label;
1659
1704
  }
1660
1705
  }
1661
1706
  return result;
@@ -2044,6 +2089,18 @@ var AnalyticsServicePlugin = class {
2044
2089
  coerceTemporalFilterValue,
2045
2090
  relationshipResolver,
2046
2091
  labelResolver,
2092
+ // ADR-0053 — source-field currency metadata for the measure currency chain.
2093
+ measureCurrency: (object, field) => {
2094
+ const f = dataEngine()?.getObject?.(object)?.fields?.[field];
2095
+ return f ? { type: f.type, defaultCurrency: f.currencyConfig?.defaultCurrency } : void 0;
2096
+ },
2097
+ // ADR-0062 D6 — a federated object carries an `external` block (ADR-0015).
2098
+ // Reported so NativeSQLStrategy declines it (its hand-compiled FROM would
2099
+ // hit the wrong physical table) and the driver-correct ObjectQL path runs.
2100
+ isExternalObject: (objectName) => {
2101
+ const obj = dataEngine()?.getObject?.(objectName);
2102
+ return !!(obj && obj.external != null);
2103
+ },
2047
2104
  draftRowsResolver
2048
2105
  };
2049
2106
  if (autoBridgedReadScope) {