@objectstack/driver-memory 2.0.7 → 3.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +20 -0
- package/dist/index.d.mts +68 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +396 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +394 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +3 -0
- package/src/memory-analytics.test.ts +346 -0
- package/src/memory-analytics.ts +518 -0
- package/src/memory-driver.ts +3 -3
package/dist/index.mjs
CHANGED
|
@@ -676,6 +676,399 @@ var InMemoryDriver = class {
|
|
|
676
676
|
}
|
|
677
677
|
};
|
|
678
678
|
|
|
679
|
+
// src/memory-analytics.ts
|
|
680
|
+
import { createLogger as createLogger2 } from "@objectstack/core";
|
|
681
|
+
var MemoryAnalyticsService = class {
|
|
682
|
+
constructor(config) {
|
|
683
|
+
this.driver = config.driver;
|
|
684
|
+
this.cubes = new Map(config.cubes.map((c) => [c.name, c]));
|
|
685
|
+
this.logger = config.logger || createLogger2({ level: "info", format: "pretty" });
|
|
686
|
+
this.logger.debug("MemoryAnalyticsService initialized", { cubeCount: this.cubes.size });
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Execute an analytical query using the memory driver's aggregation pipeline
|
|
690
|
+
*/
|
|
691
|
+
async query(query) {
|
|
692
|
+
this.logger.debug("Executing analytics query", { cube: query.cube, measures: query.measures });
|
|
693
|
+
if (!query.cube) {
|
|
694
|
+
throw new Error("Cube name is required");
|
|
695
|
+
}
|
|
696
|
+
const cube = this.cubes.get(query.cube);
|
|
697
|
+
if (!cube) {
|
|
698
|
+
throw new Error(`Cube not found: ${query.cube}`);
|
|
699
|
+
}
|
|
700
|
+
const pipeline = [];
|
|
701
|
+
if (query.filters && query.filters.length > 0) {
|
|
702
|
+
const matchStage = {};
|
|
703
|
+
for (const filter of query.filters) {
|
|
704
|
+
const mongoOp = this.convertOperatorToMongo(filter.operator);
|
|
705
|
+
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
706
|
+
if (filter.values && filter.values.length > 0) {
|
|
707
|
+
if (mongoOp === "$in") {
|
|
708
|
+
matchStage[fieldPath] = { $in: filter.values };
|
|
709
|
+
} else if (mongoOp === "$nin") {
|
|
710
|
+
matchStage[fieldPath] = { $nin: filter.values };
|
|
711
|
+
} else {
|
|
712
|
+
matchStage[fieldPath] = { [mongoOp]: filter.values[0] };
|
|
713
|
+
}
|
|
714
|
+
} else if (mongoOp === "$exists") {
|
|
715
|
+
matchStage[fieldPath] = { $exists: filter.operator === "set" };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (Object.keys(matchStage).length > 0) {
|
|
719
|
+
pipeline.push({ $match: matchStage });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
723
|
+
for (const timeDim of query.timeDimensions) {
|
|
724
|
+
const fieldPath = this.resolveFieldPath(cube, timeDim.dimension);
|
|
725
|
+
if (timeDim.dateRange) {
|
|
726
|
+
const range = Array.isArray(timeDim.dateRange) ? timeDim.dateRange : this.parseDateRangeString(timeDim.dateRange);
|
|
727
|
+
if (range.length === 2) {
|
|
728
|
+
pipeline.push({
|
|
729
|
+
$match: {
|
|
730
|
+
[fieldPath]: {
|
|
731
|
+
$gte: new Date(range[0]),
|
|
732
|
+
$lte: new Date(range[1])
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const groupStage = { _id: {} };
|
|
741
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
742
|
+
for (const dim of query.dimensions) {
|
|
743
|
+
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
744
|
+
const dimName = this.getShortName(dim);
|
|
745
|
+
groupStage._id[dimName] = `$${fieldPath}`;
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
groupStage._id = null;
|
|
749
|
+
}
|
|
750
|
+
if (query.measures && query.measures.length > 0) {
|
|
751
|
+
for (const measure of query.measures) {
|
|
752
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
753
|
+
const measureName = this.getShortName(measure);
|
|
754
|
+
if (measureDef) {
|
|
755
|
+
const aggregator = this.buildAggregator(measureDef);
|
|
756
|
+
groupStage[measureName] = aggregator;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
pipeline.push({ $group: groupStage });
|
|
761
|
+
const projectStage = { _id: 0 };
|
|
762
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
763
|
+
for (const dim of query.dimensions) {
|
|
764
|
+
const dimName = this.getShortName(dim);
|
|
765
|
+
projectStage[dimName] = `$_id.${dimName}`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (query.measures && query.measures.length > 0) {
|
|
769
|
+
for (const measure of query.measures) {
|
|
770
|
+
const measureName = this.getShortName(measure);
|
|
771
|
+
projectStage[measureName] = `$${measureName}`;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
pipeline.push({ $project: projectStage });
|
|
775
|
+
if (query.order && Object.keys(query.order).length > 0) {
|
|
776
|
+
const sortStage = {};
|
|
777
|
+
for (const [field, direction] of Object.entries(query.order)) {
|
|
778
|
+
const shortName = this.getShortName(field);
|
|
779
|
+
sortStage[shortName] = direction === "asc" ? 1 : -1;
|
|
780
|
+
}
|
|
781
|
+
pipeline.push({ $sort: sortStage });
|
|
782
|
+
}
|
|
783
|
+
if (query.offset) {
|
|
784
|
+
pipeline.push({ $skip: query.offset });
|
|
785
|
+
}
|
|
786
|
+
if (query.limit) {
|
|
787
|
+
pipeline.push({ $limit: query.limit });
|
|
788
|
+
}
|
|
789
|
+
const tableName = this.extractTableName(cube.sql);
|
|
790
|
+
const rawRows = await this.driver.aggregate(tableName, pipeline);
|
|
791
|
+
const rows = rawRows.map((row) => {
|
|
792
|
+
const renamedRow = {};
|
|
793
|
+
if (query.dimensions) {
|
|
794
|
+
for (const dim of query.dimensions) {
|
|
795
|
+
const shortName = this.getShortName(dim);
|
|
796
|
+
if (shortName in row) {
|
|
797
|
+
renamedRow[dim] = row[shortName];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (query.measures) {
|
|
802
|
+
for (const measure of query.measures) {
|
|
803
|
+
const shortName = this.getShortName(measure);
|
|
804
|
+
if (shortName in row) {
|
|
805
|
+
renamedRow[measure] = row[shortName];
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return renamedRow;
|
|
810
|
+
});
|
|
811
|
+
const fields = [];
|
|
812
|
+
if (query.dimensions) {
|
|
813
|
+
for (const dim of query.dimensions) {
|
|
814
|
+
const dimension = this.resolveDimension(cube, dim);
|
|
815
|
+
fields.push({
|
|
816
|
+
name: dim,
|
|
817
|
+
type: dimension?.type || "string"
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (query.measures) {
|
|
822
|
+
for (const measure of query.measures) {
|
|
823
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
824
|
+
fields.push({
|
|
825
|
+
name: measure,
|
|
826
|
+
type: this.measureTypeToFieldType(measureDef?.type || "count")
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
this.logger.debug("Analytics query completed", { rowCount: rows.length });
|
|
831
|
+
return {
|
|
832
|
+
rows,
|
|
833
|
+
fields,
|
|
834
|
+
sql: this.generateSqlFromPipeline(tableName, pipeline)
|
|
835
|
+
// For debugging
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Get available cube metadata for discovery
|
|
840
|
+
*/
|
|
841
|
+
async getMeta(cubeName) {
|
|
842
|
+
const cubes = cubeName ? [this.cubes.get(cubeName)].filter(Boolean) : Array.from(this.cubes.values());
|
|
843
|
+
return cubes.map((cube) => ({
|
|
844
|
+
name: cube.name,
|
|
845
|
+
title: cube.title,
|
|
846
|
+
measures: Object.entries(cube.measures).map(([key, measure]) => ({
|
|
847
|
+
name: `${cube.name}.${key}`,
|
|
848
|
+
type: measure.type,
|
|
849
|
+
title: measure.label
|
|
850
|
+
})),
|
|
851
|
+
dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({
|
|
852
|
+
name: `${cube.name}.${key}`,
|
|
853
|
+
type: dimension.type,
|
|
854
|
+
title: dimension.label
|
|
855
|
+
}))
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Generate SQL representation for debugging/transparency
|
|
860
|
+
*/
|
|
861
|
+
async generateSql(query) {
|
|
862
|
+
if (!query.cube) {
|
|
863
|
+
throw new Error("Cube name is required");
|
|
864
|
+
}
|
|
865
|
+
const cube = this.cubes.get(query.cube);
|
|
866
|
+
if (!cube) {
|
|
867
|
+
throw new Error(`Cube not found: ${query.cube}`);
|
|
868
|
+
}
|
|
869
|
+
const tableName = this.extractTableName(cube.sql);
|
|
870
|
+
const selectClauses = [];
|
|
871
|
+
const groupByClauses = [];
|
|
872
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
873
|
+
for (const dim of query.dimensions) {
|
|
874
|
+
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
875
|
+
selectClauses.push(`${fieldPath} AS "${dim}"`);
|
|
876
|
+
groupByClauses.push(fieldPath);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (query.measures && query.measures.length > 0) {
|
|
880
|
+
for (const measure of query.measures) {
|
|
881
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
882
|
+
if (measureDef) {
|
|
883
|
+
const aggSql = this.measureToSql(measureDef);
|
|
884
|
+
selectClauses.push(`${aggSql} AS "${measure}"`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const whereClauses = [];
|
|
889
|
+
if (query.filters && query.filters.length > 0) {
|
|
890
|
+
for (const filter of query.filters) {
|
|
891
|
+
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
892
|
+
const sqlOp = this.operatorToSql(filter.operator);
|
|
893
|
+
if (filter.values && filter.values.length > 0) {
|
|
894
|
+
whereClauses.push(`${fieldPath} ${sqlOp} '${filter.values[0]}'`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
let sql = `SELECT ${selectClauses.join(", ")} FROM ${tableName}`;
|
|
899
|
+
if (whereClauses.length > 0) {
|
|
900
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
901
|
+
}
|
|
902
|
+
if (groupByClauses.length > 0) {
|
|
903
|
+
sql += ` GROUP BY ${groupByClauses.join(", ")}`;
|
|
904
|
+
}
|
|
905
|
+
if (query.order) {
|
|
906
|
+
const orderClauses = Object.entries(query.order).map(
|
|
907
|
+
([field, dir]) => `"${field}" ${dir.toUpperCase()}`
|
|
908
|
+
);
|
|
909
|
+
sql += ` ORDER BY ${orderClauses.join(", ")}`;
|
|
910
|
+
}
|
|
911
|
+
if (query.limit) {
|
|
912
|
+
sql += ` LIMIT ${query.limit}`;
|
|
913
|
+
}
|
|
914
|
+
if (query.offset) {
|
|
915
|
+
sql += ` OFFSET ${query.offset}`;
|
|
916
|
+
}
|
|
917
|
+
return { sql, params: [] };
|
|
918
|
+
}
|
|
919
|
+
// ===================================
|
|
920
|
+
// Helper Methods
|
|
921
|
+
// ===================================
|
|
922
|
+
resolveFieldPath(cube, member) {
|
|
923
|
+
const parts = member.split(".");
|
|
924
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
925
|
+
const dimension = cube.dimensions[fieldName];
|
|
926
|
+
if (dimension) {
|
|
927
|
+
return dimension.sql.replace(/^\$/, "");
|
|
928
|
+
}
|
|
929
|
+
const measure = cube.measures[fieldName];
|
|
930
|
+
if (measure) {
|
|
931
|
+
return measure.sql.replace(/^\$/, "");
|
|
932
|
+
}
|
|
933
|
+
return fieldName;
|
|
934
|
+
}
|
|
935
|
+
resolveMeasure(cube, measureName) {
|
|
936
|
+
const parts = measureName.split(".");
|
|
937
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
938
|
+
return cube.measures[fieldName];
|
|
939
|
+
}
|
|
940
|
+
resolveDimension(cube, dimensionName) {
|
|
941
|
+
const parts = dimensionName.split(".");
|
|
942
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
943
|
+
return cube.dimensions[fieldName];
|
|
944
|
+
}
|
|
945
|
+
getShortName(fullName) {
|
|
946
|
+
const parts = fullName.split(".");
|
|
947
|
+
return parts.length > 1 ? parts[1] : parts[0];
|
|
948
|
+
}
|
|
949
|
+
buildAggregator(measure) {
|
|
950
|
+
const fieldPath = measure.sql.replace(/^\$/, "");
|
|
951
|
+
switch (measure.type) {
|
|
952
|
+
case "count":
|
|
953
|
+
return { $sum: 1 };
|
|
954
|
+
case "sum":
|
|
955
|
+
return { $sum: `$${fieldPath}` };
|
|
956
|
+
case "avg":
|
|
957
|
+
return { $avg: `$${fieldPath}` };
|
|
958
|
+
case "min":
|
|
959
|
+
return { $min: `$${fieldPath}` };
|
|
960
|
+
case "max":
|
|
961
|
+
return { $max: `$${fieldPath}` };
|
|
962
|
+
case "count_distinct":
|
|
963
|
+
return { $addToSet: `$${fieldPath}` };
|
|
964
|
+
// Will need post-processing for count
|
|
965
|
+
default:
|
|
966
|
+
return { $sum: 1 };
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
measureTypeToFieldType(measureType) {
|
|
970
|
+
switch (measureType) {
|
|
971
|
+
case "count":
|
|
972
|
+
case "sum":
|
|
973
|
+
case "count_distinct":
|
|
974
|
+
return "number";
|
|
975
|
+
case "avg":
|
|
976
|
+
case "min":
|
|
977
|
+
case "max":
|
|
978
|
+
return "number";
|
|
979
|
+
case "string":
|
|
980
|
+
return "string";
|
|
981
|
+
case "boolean":
|
|
982
|
+
return "boolean";
|
|
983
|
+
default:
|
|
984
|
+
return "number";
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
convertOperatorToMongo(operator) {
|
|
988
|
+
const opMap = {
|
|
989
|
+
"equals": "$eq",
|
|
990
|
+
"notEquals": "$ne",
|
|
991
|
+
"contains": "$regex",
|
|
992
|
+
"notContains": "$not",
|
|
993
|
+
"gt": "$gt",
|
|
994
|
+
"gte": "$gte",
|
|
995
|
+
"lt": "$lt",
|
|
996
|
+
"lte": "$lte",
|
|
997
|
+
"set": "$exists",
|
|
998
|
+
"notSet": "$exists",
|
|
999
|
+
"inDateRange": "$gte"
|
|
1000
|
+
// Will need special handling
|
|
1001
|
+
};
|
|
1002
|
+
return opMap[operator] || "$eq";
|
|
1003
|
+
}
|
|
1004
|
+
operatorToSql(operator) {
|
|
1005
|
+
const opMap = {
|
|
1006
|
+
"equals": "=",
|
|
1007
|
+
"notEquals": "!=",
|
|
1008
|
+
"contains": "LIKE",
|
|
1009
|
+
"notContains": "NOT LIKE",
|
|
1010
|
+
"gt": ">",
|
|
1011
|
+
"gte": ">=",
|
|
1012
|
+
"lt": "<",
|
|
1013
|
+
"lte": "<="
|
|
1014
|
+
};
|
|
1015
|
+
return opMap[operator] || "=";
|
|
1016
|
+
}
|
|
1017
|
+
measureToSql(measure) {
|
|
1018
|
+
const fieldPath = measure.sql.replace(/^\$/, "");
|
|
1019
|
+
switch (measure.type) {
|
|
1020
|
+
case "count":
|
|
1021
|
+
return "COUNT(*)";
|
|
1022
|
+
case "sum":
|
|
1023
|
+
return `SUM(${fieldPath})`;
|
|
1024
|
+
case "avg":
|
|
1025
|
+
return `AVG(${fieldPath})`;
|
|
1026
|
+
case "min":
|
|
1027
|
+
return `MIN(${fieldPath})`;
|
|
1028
|
+
case "max":
|
|
1029
|
+
return `MAX(${fieldPath})`;
|
|
1030
|
+
case "count_distinct":
|
|
1031
|
+
return `COUNT(DISTINCT ${fieldPath})`;
|
|
1032
|
+
default:
|
|
1033
|
+
return "COUNT(*)";
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
extractTableName(sql) {
|
|
1037
|
+
return sql.trim();
|
|
1038
|
+
}
|
|
1039
|
+
parseDateRangeString(range) {
|
|
1040
|
+
const now = /* @__PURE__ */ new Date();
|
|
1041
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1042
|
+
if (range === "today") {
|
|
1043
|
+
return [today.toISOString(), new Date(today.getTime() + 864e5).toISOString()];
|
|
1044
|
+
} else if (range.startsWith("last ")) {
|
|
1045
|
+
const parts = range.split(" ");
|
|
1046
|
+
const num = parseInt(parts[1]);
|
|
1047
|
+
const unit = parts[2];
|
|
1048
|
+
const start = new Date(today);
|
|
1049
|
+
if (unit.startsWith("day")) {
|
|
1050
|
+
start.setDate(start.getDate() - num);
|
|
1051
|
+
} else if (unit.startsWith("week")) {
|
|
1052
|
+
start.setDate(start.getDate() - num * 7);
|
|
1053
|
+
} else if (unit.startsWith("month")) {
|
|
1054
|
+
start.setMonth(start.getMonth() - num);
|
|
1055
|
+
} else if (unit.startsWith("year")) {
|
|
1056
|
+
start.setFullYear(start.getFullYear() - num);
|
|
1057
|
+
}
|
|
1058
|
+
return [start.toISOString(), now.toISOString()];
|
|
1059
|
+
}
|
|
1060
|
+
return [range, range];
|
|
1061
|
+
}
|
|
1062
|
+
generateSqlFromPipeline(table, pipeline) {
|
|
1063
|
+
const stages = pipeline.map((stage, idx) => {
|
|
1064
|
+
const op = Object.keys(stage)[0];
|
|
1065
|
+
return `/* Stage ${idx + 1}: ${op} */ ${JSON.stringify(stage[op])}`;
|
|
1066
|
+
}).join("\n");
|
|
1067
|
+
return `-- MongoDB Aggregation Pipeline on table: ${table}
|
|
1068
|
+
${stages}`;
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
679
1072
|
// src/index.ts
|
|
680
1073
|
var index_default = {
|
|
681
1074
|
id: "com.objectstack.driver.memory",
|
|
@@ -694,6 +1087,7 @@ var index_default = {
|
|
|
694
1087
|
};
|
|
695
1088
|
export {
|
|
696
1089
|
InMemoryDriver,
|
|
1090
|
+
MemoryAnalyticsService,
|
|
697
1091
|
index_default as default
|
|
698
1092
|
};
|
|
699
1093
|
//# sourceMappingURL=index.mjs.map
|