spice-js 2.6.64 → 2.6.66

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.
@@ -916,25 +916,76 @@ class SpiceModel {
916
916
  JSON.stringify(this);
917
917
  }
918
918
 
919
- prepColumns(columns) {
920
- if (columns && columns !== "") {
921
- var columnList = columns.split(",");
922
- return _.join(_.compact(columnList.map(column => {
923
- if (column === "meta().id") return undefined;
919
+ buildArrayProjection(alias, field) {
920
+ var safeAlias = String(alias).replace(/`/g, "");
921
+ var safeField = String(field).replace(/`/g, "");
922
+ return "ARRAY rc." + safeField + " FOR rc IN IFMISSINGORNULL(" + safeAlias + ", []) END AS `" + safeAlias + "_" + safeField + "`";
923
+ }
924
+
925
+ prepColumns(columns, protectedAliases, arrayAliases) {
926
+ if (protectedAliases === void 0) {
927
+ protectedAliases = [];
928
+ }
929
+
930
+ if (arrayAliases === void 0) {
931
+ arrayAliases = [];
932
+ }
933
+
934
+ if (!columns || columns === "") return columns;
935
+ var protectedSet = new Set(protectedAliases);
936
+ var arraySet = new Set(arrayAliases);
937
+ var tokens = columns.split(",");
938
+ var out = tokens.map(raw => {
939
+ var col = (raw || "").trim();
940
+ if (col === "" || col === "meta().id") return undefined;
941
+ if (/^\s*ARRAY\s+/i.test(col) || /\w+\s*\(/.test(col)) return col;
942
+ var m = col.match(/^\s*`?(\w+)`?\.`?(\w+)`?(?:\s+AS\s+`?([\w]+)`?)?\s*$/i);
943
+
944
+ if (m) {
945
+ var alias = m[1];
946
+ var field = m[2];
947
+ var explicitAs = m[3];
948
+
949
+ if (arraySet.has(alias)) {
950
+ var proj = this.buildArrayProjection(alias, field);
951
+
952
+ if (explicitAs && explicitAs !== alias + "_" + field) {
953
+ proj = proj.replace(/AS\s+`[^`]+`$/i, "AS `" + explicitAs + "`");
954
+ }
924
955
 
925
- if (!column.startsWith("`") && !column.endsWith("`")) {
926
- column = "`" + column.trim() + "`";
956
+ return proj;
927
957
  }
928
958
 
929
- if (column && !column.includes(".") && !column.startsWith(this.type)) {
930
- column = "`" + this.type + "`." + column.trim();
959
+ if (protectedSet.has(alias)) {
960
+ var aliased = "`" + alias + "`." + (field.startsWith("`") ? field : "`" + field + "`");
961
+ return explicitAs ? aliased + " AS `" + explicitAs + "`" : aliased;
931
962
  }
932
963
 
933
- return column;
934
- })), ",");
935
- }
964
+ var bare = col.replace(/`/g, "");
965
+ return "`" + this.type + "`." + bare;
966
+ }
967
+
968
+ var looksProtected = [...protectedSet].some(a => col === a || col === "`" + a + "`" || col.startsWith(a + ".") || col.startsWith("`" + a + "`."));
936
969
 
937
- return columns;
970
+ if (!looksProtected) {
971
+ if (!col.startsWith("`") && !col.endsWith("`") && !col.includes("(")) {
972
+ col = "`" + col + "`";
973
+ }
974
+
975
+ if (col && !col.includes(".") && !col.startsWith(this.type)) {
976
+ col = "`" + this.type + "`." + col.replace(/`/g, "");
977
+ }
978
+
979
+ return col;
980
+ }
981
+
982
+ if (!col.includes(".") && !col.startsWith("`")) {
983
+ return "`" + col + "`";
984
+ }
985
+
986
+ return col;
987
+ });
988
+ return _.join(_.compact(out), ",");
938
989
  }
939
990
 
940
991
  filterResultsByColumns(data, columns) {
@@ -989,12 +1040,20 @@ class SpiceModel {
989
1040
  return [...new Set(returnVal)];
990
1041
  }
991
1042
 
992
- createJoinSection(nestings) {
993
- return nestings.map(nesting => {
994
- if (nesting.type === _2.DataType.ARRAY || nesting.type === Array || nesting.type === "array") {
995
- return "LEFT NEST `" + (0, _fix.fixCollection)(nesting.reference) + "` AS `" + nesting.alias + "` ON KEYS `" + this.type + "`.`" + nesting.alias + "`";
1043
+ createJoinSection(mappedNestings) {
1044
+ if (!mappedNestings || mappedNestings.length === 0) return "";
1045
+ return mappedNestings.map((_ref2) => {
1046
+ var {
1047
+ alias,
1048
+ reference,
1049
+ is_array
1050
+ } = _ref2;
1051
+ var keyspace = (0, _fix.fixCollection)(reference);
1052
+
1053
+ if (is_array === true) {
1054
+ return "LEFT NEST `" + keyspace + "` AS `" + alias + "` ON KEYS `" + this.type + "`.`" + alias + "`";
996
1055
  } else {
997
- return "LEFT JOIN `" + (0, _fix.fixCollection)(nesting.reference) + "` AS `" + nesting.alias + "` ON KEYS `" + this.type + "`.`" + nesting.alias + "`";
1056
+ return "LEFT JOIN `" + keyspace + "` AS `" + alias + "` ON KEYS `" + this.type + "`.`" + alias + "`";
998
1057
  }
999
1058
  }).join(" ");
1000
1059
  }
@@ -1009,67 +1068,63 @@ class SpiceModel {
1009
1068
  } */
1010
1069
 
1011
1070
 
1012
- fixColumnName(columns) {
1013
- // Guard clause: if columns is not provided or not a string, return it as-is.
1014
- if (!columns || typeof columns !== "string") {
1015
- return columns;
1016
- } // Split the columns string on commas and trim each column expression.
1071
+ fixColumnName(columns, protectedAliases) {
1072
+ if (protectedAliases === void 0) {
1073
+ protectedAliases = [];
1074
+ }
1075
+
1076
+ if (!columns || typeof columns !== "string") return columns;
1077
+ var protectedSet = new Set(protectedAliases);
1078
+ var tokens = columns.split(",").map(s => s.trim());
1079
+ var aliasTracker = {};
1080
+ var out = tokens.map(col => {
1081
+ if (!col) return undefined; // Do not rewrite ARRAY projections
1017
1082
 
1083
+ if (/^\s*ARRAY\s+/i.test(col)) return col; // If token is literally this.type.alias → compress to `alias`
1084
+
1085
+ for (var a of protectedSet) {
1086
+ var re = new RegExp("^`?" + _.escapeRegExp(this.type) + "`?\\.`?" + _.escapeRegExp(a) + "`?$");
1087
+
1088
+ if (re.test(col)) {
1089
+ return "`" + a + "`";
1090
+ }
1091
+ } // Extract explicit AS
1018
1092
 
1019
- var columnList = columns.split(",").map(col => col.trim()); // Object to keep track of alias usage to avoid duplicates.
1020
1093
 
1021
- var aliasTracker = {};
1022
- var fixedColumns = columnList.map(col => {
1023
- // Check if an explicit alias is already provided.
1024
1094
  var aliasRegex = /\s+AS\s+`?([\w]+)`?$/i;
1025
1095
  var aliasMatch = col.match(aliasRegex);
1026
- var explicitAlias = aliasMatch ? aliasMatch[1] : null; // Remove the alias clause for easier processing.
1096
+ var explicitAlias = aliasMatch ? aliasMatch[1] : null;
1097
+ var colWithoutAlias = explicitAlias ? col.replace(aliasRegex, "").trim() : col; // `table`.`col` or table.col
1027
1098
 
1028
- var colWithoutAlias = explicitAlias ? col.replace(aliasRegex, "").trim() : col; // Use regex to extract the table and column names.
1029
- // This regex matches optional backticks around each identifier.
1030
-
1031
- var tableName = this.type;
1032
- var columnName = "";
1033
1099
  var columnRegex = /^`?([\w]+)`?\.`?([\w]+)`?$/;
1034
1100
  var match = colWithoutAlias.match(columnRegex);
1101
+ var tableName = this.type;
1102
+ var columnName = colWithoutAlias.replace(/`/g, "");
1035
1103
 
1036
1104
  if (match) {
1037
1105
  tableName = match[1];
1038
1106
  columnName = match[2];
1039
- } else {
1040
- // If no table prefix is found, use the column name only.
1041
- columnName = colWithoutAlias.replace(/`/g, "");
1042
- } // Generate the alias.
1043
- // If the column comes from a table different from this.type, then the alias
1044
- // becomes tableName_columnName. Otherwise, just use the column name.
1045
-
1107
+ } // For joined table columns, add default alias <table_col> if none
1046
1108
 
1047
- var newAlias = columnName;
1048
1109
 
1049
1110
  if (tableName && tableName !== this.type) {
1050
- newAlias = tableName + "_" + columnName; // If an explicit alias was provided, favor that.
1051
-
1052
- if (explicitAlias) {
1053
- newAlias = explicitAlias;
1054
- } // Avoid duplicates by appending a count if needed.
1055
-
1056
-
1057
- if (aliasTracker.hasOwnProperty(newAlias)) {
1058
- aliasTracker[newAlias]++;
1059
- newAlias = newAlias + "_" + aliasTracker[newAlias];
1060
- } else {
1061
- aliasTracker[newAlias] = 0;
1062
- } // If there's no explicit alias, add an alias clause.
1063
-
1111
+ var newAlias = explicitAlias || tableName + "_" + columnName;
1064
1112
 
1065
1113
  if (!explicitAlias) {
1114
+ if (aliasTracker.hasOwnProperty(newAlias)) {
1115
+ aliasTracker[newAlias]++;
1116
+ newAlias = newAlias + "_" + aliasTracker[newAlias];
1117
+ } else {
1118
+ aliasTracker[newAlias] = 0;
1119
+ }
1120
+
1066
1121
  return colWithoutAlias + " AS `" + newAlias + "`";
1067
1122
  }
1068
1123
  }
1069
1124
 
1070
1125
  return col;
1071
1126
  });
1072
- return fixedColumns.join(", ");
1127
+ return _.join(_.compact(out), ", ");
1073
1128
  }
1074
1129
 
1075
1130
  list(args) {
@@ -1083,38 +1138,55 @@ class SpiceModel {
1083
1138
  try {
1084
1139
  var _args6, _args7, _args8;
1085
1140
 
1086
- args.columns = _this12.prepColumns(args.columns);
1087
- if (args.mapping_dept) _this12[_mapping_dept] = args.mapping_dept;
1088
- var nestings = [..._this12.extractNestings((_args6 = args) == null ? void 0 : _args6.query, _this12.type), ..._this12.extractNestings((_args7 = args) == null ? void 0 : _args7.columns, _this12.type), ..._this12.extractNestings((_args8 = args) == null ? void 0 : _args8.sort, _this12.type)];
1089
- args.columns = _this12.fixColumnName(args.columns);
1141
+ if (args.mapping_dept) _this12[_mapping_dept] = args.mapping_dept; // Find alias tokens from query/columns/sort
1090
1142
 
1091
- var mappedNestings = _.compact(_.uniq(nestings).map(nesting => {
1092
- var _prop$map;
1143
+ var nestings = [..._this12.extractNestings(((_args6 = args) == null ? void 0 : _args6.query) || "", _this12.type), ..._this12.extractNestings(((_args7 = args) == null ? void 0 : _args7.columns) || "", _this12.type), ..._this12.extractNestings(((_args8 = args) == null ? void 0 : _args8.sort) || "", _this12.type)]; // Decide which aliases we can join: only when map.type===MODEL AND reference is a STRING keyspace.
1093
1144
 
1094
- var prop = _this12.props[nesting];
1145
+ var mappedNestings = _.compact(_.uniq(nestings).map(alias => {
1146
+ var prop = _this12.props[alias];
1147
+ if (!(prop == null ? void 0 : prop.map) || prop.map.type !== _2.MapType.MODEL) return null;
1148
+ var ref = prop.map.reference;
1095
1149
 
1096
- if ((prop == null ? void 0 : (_prop$map = prop.map) == null ? void 0 : _prop$map.type) === _2.MapType.MODEL) {
1097
- return {
1098
- alias: nesting,
1099
- reference: prop.map.reference.toLowerCase(),
1100
- type: prop.type,
1101
- value_field: prop.map.value_field
1102
- };
1150
+ if (typeof ref !== "string") {
1151
+ // reference is a class/function/array-of-classes → no SQL join; serializer will handle it
1152
+ return null;
1103
1153
  }
1154
+
1155
+ var is_array = prop.type === "array" || prop.type === Array || prop.type === _2.DataType.ARRAY;
1156
+ return {
1157
+ alias,
1158
+ reference: ref.toLowerCase(),
1159
+ // keyspace to join
1160
+ is_array,
1161
+ type: prop.type,
1162
+ value_field: prop.map.value_field,
1163
+ destination: prop.map.destination || alias
1164
+ };
1104
1165
  }));
1105
1166
 
1106
- args._join = _this12.createJoinSection(mappedNestings);
1167
+ var protectedAliases = mappedNestings.map(m => m.alias);
1168
+ var arrayAliases = mappedNestings.filter(m => m.is_array).map(m => m.alias); // Columns: first prepare (prefix base table, rewrite array alias.field → ARRAY proj),
1169
+ // then normalize names and add default aliases
1170
+
1171
+ args.columns = _this12.prepColumns(args.columns, protectedAliases, arrayAliases);
1172
+ args.columns = _this12.fixColumnName(args.columns, protectedAliases); // Build JOIN/NEST from the mapped keyspaces
1173
+
1174
+ args._join = _this12.createJoinSection(mappedNestings); // WHERE
1175
+
1107
1176
  var query = "";
1177
+ var deletedCondition = "(`" + _this12.type + "`.deleted = false OR `" + _this12.type + "`.deleted IS MISSING)";
1108
1178
 
1109
1179
  if (args.is_full_text === "true" || args.is_custom_query === "true") {
1110
- query = args.query;
1180
+ query = args.query || "";
1181
+ } else if (args.filters) {
1182
+ query = _this12.makeQueryFromFilter(args.filters);
1183
+ } else if (args.query) {
1184
+ query = args.query + " AND " + deletedCondition;
1111
1185
  } else {
1112
- query = args.filters ? _this12.makeQueryFromFilter(args.filters) : args.query ? args.query + " AND (`" + _this12.type + "`.deleted = false OR `" + _this12.type + "`.deleted IS MISSING)" : "(`" + _this12.type + "`.deleted = false OR `" + _this12.type + "`.deleted IS MISSING)";
1186
+ query = deletedCondition;
1113
1187
  }
1114
1188
 
1115
- if ((0, _Security.hasSQLInjection)(query)) {
1116
- return [];
1117
- }
1189
+ if ((0, _Security.hasSQLInjection)(query)) return []; // LIMIT/OFFSET/SORT
1118
1190
 
1119
1191
  args.limit = Number(args.limit) || undefined;
1120
1192
  args.offset = Number(args.offset) || 0;
@@ -1128,15 +1200,12 @@ class SpiceModel {
1128
1200
  var results;
1129
1201
 
1130
1202
  if (_this12.shouldUseCache(_this12.type)) {
1131
- var cachedResults = yield _this12.getCacheProviderObject(_this12.type).get(cacheKey);
1132
- results = cachedResults == null ? void 0 : cachedResults.value; // Check if there is no cached value
1203
+ var cached = yield _this12.getCacheProviderObject(_this12.type).get(cacheKey);
1204
+ results = cached == null ? void 0 : cached.value;
1205
+ var isEmpty = (cached == null ? void 0 : cached.value) === undefined;
1206
+ var refresh = yield _this12.shouldForceRefresh(cached);
1133
1207
 
1134
- var isCacheEmpty = (cachedResults == null ? void 0 : cachedResults.value) === undefined; // Check if the cached value should be refreshed (e.g., due to the monitor's timestamp)
1135
-
1136
- var shouldRefresh = yield _this12.shouldForceRefresh(cachedResults); // Always force a refresh for "workflow" type
1137
-
1138
- if (isCacheEmpty || shouldRefresh) {
1139
- // Force a database read and cache update
1208
+ if (isEmpty || refresh) {
1140
1209
  results = yield _this12.fetchResults(args, query);
1141
1210
 
1142
1211
  _this12.getCacheProviderObject(_this12.type).set(cacheKey, {
@@ -1146,7 +1215,8 @@ class SpiceModel {
1146
1215
  }
1147
1216
  } else {
1148
1217
  results = yield _this12.fetchResults(args, query);
1149
- }
1218
+ } // Serializer still handles class-based refs and value_field
1219
+
1150
1220
 
1151
1221
  if (args.skip_read_serialize !== true && args.skip_serialize !== true) {
1152
1222
  results.data = yield _this12.do_serialize(results.data, "read", {}, args, (yield _this12.propsToBeRemoved(results.data)));
@@ -1180,12 +1250,12 @@ class SpiceModel {
1180
1250
  })();
1181
1251
  }
1182
1252
 
1183
- addHook(_ref2) {
1253
+ addHook(_ref3) {
1184
1254
  var {
1185
1255
  operation,
1186
1256
  when,
1187
1257
  execute
1188
- } = _ref2;
1258
+ } = _ref3;
1189
1259
 
1190
1260
  this[_hooks][operation][when].push(execute);
1191
1261
  }
@@ -1362,11 +1432,11 @@ class SpiceModel {
1362
1432
  })();
1363
1433
  }
1364
1434
 
1365
- addModifier(_ref3) {
1435
+ addModifier(_ref4) {
1366
1436
  var {
1367
1437
  when,
1368
1438
  execute
1369
- } = _ref3;
1439
+ } = _ref4;
1370
1440
 
1371
1441
  if (this[_serializers][when]) {
1372
1442
  this[_serializers][when]["modifiers"].push(execute);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.6.64",
3
+ "version": "2.6.66",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -1,3 +1,4 @@
1
+
1
2
  "use strict";
2
3
 
3
4
  import { MapType, DataType } from "..";
@@ -817,32 +818,75 @@ export default class SpiceModel {
817
818
  JSON.stringify(this);
818
819
  }
819
820
 
820
- prepColumns(columns) {
821
- if (columns && columns !== "") {
822
- let columnList = columns.split(",");
823
- return _.join(
824
- _.compact(
825
- columnList.map((column) => {
826
- if (column === "meta().id") return undefined;
827
- if (!column.startsWith("`") && !column.endsWith("`")) {
828
- column = `\`${column.trim()}\``;
829
- }
830
- if (
831
- column &&
832
- !column.includes(".") &&
833
- !column.startsWith(this.type)
834
- ) {
835
- column = `\`${this.type}\`.${column.trim()}`;
836
- }
837
- return column;
838
- })
839
- ),
840
- ","
841
- );
842
- }
843
- return columns;
821
+ buildArrayProjection(alias, field) {
822
+ const safeAlias = String(alias).replace(/`/g, "");
823
+ const safeField = String(field).replace(/`/g, "");
824
+ return `ARRAY rc.${safeField} FOR rc IN IFMISSINGORNULL(${safeAlias}, []) END AS \`${safeAlias}_${safeField}\``;
844
825
  }
845
826
 
827
+
828
+ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
829
+ if (!columns || columns === "") return columns;
830
+
831
+ const protectedSet = new Set(protectedAliases);
832
+ const arraySet = new Set(arrayAliases);
833
+
834
+ const tokens = columns.split(",");
835
+
836
+ const out = tokens.map((raw) => {
837
+ let col = (raw || "").trim();
838
+ if (col === "" || col === "meta().id") return undefined;
839
+
840
+
841
+ if (/^\s*ARRAY\s+/i.test(col) || /\w+\s*\(/.test(col)) return col;
842
+
843
+ const m = col.match(/^\s*`?(\w+)`?\.`?(\w+)`?(?:\s+AS\s+`?([\w]+)`?)?\s*$/i);
844
+ if (m) {
845
+ const alias = m[1];
846
+ const field = m[2];
847
+ const explicitAs = m[3];
848
+
849
+ if (arraySet.has(alias)) {
850
+ let proj = this.buildArrayProjection(alias, field);
851
+ if (explicitAs && explicitAs !== `${alias}_${field}`) {
852
+ proj = proj.replace(/AS\s+`[^`]+`$/i, `AS \`${explicitAs}\``);
853
+ }
854
+ return proj;
855
+ }
856
+
857
+ if (protectedSet.has(alias)) {
858
+ const aliased = `\`${alias}\`.${field.startsWith("`") ? field : `\`${field}\``}`;
859
+ return explicitAs ? `${aliased} AS \`${explicitAs}\`` : aliased;
860
+ }
861
+
862
+ const bare = col.replace(/`/g, "");
863
+ return `\`${this.type}\`.${bare}`;
864
+ }
865
+
866
+ const looksProtected = [...protectedSet].some(
867
+ (a) => col === a || col === `\`${a}\`` || col.startsWith(`${a}.`) || col.startsWith(`\`${a}\`.`)
868
+ );
869
+
870
+ if (!looksProtected) {
871
+ if (!col.startsWith("`") && !col.endsWith("`") && !col.includes("(")) {
872
+ col = `\`${col}\``;
873
+ }
874
+ if (col && !col.includes(".") && !col.startsWith(this.type)) {
875
+ col = `\`${this.type}\`.${col.replace(/`/g, "")}`;
876
+ }
877
+ return col;
878
+ }
879
+
880
+ if (!col.includes(".") && !col.startsWith("`")) {
881
+ return `\`${col}\``;
882
+ }
883
+ return col;
884
+ });
885
+
886
+ return _.join(_.compact(out), ",");
887
+ }
888
+
889
+
846
890
  filterResultsByColumns(data, columns) {
847
891
  if (columns && columns !== "") {
848
892
  // Remove backticks and replace meta().id with id
@@ -898,21 +942,17 @@ export default class SpiceModel {
898
942
  return [...new Set(returnVal)];
899
943
  }
900
944
 
901
- createJoinSection(nestings) {
902
- return nestings
903
- .map((nesting) => {
904
- if (
905
- nesting.type === DataType.ARRAY ||
906
- nesting.type === Array ||
907
- nesting.type === "array"
908
- ) {
909
- return `LEFT NEST \`${fixCollection(nesting.reference)}\` AS \`${
910
- nesting.alias
911
- }\` ON KEYS \`${this.type}\`.\`${nesting.alias}\``;
945
+ createJoinSection(mappedNestings) {
946
+ if (!mappedNestings || mappedNestings.length === 0) return "";
947
+
948
+ return mappedNestings
949
+ .map(({ alias, reference, is_array }) => {
950
+
951
+ const keyspace = fixCollection(reference);
952
+ if (is_array === true) {
953
+ return `LEFT NEST \`${keyspace}\` AS \`${alias}\` ON KEYS \`${this.type}\`.\`${alias}\``;
912
954
  } else {
913
- return `LEFT JOIN \`${fixCollection(nesting.reference)}\` AS \`${
914
- nesting.alias
915
- }\` ON KEYS \`${this.type}\`.\`${nesting.alias}\``;
955
+ return `LEFT JOIN \`${keyspace}\` AS \`${alias}\` ON KEYS \`${this.type}\`.\`${alias}\``;
916
956
  }
917
957
  })
918
958
  .join(" ");
@@ -928,152 +968,155 @@ export default class SpiceModel {
928
968
  return str.replace(/[^a-zA-Z0-9]/g, "");
929
969
  } */
930
970
 
931
- fixColumnName(columns) {
932
- // Guard clause: if columns is not provided or not a string, return it as-is.
933
- if (!columns || typeof columns !== "string") {
934
- return columns;
935
- }
936
-
937
- // Split the columns string on commas and trim each column expression.
938
- const columnList = columns.split(",").map((col) => col.trim());
939
- // Object to keep track of alias usage to avoid duplicates.
971
+ fixColumnName(columns, protectedAliases = []) {
972
+ if (!columns || typeof columns !== "string") return columns;
973
+ const protectedSet = new Set(protectedAliases);
974
+
975
+ const tokens = columns.split(",").map((s) => s.trim());
940
976
  const aliasTracker = {};
941
-
942
- const fixedColumns = columnList.map((col) => {
943
- // Check if an explicit alias is already provided.
977
+
978
+ const out = tokens.map((col) => {
979
+ if (!col) return undefined;
980
+
981
+ // Do not rewrite ARRAY projections
982
+ if (/^\s*ARRAY\s+/i.test(col)) return col;
983
+
984
+ // If token is literally this.type.alias → compress to `alias`
985
+ for (const a of protectedSet) {
986
+ const re = new RegExp("^`?" + _.escapeRegExp(this.type) + "`?\\.`?" + _.escapeRegExp(a) + "`?$");
987
+ if (re.test(col)) {
988
+ return `\`${a}\``;
989
+ }
990
+ }
991
+
992
+ // Extract explicit AS
944
993
  const aliasRegex = /\s+AS\s+`?([\w]+)`?$/i;
945
994
  const aliasMatch = col.match(aliasRegex);
946
- let explicitAlias = aliasMatch ? aliasMatch[1] : null;
947
-
948
- // Remove the alias clause for easier processing.
949
- let colWithoutAlias =
950
- explicitAlias ? col.replace(aliasRegex, "").trim() : col;
951
-
952
- // Use regex to extract the table and column names.
953
- // This regex matches optional backticks around each identifier.
954
- let tableName = this.type;
955
- let columnName = "";
995
+ const explicitAlias = aliasMatch ? aliasMatch[1] : null;
996
+ const colWithoutAlias = explicitAlias ? col.replace(aliasRegex, "").trim() : col;
997
+
998
+ // `table`.`col` or table.col
956
999
  const columnRegex = /^`?([\w]+)`?\.`?([\w]+)`?$/;
957
1000
  const match = colWithoutAlias.match(columnRegex);
1001
+
1002
+ let tableName = this.type;
1003
+ let columnName = colWithoutAlias.replace(/`/g, "");
958
1004
  if (match) {
959
1005
  tableName = match[1];
960
1006
  columnName = match[2];
961
- } else {
962
- // If no table prefix is found, use the column name only.
963
- columnName = colWithoutAlias.replace(/`/g, "");
964
1007
  }
965
-
966
- // Generate the alias.
967
- // If the column comes from a table different from this.type, then the alias
968
- // becomes tableName_columnName. Otherwise, just use the column name.
969
- let newAlias = columnName;
1008
+
1009
+ // For joined table columns, add default alias <table_col> if none
970
1010
  if (tableName && tableName !== this.type) {
971
- newAlias = `${tableName}_${columnName}`;
972
-
973
- // If an explicit alias was provided, favor that.
974
- if (explicitAlias) {
975
- newAlias = explicitAlias;
976
- }
977
-
978
- // Avoid duplicates by appending a count if needed.
979
- if (aliasTracker.hasOwnProperty(newAlias)) {
980
- aliasTracker[newAlias]++;
981
- newAlias = `${newAlias}_${aliasTracker[newAlias]}`;
982
- } else {
983
- aliasTracker[newAlias] = 0;
984
- }
985
-
986
- // If there's no explicit alias, add an alias clause.
1011
+ let newAlias = explicitAlias || `${tableName}_${columnName}`;
987
1012
  if (!explicitAlias) {
1013
+ if (aliasTracker.hasOwnProperty(newAlias)) {
1014
+ aliasTracker[newAlias]++;
1015
+ newAlias = `${newAlias}_${aliasTracker[newAlias]}`;
1016
+ } else {
1017
+ aliasTracker[newAlias] = 0;
1018
+ }
988
1019
  return `${colWithoutAlias} AS \`${newAlias}\``;
989
1020
  }
990
1021
  }
1022
+
991
1023
  return col;
992
1024
  });
993
-
994
- return fixedColumns.join(", ");
1025
+
1026
+ return _.join(_.compact(out), ", ");
995
1027
  }
1028
+
996
1029
 
997
1030
  async list(args = {}) {
998
1031
  try {
999
- args.columns = this.prepColumns(args.columns);
1000
-
1001
1032
  if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
1002
-
1033
+
1034
+ // Find alias tokens from query/columns/sort
1003
1035
  const nestings = [
1004
- ...this.extractNestings(args?.query, this.type),
1005
- ...this.extractNestings(args?.columns, this.type),
1006
- ...this.extractNestings(args?.sort, this.type),
1036
+ ...this.extractNestings(args?.query || "", this.type),
1037
+ ...this.extractNestings(args?.columns || "", this.type),
1038
+ ...this.extractNestings(args?.sort || "", this.type),
1007
1039
  ];
1008
-
1009
- args.columns = this.fixColumnName(args.columns);
1010
-
1040
+
1041
+ // Decide which aliases we can join: only when map.type===MODEL AND reference is a STRING keyspace.
1011
1042
  const mappedNestings = _.compact(
1012
- _.uniq(nestings).map((nesting) => {
1013
- const prop = this.props[nesting];
1014
- if (prop?.map?.type === MapType.MODEL) {
1015
- return {
1016
- alias: nesting,
1017
- reference: prop.map.reference.toLowerCase(),
1018
- type: prop.type,
1019
- value_field: prop.map.value_field,
1020
- };
1043
+ _.uniq(nestings).map((alias) => {
1044
+ const prop = this.props[alias];
1045
+ if (!prop?.map || prop.map.type !== MapType.MODEL) return null;
1046
+
1047
+ const ref = prop.map.reference;
1048
+ if (typeof ref !== "string") {
1049
+ // reference is a class/function/array-of-classes → no SQL join; serializer will handle it
1050
+ return null;
1021
1051
  }
1052
+
1053
+ const is_array =
1054
+ prop.type === "array" || prop.type === Array || prop.type === DataType.ARRAY;
1055
+
1056
+ return {
1057
+ alias,
1058
+ reference: ref.toLowerCase(), // keyspace to join
1059
+ is_array,
1060
+ type: prop.type,
1061
+ value_field: prop.map.value_field,
1062
+ destination: prop.map.destination || alias,
1063
+ };
1022
1064
  })
1023
1065
  );
1024
-
1066
+
1067
+ const protectedAliases = mappedNestings.map((m) => m.alias);
1068
+ const arrayAliases = mappedNestings.filter((m) => m.is_array).map((m) => m.alias);
1069
+
1070
+ // Columns: first prepare (prefix base table, rewrite array alias.field → ARRAY proj),
1071
+ // then normalize names and add default aliases
1072
+ args.columns = this.prepColumns(args.columns, protectedAliases, arrayAliases);
1073
+ args.columns = this.fixColumnName(args.columns, protectedAliases);
1074
+
1075
+ // Build JOIN/NEST from the mapped keyspaces
1025
1076
  args._join = this.createJoinSection(mappedNestings);
1026
-
1077
+
1078
+ // WHERE
1027
1079
  let query = "";
1080
+ const deletedCondition = `(\`${this.type}\`.deleted = false OR \`${this.type}\`.deleted IS MISSING)`;
1028
1081
  if (args.is_full_text === "true" || args.is_custom_query === "true") {
1029
- query = args.query;
1082
+ query = args.query || "";
1083
+ } else if (args.filters) {
1084
+ query = this.makeQueryFromFilter(args.filters);
1085
+ } else if (args.query) {
1086
+ query = `${args.query} AND ${deletedCondition}`;
1030
1087
  } else {
1031
- query =
1032
- args.filters ? this.makeQueryFromFilter(args.filters)
1033
- : args.query ?
1034
- `${args.query} AND (\`${this.type}\`.deleted = false OR \`${this.type}\`.deleted IS MISSING)`
1035
- : `(\`${this.type}\`.deleted = false OR \`${this.type}\`.deleted IS MISSING)`;
1036
- }
1037
-
1038
- if (hasSQLInjection(query)) {
1039
- return [];
1088
+ query = deletedCondition;
1040
1089
  }
1041
-
1090
+
1091
+ if (hasSQLInjection(query)) return [];
1092
+
1093
+ // LIMIT/OFFSET/SORT
1042
1094
  args.limit = Number(args.limit) || undefined;
1043
1095
  args.offset = Number(args.offset) || 0;
1044
- args.sort =
1045
- args.sort ?
1046
- args.sort
1096
+ args.sort = args.sort
1097
+ ? args.sort
1047
1098
  .split(",")
1048
1099
  .map((item) =>
1049
- item.includes(".") ? item : (
1050
- `\`${this.type}\`.${this.formatSortComponent(item)}`
1051
- )
1100
+ item.includes(".")
1101
+ ? item
1102
+ : `\`${this.type}\`.${this.formatSortComponent(item)}`
1052
1103
  )
1053
1104
  .join(",")
1054
1105
  : `\`${this.type}\`.created_at DESC`;
1055
-
1106
+
1056
1107
  if (args.skip_hooks !== true) {
1057
1108
  await this.run_hook(this, "list", "before");
1058
1109
  }
1059
-
1110
+
1060
1111
  const cacheKey = `list::${this.type}::${args._join}::${query}::${args.limit}::${args.offset}::${args.sort}::${args.do_count}::${args.statement_consistent}::${args.columns}::${args.is_full_text}::${args.is_custom_query}`;
1061
1112
  let results;
1113
+
1062
1114
  if (this.shouldUseCache(this.type)) {
1063
- const cachedResults = await this.getCacheProviderObject(this.type).get(
1064
- cacheKey
1065
- );
1066
- results = cachedResults?.value;
1067
-
1068
- // Check if there is no cached value
1069
- const isCacheEmpty = cachedResults?.value === undefined;
1070
-
1071
- // Check if the cached value should be refreshed (e.g., due to the monitor's timestamp)
1072
- const shouldRefresh = await this.shouldForceRefresh(cachedResults);
1073
-
1074
- // Always force a refresh for "workflow" type
1075
- if (isCacheEmpty || shouldRefresh) {
1076
- // Force a database read and cache update
1115
+ const cached = await this.getCacheProviderObject(this.type).get(cacheKey);
1116
+ results = cached?.value;
1117
+ const isEmpty = cached?.value === undefined;
1118
+ const refresh = await this.shouldForceRefresh(cached);
1119
+ if (isEmpty || refresh) {
1077
1120
  results = await this.fetchResults(args, query);
1078
1121
  this.getCacheProviderObject(this.type).set(
1079
1122
  cacheKey,
@@ -1084,7 +1127,8 @@ export default class SpiceModel {
1084
1127
  } else {
1085
1128
  results = await this.fetchResults(args, query);
1086
1129
  }
1087
-
1130
+
1131
+ // Serializer still handles class-based refs and value_field
1088
1132
  if (args.skip_read_serialize !== true && args.skip_serialize !== true) {
1089
1133
  results.data = await this.do_serialize(
1090
1134
  results.data,
@@ -1094,11 +1138,11 @@ export default class SpiceModel {
1094
1138
  await this.propsToBeRemoved(results.data)
1095
1139
  );
1096
1140
  }
1097
-
1141
+
1098
1142
  if (args.skip_hooks !== true) {
1099
1143
  await this.run_hook(results.data, "list", "after");
1100
1144
  }
1101
-
1145
+
1102
1146
  results.data = this.filterResultsByColumns(results.data, args.columns);
1103
1147
  return results;
1104
1148
  } catch (e) {
@@ -1106,6 +1150,7 @@ export default class SpiceModel {
1106
1150
  throw e;
1107
1151
  }
1108
1152
  }
1153
+
1109
1154
 
1110
1155
  async fetchResults(args, query) {
1111
1156
  if (args.is_custom_query === "true" && args.ids.length > 0) {
@@ -1452,3 +1497,4 @@ export default class SpiceModel {
1452
1497
  }
1453
1498
  }
1454
1499
  }
1500
+