@objectstack/service-analytics 16.1.0 → 17.0.0-rc.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.cjs +815 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +285 -24
- package/dist/index.d.ts +285 -24
- package/dist/index.js +807 -65
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.d.cts
CHANGED
|
@@ -119,6 +119,7 @@ declare function compileDataset(dataset: Dataset, resolver?: RelationshipResolve
|
|
|
119
119
|
* — reading an object's field map and fetching id→label pairs — are injected via
|
|
120
120
|
* {@link DimensionLabelDeps} so this module stays free of any engine dependency.
|
|
121
121
|
*/
|
|
122
|
+
|
|
122
123
|
/** The minimal field shape this resolver needs. */
|
|
123
124
|
interface FieldMetaLite {
|
|
124
125
|
type?: string;
|
|
@@ -138,9 +139,90 @@ interface DimensionLabelDeps {
|
|
|
138
139
|
* Fetch a map of `id → display label` for the given ids of a target object.
|
|
139
140
|
* The implementation chooses the target's display field. Returning an empty
|
|
140
141
|
* map (e.g. no display field, no data access) leaves the ids unresolved.
|
|
142
|
+
*
|
|
143
|
+
* `scope` (ADR-0021 D-C, #3602) is the TARGET object's own read scope — the
|
|
144
|
+
* RLS/tenant `FilterCondition` the implementation must AND into the label
|
|
145
|
+
* lookup so this never reveals a related record the target object's RLS would
|
|
146
|
+
* hide. The label lookup is a per-record read (`group by id`) dressed as an
|
|
147
|
+
* aggregate; without the scope it leaks display names whenever the referenced
|
|
148
|
+
* object is more restricted than the base object whose rows carry the id.
|
|
149
|
+
* `undefined` means "no scope for this object" (global table / unrestricted
|
|
150
|
+
* caller) — the same contract as the read-scope provider.
|
|
151
|
+
*
|
|
152
|
+
* `context` is the request's ExecutionContext — the SECOND belt on the same
|
|
153
|
+
* read (#3602). `scope` is the analytics layer's own predicate; forwarding the
|
|
154
|
+
* context lets the ENGINE's middleware chain scope this per-record read
|
|
155
|
+
* itself, so it stays scoped even if a caller ever reaches this hook without
|
|
156
|
+
* a resolved `scope`. Implementations bridging to an ObjectQL engine MUST
|
|
157
|
+
* forward it; a bridge with nowhere to put it may ignore it.
|
|
158
|
+
*/
|
|
159
|
+
fetchRecordLabels(targetObject: string, ids: unknown[], scope?: Record<string, unknown>, context?: ExecutionContext): Promise<Map<unknown, string>>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolve the TARGET object's read scope for a label lookup (#3602). Returns the
|
|
163
|
+
* object's RLS/tenant `FilterCondition`, `null`/`undefined` when the object is
|
|
164
|
+
* unscoped, or a rejected promise when the scope cannot be resolved — in which
|
|
165
|
+
* case the resolver fails CLOSED (skips that dimension's labels) rather than
|
|
166
|
+
* fetching unscoped names.
|
|
167
|
+
*/
|
|
168
|
+
type LabelScopeResolver = (targetObject: string) => Promise<Record<string, unknown> | null | undefined> | Record<string, unknown> | null | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* Sort-key label resolution for `DatasetSelection.order` (#3680).
|
|
171
|
+
*
|
|
172
|
+
* The executor sorts the assembled grid BEFORE `queryDataset` rewrites stored
|
|
173
|
+
* dimension values into display labels, so an order key naming a `select` or
|
|
174
|
+
* `lookup`/`master_detail` dimension used to sort by the stored value / FK id —
|
|
175
|
+
* an order that presents as arbitrary once the labels render. This hook hands
|
|
176
|
+
* the executor JUST the value→label mapping for such a dimension so it can sort
|
|
177
|
+
* by what the user will actually read, while the rows keep their raw values
|
|
178
|
+
* (drill metadata depends on them) and ordering + windowing stay one adjacent
|
|
179
|
+
* step. The executor stays engine-free: it sees this interface, never the
|
|
180
|
+
* engine behind it.
|
|
181
|
+
*/
|
|
182
|
+
interface OrderLabelResolver {
|
|
183
|
+
/**
|
|
184
|
+
* Whether the dimension's stored value differs from the label it renders as
|
|
185
|
+
* (`select` options, `lookup`/`master_detail` FK ids). Synchronous — the
|
|
186
|
+
* executor consults it when deciding whether the window may be pushed into
|
|
187
|
+
* SQL, before any query runs.
|
|
188
|
+
*/
|
|
189
|
+
isLabelBearing(dimension: string): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Map the given raw stored values of one dimension to display labels.
|
|
192
|
+
* Values missing from the map sort by their raw form — the same thing the
|
|
193
|
+
* user will see rendered for them.
|
|
141
194
|
*/
|
|
142
|
-
|
|
195
|
+
resolveLabels(dimension: string, values: unknown[]): Promise<Map<unknown, string> | undefined>;
|
|
143
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Build the executor's {@link OrderLabelResolver} from the dataset's dimension
|
|
199
|
+
* list and the injected label capabilities. Mirrors the classification in
|
|
200
|
+
* {@link resolveDimensionLabels}: a dimension is label-bearing when its field
|
|
201
|
+
* carries select `options` or is a lookup/master_detail with a `reference`.
|
|
202
|
+
*
|
|
203
|
+
* - `select` resolves from field metadata — no query at all.
|
|
204
|
+
* - `lookup`/`master_detail` costs ONE batched id→name read over the distinct
|
|
205
|
+
* grouped values, scoped to the REFERENCED object's own RLS (#3602). Fail
|
|
206
|
+
* closed: an unresolvable scope degrades to sorting by the stored id rather
|
|
207
|
+
* than fetching unscoped — consistent with the display pass, which renders
|
|
208
|
+
* the raw id in that case too.
|
|
209
|
+
*/
|
|
210
|
+
declare function createOrderLabelResolver(baseObject: string, dims: Array<{
|
|
211
|
+
name: string;
|
|
212
|
+
field: string;
|
|
213
|
+
}>, deps: DimensionLabelDeps, resolveScope?: LabelScopeResolver, context?: ExecutionContext): OrderLabelResolver;
|
|
214
|
+
/**
|
|
215
|
+
* Wrap a {@link DimensionLabelDeps} so repeated `fetchRecordLabels` calls
|
|
216
|
+
* within ONE request fetch each id at most once. A selection that sorts by a
|
|
217
|
+
* lookup dimension resolves labels twice — once PRE-window for the sort keys
|
|
218
|
+
* (#3680, over the full grid's ids), once post-window for display (a subset of
|
|
219
|
+
* the same ids) — so with this cache the display pass costs no extra query.
|
|
220
|
+
*
|
|
221
|
+
* Per-request only: entries are keyed by target object alone, which is safe
|
|
222
|
+
* because an object's read scope is constant within one request. Never share
|
|
223
|
+
* an instance across requests.
|
|
224
|
+
*/
|
|
225
|
+
declare function withLabelFetchCache(deps: DimensionLabelDeps): DimensionLabelDeps;
|
|
144
226
|
/** Date-dimension granularity (mirrors the dataset `dateGranularity` enum). */
|
|
145
227
|
type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
146
228
|
/**
|
|
@@ -151,13 +233,22 @@ type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
|
151
233
|
* (row key = `name`)
|
|
152
234
|
* @param rows - result rows, mutated in place
|
|
153
235
|
* @param deps - injected runtime capabilities
|
|
236
|
+
* @param resolveScope - (ADR-0021 D-C, #3602) resolves the referenced object's
|
|
237
|
+
* own read scope for a lookup/master_detail dimension's label fetch. When it
|
|
238
|
+
* throws, that dimension's labels are SKIPPED (fail-closed — the raw id renders
|
|
239
|
+
* instead) rather than fetched unscoped. Omit when no read-scope provider is
|
|
240
|
+
* configured (labels then fetch unscoped, as before — no security in play).
|
|
241
|
+
* @param context - the request's ExecutionContext, forwarded to
|
|
242
|
+
* {@link DimensionLabelDeps.fetchRecordLabels} so the engine's own middleware
|
|
243
|
+
* scopes the per-record label read too — the second belt beside `resolveScope`
|
|
244
|
+
* (#3602)
|
|
154
245
|
*/
|
|
155
246
|
declare function resolveDimensionLabels(baseObject: string, dims: Array<{
|
|
156
247
|
name: string;
|
|
157
248
|
field: string;
|
|
158
249
|
type?: string;
|
|
159
250
|
dateGranularity?: DateGranularity | string;
|
|
160
|
-
}>, rows: Record<string, unknown>[], deps: DimensionLabelDeps): Promise<void>;
|
|
251
|
+
}>, rows: Record<string, unknown>[], deps: DimensionLabelDeps, resolveScope?: LabelScopeResolver, context?: ExecutionContext): Promise<void>;
|
|
161
252
|
/**
|
|
162
253
|
* Pick the display field for an object from its field map, by convention:
|
|
163
254
|
* an explicit `name`/`title`/`label` field, else the first text-like field.
|
|
@@ -195,6 +286,15 @@ interface AnalyticsServiceConfig {
|
|
|
195
286
|
alias: string;
|
|
196
287
|
}>;
|
|
197
288
|
filter?: Record<string, unknown>;
|
|
289
|
+
/** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */
|
|
290
|
+
timezone?: string;
|
|
291
|
+
/**
|
|
292
|
+
* ADR-0021 D-C (#3602) — the request's ExecutionContext. Bridges MUST
|
|
293
|
+
* forward it to `engine.aggregate` so engine-side RLS applies; see
|
|
294
|
+
* `StrategyContext.executeAggregate` for why this is a second belt rather
|
|
295
|
+
* than a replacement for `getReadScope`.
|
|
296
|
+
*/
|
|
297
|
+
context?: ExecutionContext;
|
|
198
298
|
}) => Promise<Record<string, unknown>[]>;
|
|
199
299
|
/**
|
|
200
300
|
* Fallback IAnalyticsService (e.g. MemoryAnalyticsService).
|
|
@@ -244,6 +344,24 @@ interface AnalyticsServiceConfig {
|
|
|
244
344
|
* `StrategyContext.isExternalObject`.
|
|
245
345
|
*/
|
|
246
346
|
isExternalObject?: (objectName: string) => boolean;
|
|
347
|
+
/**
|
|
348
|
+
* [#3867] Is `name` a registered object in this kernel's schema registry?
|
|
349
|
+
*
|
|
350
|
+
* Consulted by {@link AnalyticsService.ensureCube} on the auto-inference
|
|
351
|
+
* path only. When no Cube is registered under the queried name, the service
|
|
352
|
+
* infers a minimal one whose `sql` IS that name — the intended "metric over
|
|
353
|
+
* an object" path (an `object-metric` KPI widget queries `crm_account`
|
|
354
|
+
* without anyone authoring a Cube). Without this hook that inference accepts
|
|
355
|
+
* ANY string, so an arbitrary physical table name reached the driver: the
|
|
356
|
+
* analytics-side twin of the data-path gap closed in #3770.
|
|
357
|
+
*
|
|
358
|
+
* Optional, and absence means "skip the check" — same tiering as #3770's
|
|
359
|
+
* `assertObjectRegistered`: with no registry to consult the question cannot
|
|
360
|
+
* be answered, and failing closed would break every embedding that runs
|
|
361
|
+
* analytics without a data engine. The production bridge in `plugin.ts`
|
|
362
|
+
* always wires it.
|
|
363
|
+
*/
|
|
364
|
+
isRegisteredObject?: (name: string) => boolean;
|
|
247
365
|
/**
|
|
248
366
|
* ADR-0021 — optional object-graph resolver used when compiling datasets:
|
|
249
367
|
* `(baseObject, relationshipName) => relatedObjectName | undefined`. When
|
|
@@ -315,6 +433,10 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
315
433
|
private readonly labelResolver?;
|
|
316
434
|
/** ADR-0037 P3: pending-seed row resolver for draft data preview. */
|
|
317
435
|
private readonly draftRowsResolver?;
|
|
436
|
+
/** [#3867] Schema-registry probe gating cube auto-inference. */
|
|
437
|
+
private readonly isRegisteredObject?;
|
|
438
|
+
/** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */
|
|
439
|
+
private warnedNoObjectRegistry;
|
|
318
440
|
readonly cubeRegistry: CubeRegistry;
|
|
319
441
|
private readonly logger;
|
|
320
442
|
constructor(config?: AnalyticsServiceConfig);
|
|
@@ -389,6 +511,20 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
389
511
|
* strategies pick the right aggregation function and field.
|
|
390
512
|
*/
|
|
391
513
|
private ensureCube;
|
|
514
|
+
/**
|
|
515
|
+
* [#3867] Gate on the cube auto-inference path: a name with no registered
|
|
516
|
+
* Cube may only be inferred into one if it is a registered object.
|
|
517
|
+
*
|
|
518
|
+
* Rejects with `status: 404` / `code: 'CUBE_NOT_FOUND'` so the HTTP boundary
|
|
519
|
+
* answers "no such cube" instead of letting the name reach the driver as a
|
|
520
|
+
* table and surfacing whatever the driver says about it. The message names
|
|
521
|
+
* both ways the request could be made valid, because from here the two are
|
|
522
|
+
* genuinely indistinguishable: register a Cube, or register the object.
|
|
523
|
+
*
|
|
524
|
+
* Skips when `isRegisteredObject` was not supplied — see the config field's
|
|
525
|
+
* doc for why that tier is a deliberate stand-down and not a hole.
|
|
526
|
+
*/
|
|
527
|
+
private assertInferableCube;
|
|
392
528
|
/** Build a minimal Cube from the fields referenced by an AnalyticsQuery. */
|
|
393
529
|
private inferCubeFromQuery;
|
|
394
530
|
/**
|
|
@@ -427,6 +563,12 @@ interface AnalyticsServicePluginOptions {
|
|
|
427
563
|
filter?: Record<string, unknown>;
|
|
428
564
|
/** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */
|
|
429
565
|
timezone?: string;
|
|
566
|
+
/**
|
|
567
|
+
* ADR-0021 D-C (#3602) — the request's ExecutionContext. A custom bridge
|
|
568
|
+
* MUST forward it to its engine so engine-side RLS applies; dropping it is
|
|
569
|
+
* what made the built-in bridge fall open in #3597.
|
|
570
|
+
*/
|
|
571
|
+
context?: ExecutionContext;
|
|
430
572
|
}) => Promise<Record<string, unknown>[]>;
|
|
431
573
|
/**
|
|
432
574
|
* ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The
|
|
@@ -488,25 +630,6 @@ declare class AnalyticsServicePlugin implements Plugin {
|
|
|
488
630
|
|
|
489
631
|
/** @deprecated use DatasetCompareTo from @objectstack/spec/contracts */
|
|
490
632
|
type CompareTo = DatasetCompareTo;
|
|
491
|
-
/**
|
|
492
|
-
* Dataset executor (ADR-0021 WS2).
|
|
493
|
-
*
|
|
494
|
-
* Turns a compiled dataset + a presentation's selection (dimensions, measures,
|
|
495
|
-
* runtime filter, compareTo) into one or more `AnalyticsQuery`s against the Cube
|
|
496
|
-
* runtime, then post-processes the results:
|
|
497
|
-
* - resolves the base measures a selection needs (including derived deps),
|
|
498
|
-
* - applies measure-scoped filters via supplementary grouped queries,
|
|
499
|
-
* - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),
|
|
500
|
-
* - shifts the query for `compareTo` (previousPeriod / previousYear) and
|
|
501
|
-
* attaches `<measure>__compare` columns,
|
|
502
|
-
* - computes server-side totals (`selection.totals.groupings`, #1753) by
|
|
503
|
-
* re-running the selection per dimension subset, so matrix subtotals and
|
|
504
|
-
* the grand total use each measure's true aggregate.
|
|
505
|
-
*
|
|
506
|
-
* RLS/tenant scoping is NOT handled here — it is enforced inside the strategy
|
|
507
|
-
* via the StrategyContext read-scope hook (D-C). This layer is pure query
|
|
508
|
-
* shaping + arithmetic.
|
|
509
|
-
*/
|
|
510
633
|
/** AND two optional FilterConditions into one (MongoDB-style). */
|
|
511
634
|
declare function combineFilters(a?: FilterCondition, b?: FilterCondition): FilterCondition | undefined;
|
|
512
635
|
/**
|
|
@@ -518,7 +641,16 @@ declare function evaluateDerivedMeasures(rows: Record<string, unknown>[], derive
|
|
|
518
641
|
declare function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string];
|
|
519
642
|
declare class DatasetExecutor {
|
|
520
643
|
private readonly service;
|
|
521
|
-
|
|
644
|
+
private readonly orderLabels?;
|
|
645
|
+
/**
|
|
646
|
+
* @param service - The analytics service the executor issues its queries to.
|
|
647
|
+
* @param orderLabels - Optional sort-key label hook (#3680). When provided,
|
|
648
|
+
* an order key naming a label-bearing (`select`/`lookup`) dimension sorts
|
|
649
|
+
* by its display label instead of the stored value. Omit to sort by stored
|
|
650
|
+
* values everywhere (e.g. the draft-preview path, whose seed rows already
|
|
651
|
+
* carry display names).
|
|
652
|
+
*/
|
|
653
|
+
constructor(service: IAnalyticsService, orderLabels?: OrderLabelResolver | undefined);
|
|
522
654
|
/**
|
|
523
655
|
* Execute a dataset selection and return the shaped rows (+ field metadata).
|
|
524
656
|
*
|
|
@@ -526,7 +658,7 @@ declare class DatasetExecutor {
|
|
|
526
658
|
* underlying `IAnalyticsService.query` so the tenant/RLS read scope is
|
|
527
659
|
* applied per request (ADR-0021 D-C).
|
|
528
660
|
*/
|
|
529
|
-
execute(
|
|
661
|
+
execute(compiledInput: CompiledDataset, selectionInput: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
530
662
|
private executeSelection;
|
|
531
663
|
private buildQuery;
|
|
532
664
|
private runCompare;
|
|
@@ -652,10 +784,83 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
652
784
|
readonly priority = 20;
|
|
653
785
|
canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean;
|
|
654
786
|
execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult>;
|
|
787
|
+
/**
|
|
788
|
+
* Render a REPRESENTATIVE SQL string for an ObjectQL aggregate query.
|
|
789
|
+
*
|
|
790
|
+
* This path executes through `engine.aggregate()`, not raw SQL, so the string
|
|
791
|
+
* is documentation rather than the literal statement — but it must be an
|
|
792
|
+
* honest account of what the query does, because dataset responses echo it
|
|
793
|
+
* and authors read it to verify their widget options landed (#3588). It
|
|
794
|
+
* therefore renders date bucketing (`date_trunc`), the WHERE predicate,
|
|
795
|
+
* ordering, and the row window.
|
|
796
|
+
*
|
|
797
|
+
* Filter VALUES are rendered as `$n` placeholders and returned in `params`,
|
|
798
|
+
* never inlined: the echoed statement travels to the browser, and a filter
|
|
799
|
+
* comparand can carry tenant data.
|
|
800
|
+
*/
|
|
655
801
|
generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{
|
|
656
802
|
sql: string;
|
|
657
803
|
params: unknown[];
|
|
658
804
|
}>;
|
|
805
|
+
/**
|
|
806
|
+
* ADR-0021 D-C (#3597) — AND the object's read scope (tenant + RLS) into the
|
|
807
|
+
* filter handed to `engine.aggregate`.
|
|
808
|
+
*
|
|
809
|
+
* This path used to drop the scope entirely, and the engine could not make up
|
|
810
|
+
* for it: the aggregate bridge passes no `ExecutionContext`, so the security
|
|
811
|
+
* middleware's principal-less fall-open skipped its own RLS injection. Both
|
|
812
|
+
* belts were off at once — an authenticated caller received aggregates
|
|
813
|
+
* computed over EVERY tenant's rows.
|
|
814
|
+
*
|
|
815
|
+
* Composed with `$and`, never by key merge: the query's own filter and the
|
|
816
|
+
* scope can name the SAME field (e.g. a dashboard filtering `organization_id`),
|
|
817
|
+
* and a spread would let caller input silently overwrite the security
|
|
818
|
+
* predicate. `$and` makes that structurally impossible.
|
|
819
|
+
*/
|
|
820
|
+
private withReadScope;
|
|
821
|
+
/** Is `field` a resolved cross-object (relationship-traversal) reference? */
|
|
822
|
+
private isCrossObjectField;
|
|
823
|
+
/**
|
|
824
|
+
* Plan how to serve cross-object references on this join-less path (#3654).
|
|
825
|
+
*
|
|
826
|
+
* `engine.aggregate()` cannot join. A cross-object DIMENSION within a
|
|
827
|
+
* supported envelope is served by an FK-expand (`executeCrossObject`): group
|
|
828
|
+
* the base aggregate on the lookup FK, resolve the FK to the related attribute
|
|
829
|
+
* with a SCOPED read, re-bucket in memory. Returns `null` for a base-only
|
|
830
|
+
* query (direct path), a plan for an in-envelope cross-object query.
|
|
831
|
+
*
|
|
832
|
+
* THROWS for anything outside the envelope — a cross-object MEASURE or FILTER
|
|
833
|
+
* (needs a real join to evaluate), a MULTI-HOP dimension (`a.b.c`), or a
|
|
834
|
+
* non-recombinable measure (`avg`/`count_distinct`, whose sub-bucket values
|
|
835
|
+
* cannot be merged). A loud error beats the silent mis-bucket #3654 kills.
|
|
836
|
+
* `generateSql()` calls this too, so the preview accepts/rejects the same set.
|
|
837
|
+
*
|
|
838
|
+
* Detection is on RESOLVED field names, so a dotted dimension the cube
|
|
839
|
+
* flattens to a real column is treated as base, not cross-object.
|
|
840
|
+
*/
|
|
841
|
+
private planCrossObject;
|
|
842
|
+
/**
|
|
843
|
+
* Serve a cross-object-dimension query by FK-expand (#3654). The pure
|
|
844
|
+
* re-bucketing step lives in `cross-object-rebucket.ts`.
|
|
845
|
+
*/
|
|
846
|
+
private executeCrossObject;
|
|
847
|
+
/**
|
|
848
|
+
* Resolve `fkValues` (ids of `refObject`) to their `attr` values, applying the
|
|
849
|
+
* referenced object's OWN read scope (#3654 / #3602). Reuses the aggregate
|
|
850
|
+
* bridge — `group by (id, attr)` is one row per record. Ids the scope hides
|
|
851
|
+
* are simply absent from the map (⇒ RESTRICTED bucket downstream).
|
|
852
|
+
*/
|
|
853
|
+
private resolveFkAttr;
|
|
854
|
+
/**
|
|
855
|
+
* Render one normalized filter as a display SQL predicate for `generateSql`.
|
|
856
|
+
*
|
|
857
|
+
* Mirrors `NativeSQLStrategy.buildFilterClause`'s operator vocabulary so the
|
|
858
|
+
* two previews read alike, but binds through `coerceFilterValueForObjectQL`:
|
|
859
|
+
* the comparand shown is the one THIS path actually hands the engine (a real
|
|
860
|
+
* boolean, not SQL's 1/0). Returns null for an operator/value combination
|
|
861
|
+
* that carries no predicate, matching `execute()`, which drops it too.
|
|
862
|
+
*/
|
|
863
|
+
private buildFilterClauseSql;
|
|
659
864
|
/**
|
|
660
865
|
* Resolve a member ref to a `{ sql, type? }` definition.
|
|
661
866
|
*
|
|
@@ -671,9 +876,65 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
671
876
|
private lookupMember;
|
|
672
877
|
private resolveFieldName;
|
|
673
878
|
private resolveMeasureAggregation;
|
|
879
|
+
/**
|
|
880
|
+
* AND one more operand onto `filter[field]`, merging operator objects rather
|
|
881
|
+
* than overwriting them. Returns a standalone conjunct when the two cannot
|
|
882
|
+
* share one entry, or `null` when the merge absorbed the operand.
|
|
883
|
+
*
|
|
884
|
+
* Every predicate this strategy contributes goes through here — the caller's
|
|
885
|
+
* `where` and the time-dimension `dateRange` alike. Two operands on one field
|
|
886
|
+
* are the normal case (`{$gte}` from a `where` plus `{$gte,$lte}` from a
|
|
887
|
+
* window on `close_date`), and a plain assignment would keep only the last:
|
|
888
|
+
* that is how a range used to lose a bound.
|
|
889
|
+
*
|
|
890
|
+
* Spreading is sound only while the operands name DIFFERENT operators. Where
|
|
891
|
+
* they collide — two `$gte` bounds on one field, which a window makes routine
|
|
892
|
+
* and which a `where` can already produce on its own through `$and` — the
|
|
893
|
+
* spread keeps whichever came last and WIDENS the query. Same for a bare
|
|
894
|
+
* equality meeting an operator object: neither can absorb the other. Those
|
|
895
|
+
* are handed back for the caller to AND in separately, so the engine
|
|
896
|
+
* intersects them instead of the strategy picking a winner.
|
|
897
|
+
*/
|
|
898
|
+
private mergeFilterOperand;
|
|
899
|
+
/**
|
|
900
|
+
* Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).
|
|
901
|
+
*
|
|
902
|
+
* `dateRange` states a WINDOW on a time dimension; it is a SIBLING of `where`,
|
|
903
|
+
* never folded into it. `normalizeAnalyticsFilters` reads only `where`, so
|
|
904
|
+
* this path used to drop the window on the floor — no error, just every row
|
|
905
|
+
* ever recorded. Nor is that a corner case: `NativeSQLStrategy.canHandle`
|
|
906
|
+
* declines any query carrying a `granularity`, so a date-bucketed trend lands
|
|
907
|
+
* HERE on every driver — and "bucketed trend" is precisely the shape that also
|
|
908
|
+
* carries a range ("last 12 months", "this quarter").
|
|
909
|
+
*
|
|
910
|
+
* Bounds are inclusive on both ends — the same `$gte`/`$lte` pair
|
|
911
|
+
* `NativeSQLStrategy` binds as `BETWEEN` and the memory driver builds as a
|
|
912
|
+
* `$match`, so one dashboard reads the same on every driver.
|
|
913
|
+
*
|
|
914
|
+
* Comparands are coerced by the SAME helper the `where` path uses, so an
|
|
915
|
+
* epoch-ms bound recovers as a number and an ISO string stays a string. No
|
|
916
|
+
* STORAGE coercion happens here, deliberately: `NativeSQLStrategy` needs
|
|
917
|
+
* `coerceTemporal` because it binds into raw SQL and had to learn that a
|
|
918
|
+
* SQLite `Field.datetime` is an INTEGER epoch (#2034); this path goes through
|
|
919
|
+
* `engine.aggregate()`, where the driver's own CRUD filter coercion applies —
|
|
920
|
+
* the very coercion that already makes a `where` bound on that same column
|
|
921
|
+
* work today.
|
|
922
|
+
*
|
|
923
|
+
* A bare-string `dateRange` degenerates to the single point `[s, s]`, matching
|
|
924
|
+
* `NativeSQLStrategy`. Relative phrases ("Last 7 days") are NOT resolved here;
|
|
925
|
+
* neither SQL path resolves them, and inventing a second interpretation on the
|
|
926
|
+
* driver-independent path is how the two would drift apart again.
|
|
927
|
+
*
|
|
928
|
+
* An oddly-sized array (the schema types `dateRange` as a plain `string[]`)
|
|
929
|
+
* takes its first two entries, a one-entry array degenerating to a point.
|
|
930
|
+
* `NativeSQLStrategy` drops such a window entirely — but "drop the window"
|
|
931
|
+
* means "plot all of history", which is the very failure this fixes, so the
|
|
932
|
+
* fallback here errs toward the narrower query instead.
|
|
933
|
+
*/
|
|
934
|
+
private dateRangeBounds;
|
|
674
935
|
private convertFilter;
|
|
675
936
|
private extractObjectName;
|
|
676
937
|
private buildFieldMeta;
|
|
677
938
|
}
|
|
678
939
|
|
|
679
|
-
export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, type DimensionLabelDeps, type FieldMetaLite, NativeSQLStrategy, ObjectQLStrategy, type RelationshipResolver, type RelationshipTarget, combineFilters, compileDataset, compileScopedFilterToSql, evaluateDerivedMeasures, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange };
|
|
940
|
+
export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, type DimensionLabelDeps, type FieldMetaLite, NativeSQLStrategy, ObjectQLStrategy, type OrderLabelResolver, type RelationshipResolver, type RelationshipTarget, combineFilters, compileDataset, compileScopedFilterToSql, createOrderLabelResolver, evaluateDerivedMeasures, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange, withLabelFetchCache };
|