spice-js 2.6.64 → 2.6.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.6.64",
3
+ "version": "2.6.65",
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
+