spice-js 2.6.67 → 2.6.69

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.
@@ -932,6 +932,17 @@ class SpiceModel {
932
932
  }
933
933
 
934
934
  if (!columns || columns === "") return columns;
935
+
936
+ var q = function q(s) {
937
+ if (s === void 0) {
938
+ s = "";
939
+ }
940
+
941
+ var t = (s || "").trim();
942
+ if (t.startsWith("`") && t.endsWith("`")) return t;
943
+ return "`" + t.replace(/`/g, "``") + "`";
944
+ };
945
+
935
946
  var protectedSet = new Set(protectedAliases);
936
947
  var arraySet = new Set(arrayAliases);
937
948
  var tokens = columns.split(",");
@@ -944,43 +955,52 @@ class SpiceModel {
944
955
  if (m) {
945
956
  var alias = m[1];
946
957
  var field = m[2];
947
- var explicitAs = m[3];
958
+ var explicitAs = m[3]; // If alias matches this.type, don't prepend again
959
+
960
+ if (alias === this.type) {
961
+ var _qualified = q(alias) + "." + q(field);
962
+
963
+ return explicitAs ? _qualified + " AS " + q(explicitAs) : _qualified;
964
+ }
948
965
 
949
966
  if (arraySet.has(alias)) {
950
967
  var proj = this.buildArrayProjection(alias, field);
951
968
 
952
969
  if (explicitAs && explicitAs !== alias + "_" + field) {
953
- proj = proj.replace(/AS\s+`[^`]+`$/i, "AS `" + explicitAs + "`");
970
+ proj = proj.replace(/AS\s+`[^`]+`$/i, "AS " + q(explicitAs));
954
971
  }
955
972
 
956
973
  return proj;
957
974
  }
958
975
 
959
976
  if (protectedSet.has(alias)) {
960
- var aliased = "`" + alias + "`." + (field.startsWith("`") ? field : "`" + field + "`");
961
- return explicitAs ? aliased + " AS `" + explicitAs + "`" : aliased;
977
+ var aliased = q(alias) + "." + (field.startsWith("`") ? field : q(field));
978
+ return explicitAs ? aliased + " AS " + q(explicitAs) : aliased;
962
979
  }
963
980
 
964
- var bare = col.replace(/`/g, "");
965
- return "`" + this.type + "`." + bare;
981
+ var qualified = q(this.type) + "." + q(alias) + "." + q(field);
982
+ return explicitAs ? qualified + " AS " + q(explicitAs) : qualified;
966
983
  }
967
984
 
968
- var looksProtected = [...protectedSet].some(a => col === a || col === "`" + a + "`" || col.startsWith(a + ".") || col.startsWith("`" + a + "`."));
985
+ var looksProtected = [...protectedSet].some(a => col === a || col === q(a) || col.startsWith(a + ".") || col.startsWith(q(a) + "."));
969
986
 
970
987
  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, "");
988
+ if (!col.includes(".")) {
989
+ var fieldQuoted = col.startsWith("`") ? col : q(col);
990
+ return q(this.type) + "." + fieldQuoted;
977
991
  }
978
992
 
979
- return col;
993
+ var parts = col.split(".");
994
+ var safeParts = parts.map(p => {
995
+ var t = p.trim();
996
+ if (t === "" || /\w+\s*\(/.test(t)) return t;
997
+ return t.startsWith("`") ? t : q(t);
998
+ });
999
+ return q(this.type) + "." + safeParts.join(".");
980
1000
  }
981
1001
 
982
1002
  if (!col.includes(".") && !col.startsWith("`")) {
983
- return "`" + col + "`";
1003
+ return q(col);
984
1004
  }
985
1005
 
986
1006
  return col;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.6.67",
3
+ "version": "2.6.69",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -1,4 +1,3 @@
1
-
2
1
  "use strict";
3
2
 
4
3
  import { MapType, DataType } from "..";
@@ -166,17 +165,15 @@ export default class SpiceModel {
166
165
  }
167
166
  case Number:
168
167
  case "number": {
169
- this[i] =
170
- _.isNumber(args.args[i]) ?
171
- args.args[i]
168
+ this[i] = _.isNumber(args.args[i])
169
+ ? args.args[i]
172
170
  : Number(args.args[i]);
173
171
  break;
174
172
  }
175
173
  case Boolean:
176
174
  case "boolean": {
177
- this[i] =
178
- _.isBoolean(args.args[i]) ?
179
- args.args[i]
175
+ this[i] = _.isBoolean(args.args[i])
176
+ ? args.args[i]
180
177
  : args.args[i] == "true" ||
181
178
  args.args[i] == 1 ||
182
179
  args.args[i] == "True";
@@ -189,17 +186,15 @@ export default class SpiceModel {
189
186
  }
190
187
  case Array:
191
188
  case "array": {
192
- this[i] =
193
- _.isArray(args.args[i]) ?
194
- args.args[i]
189
+ this[i] = _.isArray(args.args[i])
190
+ ? args.args[i]
195
191
  : JSON.parse(args.args[i]);
196
192
  break;
197
193
  }
198
194
  case Object:
199
195
  case "object": {
200
- this[i] =
201
- _.isObject(args.args[i]) ?
202
- args.args[i]
196
+ this[i] = _.isObject(args.args[i])
197
+ ? args.args[i]
203
198
  : JSON.parse(args.args[i]);
204
199
  break;
205
200
  }
@@ -824,68 +819,88 @@ export default class SpiceModel {
824
819
  return `ARRAY rc.${safeField} FOR rc IN IFMISSINGORNULL(${safeAlias}, []) END AS \`${safeAlias}_${safeField}\``;
825
820
  }
826
821
 
822
+ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
823
+ if (!columns || columns === "") return columns;
827
824
 
828
- prepColumns(columns, protectedAliases = [], arrayAliases = []) {
829
- if (!columns || columns === "") return columns;
825
+ const q = (s = "") => {
826
+ const t = (s || "").trim();
827
+ if (t.startsWith("`") && t.endsWith("`")) return t;
828
+ return "`" + t.replace(/`/g, "``") + "`";
829
+ };
830
830
 
831
- const protectedSet = new Set(protectedAliases);
832
- const arraySet = new Set(arrayAliases);
831
+ const protectedSet = new Set(protectedAliases);
832
+ const arraySet = new Set(arrayAliases);
833
833
 
834
- const tokens = columns.split(",");
834
+ const tokens = columns.split(",");
835
835
 
836
- const out = tokens.map((raw) => {
837
- let col = (raw || "").trim();
838
- if (col === "" || col === "meta().id") return undefined;
836
+ const out = tokens.map((raw) => {
837
+ let col = (raw || "").trim();
838
+ if (col === "" || col === "meta().id") return undefined;
839
839
 
840
+ if (/^\s*ARRAY\s+/i.test(col) || /\w+\s*\(/.test(col)) return col;
840
841
 
841
- if (/^\s*ARRAY\s+/i.test(col) || /\w+\s*\(/.test(col)) return col;
842
+ const m = col.match(
843
+ /^\s*`?(\w+)`?\.`?(\w+)`?(?:\s+AS\s+`?([\w]+)`?)?\s*$/i
844
+ );
845
+ if (m) {
846
+ const alias = m[1];
847
+ const field = m[2];
848
+ const explicitAs = m[3];
849
+
850
+ // If alias matches this.type, don't prepend again
851
+ if (alias === this.type) {
852
+ const qualified = `${q(alias)}.${q(field)}`;
853
+ return explicitAs ? `${qualified} AS ${q(explicitAs)}` : qualified;
854
+ }
842
855
 
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];
856
+ if (arraySet.has(alias)) {
857
+ let proj = this.buildArrayProjection(alias, field);
858
+ if (explicitAs && explicitAs !== `${alias}_${field}`) {
859
+ proj = proj.replace(/AS\s+`[^`]+`$/i, `AS ${q(explicitAs)}`);
860
+ }
861
+ return proj;
862
+ }
848
863
 
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}\``);
864
+ if (protectedSet.has(alias)) {
865
+ const aliased = `${q(alias)}.${field.startsWith("`") ? field : q(field)}`;
866
+ return explicitAs ? `${aliased} AS ${q(explicitAs)}` : aliased;
853
867
  }
854
- return proj;
855
- }
856
868
 
857
- if (protectedSet.has(alias)) {
858
- const aliased = `\`${alias}\`.${field.startsWith("`") ? field : `\`${field}\``}`;
859
- return explicitAs ? `${aliased} AS \`${explicitAs}\`` : aliased;
869
+ const qualified = `${q(this.type)}.${q(alias)}.${q(field)}`;
870
+ return explicitAs ? `${qualified} AS ${q(explicitAs)}` : qualified;
860
871
  }
861
872
 
862
- const bare = col.replace(/`/g, "");
863
- return `\`${this.type}\`.${bare}`;
864
- }
873
+ const looksProtected = [...protectedSet].some(
874
+ (a) =>
875
+ col === a ||
876
+ col === q(a) ||
877
+ col.startsWith(`${a}.`) ||
878
+ col.startsWith(`${q(a)}.`)
879
+ );
865
880
 
866
- const looksProtected = [...protectedSet].some(
867
- (a) => col === a || col === `\`${a}\`` || col.startsWith(`${a}.`) || col.startsWith(`\`${a}\`.`)
868
- );
881
+ if (!looksProtected) {
882
+ if (!col.includes(".")) {
883
+ const fieldQuoted = col.startsWith("`") ? col : q(col);
884
+ return `${q(this.type)}.${fieldQuoted}`;
885
+ }
869
886
 
870
- if (!looksProtected) {
871
- if (!col.startsWith("`") && !col.endsWith("`") && !col.includes("(")) {
872
- col = `\`${col}\``;
887
+ const parts = col.split(".");
888
+ const safeParts = parts.map((p) => {
889
+ const t = p.trim();
890
+ if (t === "" || /\w+\s*\(/.test(t)) return t;
891
+ return t.startsWith("`") ? t : q(t);
892
+ });
893
+ return `${q(this.type)}.${safeParts.join(".")}`;
873
894
  }
874
- if (col && !col.includes(".") && !col.startsWith(this.type)) {
875
- col = `\`${this.type}\`.${col.replace(/`/g, "")}`;
895
+
896
+ if (!col.includes(".") && !col.startsWith("`")) {
897
+ return q(col);
876
898
  }
877
899
  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
- }
900
+ });
888
901
 
902
+ return _.join(_.compact(out), ",");
903
+ }
889
904
 
890
905
  filterResultsByColumns(data, columns) {
891
906
  if (columns && columns !== "") {
@@ -944,10 +959,9 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
944
959
 
945
960
  createJoinSection(mappedNestings) {
946
961
  if (!mappedNestings || mappedNestings.length === 0) return "";
947
-
962
+
948
963
  return mappedNestings
949
964
  .map(({ alias, reference, is_array }) => {
950
-
951
965
  const keyspace = fixCollection(reference);
952
966
  if (is_array === true) {
953
967
  return `LEFT NEST \`${keyspace}\` AS \`${alias}\` ON KEYS \`${this.type}\`.\`${alias}\``;
@@ -971,41 +985,49 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
971
985
  fixColumnName(columns, protectedAliases = []) {
972
986
  if (!columns || typeof columns !== "string") return columns;
973
987
  const protectedSet = new Set(protectedAliases);
974
-
988
+
975
989
  const tokens = columns.split(",").map((s) => s.trim());
976
990
  const aliasTracker = {};
977
-
991
+
978
992
  const out = tokens.map((col) => {
979
993
  if (!col) return undefined;
980
-
994
+
981
995
  // Do not rewrite ARRAY projections
982
996
  if (/^\s*ARRAY\s+/i.test(col)) return col;
983
-
997
+
984
998
  // If token is literally this.type.alias → compress to `alias`
985
999
  for (const a of protectedSet) {
986
- const re = new RegExp("^`?" + _.escapeRegExp(this.type) + "`?\\.`?" + _.escapeRegExp(a) + "`?$");
1000
+ const re = new RegExp(
1001
+ "^`?" +
1002
+ _.escapeRegExp(this.type) +
1003
+ "`?\\.`?" +
1004
+ _.escapeRegExp(a) +
1005
+ "`?$"
1006
+ );
987
1007
  if (re.test(col)) {
988
1008
  return `\`${a}\``;
989
1009
  }
990
1010
  }
991
-
1011
+
992
1012
  // Extract explicit AS
993
1013
  const aliasRegex = /\s+AS\s+`?([\w]+)`?$/i;
994
1014
  const aliasMatch = col.match(aliasRegex);
995
1015
  const explicitAlias = aliasMatch ? aliasMatch[1] : null;
996
- const colWithoutAlias = explicitAlias ? col.replace(aliasRegex, "").trim() : col;
997
-
1016
+ const colWithoutAlias = explicitAlias
1017
+ ? col.replace(aliasRegex, "").trim()
1018
+ : col;
1019
+
998
1020
  // `table`.`col` or table.col
999
1021
  const columnRegex = /^`?([\w]+)`?\.`?([\w]+)`?$/;
1000
1022
  const match = colWithoutAlias.match(columnRegex);
1001
-
1023
+
1002
1024
  let tableName = this.type;
1003
1025
  let columnName = colWithoutAlias.replace(/`/g, "");
1004
1026
  if (match) {
1005
1027
  tableName = match[1];
1006
1028
  columnName = match[2];
1007
1029
  }
1008
-
1030
+
1009
1031
  // For joined table columns, add default alias <table_col> if none
1010
1032
  if (tableName && tableName !== this.type) {
1011
1033
  let newAlias = explicitAlias || `${tableName}_${columnName}`;
@@ -1019,40 +1041,41 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1019
1041
  return `${colWithoutAlias} AS \`${newAlias}\``;
1020
1042
  }
1021
1043
  }
1022
-
1044
+
1023
1045
  return col;
1024
1046
  });
1025
-
1047
+
1026
1048
  return _.join(_.compact(out), ", ");
1027
1049
  }
1028
-
1029
1050
 
1030
1051
  async list(args = {}) {
1031
1052
  try {
1032
1053
  if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
1033
-
1054
+
1034
1055
  // Find alias tokens from query/columns/sort
1035
1056
  const nestings = [
1036
1057
  ...this.extractNestings(args?.query || "", this.type),
1037
1058
  ...this.extractNestings(args?.columns || "", this.type),
1038
1059
  ...this.extractNestings(args?.sort || "", this.type),
1039
1060
  ];
1040
-
1061
+
1041
1062
  // Decide which aliases we can join: only when map.type===MODEL AND reference is a STRING keyspace.
1042
1063
  const mappedNestings = _.compact(
1043
1064
  _.uniq(nestings).map((alias) => {
1044
1065
  const prop = this.props[alias];
1045
1066
  if (!prop?.map || prop.map.type !== MapType.MODEL) return null;
1046
-
1067
+
1047
1068
  const ref = prop.map.reference;
1048
1069
  if (typeof ref !== "string") {
1049
1070
  // reference is a class/function/array-of-classes → no SQL join; serializer will handle it
1050
1071
  return null;
1051
1072
  }
1052
-
1073
+
1053
1074
  const is_array =
1054
- prop.type === "array" || prop.type === Array || prop.type === DataType.ARRAY;
1055
-
1075
+ prop.type === "array" ||
1076
+ prop.type === Array ||
1077
+ prop.type === DataType.ARRAY;
1078
+
1056
1079
  return {
1057
1080
  alias,
1058
1081
  reference: ref.toLowerCase(), // keyspace to join
@@ -1063,18 +1086,24 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1063
1086
  };
1064
1087
  })
1065
1088
  );
1066
-
1089
+
1067
1090
  const protectedAliases = mappedNestings.map((m) => m.alias);
1068
- const arrayAliases = mappedNestings.filter((m) => m.is_array).map((m) => m.alias);
1069
-
1091
+ const arrayAliases = mappedNestings
1092
+ .filter((m) => m.is_array)
1093
+ .map((m) => m.alias);
1094
+
1070
1095
  // Columns: first prepare (prefix base table, rewrite array alias.field → ARRAY proj),
1071
1096
  // then normalize names and add default aliases
1072
- args.columns = this.prepColumns(args.columns, protectedAliases, arrayAliases);
1097
+ args.columns = this.prepColumns(
1098
+ args.columns,
1099
+ protectedAliases,
1100
+ arrayAliases
1101
+ );
1073
1102
  args.columns = this.fixColumnName(args.columns, protectedAliases);
1074
-
1103
+
1075
1104
  // Build JOIN/NEST from the mapped keyspaces
1076
1105
  args._join = this.createJoinSection(mappedNestings);
1077
-
1106
+
1078
1107
  // WHERE
1079
1108
  let query = "";
1080
1109
  const deletedCondition = `(\`${this.type}\`.deleted = false OR \`${this.type}\`.deleted IS MISSING)`;
@@ -1087,9 +1116,9 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1087
1116
  } else {
1088
1117
  query = deletedCondition;
1089
1118
  }
1090
-
1119
+
1091
1120
  if (hasSQLInjection(query)) return [];
1092
-
1121
+
1093
1122
  // LIMIT/OFFSET/SORT
1094
1123
  args.limit = Number(args.limit) || undefined;
1095
1124
  args.offset = Number(args.offset) || 0;
@@ -1103,16 +1132,18 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1103
1132
  )
1104
1133
  .join(",")
1105
1134
  : `\`${this.type}\`.created_at DESC`;
1106
-
1135
+
1107
1136
  if (args.skip_hooks !== true) {
1108
1137
  await this.run_hook(this, "list", "before");
1109
1138
  }
1110
-
1139
+
1111
1140
  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}`;
1112
1141
  let results;
1113
-
1142
+
1114
1143
  if (this.shouldUseCache(this.type)) {
1115
- const cached = await this.getCacheProviderObject(this.type).get(cacheKey);
1144
+ const cached = await this.getCacheProviderObject(this.type).get(
1145
+ cacheKey
1146
+ );
1116
1147
  results = cached?.value;
1117
1148
  const isEmpty = cached?.value === undefined;
1118
1149
  const refresh = await this.shouldForceRefresh(cached);
@@ -1127,7 +1158,7 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1127
1158
  } else {
1128
1159
  results = await this.fetchResults(args, query);
1129
1160
  }
1130
-
1161
+
1131
1162
  // Serializer still handles class-based refs and value_field
1132
1163
  if (args.skip_read_serialize !== true && args.skip_serialize !== true) {
1133
1164
  results.data = await this.do_serialize(
@@ -1138,11 +1169,11 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1138
1169
  await this.propsToBeRemoved(results.data)
1139
1170
  );
1140
1171
  }
1141
-
1172
+
1142
1173
  if (args.skip_hooks !== true) {
1143
1174
  await this.run_hook(results.data, "list", "after");
1144
1175
  }
1145
-
1176
+
1146
1177
  results.data = this.filterResultsByColumns(results.data, args.columns);
1147
1178
  return results;
1148
1179
  } catch (e) {
@@ -1150,7 +1181,6 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1150
1181
  throw e;
1151
1182
  }
1152
1183
  }
1153
-
1154
1184
 
1155
1185
  async fetchResults(args, query) {
1156
1186
  if (args.is_custom_query === "true" && args.ids.length > 0) {
@@ -1249,7 +1279,6 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1249
1279
 
1250
1280
  var returned_all = await Promise.allSettled(
1251
1281
  _.map(classes, (obj) => {
1252
-
1253
1282
  return new obj({
1254
1283
  ...this[_args],
1255
1284
  skip_cache: this[_skip_cache],
@@ -1288,7 +1317,6 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1288
1317
  store_property,
1289
1318
  property
1290
1319
  ) {
1291
-
1292
1320
  let original_is_array = _.isArray(data);
1293
1321
  if (!original_is_array) {
1294
1322
  data = Array.of(data);
@@ -1310,7 +1338,7 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1310
1338
  ids = _.union(ids, items);
1311
1339
  });
1312
1340
 
1313
- let classes = _.compact(_.isArray(Class) ? Class : [Class])
1341
+ let classes = _.compact(_.isArray(Class) ? Class : [Class]);
1314
1342
  var returned_all = await Promise.allSettled(
1315
1343
  _.map(classes, (obj) => {
1316
1344
  return new obj({
@@ -1389,9 +1417,9 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1389
1417
  execute: async (data) => {
1390
1418
  return await this.mapToObject(
1391
1419
  data,
1392
- _.isString(properties[i].map.reference) ?
1393
- spice.models[properties[i].map.reference]
1394
- : properties[i].map.reference,
1420
+ _.isString(properties[i].map.reference)
1421
+ ? spice.models[properties[i].map.reference]
1422
+ : properties[i].map.reference,
1395
1423
  i,
1396
1424
  properties[i].map.destination || i,
1397
1425
  properties[i]
@@ -1407,9 +1435,9 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1407
1435
  execute: async (data) => {
1408
1436
  return await this.mapToObjectArray(
1409
1437
  data,
1410
- _.isString(properties[i].map.reference) ?
1411
- spice.models[properties[i].map.reference]
1412
- : properties[i].map.reference,
1438
+ _.isString(properties[i].map.reference)
1439
+ ? spice.models[properties[i].map.reference]
1440
+ : properties[i].map.reference,
1413
1441
  i,
1414
1442
  properties[i].map.destination || i,
1415
1443
  properties[i]
@@ -1462,9 +1490,8 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1462
1490
  const defaults = Object.keys(this.props).reduce((acc, key) => {
1463
1491
  const def = this.props[key]?.defaults?.[type];
1464
1492
  if (def !== undefined) {
1465
- acc[key] =
1466
- _.isFunction(def) ?
1467
- def({ old_data: data, new_data: old_data })
1493
+ acc[key] = _.isFunction(def)
1494
+ ? def({ old_data: data, new_data: old_data })
1468
1495
  : def;
1469
1496
  }
1470
1497
  return acc;
@@ -1497,4 +1524,3 @@ prepColumns(columns, protectedAliases = [], arrayAliases = []) {
1497
1524
  }
1498
1525
  }
1499
1526
  }
1500
-