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 +1 -1
- package/src/models/SpiceModel.js +188 -142
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
|
+
|