@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.cjs +625 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +184 -7
- package/dist/index.d.ts +184 -7
- package/dist/index.js +617 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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 };
|