spice-js 2.6.68 → 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.
@@ -955,7 +955,13 @@ class SpiceModel {
955
955
  if (m) {
956
956
  var alias = m[1];
957
957
  var field = m[2];
958
- 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
+ }
959
965
 
960
966
  if (arraySet.has(alias)) {
961
967
  var proj = this.buildArrayProjection(alias, field);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.6.68",
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,35 +819,40 @@ export default class SpiceModel {
824
819
  return `ARRAY rc.${safeField} FOR rc IN IFMISSINGORNULL(${safeAlias}, []) END AS \`${safeAlias}_${safeField}\``;
825
820
  }
826
821
 
827
-
828
822
  prepColumns(columns, protectedAliases = [], arrayAliases = []) {
829
823
  if (!columns || columns === "") return columns;
830
-
824
+
831
825
  const q = (s = "") => {
832
826
  const t = (s || "").trim();
833
- if (t.startsWith("`") && t.endsWith("`")) return t;
827
+ if (t.startsWith("`") && t.endsWith("`")) return t;
834
828
  return "`" + t.replace(/`/g, "``") + "`";
835
829
  };
836
-
830
+
837
831
  const protectedSet = new Set(protectedAliases);
838
832
  const arraySet = new Set(arrayAliases);
839
-
833
+
840
834
  const tokens = columns.split(",");
841
-
835
+
842
836
  const out = tokens.map((raw) => {
843
837
  let col = (raw || "").trim();
844
838
  if (col === "" || col === "meta().id") return undefined;
845
-
846
-
839
+
847
840
  if (/^\s*ARRAY\s+/i.test(col) || /\w+\s*\(/.test(col)) return col;
848
-
849
-
850
- const m = col.match(/^\s*`?(\w+)`?\.`?(\w+)`?(?:\s+AS\s+`?([\w]+)`?)?\s*$/i);
841
+
842
+ const m = col.match(
843
+ /^\s*`?(\w+)`?\.`?(\w+)`?(?:\s+AS\s+`?([\w]+)`?)?\s*$/i
844
+ );
851
845
  if (m) {
852
846
  const alias = m[1];
853
847
  const field = m[2];
854
848
  const explicitAs = m[3];
855
-
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
+ }
855
+
856
856
  if (arraySet.has(alias)) {
857
857
  let proj = this.buildArrayProjection(alias, field);
858
858
  if (explicitAs && explicitAs !== `${alias}_${field}`) {
@@ -860,18 +860,16 @@ export default class SpiceModel {
860
860
  }
861
861
  return proj;
862
862
  }
863
-
863
+
864
864
  if (protectedSet.has(alias)) {
865
865
  const aliased = `${q(alias)}.${field.startsWith("`") ? field : q(field)}`;
866
866
  return explicitAs ? `${aliased} AS ${q(explicitAs)}` : aliased;
867
867
  }
868
-
869
-
868
+
870
869
  const qualified = `${q(this.type)}.${q(alias)}.${q(field)}`;
871
870
  return explicitAs ? `${qualified} AS ${q(explicitAs)}` : qualified;
872
871
  }
873
-
874
-
872
+
875
873
  const looksProtected = [...protectedSet].some(
876
874
  (a) =>
877
875
  col === a ||
@@ -879,33 +877,31 @@ export default class SpiceModel {
879
877
  col.startsWith(`${a}.`) ||
880
878
  col.startsWith(`${q(a)}.`)
881
879
  );
882
-
880
+
883
881
  if (!looksProtected) {
884
-
885
882
  if (!col.includes(".")) {
886
883
  const fieldQuoted = col.startsWith("`") ? col : q(col);
887
884
  return `${q(this.type)}.${fieldQuoted}`;
888
885
  }
889
-
886
+
890
887
  const parts = col.split(".");
891
888
  const safeParts = parts.map((p) => {
892
889
  const t = p.trim();
893
- if (t === "" || /\w+\s*\(/.test(t)) return t;
890
+ if (t === "" || /\w+\s*\(/.test(t)) return t;
894
891
  return t.startsWith("`") ? t : q(t);
895
892
  });
896
893
  return `${q(this.type)}.${safeParts.join(".")}`;
897
894
  }
898
-
895
+
899
896
  if (!col.includes(".") && !col.startsWith("`")) {
900
897
  return q(col);
901
898
  }
902
899
  return col;
903
900
  });
904
-
901
+
905
902
  return _.join(_.compact(out), ",");
906
903
  }
907
904
 
908
-
909
905
  filterResultsByColumns(data, columns) {
910
906
  if (columns && columns !== "") {
911
907
  // Remove backticks and replace meta().id with id
@@ -963,10 +959,9 @@ export default class SpiceModel {
963
959
 
964
960
  createJoinSection(mappedNestings) {
965
961
  if (!mappedNestings || mappedNestings.length === 0) return "";
966
-
962
+
967
963
  return mappedNestings
968
964
  .map(({ alias, reference, is_array }) => {
969
-
970
965
  const keyspace = fixCollection(reference);
971
966
  if (is_array === true) {
972
967
  return `LEFT NEST \`${keyspace}\` AS \`${alias}\` ON KEYS \`${this.type}\`.\`${alias}\``;
@@ -990,41 +985,49 @@ export default class SpiceModel {
990
985
  fixColumnName(columns, protectedAliases = []) {
991
986
  if (!columns || typeof columns !== "string") return columns;
992
987
  const protectedSet = new Set(protectedAliases);
993
-
988
+
994
989
  const tokens = columns.split(",").map((s) => s.trim());
995
990
  const aliasTracker = {};
996
-
991
+
997
992
  const out = tokens.map((col) => {
998
993
  if (!col) return undefined;
999
-
994
+
1000
995
  // Do not rewrite ARRAY projections
1001
996
  if (/^\s*ARRAY\s+/i.test(col)) return col;
1002
-
997
+
1003
998
  // If token is literally this.type.alias → compress to `alias`
1004
999
  for (const a of protectedSet) {
1005
- 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
+ );
1006
1007
  if (re.test(col)) {
1007
1008
  return `\`${a}\``;
1008
1009
  }
1009
1010
  }
1010
-
1011
+
1011
1012
  // Extract explicit AS
1012
1013
  const aliasRegex = /\s+AS\s+`?([\w]+)`?$/i;
1013
1014
  const aliasMatch = col.match(aliasRegex);
1014
1015
  const explicitAlias = aliasMatch ? aliasMatch[1] : null;
1015
- const colWithoutAlias = explicitAlias ? col.replace(aliasRegex, "").trim() : col;
1016
-
1016
+ const colWithoutAlias = explicitAlias
1017
+ ? col.replace(aliasRegex, "").trim()
1018
+ : col;
1019
+
1017
1020
  // `table`.`col` or table.col
1018
1021
  const columnRegex = /^`?([\w]+)`?\.`?([\w]+)`?$/;
1019
1022
  const match = colWithoutAlias.match(columnRegex);
1020
-
1023
+
1021
1024
  let tableName = this.type;
1022
1025
  let columnName = colWithoutAlias.replace(/`/g, "");
1023
1026
  if (match) {
1024
1027
  tableName = match[1];
1025
1028
  columnName = match[2];
1026
1029
  }
1027
-
1030
+
1028
1031
  // For joined table columns, add default alias <table_col> if none
1029
1032
  if (tableName && tableName !== this.type) {
1030
1033
  let newAlias = explicitAlias || `${tableName}_${columnName}`;
@@ -1038,40 +1041,41 @@ export default class SpiceModel {
1038
1041
  return `${colWithoutAlias} AS \`${newAlias}\``;
1039
1042
  }
1040
1043
  }
1041
-
1044
+
1042
1045
  return col;
1043
1046
  });
1044
-
1047
+
1045
1048
  return _.join(_.compact(out), ", ");
1046
1049
  }
1047
-
1048
1050
 
1049
1051
  async list(args = {}) {
1050
1052
  try {
1051
1053
  if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
1052
-
1054
+
1053
1055
  // Find alias tokens from query/columns/sort
1054
1056
  const nestings = [
1055
1057
  ...this.extractNestings(args?.query || "", this.type),
1056
1058
  ...this.extractNestings(args?.columns || "", this.type),
1057
1059
  ...this.extractNestings(args?.sort || "", this.type),
1058
1060
  ];
1059
-
1061
+
1060
1062
  // Decide which aliases we can join: only when map.type===MODEL AND reference is a STRING keyspace.
1061
1063
  const mappedNestings = _.compact(
1062
1064
  _.uniq(nestings).map((alias) => {
1063
1065
  const prop = this.props[alias];
1064
1066
  if (!prop?.map || prop.map.type !== MapType.MODEL) return null;
1065
-
1067
+
1066
1068
  const ref = prop.map.reference;
1067
1069
  if (typeof ref !== "string") {
1068
1070
  // reference is a class/function/array-of-classes → no SQL join; serializer will handle it
1069
1071
  return null;
1070
1072
  }
1071
-
1073
+
1072
1074
  const is_array =
1073
- prop.type === "array" || prop.type === Array || prop.type === DataType.ARRAY;
1074
-
1075
+ prop.type === "array" ||
1076
+ prop.type === Array ||
1077
+ prop.type === DataType.ARRAY;
1078
+
1075
1079
  return {
1076
1080
  alias,
1077
1081
  reference: ref.toLowerCase(), // keyspace to join
@@ -1082,18 +1086,24 @@ export default class SpiceModel {
1082
1086
  };
1083
1087
  })
1084
1088
  );
1085
-
1089
+
1086
1090
  const protectedAliases = mappedNestings.map((m) => m.alias);
1087
- const arrayAliases = mappedNestings.filter((m) => m.is_array).map((m) => m.alias);
1088
-
1091
+ const arrayAliases = mappedNestings
1092
+ .filter((m) => m.is_array)
1093
+ .map((m) => m.alias);
1094
+
1089
1095
  // Columns: first prepare (prefix base table, rewrite array alias.field → ARRAY proj),
1090
1096
  // then normalize names and add default aliases
1091
- args.columns = this.prepColumns(args.columns, protectedAliases, arrayAliases);
1097
+ args.columns = this.prepColumns(
1098
+ args.columns,
1099
+ protectedAliases,
1100
+ arrayAliases
1101
+ );
1092
1102
  args.columns = this.fixColumnName(args.columns, protectedAliases);
1093
-
1103
+
1094
1104
  // Build JOIN/NEST from the mapped keyspaces
1095
1105
  args._join = this.createJoinSection(mappedNestings);
1096
-
1106
+
1097
1107
  // WHERE
1098
1108
  let query = "";
1099
1109
  const deletedCondition = `(\`${this.type}\`.deleted = false OR \`${this.type}\`.deleted IS MISSING)`;
@@ -1106,9 +1116,9 @@ export default class SpiceModel {
1106
1116
  } else {
1107
1117
  query = deletedCondition;
1108
1118
  }
1109
-
1119
+
1110
1120
  if (hasSQLInjection(query)) return [];
1111
-
1121
+
1112
1122
  // LIMIT/OFFSET/SORT
1113
1123
  args.limit = Number(args.limit) || undefined;
1114
1124
  args.offset = Number(args.offset) || 0;
@@ -1122,16 +1132,18 @@ export default class SpiceModel {
1122
1132
  )
1123
1133
  .join(",")
1124
1134
  : `\`${this.type}\`.created_at DESC`;
1125
-
1135
+
1126
1136
  if (args.skip_hooks !== true) {
1127
1137
  await this.run_hook(this, "list", "before");
1128
1138
  }
1129
-
1139
+
1130
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}`;
1131
1141
  let results;
1132
-
1142
+
1133
1143
  if (this.shouldUseCache(this.type)) {
1134
- const cached = await this.getCacheProviderObject(this.type).get(cacheKey);
1144
+ const cached = await this.getCacheProviderObject(this.type).get(
1145
+ cacheKey
1146
+ );
1135
1147
  results = cached?.value;
1136
1148
  const isEmpty = cached?.value === undefined;
1137
1149
  const refresh = await this.shouldForceRefresh(cached);
@@ -1146,7 +1158,7 @@ export default class SpiceModel {
1146
1158
  } else {
1147
1159
  results = await this.fetchResults(args, query);
1148
1160
  }
1149
-
1161
+
1150
1162
  // Serializer still handles class-based refs and value_field
1151
1163
  if (args.skip_read_serialize !== true && args.skip_serialize !== true) {
1152
1164
  results.data = await this.do_serialize(
@@ -1157,11 +1169,11 @@ export default class SpiceModel {
1157
1169
  await this.propsToBeRemoved(results.data)
1158
1170
  );
1159
1171
  }
1160
-
1172
+
1161
1173
  if (args.skip_hooks !== true) {
1162
1174
  await this.run_hook(results.data, "list", "after");
1163
1175
  }
1164
-
1176
+
1165
1177
  results.data = this.filterResultsByColumns(results.data, args.columns);
1166
1178
  return results;
1167
1179
  } catch (e) {
@@ -1169,7 +1181,6 @@ export default class SpiceModel {
1169
1181
  throw e;
1170
1182
  }
1171
1183
  }
1172
-
1173
1184
 
1174
1185
  async fetchResults(args, query) {
1175
1186
  if (args.is_custom_query === "true" && args.ids.length > 0) {
@@ -1268,7 +1279,6 @@ export default class SpiceModel {
1268
1279
 
1269
1280
  var returned_all = await Promise.allSettled(
1270
1281
  _.map(classes, (obj) => {
1271
-
1272
1282
  return new obj({
1273
1283
  ...this[_args],
1274
1284
  skip_cache: this[_skip_cache],
@@ -1307,7 +1317,6 @@ export default class SpiceModel {
1307
1317
  store_property,
1308
1318
  property
1309
1319
  ) {
1310
-
1311
1320
  let original_is_array = _.isArray(data);
1312
1321
  if (!original_is_array) {
1313
1322
  data = Array.of(data);
@@ -1329,7 +1338,7 @@ export default class SpiceModel {
1329
1338
  ids = _.union(ids, items);
1330
1339
  });
1331
1340
 
1332
- let classes = _.compact(_.isArray(Class) ? Class : [Class])
1341
+ let classes = _.compact(_.isArray(Class) ? Class : [Class]);
1333
1342
  var returned_all = await Promise.allSettled(
1334
1343
  _.map(classes, (obj) => {
1335
1344
  return new obj({
@@ -1408,9 +1417,9 @@ export default class SpiceModel {
1408
1417
  execute: async (data) => {
1409
1418
  return await this.mapToObject(
1410
1419
  data,
1411
- _.isString(properties[i].map.reference) ?
1412
- spice.models[properties[i].map.reference]
1413
- : properties[i].map.reference,
1420
+ _.isString(properties[i].map.reference)
1421
+ ? spice.models[properties[i].map.reference]
1422
+ : properties[i].map.reference,
1414
1423
  i,
1415
1424
  properties[i].map.destination || i,
1416
1425
  properties[i]
@@ -1426,9 +1435,9 @@ export default class SpiceModel {
1426
1435
  execute: async (data) => {
1427
1436
  return await this.mapToObjectArray(
1428
1437
  data,
1429
- _.isString(properties[i].map.reference) ?
1430
- spice.models[properties[i].map.reference]
1431
- : properties[i].map.reference,
1438
+ _.isString(properties[i].map.reference)
1439
+ ? spice.models[properties[i].map.reference]
1440
+ : properties[i].map.reference,
1432
1441
  i,
1433
1442
  properties[i].map.destination || i,
1434
1443
  properties[i]
@@ -1481,9 +1490,8 @@ export default class SpiceModel {
1481
1490
  const defaults = Object.keys(this.props).reduce((acc, key) => {
1482
1491
  const def = this.props[key]?.defaults?.[type];
1483
1492
  if (def !== undefined) {
1484
- acc[key] =
1485
- _.isFunction(def) ?
1486
- def({ old_data: data, new_data: old_data })
1493
+ acc[key] = _.isFunction(def)
1494
+ ? def({ old_data: data, new_data: old_data })
1487
1495
  : def;
1488
1496
  }
1489
1497
  return acc;
@@ -1516,4 +1524,3 @@ export default class SpiceModel {
1516
1524
  }
1517
1525
  }
1518
1526
  }
1519
-