@objectstack/service-analytics 17.0.0-rc.0 → 17.0.0-rc.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/CHANGELOG.md +2656 -0
- package/dist/index.cjs +414 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +406 -124
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.d.cts
CHANGED
|
@@ -336,6 +336,14 @@ interface AnalyticsServiceConfig {
|
|
|
336
336
|
* `StrategyContext.coerceTemporalFilterValue` for the full rationale.
|
|
337
337
|
*/
|
|
338
338
|
coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;
|
|
339
|
+
/**
|
|
340
|
+
* Normalise the COLUMN side of the same comparison to that storage form — the
|
|
341
|
+
* other half of the fix, needed because a SQLite `Field.datetime` holds both an
|
|
342
|
+
* INTEGER epoch (a `Date` write) and ISO TEXT (a REST/JSON write, a `NOW()`
|
|
343
|
+
* default) at once, so coercing only the comparand matches one of them and
|
|
344
|
+
* misses the other (#3912). See `StrategyContext.coerceTemporalFilterColumn`.
|
|
345
|
+
*/
|
|
346
|
+
coerceTemporalFilterColumn?: (objectName: string, fieldName: string, columnSql: string) => string;
|
|
339
347
|
/**
|
|
340
348
|
* ADR-0062 D6 — report whether an object is federated (external datasource).
|
|
341
349
|
* Threaded into the StrategyContext so `NativeSQLStrategy` declines external
|
|
@@ -617,6 +625,11 @@ interface AnalyticsServicePluginOptions {
|
|
|
617
625
|
*/
|
|
618
626
|
declare class AnalyticsServicePlugin implements Plugin {
|
|
619
627
|
name: string;
|
|
628
|
+
/**
|
|
629
|
+
* Services init() registers on every path (ADR-0116, #4131) — lets the
|
|
630
|
+
* kernel name this plugin when a consumer requires one before it inits.
|
|
631
|
+
*/
|
|
632
|
+
providesServices: string[];
|
|
620
633
|
version: string;
|
|
621
634
|
type: "standard";
|
|
622
635
|
dependencies: string[];
|
|
@@ -660,6 +673,17 @@ declare class DatasetExecutor {
|
|
|
660
673
|
*/
|
|
661
674
|
execute(compiledInput: CompiledDataset, selectionInput: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
662
675
|
private executeSelection;
|
|
676
|
+
/**
|
|
677
|
+
* The selected dimensions the compiled cube types as `time`, in selection
|
|
678
|
+
* order (#3916) — the axis {@link resolveOrdering} defaults to ascending.
|
|
679
|
+
*
|
|
680
|
+
* Membership is decided by the DIMENSION's declared type, not by whether the
|
|
681
|
+
* selection happens to bucket it: a `date` dimension left ungranulated groups
|
|
682
|
+
* raw timestamps, and those want chronological order every bit as much as
|
|
683
|
+
* month buckets do. (Both sort correctly — `compareValues` compares Dates and
|
|
684
|
+
* ISO strings chronologically, and bucket keys are minted sort-stable.)
|
|
685
|
+
*/
|
|
686
|
+
private timeDimensionsOf;
|
|
663
687
|
private buildQuery;
|
|
664
688
|
private runCompare;
|
|
665
689
|
}
|
|
@@ -681,6 +705,12 @@ declare function compileScopedFilterToSql(filter: FilterCondition, alias: string
|
|
|
681
705
|
* Pushes the analytics query down to the database as a native SQL statement.
|
|
682
706
|
* This is the most efficient path and is preferred whenever the backing driver
|
|
683
707
|
* supports raw SQL execution (e.g. Postgres, MySQL, SQLite).
|
|
708
|
+
*
|
|
709
|
+
* `resolveMeasureSql` used to answer `COUNT(*)` to three different questions it
|
|
710
|
+
* could not otherwise answer — an undeclared measure, a custom-SQL-expression
|
|
711
|
+
* metric type, and an unrecognised type. All three returned a plausible number
|
|
712
|
+
* for a query that asked for something else. They now emit the expression or
|
|
713
|
+
* throw; see that method. #4157.
|
|
684
714
|
*/
|
|
685
715
|
declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
686
716
|
readonly name = "NativeSQLStrategy";
|
|
@@ -767,6 +797,34 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
|
767
797
|
* correctly.
|
|
768
798
|
*/
|
|
769
799
|
private coerceTemporal;
|
|
800
|
+
/**
|
|
801
|
+
* The column side of {@link coerceTemporal}: normalise the reference so it
|
|
802
|
+
* reads in the storage form the comparand was coerced into.
|
|
803
|
+
*
|
|
804
|
+
* A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)
|
|
805
|
+
* and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's
|
|
806
|
+
* own `created_at`) at the SAME time, so coercing the value alone fixes one half
|
|
807
|
+
* and empties the other. That is #3912: a `dateRange: last_30_days` on
|
|
808
|
+
* `created_date` read 0 with 29 rows in range. Every other column and dialect
|
|
809
|
+
* gets its reference back verbatim.
|
|
810
|
+
*/
|
|
811
|
+
private temporalColumn;
|
|
812
|
+
/**
|
|
813
|
+
* Compile a normalized filter node into a boolean SQL expression, recursing
|
|
814
|
+
* through the combinators. `null` = no constraint.
|
|
815
|
+
*
|
|
816
|
+
* Leaves go through {@link buildFilterClause} exactly as they did when this
|
|
817
|
+
* was a flat loop, so the storage-form coercion and the calendar-day
|
|
818
|
+
* upper-bound rule (#3777) apply at every depth — including inside an `$or`,
|
|
819
|
+
* where a second, combinator-aware implementation would have been free to
|
|
820
|
+
* drift from the first.
|
|
821
|
+
*
|
|
822
|
+
* Parenthesisation is explicit rather than left to SQL's precedence: `AND`
|
|
823
|
+
* does bind tighter than `OR`, so `a AND b OR c` happens to be right, but
|
|
824
|
+
* being right by construction is what keeps a future edit from making it
|
|
825
|
+
* wrong.
|
|
826
|
+
*/
|
|
827
|
+
private compileFilterNode;
|
|
770
828
|
private buildFilterClause;
|
|
771
829
|
private extractObjectName;
|
|
772
830
|
private buildFieldMeta;
|
|
@@ -895,6 +953,26 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
895
953
|
* are handed back for the caller to AND in separately, so the engine
|
|
896
954
|
* intersects them instead of the strategy picking a winner.
|
|
897
955
|
*/
|
|
956
|
+
/**
|
|
957
|
+
* Fold a normalized filter node into the engine filter being built.
|
|
958
|
+
*
|
|
959
|
+
* AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly
|
|
960
|
+
* as the flat loop this replaced did — so a query without combinators still
|
|
961
|
+
* produces byte-identical engine input. Anything structural (`$or`, `$not`,
|
|
962
|
+
* a nested `$and` that cannot merge) becomes its own conjunct, which the
|
|
963
|
+
* caller ANDs in. The engine speaks these combinators natively
|
|
964
|
+
* (`FilterCondition` declares them and every driver compiles them), so this
|
|
965
|
+
* path hands them over rather than lowering them.
|
|
966
|
+
*/
|
|
967
|
+
private applyFilterNode;
|
|
968
|
+
/** A node as a standalone `FilterCondition` the engine can consume. */
|
|
969
|
+
private filterNodeToCondition;
|
|
970
|
+
/**
|
|
971
|
+
* Render a normalized filter node as the display SQL `/analytics/sql`
|
|
972
|
+
* echoes. Values still bind as `$n` placeholders — the echo travels to the
|
|
973
|
+
* browser, so a comparand is never inlined.
|
|
974
|
+
*/
|
|
975
|
+
private renderFilterNodeSql;
|
|
898
976
|
private mergeFilterOperand;
|
|
899
977
|
/**
|
|
900
978
|
* Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).
|
|
@@ -907,9 +985,13 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
907
985
|
* HERE on every driver — and "bucketed trend" is precisely the shape that also
|
|
908
986
|
* carries a range ("last 12 months", "this quarter").
|
|
909
987
|
*
|
|
910
|
-
* Bounds are inclusive on both ends —
|
|
911
|
-
* `
|
|
912
|
-
*
|
|
988
|
+
* Bounds are inclusive on both ends — logically "from day X through day Y".
|
|
989
|
+
* The `$lte` end is left as the bare calendar day on purpose: the driver's
|
|
990
|
+
* filter compiler owns the calendar-day → instant translation, compiling a
|
|
991
|
+
* bare-day `$lte` on a `datetime` column into the half-open `< nextDay`
|
|
992
|
+
* (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`
|
|
993
|
+
* performs the same half-open translation itself because it binds into raw
|
|
994
|
+
* SQL, so one dashboard reads the same on every driver.
|
|
913
995
|
*
|
|
914
996
|
* Comparands are coerced by the SAME helper the `where` path uses, so an
|
|
915
997
|
* epoch-ms bound recovers as a number and an ISO string stays a string. No
|
|
@@ -934,6 +1016,25 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
934
1016
|
private dateRangeBounds;
|
|
935
1017
|
private convertFilter;
|
|
936
1018
|
private extractObjectName;
|
|
1019
|
+
/**
|
|
1020
|
+
* The dimensions this query PROJECTS, in the order the result carries them:
|
|
1021
|
+
* every `dimensions` entry, then every granular `timeDimensions` entry that
|
|
1022
|
+
* is not already one of them.
|
|
1023
|
+
*
|
|
1024
|
+
* `timeDimensions` is not merely a filter carrier. An entry with a
|
|
1025
|
+
* `granularity` is GROUPED BY — see the `td.granularity` sites that build
|
|
1026
|
+
* groupBy here, in `generateSql` and in the cross-object path — so its
|
|
1027
|
+
* bucket is a COLUMN of the result; an entry without one only contributes a
|
|
1028
|
+
* `dateRange` predicate and must NOT be projected.
|
|
1029
|
+
*
|
|
1030
|
+
* Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly
|
|
1031
|
+
* that set. When they did not, a bucketed query returned rows carrying only
|
|
1032
|
+
* the measures and a `fields` list that never mentioned the bucket — a trend
|
|
1033
|
+
* chart got N values and no x-axis (#4033) — even though the SQL had
|
|
1034
|
+
* selected `date_trunc(…) AS "<dim>"` all along. One definition, every
|
|
1035
|
+
* consumer.
|
|
1036
|
+
*/
|
|
1037
|
+
private projectedDimensions;
|
|
937
1038
|
private buildFieldMeta;
|
|
938
1039
|
}
|
|
939
1040
|
|
package/dist/index.d.ts
CHANGED
|
@@ -336,6 +336,14 @@ interface AnalyticsServiceConfig {
|
|
|
336
336
|
* `StrategyContext.coerceTemporalFilterValue` for the full rationale.
|
|
337
337
|
*/
|
|
338
338
|
coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;
|
|
339
|
+
/**
|
|
340
|
+
* Normalise the COLUMN side of the same comparison to that storage form — the
|
|
341
|
+
* other half of the fix, needed because a SQLite `Field.datetime` holds both an
|
|
342
|
+
* INTEGER epoch (a `Date` write) and ISO TEXT (a REST/JSON write, a `NOW()`
|
|
343
|
+
* default) at once, so coercing only the comparand matches one of them and
|
|
344
|
+
* misses the other (#3912). See `StrategyContext.coerceTemporalFilterColumn`.
|
|
345
|
+
*/
|
|
346
|
+
coerceTemporalFilterColumn?: (objectName: string, fieldName: string, columnSql: string) => string;
|
|
339
347
|
/**
|
|
340
348
|
* ADR-0062 D6 — report whether an object is federated (external datasource).
|
|
341
349
|
* Threaded into the StrategyContext so `NativeSQLStrategy` declines external
|
|
@@ -617,6 +625,11 @@ interface AnalyticsServicePluginOptions {
|
|
|
617
625
|
*/
|
|
618
626
|
declare class AnalyticsServicePlugin implements Plugin {
|
|
619
627
|
name: string;
|
|
628
|
+
/**
|
|
629
|
+
* Services init() registers on every path (ADR-0116, #4131) — lets the
|
|
630
|
+
* kernel name this plugin when a consumer requires one before it inits.
|
|
631
|
+
*/
|
|
632
|
+
providesServices: string[];
|
|
620
633
|
version: string;
|
|
621
634
|
type: "standard";
|
|
622
635
|
dependencies: string[];
|
|
@@ -660,6 +673,17 @@ declare class DatasetExecutor {
|
|
|
660
673
|
*/
|
|
661
674
|
execute(compiledInput: CompiledDataset, selectionInput: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
662
675
|
private executeSelection;
|
|
676
|
+
/**
|
|
677
|
+
* The selected dimensions the compiled cube types as `time`, in selection
|
|
678
|
+
* order (#3916) — the axis {@link resolveOrdering} defaults to ascending.
|
|
679
|
+
*
|
|
680
|
+
* Membership is decided by the DIMENSION's declared type, not by whether the
|
|
681
|
+
* selection happens to bucket it: a `date` dimension left ungranulated groups
|
|
682
|
+
* raw timestamps, and those want chronological order every bit as much as
|
|
683
|
+
* month buckets do. (Both sort correctly — `compareValues` compares Dates and
|
|
684
|
+
* ISO strings chronologically, and bucket keys are minted sort-stable.)
|
|
685
|
+
*/
|
|
686
|
+
private timeDimensionsOf;
|
|
663
687
|
private buildQuery;
|
|
664
688
|
private runCompare;
|
|
665
689
|
}
|
|
@@ -681,6 +705,12 @@ declare function compileScopedFilterToSql(filter: FilterCondition, alias: string
|
|
|
681
705
|
* Pushes the analytics query down to the database as a native SQL statement.
|
|
682
706
|
* This is the most efficient path and is preferred whenever the backing driver
|
|
683
707
|
* supports raw SQL execution (e.g. Postgres, MySQL, SQLite).
|
|
708
|
+
*
|
|
709
|
+
* `resolveMeasureSql` used to answer `COUNT(*)` to three different questions it
|
|
710
|
+
* could not otherwise answer — an undeclared measure, a custom-SQL-expression
|
|
711
|
+
* metric type, and an unrecognised type. All three returned a plausible number
|
|
712
|
+
* for a query that asked for something else. They now emit the expression or
|
|
713
|
+
* throw; see that method. #4157.
|
|
684
714
|
*/
|
|
685
715
|
declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
686
716
|
readonly name = "NativeSQLStrategy";
|
|
@@ -767,6 +797,34 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
|
767
797
|
* correctly.
|
|
768
798
|
*/
|
|
769
799
|
private coerceTemporal;
|
|
800
|
+
/**
|
|
801
|
+
* The column side of {@link coerceTemporal}: normalise the reference so it
|
|
802
|
+
* reads in the storage form the comparand was coerced into.
|
|
803
|
+
*
|
|
804
|
+
* A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)
|
|
805
|
+
* and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's
|
|
806
|
+
* own `created_at`) at the SAME time, so coercing the value alone fixes one half
|
|
807
|
+
* and empties the other. That is #3912: a `dateRange: last_30_days` on
|
|
808
|
+
* `created_date` read 0 with 29 rows in range. Every other column and dialect
|
|
809
|
+
* gets its reference back verbatim.
|
|
810
|
+
*/
|
|
811
|
+
private temporalColumn;
|
|
812
|
+
/**
|
|
813
|
+
* Compile a normalized filter node into a boolean SQL expression, recursing
|
|
814
|
+
* through the combinators. `null` = no constraint.
|
|
815
|
+
*
|
|
816
|
+
* Leaves go through {@link buildFilterClause} exactly as they did when this
|
|
817
|
+
* was a flat loop, so the storage-form coercion and the calendar-day
|
|
818
|
+
* upper-bound rule (#3777) apply at every depth — including inside an `$or`,
|
|
819
|
+
* where a second, combinator-aware implementation would have been free to
|
|
820
|
+
* drift from the first.
|
|
821
|
+
*
|
|
822
|
+
* Parenthesisation is explicit rather than left to SQL's precedence: `AND`
|
|
823
|
+
* does bind tighter than `OR`, so `a AND b OR c` happens to be right, but
|
|
824
|
+
* being right by construction is what keeps a future edit from making it
|
|
825
|
+
* wrong.
|
|
826
|
+
*/
|
|
827
|
+
private compileFilterNode;
|
|
770
828
|
private buildFilterClause;
|
|
771
829
|
private extractObjectName;
|
|
772
830
|
private buildFieldMeta;
|
|
@@ -895,6 +953,26 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
895
953
|
* are handed back for the caller to AND in separately, so the engine
|
|
896
954
|
* intersects them instead of the strategy picking a winner.
|
|
897
955
|
*/
|
|
956
|
+
/**
|
|
957
|
+
* Fold a normalized filter node into the engine filter being built.
|
|
958
|
+
*
|
|
959
|
+
* AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly
|
|
960
|
+
* as the flat loop this replaced did — so a query without combinators still
|
|
961
|
+
* produces byte-identical engine input. Anything structural (`$or`, `$not`,
|
|
962
|
+
* a nested `$and` that cannot merge) becomes its own conjunct, which the
|
|
963
|
+
* caller ANDs in. The engine speaks these combinators natively
|
|
964
|
+
* (`FilterCondition` declares them and every driver compiles them), so this
|
|
965
|
+
* path hands them over rather than lowering them.
|
|
966
|
+
*/
|
|
967
|
+
private applyFilterNode;
|
|
968
|
+
/** A node as a standalone `FilterCondition` the engine can consume. */
|
|
969
|
+
private filterNodeToCondition;
|
|
970
|
+
/**
|
|
971
|
+
* Render a normalized filter node as the display SQL `/analytics/sql`
|
|
972
|
+
* echoes. Values still bind as `$n` placeholders — the echo travels to the
|
|
973
|
+
* browser, so a comparand is never inlined.
|
|
974
|
+
*/
|
|
975
|
+
private renderFilterNodeSql;
|
|
898
976
|
private mergeFilterOperand;
|
|
899
977
|
/**
|
|
900
978
|
* Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).
|
|
@@ -907,9 +985,13 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
907
985
|
* HERE on every driver — and "bucketed trend" is precisely the shape that also
|
|
908
986
|
* carries a range ("last 12 months", "this quarter").
|
|
909
987
|
*
|
|
910
|
-
* Bounds are inclusive on both ends —
|
|
911
|
-
* `
|
|
912
|
-
*
|
|
988
|
+
* Bounds are inclusive on both ends — logically "from day X through day Y".
|
|
989
|
+
* The `$lte` end is left as the bare calendar day on purpose: the driver's
|
|
990
|
+
* filter compiler owns the calendar-day → instant translation, compiling a
|
|
991
|
+
* bare-day `$lte` on a `datetime` column into the half-open `< nextDay`
|
|
992
|
+
* (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`
|
|
993
|
+
* performs the same half-open translation itself because it binds into raw
|
|
994
|
+
* SQL, so one dashboard reads the same on every driver.
|
|
913
995
|
*
|
|
914
996
|
* Comparands are coerced by the SAME helper the `where` path uses, so an
|
|
915
997
|
* epoch-ms bound recovers as a number and an ISO string stays a string. No
|
|
@@ -934,6 +1016,25 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
934
1016
|
private dateRangeBounds;
|
|
935
1017
|
private convertFilter;
|
|
936
1018
|
private extractObjectName;
|
|
1019
|
+
/**
|
|
1020
|
+
* The dimensions this query PROJECTS, in the order the result carries them:
|
|
1021
|
+
* every `dimensions` entry, then every granular `timeDimensions` entry that
|
|
1022
|
+
* is not already one of them.
|
|
1023
|
+
*
|
|
1024
|
+
* `timeDimensions` is not merely a filter carrier. An entry with a
|
|
1025
|
+
* `granularity` is GROUPED BY — see the `td.granularity` sites that build
|
|
1026
|
+
* groupBy here, in `generateSql` and in the cross-object path — so its
|
|
1027
|
+
* bucket is a COLUMN of the result; an entry without one only contributes a
|
|
1028
|
+
* `dateRange` predicate and must NOT be projected.
|
|
1029
|
+
*
|
|
1030
|
+
* Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly
|
|
1031
|
+
* that set. When they did not, a bucketed query returned rows carrying only
|
|
1032
|
+
* the measures and a `fields` list that never mentioned the bucket — a trend
|
|
1033
|
+
* chart got N values and no x-axis (#4033) — even though the SQL had
|
|
1034
|
+
* selected `date_trunc(…) AS "<dim>"` all along. One definition, every
|
|
1035
|
+
* consumer.
|
|
1036
|
+
*/
|
|
1037
|
+
private projectedDimensions;
|
|
937
1038
|
private buildFieldMeta;
|
|
938
1039
|
}
|
|
939
1040
|
|