@objectstack/service-analytics 7.9.0 → 8.0.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/dist/index.d.cts CHANGED
@@ -1,6 +1,8 @@
1
- import { IAnalyticsService, Logger, DriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, CubeMeta, StrategyContext } from '@objectstack/spec/contracts';
2
- export { AnalyticsStrategy, DriverCapabilities, StrategyContext } from '@objectstack/spec/contracts';
3
- import { Cube } from '@objectstack/spec/data';
1
+ import { IAnalyticsService, Logger, DriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, DatasetSelection, CubeMeta, DatasetCompareTo, StrategyContext } from '@objectstack/spec/contracts';
2
+ export { AnalyticsStrategy, DatasetSelection, DriverCapabilities, StrategyContext } from '@objectstack/spec/contracts';
3
+ import { Cube, FilterCondition } from '@objectstack/spec/data';
4
+ import { ExecutionContext } from '@objectstack/spec/kernel';
5
+ import { Dataset } from '@objectstack/spec/ui';
4
6
  import { Plugin, PluginContext } from '@objectstack/core';
5
7
 
6
8
  /**
@@ -52,6 +54,35 @@ declare class CubeRegistry {
52
54
  private fieldTypeToDimensionType;
53
55
  }
54
56
 
57
+ interface DerivedMeasureSpec {
58
+ name: string;
59
+ op: 'ratio' | 'sum' | 'difference' | 'product';
60
+ of: string[];
61
+ }
62
+ interface CompiledDataset {
63
+ /** The Cube the dataset compiles to (consumed by the strategy chain). */
64
+ cube: Cube;
65
+ /**
66
+ * Relationship names declared in `include`. The join allowlist (D-C):
67
+ * the NativeSQLStrategy rejects any join alias not in this set.
68
+ */
69
+ allowedRelationships: Set<string>;
70
+ /** Derived measures, computed post-aggregation by the executor (Q1). */
71
+ derived: DerivedMeasureSpec[];
72
+ /** Definition-level filter (the dataset's intrinsic scope). */
73
+ filter?: FilterCondition;
74
+ /** Per-measure scoped filters, keyed by measure name (applied by executor). */
75
+ measureFilters: Record<string, FilterCondition>;
76
+ }
77
+ /**
78
+ * Resolves a relationship name on a base object to the related object/table
79
+ * name, using the runtime's object graph. Optional: when omitted the compiler
80
+ * trusts the declared `include` names (the NativeSQLStrategy convention assumes
81
+ * the relationship name equals the related table name).
82
+ */
83
+ type RelationshipResolver = (baseObject: string, relationshipName: string) => string | undefined;
84
+ declare function compileDataset(dataset: Dataset, resolver?: RelationshipResolver): CompiledDataset;
85
+
55
86
  /**
56
87
  * Configuration for AnalyticsService.
57
88
  */
@@ -93,6 +124,35 @@ interface AnalyticsServiceConfig {
93
124
  * They are merged with the built-in strategies and sorted by priority.
94
125
  */
95
126
  strategies?: AnalyticsStrategy[];
127
+ /**
128
+ * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). Supplied
129
+ * by the runtime that owns the sharing middleware; receives the current
130
+ * request's ExecutionContext and returns the RLS `FilterCondition` for the
131
+ * object (exactly what `RLSCompiler` emits). The service binds the active
132
+ * context per query and the strategy compiles the filter into alias-qualified
133
+ * SQL injected into every base and joined table.
134
+ *
135
+ * MAY be async: the production bridge resolves RLS from the `security`
136
+ * service's `getReadFilter`, which can hit the database. The service
137
+ * pre-resolves the scope for every base + joined object of a query (before
138
+ * the synchronous SQL builder runs), so a sync return still works unchanged.
139
+ */
140
+ getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
141
+ /**
142
+ * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
143
+ * Joins outside this set are rejected by the strategy. Compiled datasets
144
+ * (via `queryDataset`/`registerDataset`) supply this automatically; this
145
+ * config hook is a fallback for legacy hand-authored cubes.
146
+ */
147
+ getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;
148
+ /**
149
+ * ADR-0021 — optional object-graph resolver used when compiling datasets:
150
+ * `(baseObject, relationshipName) => relatedObjectName | undefined`. When
151
+ * provided, `queryDataset` validates that every declared `include` exists.
152
+ */
153
+ relationshipResolver?: RelationshipResolver;
154
+ /** Pre-defined datasets to compile + register at construction (ADR-0021). */
155
+ datasets?: Dataset[];
96
156
  }
97
157
  /**
98
158
  * AnalyticsService — Multi-driver analytics orchestrator.
@@ -114,14 +174,54 @@ interface AnalyticsServiceConfig {
114
174
  */
115
175
  declare class AnalyticsService implements IAnalyticsService {
116
176
  private readonly strategies;
117
- private readonly strategyCtx;
177
+ /** Context-independent part of the StrategyContext (no per-request scope). */
178
+ private readonly baseCtx;
179
+ /** Context-aware read-scope provider (bound to the request's context per call). */
180
+ private readonly readScopeProvider?;
181
+ /** Compiled datasets by name — feeds the join allowlist (D-C) and queryDataset. */
182
+ private readonly datasetRegistry;
183
+ /** Optional object-graph resolver used when compiling datasets. */
184
+ private readonly relationshipResolver?;
118
185
  readonly cubeRegistry: CubeRegistry;
119
186
  private readonly logger;
120
187
  constructor(config?: AnalyticsServiceConfig);
188
+ /**
189
+ * Build a per-call StrategyContext that binds the read-scope provider to the
190
+ * current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a
191
+ * `getReadScope(objectName)` that already knows the active tenant.
192
+ */
193
+ private callCtx;
194
+ /**
195
+ * Resolve the read scope (tenant + RLS `FilterCondition`) for the base object
196
+ * AND every joined object of the query's cube, keyed by object name. This is
197
+ * the async pre-pass that lets the synchronous strategy enforce scoping even
198
+ * when the provider (security `getReadFilter`) resolves asynchronously.
199
+ *
200
+ * The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a
201
+ * SUPERSET of what the strategy actually scans (the strategy only joins along
202
+ * declared relationships), so no scanned object is ever left unscoped.
203
+ *
204
+ * Fail-closed: if the provider throws for an object, the whole query is
205
+ * rejected rather than emitting SQL with that object unscoped.
206
+ */
207
+ private resolveReadScopes;
121
208
  /**
122
209
  * Execute an analytical query by delegating to the first capable strategy.
123
210
  */
124
- query(query: AnalyticsQuery): Promise<AnalyticsResult>;
211
+ query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult>;
212
+ /**
213
+ * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it
214
+ * can be queried by name. Idempotent (re-registering overwrites). Returns the
215
+ * compiled dataset.
216
+ */
217
+ registerDataset(dataset: Dataset): CompiledDataset;
218
+ /**
219
+ * Execute a semantic-layer dataset (ADR-0021). Compiles the dataset (saved or
220
+ * inline draft — Studio preview), registers its Cube + join allowlist, then
221
+ * runs the selection through the `DatasetExecutor` with the request context so
222
+ * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.
223
+ */
224
+ queryDataset(dataset: Dataset, selection: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
125
225
  /**
126
226
  * Get cube metadata for discovery.
127
227
  */
@@ -129,7 +229,7 @@ declare class AnalyticsService implements IAnalyticsService {
129
229
  /**
130
230
  * Generate SQL for a query without executing it (dry-run).
131
231
  */
132
- generateSql(query: AnalyticsQuery): Promise<{
232
+ generateSql(query: AnalyticsQuery, context?: ExecutionContext): Promise<{
133
233
  sql: string;
134
234
  params: unknown[];
135
235
  }>;
@@ -181,6 +281,20 @@ interface AnalyticsServicePluginOptions {
181
281
  }>;
182
282
  filter?: Record<string, unknown>;
183
283
  }) => Promise<Record<string, unknown>[]>;
284
+ /**
285
+ * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The
286
+ * runtime supplies this from its sharing middleware so the analytics raw-SQL
287
+ * path cannot bypass tenant isolation. Receives the request's ExecutionContext
288
+ * and returns the RLS `FilterCondition` for the object (what `RLSCompiler`
289
+ * emits). When omitted, the plugin auto-bridges to a registered `'security'`
290
+ * service exposing `getReadFilter(object, context)` if one is present.
291
+ */
292
+ getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
293
+ /**
294
+ * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
295
+ * Typically wired from the dataset registry's compiled `allowedRelationships`.
296
+ */
297
+ getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;
184
298
  /** Enable debug logging. */
185
299
  debug?: boolean;
186
300
  }
@@ -225,6 +339,59 @@ declare class AnalyticsServicePlugin implements Plugin {
225
339
  destroy(): Promise<void>;
226
340
  }
227
341
 
342
+ /** @deprecated use DatasetCompareTo from @objectstack/spec/contracts */
343
+ type CompareTo = DatasetCompareTo;
344
+ /**
345
+ * Dataset executor (ADR-0021 WS2).
346
+ *
347
+ * Turns a compiled dataset + a presentation's selection (dimensions, measures,
348
+ * runtime filter, compareTo) into one or more `AnalyticsQuery`s against the Cube
349
+ * runtime, then post-processes the results:
350
+ * - resolves the base measures a selection needs (including derived deps),
351
+ * - applies measure-scoped filters via supplementary grouped queries,
352
+ * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),
353
+ * - shifts the query for `compareTo` (previousPeriod / previousYear) and
354
+ * attaches `<measure>__compare` columns.
355
+ *
356
+ * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy
357
+ * via the StrategyContext read-scope hook (D-C). This layer is pure query
358
+ * shaping + arithmetic.
359
+ */
360
+ /** AND two optional FilterConditions into one (MongoDB-style). */
361
+ declare function combineFilters(a?: FilterCondition, b?: FilterCondition): FilterCondition | undefined;
362
+ /**
363
+ * Evaluate derived measures on each aggregated row, mutating a shallow copy.
364
+ * Division by zero (and missing operands) yields `null` rather than Infinity/NaN.
365
+ */
366
+ declare function evaluateDerivedMeasures(rows: Record<string, unknown>[], derived: DerivedMeasureSpec[]): Record<string, unknown>[];
367
+ /** Compute the comparison window for a [start,end] range. */
368
+ declare function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string];
369
+ declare class DatasetExecutor {
370
+ private readonly service;
371
+ constructor(service: IAnalyticsService);
372
+ /**
373
+ * Execute a dataset selection and return the shaped rows (+ field metadata).
374
+ *
375
+ * @param context - The request's ExecutionContext, threaded into every
376
+ * underlying `IAnalyticsService.query` so the tenant/RLS read scope is
377
+ * applied per request (ADR-0021 D-C).
378
+ */
379
+ execute(compiled: CompiledDataset, selection: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
380
+ private buildQuery;
381
+ private runCompare;
382
+ }
383
+ /**
384
+ * Left-merge `extra` rows onto `base` rows by their dimension-key tuple,
385
+ * copying the listed value columns. Rows in `extra` with no base match are
386
+ * appended (outer-ish merge so comparison-only buckets still surface).
387
+ */
388
+ declare function mergeByDimensions(base: Record<string, unknown>[], extra: Record<string, unknown>[], dimensions: string[], valueColumns: string[]): Record<string, unknown>[];
389
+
390
+ declare function compileScopedFilterToSql(filter: FilterCondition, alias: string): {
391
+ sql: string;
392
+ params: unknown[];
393
+ };
394
+
228
395
  /**
229
396
  * NativeSQLStrategy — Priority 1
230
397
  *
@@ -241,6 +408,16 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
241
408
  sql: string;
242
409
  params: unknown[];
243
410
  }>;
411
+ /**
412
+ * ADR-0021 D-C — inject an object's read scope (tenant + RLS predicate) into
413
+ * the WHERE clause. The scope is a canonical `FilterCondition` (what the
414
+ * RLSCompiler emits); `compileScopedFilterToSql` turns it into alias-qualified,
415
+ * parameterized SQL (fail-closed — it throws rather than drop a predicate).
416
+ * The `?` placeholders are then renumbered into the strategy's `$N` scheme.
417
+ * No-op when the runtime provides no scope hook (the caller is then
418
+ * responsible for isolation — see contract note).
419
+ */
420
+ private applyReadScope;
244
421
  /**
245
422
  * Resolve a dimension/measure/filter SQL expression that may reference a
246
423
  * related table via dot notation (e.g. `account.industry`).
@@ -318,4 +495,4 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
318
495
  private buildFieldMeta;
319
496
  }
320
497
 
321
- export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, CubeRegistry, NativeSQLStrategy, ObjectQLStrategy };
498
+ export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, NativeSQLStrategy, ObjectQLStrategy, type RelationshipResolver, combineFilters, compileDataset, compileScopedFilterToSql, evaluateDerivedMeasures, mergeByDimensions, shiftRange };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { IAnalyticsService, Logger, DriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, CubeMeta, StrategyContext } from '@objectstack/spec/contracts';
2
- export { AnalyticsStrategy, DriverCapabilities, StrategyContext } from '@objectstack/spec/contracts';
3
- import { Cube } from '@objectstack/spec/data';
1
+ import { IAnalyticsService, Logger, DriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, DatasetSelection, CubeMeta, DatasetCompareTo, StrategyContext } from '@objectstack/spec/contracts';
2
+ export { AnalyticsStrategy, DatasetSelection, DriverCapabilities, StrategyContext } from '@objectstack/spec/contracts';
3
+ import { Cube, FilterCondition } from '@objectstack/spec/data';
4
+ import { ExecutionContext } from '@objectstack/spec/kernel';
5
+ import { Dataset } from '@objectstack/spec/ui';
4
6
  import { Plugin, PluginContext } from '@objectstack/core';
5
7
 
6
8
  /**
@@ -52,6 +54,35 @@ declare class CubeRegistry {
52
54
  private fieldTypeToDimensionType;
53
55
  }
54
56
 
57
+ interface DerivedMeasureSpec {
58
+ name: string;
59
+ op: 'ratio' | 'sum' | 'difference' | 'product';
60
+ of: string[];
61
+ }
62
+ interface CompiledDataset {
63
+ /** The Cube the dataset compiles to (consumed by the strategy chain). */
64
+ cube: Cube;
65
+ /**
66
+ * Relationship names declared in `include`. The join allowlist (D-C):
67
+ * the NativeSQLStrategy rejects any join alias not in this set.
68
+ */
69
+ allowedRelationships: Set<string>;
70
+ /** Derived measures, computed post-aggregation by the executor (Q1). */
71
+ derived: DerivedMeasureSpec[];
72
+ /** Definition-level filter (the dataset's intrinsic scope). */
73
+ filter?: FilterCondition;
74
+ /** Per-measure scoped filters, keyed by measure name (applied by executor). */
75
+ measureFilters: Record<string, FilterCondition>;
76
+ }
77
+ /**
78
+ * Resolves a relationship name on a base object to the related object/table
79
+ * name, using the runtime's object graph. Optional: when omitted the compiler
80
+ * trusts the declared `include` names (the NativeSQLStrategy convention assumes
81
+ * the relationship name equals the related table name).
82
+ */
83
+ type RelationshipResolver = (baseObject: string, relationshipName: string) => string | undefined;
84
+ declare function compileDataset(dataset: Dataset, resolver?: RelationshipResolver): CompiledDataset;
85
+
55
86
  /**
56
87
  * Configuration for AnalyticsService.
57
88
  */
@@ -93,6 +124,35 @@ interface AnalyticsServiceConfig {
93
124
  * They are merged with the built-in strategies and sorted by priority.
94
125
  */
95
126
  strategies?: AnalyticsStrategy[];
127
+ /**
128
+ * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). Supplied
129
+ * by the runtime that owns the sharing middleware; receives the current
130
+ * request's ExecutionContext and returns the RLS `FilterCondition` for the
131
+ * object (exactly what `RLSCompiler` emits). The service binds the active
132
+ * context per query and the strategy compiles the filter into alias-qualified
133
+ * SQL injected into every base and joined table.
134
+ *
135
+ * MAY be async: the production bridge resolves RLS from the `security`
136
+ * service's `getReadFilter`, which can hit the database. The service
137
+ * pre-resolves the scope for every base + joined object of a query (before
138
+ * the synchronous SQL builder runs), so a sync return still works unchanged.
139
+ */
140
+ getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
141
+ /**
142
+ * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
143
+ * Joins outside this set are rejected by the strategy. Compiled datasets
144
+ * (via `queryDataset`/`registerDataset`) supply this automatically; this
145
+ * config hook is a fallback for legacy hand-authored cubes.
146
+ */
147
+ getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;
148
+ /**
149
+ * ADR-0021 — optional object-graph resolver used when compiling datasets:
150
+ * `(baseObject, relationshipName) => relatedObjectName | undefined`. When
151
+ * provided, `queryDataset` validates that every declared `include` exists.
152
+ */
153
+ relationshipResolver?: RelationshipResolver;
154
+ /** Pre-defined datasets to compile + register at construction (ADR-0021). */
155
+ datasets?: Dataset[];
96
156
  }
97
157
  /**
98
158
  * AnalyticsService — Multi-driver analytics orchestrator.
@@ -114,14 +174,54 @@ interface AnalyticsServiceConfig {
114
174
  */
115
175
  declare class AnalyticsService implements IAnalyticsService {
116
176
  private readonly strategies;
117
- private readonly strategyCtx;
177
+ /** Context-independent part of the StrategyContext (no per-request scope). */
178
+ private readonly baseCtx;
179
+ /** Context-aware read-scope provider (bound to the request's context per call). */
180
+ private readonly readScopeProvider?;
181
+ /** Compiled datasets by name — feeds the join allowlist (D-C) and queryDataset. */
182
+ private readonly datasetRegistry;
183
+ /** Optional object-graph resolver used when compiling datasets. */
184
+ private readonly relationshipResolver?;
118
185
  readonly cubeRegistry: CubeRegistry;
119
186
  private readonly logger;
120
187
  constructor(config?: AnalyticsServiceConfig);
188
+ /**
189
+ * Build a per-call StrategyContext that binds the read-scope provider to the
190
+ * current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a
191
+ * `getReadScope(objectName)` that already knows the active tenant.
192
+ */
193
+ private callCtx;
194
+ /**
195
+ * Resolve the read scope (tenant + RLS `FilterCondition`) for the base object
196
+ * AND every joined object of the query's cube, keyed by object name. This is
197
+ * the async pre-pass that lets the synchronous strategy enforce scoping even
198
+ * when the provider (security `getReadFilter`) resolves asynchronously.
199
+ *
200
+ * The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a
201
+ * SUPERSET of what the strategy actually scans (the strategy only joins along
202
+ * declared relationships), so no scanned object is ever left unscoped.
203
+ *
204
+ * Fail-closed: if the provider throws for an object, the whole query is
205
+ * rejected rather than emitting SQL with that object unscoped.
206
+ */
207
+ private resolveReadScopes;
121
208
  /**
122
209
  * Execute an analytical query by delegating to the first capable strategy.
123
210
  */
124
- query(query: AnalyticsQuery): Promise<AnalyticsResult>;
211
+ query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult>;
212
+ /**
213
+ * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it
214
+ * can be queried by name. Idempotent (re-registering overwrites). Returns the
215
+ * compiled dataset.
216
+ */
217
+ registerDataset(dataset: Dataset): CompiledDataset;
218
+ /**
219
+ * Execute a semantic-layer dataset (ADR-0021). Compiles the dataset (saved or
220
+ * inline draft — Studio preview), registers its Cube + join allowlist, then
221
+ * runs the selection through the `DatasetExecutor` with the request context so
222
+ * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.
223
+ */
224
+ queryDataset(dataset: Dataset, selection: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
125
225
  /**
126
226
  * Get cube metadata for discovery.
127
227
  */
@@ -129,7 +229,7 @@ declare class AnalyticsService implements IAnalyticsService {
129
229
  /**
130
230
  * Generate SQL for a query without executing it (dry-run).
131
231
  */
132
- generateSql(query: AnalyticsQuery): Promise<{
232
+ generateSql(query: AnalyticsQuery, context?: ExecutionContext): Promise<{
133
233
  sql: string;
134
234
  params: unknown[];
135
235
  }>;
@@ -181,6 +281,20 @@ interface AnalyticsServicePluginOptions {
181
281
  }>;
182
282
  filter?: Record<string, unknown>;
183
283
  }) => Promise<Record<string, unknown>[]>;
284
+ /**
285
+ * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The
286
+ * runtime supplies this from its sharing middleware so the analytics raw-SQL
287
+ * path cannot bypass tenant isolation. Receives the request's ExecutionContext
288
+ * and returns the RLS `FilterCondition` for the object (what `RLSCompiler`
289
+ * emits). When omitted, the plugin auto-bridges to a registered `'security'`
290
+ * service exposing `getReadFilter(object, context)` if one is present.
291
+ */
292
+ getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
293
+ /**
294
+ * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
295
+ * Typically wired from the dataset registry's compiled `allowedRelationships`.
296
+ */
297
+ getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;
184
298
  /** Enable debug logging. */
185
299
  debug?: boolean;
186
300
  }
@@ -225,6 +339,59 @@ declare class AnalyticsServicePlugin implements Plugin {
225
339
  destroy(): Promise<void>;
226
340
  }
227
341
 
342
+ /** @deprecated use DatasetCompareTo from @objectstack/spec/contracts */
343
+ type CompareTo = DatasetCompareTo;
344
+ /**
345
+ * Dataset executor (ADR-0021 WS2).
346
+ *
347
+ * Turns a compiled dataset + a presentation's selection (dimensions, measures,
348
+ * runtime filter, compareTo) into one or more `AnalyticsQuery`s against the Cube
349
+ * runtime, then post-processes the results:
350
+ * - resolves the base measures a selection needs (including derived deps),
351
+ * - applies measure-scoped filters via supplementary grouped queries,
352
+ * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),
353
+ * - shifts the query for `compareTo` (previousPeriod / previousYear) and
354
+ * attaches `<measure>__compare` columns.
355
+ *
356
+ * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy
357
+ * via the StrategyContext read-scope hook (D-C). This layer is pure query
358
+ * shaping + arithmetic.
359
+ */
360
+ /** AND two optional FilterConditions into one (MongoDB-style). */
361
+ declare function combineFilters(a?: FilterCondition, b?: FilterCondition): FilterCondition | undefined;
362
+ /**
363
+ * Evaluate derived measures on each aggregated row, mutating a shallow copy.
364
+ * Division by zero (and missing operands) yields `null` rather than Infinity/NaN.
365
+ */
366
+ declare function evaluateDerivedMeasures(rows: Record<string, unknown>[], derived: DerivedMeasureSpec[]): Record<string, unknown>[];
367
+ /** Compute the comparison window for a [start,end] range. */
368
+ declare function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string];
369
+ declare class DatasetExecutor {
370
+ private readonly service;
371
+ constructor(service: IAnalyticsService);
372
+ /**
373
+ * Execute a dataset selection and return the shaped rows (+ field metadata).
374
+ *
375
+ * @param context - The request's ExecutionContext, threaded into every
376
+ * underlying `IAnalyticsService.query` so the tenant/RLS read scope is
377
+ * applied per request (ADR-0021 D-C).
378
+ */
379
+ execute(compiled: CompiledDataset, selection: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
380
+ private buildQuery;
381
+ private runCompare;
382
+ }
383
+ /**
384
+ * Left-merge `extra` rows onto `base` rows by their dimension-key tuple,
385
+ * copying the listed value columns. Rows in `extra` with no base match are
386
+ * appended (outer-ish merge so comparison-only buckets still surface).
387
+ */
388
+ declare function mergeByDimensions(base: Record<string, unknown>[], extra: Record<string, unknown>[], dimensions: string[], valueColumns: string[]): Record<string, unknown>[];
389
+
390
+ declare function compileScopedFilterToSql(filter: FilterCondition, alias: string): {
391
+ sql: string;
392
+ params: unknown[];
393
+ };
394
+
228
395
  /**
229
396
  * NativeSQLStrategy — Priority 1
230
397
  *
@@ -241,6 +408,16 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
241
408
  sql: string;
242
409
  params: unknown[];
243
410
  }>;
411
+ /**
412
+ * ADR-0021 D-C — inject an object's read scope (tenant + RLS predicate) into
413
+ * the WHERE clause. The scope is a canonical `FilterCondition` (what the
414
+ * RLSCompiler emits); `compileScopedFilterToSql` turns it into alias-qualified,
415
+ * parameterized SQL (fail-closed — it throws rather than drop a predicate).
416
+ * The `?` placeholders are then renumbered into the strategy's `$N` scheme.
417
+ * No-op when the runtime provides no scope hook (the caller is then
418
+ * responsible for isolation — see contract note).
419
+ */
420
+ private applyReadScope;
244
421
  /**
245
422
  * Resolve a dimension/measure/filter SQL expression that may reference a
246
423
  * related table via dot notation (e.g. `account.industry`).
@@ -318,4 +495,4 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
318
495
  private buildFieldMeta;
319
496
  }
320
497
 
321
- export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, CubeRegistry, NativeSQLStrategy, ObjectQLStrategy };
498
+ export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, NativeSQLStrategy, ObjectQLStrategy, type RelationshipResolver, combineFilters, compileDataset, compileScopedFilterToSql, evaluateDerivedMeasures, mergeByDimensions, shiftRange };