@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/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