@objectstack/service-analytics 17.0.0-rc.1 → 17.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +437 -0
- package/dist/index.cjs +201 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -12
- package/dist/index.d.ts +154 -12
- package/dist/index.js +200 -40
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IAnalyticsService, Logger,
|
|
2
|
-
export { AnalyticsStrategy, DatasetSelection,
|
|
1
|
+
import { IAnalyticsService, Logger, AnalyticsDriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, DatasetSelection, CubeMeta, DatasetCompareTo, StrategyContext } from '@objectstack/spec/contracts';
|
|
2
|
+
export { AnalyticsDriverCapabilities, AnalyticsStrategy, DatasetSelection, StrategyContext } from '@objectstack/spec/contracts';
|
|
3
3
|
import { Cube, FilterCondition } from '@objectstack/spec/data';
|
|
4
4
|
import { ExecutionContext } from '@objectstack/spec/kernel';
|
|
5
5
|
import { Dataset } from '@objectstack/spec/ui';
|
|
@@ -268,7 +268,7 @@ interface AnalyticsServiceConfig {
|
|
|
268
268
|
* Probe driver capabilities for the object that backs a cube.
|
|
269
269
|
* The service calls this function to decide which strategy can handle a query.
|
|
270
270
|
*/
|
|
271
|
-
queryCapabilities?: (cubeName: string) =>
|
|
271
|
+
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
|
|
272
272
|
/**
|
|
273
273
|
* Execute raw SQL on the driver for a given object.
|
|
274
274
|
* Required for NativeSQLStrategy.
|
|
@@ -370,6 +370,26 @@ interface AnalyticsServiceConfig {
|
|
|
370
370
|
* always wires it.
|
|
371
371
|
*/
|
|
372
372
|
isRegisteredObject?: (name: string) => boolean;
|
|
373
|
+
/**
|
|
374
|
+
* [#4437] The FIELD NAMES `objectName` declares, or `undefined` when nothing
|
|
375
|
+
* authoritative can answer.
|
|
376
|
+
*
|
|
377
|
+
* Consulted by {@link AnalyticsService.ensureCube} to validate the SOURCE
|
|
378
|
+
* FIELD a measure resolves to BEFORE any SQL is built. `inferMeasure` maps a
|
|
379
|
+
* suffix convention onto a field name (`ghost_sum` → `SUM(ghost)`) and used
|
|
380
|
+
* to accept any spelling, so a typo'd measure reached the driver as a column
|
|
381
|
+
* and came back as an opaque `500 SQLITE_ERROR` — a driver error class on the
|
|
382
|
+
* wire for a caller-shaped mistake (ADR-0112). The DATA route already refuses
|
|
383
|
+
* the same mistake with a `400 INVALID_FIELD` naming the field (#4315/#4254);
|
|
384
|
+
* this hook is what lets the ANALYTICS route give the same answer.
|
|
385
|
+
*
|
|
386
|
+
* Same tiering as {@link isRegisteredObject}: absence means "skip the check"
|
|
387
|
+
* (registry-less hosts, engine doubles, external datasources whose columns
|
|
388
|
+
* are not mirrored locally). The production bridge in `plugin.ts` wires it
|
|
389
|
+
* from the same schema registry the data path's gate reads, so "which fields
|
|
390
|
+
* exist" has ONE answer across `/data` and `/analytics`.
|
|
391
|
+
*/
|
|
392
|
+
getObjectFieldNames?: (objectName: string) => readonly string[] | undefined;
|
|
373
393
|
/**
|
|
374
394
|
* ADR-0021 — optional object-graph resolver used when compiling datasets:
|
|
375
395
|
* `(baseObject, relationshipName) => relatedObjectName | undefined`. When
|
|
@@ -377,15 +397,23 @@ interface AnalyticsServiceConfig {
|
|
|
377
397
|
*/
|
|
378
398
|
relationshipResolver?: RelationshipResolver;
|
|
379
399
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
400
|
+
* Resolve the metadata of a dimension's or measure's SOURCE FIELD on the
|
|
401
|
+
* dataset's base object — the one seam through which display semantics that
|
|
402
|
+
* live on the field reach the result columns. `undefined` for an unknown
|
|
403
|
+
* field. Feeds three chains:
|
|
404
|
+
*
|
|
405
|
+
* - ADR-0053 currency: a monetary measure that omits an explicit `currency`
|
|
406
|
+
* falls back to the field's declared currency, then the tenant default
|
|
407
|
+
* (`ctx.currency`). Non-`currency` fields never get a code.
|
|
408
|
+
* - Percent scale (objectui#3136): a measure over a `percent` field inherits
|
|
409
|
+
* that field's storage scale via `percentScaleOf`, so a renderer scales by
|
|
410
|
+
* declared metadata instead of guessing from the value.
|
|
411
|
+
* - Date bucketing: a date vs datetime dimension drills by the right bound.
|
|
385
412
|
*/
|
|
386
|
-
|
|
413
|
+
sourceFieldMeta?: (object: string, field: string) => {
|
|
387
414
|
type?: string;
|
|
388
415
|
defaultCurrency?: string;
|
|
416
|
+
max?: number;
|
|
389
417
|
} | undefined;
|
|
390
418
|
/** Pre-defined datasets to compile + register at construction (ADR-0021). */
|
|
391
419
|
datasets?: Dataset[];
|
|
@@ -436,13 +464,15 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
436
464
|
private readonly datasetRegistry;
|
|
437
465
|
/** Optional object-graph resolver used when compiling datasets. */
|
|
438
466
|
private readonly relationshipResolver?;
|
|
439
|
-
private readonly
|
|
467
|
+
private readonly sourceFieldMeta?;
|
|
440
468
|
/** Optional dimension display-label resolver (select options / lookup names). */
|
|
441
469
|
private readonly labelResolver?;
|
|
442
470
|
/** ADR-0037 P3: pending-seed row resolver for draft data preview. */
|
|
443
471
|
private readonly draftRowsResolver?;
|
|
444
472
|
/** [#3867] Schema-registry probe gating cube auto-inference. */
|
|
445
473
|
private readonly isRegisteredObject?;
|
|
474
|
+
/** [#4437] Field-name probe gating measure source-field resolution. */
|
|
475
|
+
private readonly getObjectFieldNames?;
|
|
446
476
|
/** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */
|
|
447
477
|
private warnedNoObjectRegistry;
|
|
448
478
|
readonly cubeRegistry: CubeRegistry;
|
|
@@ -519,6 +549,35 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
519
549
|
* strategies pick the right aggregation function and field.
|
|
520
550
|
*/
|
|
521
551
|
private ensureCube;
|
|
552
|
+
/**
|
|
553
|
+
* [#4437] Reject a measure whose SOURCE FIELD the backing object does not
|
|
554
|
+
* have, BEFORE the strategy compiles it into SQL.
|
|
555
|
+
*
|
|
556
|
+
* `inferMeasure` maps a suffix convention onto a field name and has no way to
|
|
557
|
+
* know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the
|
|
558
|
+
* driver threw `no such column`, and the caller got
|
|
559
|
+
* `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver
|
|
560
|
+
* error class on the wire, and nothing actionable, for what is a plain typo.
|
|
561
|
+
* The DATA route has refused the same mistake with a `400 INVALID_FIELD`
|
|
562
|
+
* naming the field since #4315/#4254; this is the analytics half of that
|
|
563
|
+
* answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/
|
|
564
|
+
* `param`) so one mistake has one shape across both routes.
|
|
565
|
+
*
|
|
566
|
+
* What it checks, and what it deliberately does not:
|
|
567
|
+
*
|
|
568
|
+
* - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose
|
|
569
|
+
* `sql` is a real SQL expression has no field list to check against.
|
|
570
|
+
* - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.
|
|
571
|
+
* Absent hook / unknown object → stand down (see the config field's doc).
|
|
572
|
+
* - Only measures whose source is a BARE COLUMN. `count(*)` has no source
|
|
573
|
+
* field, and a dotted reference (`account.industry`) resolves through a
|
|
574
|
+
* join whose target this check cannot see — both pass through untouched.
|
|
575
|
+
* - `id` / `created_at` / `updated_at` are admitted unconditionally, matching
|
|
576
|
+
* the data path's `resolveQueryFields`: they are engine-assigned rather than
|
|
577
|
+
* declared, and a gate stricter than the engine it guards would reject
|
|
578
|
+
* queries that used to work.
|
|
579
|
+
*/
|
|
580
|
+
private assertMeasureFields;
|
|
522
581
|
/**
|
|
523
582
|
* [#3867] Gate on the cube auto-inference path: a name with no registered
|
|
524
583
|
* Cube may only be inferred into one if it is a registered object.
|
|
@@ -553,7 +612,7 @@ interface AnalyticsServicePluginOptions {
|
|
|
553
612
|
* Probe driver capabilities for a given cube.
|
|
554
613
|
* When omitted, defaults to in-memory only.
|
|
555
614
|
*/
|
|
556
|
-
queryCapabilities?: (cubeName: string) =>
|
|
615
|
+
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
|
|
557
616
|
/**
|
|
558
617
|
* Execute raw SQL on a driver. Enables NativeSQLStrategy.
|
|
559
618
|
*/
|
|
@@ -633,6 +692,14 @@ declare class AnalyticsServicePlugin implements Plugin {
|
|
|
633
692
|
version: string;
|
|
634
693
|
type: "standard";
|
|
635
694
|
dependencies: string[];
|
|
695
|
+
/**
|
|
696
|
+
* init() probes the `data` engine ObjectQLPlugin provides for the
|
|
697
|
+
* auto-bridge — order-if-present so the probe verdict is deterministic
|
|
698
|
+
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
|
|
699
|
+
* degrades on purpose (per-query lazy resolution / explicit
|
|
700
|
+
* `executeAggregate`).
|
|
701
|
+
*/
|
|
702
|
+
optionalDependencies: string[];
|
|
636
703
|
private service?;
|
|
637
704
|
private readonly options;
|
|
638
705
|
constructor(options?: AnalyticsServicePluginOptions);
|
|
@@ -650,6 +717,50 @@ declare function combineFilters(a?: FilterCondition, b?: FilterCondition): Filte
|
|
|
650
717
|
* Division by zero (and missing operands) yields `null` rather than Infinity/NaN.
|
|
651
718
|
*/
|
|
652
719
|
declare function evaluateDerivedMeasures(rows: Record<string, unknown>[], derived: DerivedMeasureSpec[]): Record<string, unknown>[];
|
|
720
|
+
/**
|
|
721
|
+
* Fill the EMPTY-GROUP value into every measure column the assembled grid
|
|
722
|
+
* LISTS but no query REPORTED — by aggregate kind (#4708, objectui#3136).
|
|
723
|
+
*
|
|
724
|
+
* The grid is assembled from several results: the primary query, one
|
|
725
|
+
* supplementary query per measure-scoped filter, and (for `compareTo`) a
|
|
726
|
+
* shifted pass. {@link mergeByDimensions} writes a measure's column only onto
|
|
727
|
+
* rows its source result returned, and a `GROUP BY` over a filtered row set
|
|
728
|
+
* emits NO group at all for a dimension value the filter excludes entirely.
|
|
729
|
+
* The column therefore comes back **absent**, not `0` — and absent renders as
|
|
730
|
+
* "no data for this row", which for a count is the opposite of what the row
|
|
731
|
+
* means. A derived ratio over it goes null as well ({@link computeDerived}
|
|
732
|
+
* treats a missing operand as unknowable), so the blank spreads.
|
|
733
|
+
*
|
|
734
|
+
* The bias runs the worst possible way: the rows that blank are the ones whose
|
|
735
|
+
* numerator the filter excluded — the WORST-performing rows. A `lead_source`
|
|
736
|
+
* that won nothing renders as "no data" while one that won everything renders
|
|
737
|
+
* fine.
|
|
738
|
+
*
|
|
739
|
+
* **Filled strictly by aggregate kind**, never wholesale. `count` /
|
|
740
|
+
* `count_distinct` over an excluded group is unambiguously `0` ("how many rows
|
|
741
|
+
* matched" has an exact answer when the answer is none), and `sum` over the
|
|
742
|
+
* empty set is its identity `0`. `avg` / `min` / `max` are genuinely null —
|
|
743
|
+
* there is nothing to average — and flattening those to `0` would trade this
|
|
744
|
+
* lie for the opposite one, reporting a measurement nobody made. The
|
|
745
|
+
* kind→identity mapping is `emptyGroupValueFor` in `@objectstack/spec/data`,
|
|
746
|
+
* shared with the authoring-side coherence checks so the two cannot drift.
|
|
747
|
+
*
|
|
748
|
+
* **Only rows that already exist are touched** — no group is invented. A
|
|
749
|
+
* dimension value no query reported at all has genuinely no data and stays out
|
|
750
|
+
* of the grid; this fills the cell, never the row.
|
|
751
|
+
*
|
|
752
|
+
* Deliberately NOT a `?? 0` in the widget or a `coalesce` in the measure: a
|
|
753
|
+
* consumer-side patch must be repeated by every author of every ratio widget
|
|
754
|
+
* forever, and forgetting it is silent. Only the executor knows which aggregate
|
|
755
|
+
* produced the gap, so only the executor can tell `0` from unknown.
|
|
756
|
+
*
|
|
757
|
+
* Mutates `rows` in place (they are already this pipeline's own copies) and
|
|
758
|
+
* returns them for chaining.
|
|
759
|
+
*
|
|
760
|
+
* @param columnAggregates - Grid column → the aggregate that produced it.
|
|
761
|
+
* Includes `<measure>__compare` columns, which merge through the same seam.
|
|
762
|
+
*/
|
|
763
|
+
declare function fillEmptyGroups(rows: Record<string, unknown>[], columnAggregates: Record<string, string | undefined>): Record<string, unknown>[];
|
|
653
764
|
/** Compute the comparison window for a [start,end] range. */
|
|
654
765
|
declare function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string];
|
|
655
766
|
declare class DatasetExecutor {
|
|
@@ -673,6 +784,37 @@ declare class DatasetExecutor {
|
|
|
673
784
|
*/
|
|
674
785
|
execute(compiledInput: CompiledDataset, selectionInput: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
675
786
|
private executeSelection;
|
|
787
|
+
/**
|
|
788
|
+
* Run ONE grouped pass over a set of base measures, honouring each measure's
|
|
789
|
+
* own scoped `filter`: the unfiltered measures in a single query, plus one
|
|
790
|
+
* supplementary query per filter-scoped measure, merged back by dimension key.
|
|
791
|
+
*
|
|
792
|
+
* **This is the executor's only implementation of "how a measure filter is
|
|
793
|
+
* applied", and every window goes through it** — the current period, each
|
|
794
|
+
* `totals` subset (which re-enters via `executeSelection`), and the
|
|
795
|
+
* `compareTo` window. Before #4820 the comparison window had its own,
|
|
796
|
+
* simpler answer: one shifted query over all base measures with only the
|
|
797
|
+
* base filter, so `compiled.measureFilters` was never read on that path.
|
|
798
|
+
* `won_count` counted won deals and `won_count__compare` counted every deal,
|
|
799
|
+
* under one label, in adjacent columns. Only measures carrying a filter were
|
|
800
|
+
* wrong — which is what made it survive: the unfiltered ones next to them
|
|
801
|
+
* compared correctly.
|
|
802
|
+
*
|
|
803
|
+
* The caller supplies the `selection` this pass queries under, which is how
|
|
804
|
+
* the comparison window differs at all: same measures, same dimensions, same
|
|
805
|
+
* filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else
|
|
806
|
+
* about the two passes may drift, because anything that does becomes a
|
|
807
|
+
* discrepancy between two columns the reader is invited to subtract.
|
|
808
|
+
*
|
|
809
|
+
* Cost: one extra query per filter-scoped measure when `compareTo` is set.
|
|
810
|
+
* The alternative — declaring the discrepancy in the response — is not one,
|
|
811
|
+
* since the two columns exist to be directly comparable.
|
|
812
|
+
*
|
|
813
|
+
* @param window - Ordering/window to push into the SQL. Only ever set for a
|
|
814
|
+
* selection the caller proved is a single self-sufficient query; a pass
|
|
815
|
+
* that fans out must return its whole grid for the merge.
|
|
816
|
+
*/
|
|
817
|
+
private runMeasurePass;
|
|
676
818
|
/**
|
|
677
819
|
* The selected dimensions the compiled cube types as `time`, in selection
|
|
678
820
|
* order (#3916) — the axis {@link resolveOrdering} defaults to ascending.
|
|
@@ -1038,4 +1180,4 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1038
1180
|
private buildFieldMeta;
|
|
1039
1181
|
}
|
|
1040
1182
|
|
|
1041
|
-
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 };
|
|
1183
|
+
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, fillEmptyGroups, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange, withLabelFetchCache };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IAnalyticsService, Logger,
|
|
2
|
-
export { AnalyticsStrategy, DatasetSelection,
|
|
1
|
+
import { IAnalyticsService, Logger, AnalyticsDriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, DatasetSelection, CubeMeta, DatasetCompareTo, StrategyContext } from '@objectstack/spec/contracts';
|
|
2
|
+
export { AnalyticsDriverCapabilities, AnalyticsStrategy, DatasetSelection, StrategyContext } from '@objectstack/spec/contracts';
|
|
3
3
|
import { Cube, FilterCondition } from '@objectstack/spec/data';
|
|
4
4
|
import { ExecutionContext } from '@objectstack/spec/kernel';
|
|
5
5
|
import { Dataset } from '@objectstack/spec/ui';
|
|
@@ -268,7 +268,7 @@ interface AnalyticsServiceConfig {
|
|
|
268
268
|
* Probe driver capabilities for the object that backs a cube.
|
|
269
269
|
* The service calls this function to decide which strategy can handle a query.
|
|
270
270
|
*/
|
|
271
|
-
queryCapabilities?: (cubeName: string) =>
|
|
271
|
+
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
|
|
272
272
|
/**
|
|
273
273
|
* Execute raw SQL on the driver for a given object.
|
|
274
274
|
* Required for NativeSQLStrategy.
|
|
@@ -370,6 +370,26 @@ interface AnalyticsServiceConfig {
|
|
|
370
370
|
* always wires it.
|
|
371
371
|
*/
|
|
372
372
|
isRegisteredObject?: (name: string) => boolean;
|
|
373
|
+
/**
|
|
374
|
+
* [#4437] The FIELD NAMES `objectName` declares, or `undefined` when nothing
|
|
375
|
+
* authoritative can answer.
|
|
376
|
+
*
|
|
377
|
+
* Consulted by {@link AnalyticsService.ensureCube} to validate the SOURCE
|
|
378
|
+
* FIELD a measure resolves to BEFORE any SQL is built. `inferMeasure` maps a
|
|
379
|
+
* suffix convention onto a field name (`ghost_sum` → `SUM(ghost)`) and used
|
|
380
|
+
* to accept any spelling, so a typo'd measure reached the driver as a column
|
|
381
|
+
* and came back as an opaque `500 SQLITE_ERROR` — a driver error class on the
|
|
382
|
+
* wire for a caller-shaped mistake (ADR-0112). The DATA route already refuses
|
|
383
|
+
* the same mistake with a `400 INVALID_FIELD` naming the field (#4315/#4254);
|
|
384
|
+
* this hook is what lets the ANALYTICS route give the same answer.
|
|
385
|
+
*
|
|
386
|
+
* Same tiering as {@link isRegisteredObject}: absence means "skip the check"
|
|
387
|
+
* (registry-less hosts, engine doubles, external datasources whose columns
|
|
388
|
+
* are not mirrored locally). The production bridge in `plugin.ts` wires it
|
|
389
|
+
* from the same schema registry the data path's gate reads, so "which fields
|
|
390
|
+
* exist" has ONE answer across `/data` and `/analytics`.
|
|
391
|
+
*/
|
|
392
|
+
getObjectFieldNames?: (objectName: string) => readonly string[] | undefined;
|
|
373
393
|
/**
|
|
374
394
|
* ADR-0021 — optional object-graph resolver used when compiling datasets:
|
|
375
395
|
* `(baseObject, relationshipName) => relatedObjectName | undefined`. When
|
|
@@ -377,15 +397,23 @@ interface AnalyticsServiceConfig {
|
|
|
377
397
|
*/
|
|
378
398
|
relationshipResolver?: RelationshipResolver;
|
|
379
399
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
400
|
+
* Resolve the metadata of a dimension's or measure's SOURCE FIELD on the
|
|
401
|
+
* dataset's base object — the one seam through which display semantics that
|
|
402
|
+
* live on the field reach the result columns. `undefined` for an unknown
|
|
403
|
+
* field. Feeds three chains:
|
|
404
|
+
*
|
|
405
|
+
* - ADR-0053 currency: a monetary measure that omits an explicit `currency`
|
|
406
|
+
* falls back to the field's declared currency, then the tenant default
|
|
407
|
+
* (`ctx.currency`). Non-`currency` fields never get a code.
|
|
408
|
+
* - Percent scale (objectui#3136): a measure over a `percent` field inherits
|
|
409
|
+
* that field's storage scale via `percentScaleOf`, so a renderer scales by
|
|
410
|
+
* declared metadata instead of guessing from the value.
|
|
411
|
+
* - Date bucketing: a date vs datetime dimension drills by the right bound.
|
|
385
412
|
*/
|
|
386
|
-
|
|
413
|
+
sourceFieldMeta?: (object: string, field: string) => {
|
|
387
414
|
type?: string;
|
|
388
415
|
defaultCurrency?: string;
|
|
416
|
+
max?: number;
|
|
389
417
|
} | undefined;
|
|
390
418
|
/** Pre-defined datasets to compile + register at construction (ADR-0021). */
|
|
391
419
|
datasets?: Dataset[];
|
|
@@ -436,13 +464,15 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
436
464
|
private readonly datasetRegistry;
|
|
437
465
|
/** Optional object-graph resolver used when compiling datasets. */
|
|
438
466
|
private readonly relationshipResolver?;
|
|
439
|
-
private readonly
|
|
467
|
+
private readonly sourceFieldMeta?;
|
|
440
468
|
/** Optional dimension display-label resolver (select options / lookup names). */
|
|
441
469
|
private readonly labelResolver?;
|
|
442
470
|
/** ADR-0037 P3: pending-seed row resolver for draft data preview. */
|
|
443
471
|
private readonly draftRowsResolver?;
|
|
444
472
|
/** [#3867] Schema-registry probe gating cube auto-inference. */
|
|
445
473
|
private readonly isRegisteredObject?;
|
|
474
|
+
/** [#4437] Field-name probe gating measure source-field resolution. */
|
|
475
|
+
private readonly getObjectFieldNames?;
|
|
446
476
|
/** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */
|
|
447
477
|
private warnedNoObjectRegistry;
|
|
448
478
|
readonly cubeRegistry: CubeRegistry;
|
|
@@ -519,6 +549,35 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
519
549
|
* strategies pick the right aggregation function and field.
|
|
520
550
|
*/
|
|
521
551
|
private ensureCube;
|
|
552
|
+
/**
|
|
553
|
+
* [#4437] Reject a measure whose SOURCE FIELD the backing object does not
|
|
554
|
+
* have, BEFORE the strategy compiles it into SQL.
|
|
555
|
+
*
|
|
556
|
+
* `inferMeasure` maps a suffix convention onto a field name and has no way to
|
|
557
|
+
* know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the
|
|
558
|
+
* driver threw `no such column`, and the caller got
|
|
559
|
+
* `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver
|
|
560
|
+
* error class on the wire, and nothing actionable, for what is a plain typo.
|
|
561
|
+
* The DATA route has refused the same mistake with a `400 INVALID_FIELD`
|
|
562
|
+
* naming the field since #4315/#4254; this is the analytics half of that
|
|
563
|
+
* answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/
|
|
564
|
+
* `param`) so one mistake has one shape across both routes.
|
|
565
|
+
*
|
|
566
|
+
* What it checks, and what it deliberately does not:
|
|
567
|
+
*
|
|
568
|
+
* - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose
|
|
569
|
+
* `sql` is a real SQL expression has no field list to check against.
|
|
570
|
+
* - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.
|
|
571
|
+
* Absent hook / unknown object → stand down (see the config field's doc).
|
|
572
|
+
* - Only measures whose source is a BARE COLUMN. `count(*)` has no source
|
|
573
|
+
* field, and a dotted reference (`account.industry`) resolves through a
|
|
574
|
+
* join whose target this check cannot see — both pass through untouched.
|
|
575
|
+
* - `id` / `created_at` / `updated_at` are admitted unconditionally, matching
|
|
576
|
+
* the data path's `resolveQueryFields`: they are engine-assigned rather than
|
|
577
|
+
* declared, and a gate stricter than the engine it guards would reject
|
|
578
|
+
* queries that used to work.
|
|
579
|
+
*/
|
|
580
|
+
private assertMeasureFields;
|
|
522
581
|
/**
|
|
523
582
|
* [#3867] Gate on the cube auto-inference path: a name with no registered
|
|
524
583
|
* Cube may only be inferred into one if it is a registered object.
|
|
@@ -553,7 +612,7 @@ interface AnalyticsServicePluginOptions {
|
|
|
553
612
|
* Probe driver capabilities for a given cube.
|
|
554
613
|
* When omitted, defaults to in-memory only.
|
|
555
614
|
*/
|
|
556
|
-
queryCapabilities?: (cubeName: string) =>
|
|
615
|
+
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
|
|
557
616
|
/**
|
|
558
617
|
* Execute raw SQL on a driver. Enables NativeSQLStrategy.
|
|
559
618
|
*/
|
|
@@ -633,6 +692,14 @@ declare class AnalyticsServicePlugin implements Plugin {
|
|
|
633
692
|
version: string;
|
|
634
693
|
type: "standard";
|
|
635
694
|
dependencies: string[];
|
|
695
|
+
/**
|
|
696
|
+
* init() probes the `data` engine ObjectQLPlugin provides for the
|
|
697
|
+
* auto-bridge — order-if-present so the probe verdict is deterministic
|
|
698
|
+
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
|
|
699
|
+
* degrades on purpose (per-query lazy resolution / explicit
|
|
700
|
+
* `executeAggregate`).
|
|
701
|
+
*/
|
|
702
|
+
optionalDependencies: string[];
|
|
636
703
|
private service?;
|
|
637
704
|
private readonly options;
|
|
638
705
|
constructor(options?: AnalyticsServicePluginOptions);
|
|
@@ -650,6 +717,50 @@ declare function combineFilters(a?: FilterCondition, b?: FilterCondition): Filte
|
|
|
650
717
|
* Division by zero (and missing operands) yields `null` rather than Infinity/NaN.
|
|
651
718
|
*/
|
|
652
719
|
declare function evaluateDerivedMeasures(rows: Record<string, unknown>[], derived: DerivedMeasureSpec[]): Record<string, unknown>[];
|
|
720
|
+
/**
|
|
721
|
+
* Fill the EMPTY-GROUP value into every measure column the assembled grid
|
|
722
|
+
* LISTS but no query REPORTED — by aggregate kind (#4708, objectui#3136).
|
|
723
|
+
*
|
|
724
|
+
* The grid is assembled from several results: the primary query, one
|
|
725
|
+
* supplementary query per measure-scoped filter, and (for `compareTo`) a
|
|
726
|
+
* shifted pass. {@link mergeByDimensions} writes a measure's column only onto
|
|
727
|
+
* rows its source result returned, and a `GROUP BY` over a filtered row set
|
|
728
|
+
* emits NO group at all for a dimension value the filter excludes entirely.
|
|
729
|
+
* The column therefore comes back **absent**, not `0` — and absent renders as
|
|
730
|
+
* "no data for this row", which for a count is the opposite of what the row
|
|
731
|
+
* means. A derived ratio over it goes null as well ({@link computeDerived}
|
|
732
|
+
* treats a missing operand as unknowable), so the blank spreads.
|
|
733
|
+
*
|
|
734
|
+
* The bias runs the worst possible way: the rows that blank are the ones whose
|
|
735
|
+
* numerator the filter excluded — the WORST-performing rows. A `lead_source`
|
|
736
|
+
* that won nothing renders as "no data" while one that won everything renders
|
|
737
|
+
* fine.
|
|
738
|
+
*
|
|
739
|
+
* **Filled strictly by aggregate kind**, never wholesale. `count` /
|
|
740
|
+
* `count_distinct` over an excluded group is unambiguously `0` ("how many rows
|
|
741
|
+
* matched" has an exact answer when the answer is none), and `sum` over the
|
|
742
|
+
* empty set is its identity `0`. `avg` / `min` / `max` are genuinely null —
|
|
743
|
+
* there is nothing to average — and flattening those to `0` would trade this
|
|
744
|
+
* lie for the opposite one, reporting a measurement nobody made. The
|
|
745
|
+
* kind→identity mapping is `emptyGroupValueFor` in `@objectstack/spec/data`,
|
|
746
|
+
* shared with the authoring-side coherence checks so the two cannot drift.
|
|
747
|
+
*
|
|
748
|
+
* **Only rows that already exist are touched** — no group is invented. A
|
|
749
|
+
* dimension value no query reported at all has genuinely no data and stays out
|
|
750
|
+
* of the grid; this fills the cell, never the row.
|
|
751
|
+
*
|
|
752
|
+
* Deliberately NOT a `?? 0` in the widget or a `coalesce` in the measure: a
|
|
753
|
+
* consumer-side patch must be repeated by every author of every ratio widget
|
|
754
|
+
* forever, and forgetting it is silent. Only the executor knows which aggregate
|
|
755
|
+
* produced the gap, so only the executor can tell `0` from unknown.
|
|
756
|
+
*
|
|
757
|
+
* Mutates `rows` in place (they are already this pipeline's own copies) and
|
|
758
|
+
* returns them for chaining.
|
|
759
|
+
*
|
|
760
|
+
* @param columnAggregates - Grid column → the aggregate that produced it.
|
|
761
|
+
* Includes `<measure>__compare` columns, which merge through the same seam.
|
|
762
|
+
*/
|
|
763
|
+
declare function fillEmptyGroups(rows: Record<string, unknown>[], columnAggregates: Record<string, string | undefined>): Record<string, unknown>[];
|
|
653
764
|
/** Compute the comparison window for a [start,end] range. */
|
|
654
765
|
declare function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string];
|
|
655
766
|
declare class DatasetExecutor {
|
|
@@ -673,6 +784,37 @@ declare class DatasetExecutor {
|
|
|
673
784
|
*/
|
|
674
785
|
execute(compiledInput: CompiledDataset, selectionInput: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
675
786
|
private executeSelection;
|
|
787
|
+
/**
|
|
788
|
+
* Run ONE grouped pass over a set of base measures, honouring each measure's
|
|
789
|
+
* own scoped `filter`: the unfiltered measures in a single query, plus one
|
|
790
|
+
* supplementary query per filter-scoped measure, merged back by dimension key.
|
|
791
|
+
*
|
|
792
|
+
* **This is the executor's only implementation of "how a measure filter is
|
|
793
|
+
* applied", and every window goes through it** — the current period, each
|
|
794
|
+
* `totals` subset (which re-enters via `executeSelection`), and the
|
|
795
|
+
* `compareTo` window. Before #4820 the comparison window had its own,
|
|
796
|
+
* simpler answer: one shifted query over all base measures with only the
|
|
797
|
+
* base filter, so `compiled.measureFilters` was never read on that path.
|
|
798
|
+
* `won_count` counted won deals and `won_count__compare` counted every deal,
|
|
799
|
+
* under one label, in adjacent columns. Only measures carrying a filter were
|
|
800
|
+
* wrong — which is what made it survive: the unfiltered ones next to them
|
|
801
|
+
* compared correctly.
|
|
802
|
+
*
|
|
803
|
+
* The caller supplies the `selection` this pass queries under, which is how
|
|
804
|
+
* the comparison window differs at all: same measures, same dimensions, same
|
|
805
|
+
* filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else
|
|
806
|
+
* about the two passes may drift, because anything that does becomes a
|
|
807
|
+
* discrepancy between two columns the reader is invited to subtract.
|
|
808
|
+
*
|
|
809
|
+
* Cost: one extra query per filter-scoped measure when `compareTo` is set.
|
|
810
|
+
* The alternative — declaring the discrepancy in the response — is not one,
|
|
811
|
+
* since the two columns exist to be directly comparable.
|
|
812
|
+
*
|
|
813
|
+
* @param window - Ordering/window to push into the SQL. Only ever set for a
|
|
814
|
+
* selection the caller proved is a single self-sufficient query; a pass
|
|
815
|
+
* that fans out must return its whole grid for the merge.
|
|
816
|
+
*/
|
|
817
|
+
private runMeasurePass;
|
|
676
818
|
/**
|
|
677
819
|
* The selected dimensions the compiled cube types as `time`, in selection
|
|
678
820
|
* order (#3916) — the axis {@link resolveOrdering} defaults to ascending.
|
|
@@ -1038,4 +1180,4 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1038
1180
|
private buildFieldMeta;
|
|
1039
1181
|
}
|
|
1040
1182
|
|
|
1041
|
-
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 };
|
|
1183
|
+
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, fillEmptyGroups, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange, withLabelFetchCache };
|