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.
- package/build/models/SpiceModel.js +159 -89
- package/package.json +1 -1
- package/src/models/SpiceModel.js +188 -142
|
@@ -916,25 +916,76 @@ class SpiceModel {
|
|
|
916
916
|
JSON.stringify(this);
|
|
917
917
|
}
|
|
918
918
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
926
|
-
column = "`" + column.trim() + "`";
|
|
956
|
+
return proj;
|
|
927
957
|
}
|
|
928
958
|
|
|
929
|
-
if (
|
|
930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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 `" +
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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;
|
|
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
|
-
}
|
|
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;
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1132
|
-
results =
|
|
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
|
-
|
|
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(
|
|
1253
|
+
addHook(_ref3) {
|
|
1184
1254
|
var {
|
|
1185
1255
|
operation,
|
|
1186
1256
|
when,
|
|
1187
1257
|
execute
|
|
1188
|
-
} =
|
|
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(
|
|
1435
|
+
addModifier(_ref4) {
|
|
1366
1436
|
var {
|
|
1367
1437
|
when,
|
|
1368
1438
|
execute
|
|
1369
|
-
} =
|
|
1439
|
+
} = _ref4;
|
|
1370
1440
|
|
|
1371
1441
|
if (this[_serializers][when]) {
|
|
1372
1442
|
this[_serializers][when]["modifiers"].push(execute);
|
package/package.json
CHANGED
package/src/models/SpiceModel.js
CHANGED
|
@@ -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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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(
|
|
902
|
-
return
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
) {
|
|
909
|
-
return `LEFT NEST \`${
|
|
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 \`${
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
|
943
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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((
|
|
1013
|
-
const prop = this.props[
|
|
1014
|
-
if (prop?.map
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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(".")
|
|
1050
|
-
|
|
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
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
+
|