spice-js 2.6.65 → 2.6.67

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.
@@ -916,25 +916,76 @@ class SpiceModel {
916
916
  JSON.stringify(this);
917
917
  }
918
918
 
919
- prepColumns(columns) {
920
- if (columns && columns !== "") {
921
- var columnList = columns.split(",");
922
- return _.join(_.compact(columnList.map(column => {
923
- if (column === "meta().id") return undefined;
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
- if (!column.startsWith("`") && !column.endsWith("`")) {
926
- column = "`" + column.trim() + "`";
956
+ return proj;
927
957
  }
928
958
 
929
- if (column && !column.includes(".") && !column.startsWith(this.type)) {
930
- column = "`" + this.type + "`." + column.trim();
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
- return column;
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
- return columns;
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(nestings) {
993
- return nestings.map(nesting => {
994
- if (nesting.type === _2.DataType.ARRAY || nesting.type === Array || nesting.type === "array") {
995
- return "LEFT NEST `" + (0, _fix.fixCollection)(nesting.reference) + "` AS `" + nesting.alias + "` ON KEYS `" + this.type + "`.`" + nesting.alias + "`";
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 `" + (0, _fix.fixCollection)(nesting.reference) + "` AS `" + nesting.alias + "` ON KEYS `" + this.type + "`.`" + nesting.alias + "`";
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
- // Guard clause: if columns is not provided or not a string, return it as-is.
1014
- if (!columns || typeof columns !== "string") {
1015
- return columns;
1016
- } // Split the columns string on commas and trim each column expression.
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; // Remove the alias clause for easier processing.
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
- } else {
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; // If an explicit alias was provided, favor that.
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 fixedColumns.join(", ");
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.columns = _this12.prepColumns(args.columns);
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 mappedNestings = _.compact(_.uniq(nestings).map(nesting => {
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
- var prop = _this12.props[nesting];
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 ((prop == null ? void 0 : (_prop$map = prop.map) == null ? void 0 : _prop$map.type) === _2.MapType.MODEL) {
1097
- return {
1098
- alias: nesting,
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
- args._join = _this12.createJoinSection(mappedNestings);
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 = args.filters ? _this12.makeQueryFromFilter(args.filters) : args.query ? args.query + " AND (`" + _this12.type + "`.deleted = false OR `" + _this12.type + "`.deleted IS MISSING)" : "(`" + _this12.type + "`.deleted = false OR `" + _this12.type + "`.deleted IS MISSING)";
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 cachedResults = yield _this12.getCacheProviderObject(_this12.type).get(cacheKey);
1132
- results = cachedResults == null ? void 0 : cachedResults.value; // Check if there is no cached value
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
- var isCacheEmpty = (cachedResults == null ? void 0 : cachedResults.value) === undefined; // Check if the cached value should be refreshed (e.g., due to the monitor's timestamp)
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(_ref2) {
1253
+ addHook(_ref3) {
1184
1254
  var {
1185
1255
  operation,
1186
1256
  when,
1187
1257
  execute
1188
- } = _ref2;
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(_ref3) {
1435
+ addModifier(_ref4) {
1366
1436
  var {
1367
1437
  when,
1368
1438
  execute
1369
- } = _ref3;
1439
+ } = _ref4;
1370
1440
 
1371
1441
  if (this[_serializers][when]) {
1372
1442
  this[_serializers][when]["modifiers"].push(execute);
@@ -11,6 +11,8 @@ function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return
11
11
 
12
12
  function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
13
13
 
14
+ function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
15
+
14
16
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
15
17
 
16
18
  function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
@@ -66,55 +68,154 @@ class RestHelper {
66
68
 
67
69
  static send_download(ctx, next) {
68
70
  return _asyncToGenerator(function* () {
69
- function makeDirectory(dir) {
70
- if (!fs.existsSync(dir)) {
71
- fs.mkdirSync(dir);
71
+ var makeDirectory = dir => {
72
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, {
73
+ recursive: true
74
+ });
75
+ };
76
+
77
+ var deepTrimStrings = val => {
78
+ if (Array.isArray(val)) return val.map(deepTrimStrings);
79
+
80
+ if (val && typeof val === "object") {
81
+ var out = {};
82
+
83
+ for (var [k, v] of Object.entries(val)) {
84
+ out[k] = deepTrimStrings(v);
85
+ }
86
+
87
+ return out;
72
88
  }
73
- }
74
89
 
75
- try {
76
- var download_type = ctx.request.query.format || "csv";
77
- var include_id = ctx.request.query.include_id;
78
- var content;
90
+ return typeof val === "string" ? val.trim() : val;
91
+ };
79
92
 
80
- if (download_type == "csv") {
81
- var {
82
- flatten
83
- } = yield Promise.resolve().then(() => _interopRequireWildcard(require("flat")));
93
+ var stripTopLevel = (obj, _ref) => {
94
+ var {
95
+ removeId
96
+ } = _ref;
97
+ if (Array.isArray(obj)) return obj.map(i => stripTopLevel(i, {
98
+ removeId
99
+ }));
84
100
 
85
- var items = _lodash.default.map(ctx.data, item => {
86
- if (!include_id || include_id == "false") {
87
- delete item.id;
88
- }
101
+ if (obj && typeof obj === "object") {
102
+ var copy = _extends({}, obj);
89
103
 
90
- delete item._permissions_;
91
- return flatten(item);
92
- });
104
+ if (removeId) delete copy.id;
105
+ delete copy._permissions_;
106
+ return copy;
107
+ }
93
108
 
94
- var fields = _lodash.default.union(_lodash.default.keys(_lodash.default.first(items)), _lodash.default.keys(_lodash.default.last(items)), _lodash.default.keys(_lodash.default.nth(items.length / 2)));
109
+ return obj;
110
+ };
95
111
 
96
- var opts = {
97
- fields
98
- };
99
- content = parse(items, opts);
100
- ctx.set("content-type", "text/csv");
101
- } else {
102
- content = JSON.stringify(ctx.data);
103
- ctx.set("content-type", "application/json");
112
+ var normalizeEmptyArraysForCsv = val => {
113
+ if (Array.isArray(val)) {
114
+ if (val.length === 0) return undefined;
115
+ return val.map(v => normalizeEmptyArraysForCsv(v));
104
116
  }
105
117
 
106
- makeDirectory("./storage/exports");
107
- makeDirectory("./storage/exports/" + download_type + "/");
108
- var file = path.resolve("./storage/exports/" + download_type + "/" + RestHelper.makeid(9) + "." + download_type);
109
- yield fs.writeFile(file, content, function (err) {
110
- if (err) throw err;
118
+ if (val && typeof val === "object") {
119
+ var out = {};
120
+
121
+ for (var [k, v] of Object.entries(val)) {
122
+ var nv = normalizeEmptyArraysForCsv(v);
123
+ if (nv !== undefined) out[k] = nv;
124
+ }
125
+
126
+ return out;
127
+ }
128
+
129
+ return val;
130
+ };
131
+
132
+ var safeJSONStringify = function safeJSONStringify(value, space) {
133
+ if (space === void 0) {
134
+ space = 2;
135
+ }
136
+
137
+ var seen = new WeakSet();
138
+
139
+ var replacer = (_k, v) => {
140
+ if (typeof v === "bigint") return v.toString();
141
+ if (v instanceof Date) return v.toISOString();
142
+
143
+ if (v && typeof v === "object") {
144
+ if (seen.has(v)) return "[Circular]";
145
+ seen.add(v);
146
+ }
147
+
148
+ return v;
149
+ };
150
+
151
+ return JSON.stringify(value, replacer, space);
152
+ };
153
+
154
+ try {
155
+ var download_type = (ctx.request.query.format || "csv").toLowerCase();
156
+ var include_id = ctx.request.query.include_id;
157
+
158
+ var original = _lodash.default.cloneDeep(ctx.data);
159
+
160
+ var trimmed = deepTrimStrings(original);
161
+ var cleaned = stripTopLevel(trimmed, {
162
+ removeId: !include_id || include_id === "false"
111
163
  });
112
- ctx.set("content-disposition", "attachment");
113
- ctx.response.attachment(file);
164
+ var filename, filePath;
165
+
166
+ if (download_type === "csv") {
167
+ var _ret = yield* function* () {
168
+ var {
169
+ flatten
170
+ } = yield Promise.resolve().then(() => _interopRequireWildcard(require("flat")));
171
+ var rows = Array.isArray(cleaned) ? cleaned : [cleaned];
172
+ var csvReady = rows.map(normalizeEmptyArraysForCsv);
173
+ var flatRows = csvReady.map(row => flatten(row, {
174
+ safe: false,
175
+ delimiter: "."
176
+ }));
177
+ var fieldSet = new Set();
178
+
179
+ for (var r of flatRows) {
180
+ Object.keys(r).forEach(k => fieldSet.add(k));
181
+ }
182
+
183
+ var fields = Array.from(fieldSet).sort((a, b) => a.localeCompare(b, undefined, {
184
+ numeric: true,
185
+ sensitivity: "base"
186
+ }));
187
+ var csv = parse(flatRows, {
188
+ fields,
189
+ defaultValue: "",
190
+ excelStrings: true
191
+ });
192
+ makeDirectory("./storage/exports/csv");
193
+ filename = RestHelper.makeid(9) + ".csv";
194
+ filePath = path.resolve("./storage/exports/csv/" + filename);
195
+ yield fs.promises.writeFile(filePath, csv, "utf8");
196
+ ctx.set("Content-Disposition", "attachment; filename=\"" + filename + "\"");
197
+ ctx.type = "text/csv; charset=utf-8";
198
+ ctx.status = 200;
199
+ ctx.body = fs.createReadStream(filePath);
200
+ return {
201
+ v: void 0
202
+ };
203
+ }();
204
+
205
+ if (typeof _ret === "object") return _ret.v;
206
+ }
207
+
208
+ var jsonText = safeJSONStringify(cleaned, 2);
209
+ makeDirectory("./storage/exports/json");
210
+ filename = RestHelper.makeid(9) + ".json";
211
+ filePath = path.resolve("./storage/exports/json/" + filename);
212
+ yield fs.promises.writeFile(filePath, jsonText, "utf8");
213
+ ctx.set("Content-Disposition", "attachment; filename=\"" + filename + "\"");
214
+ ctx.type = "application/json; charset=utf-8";
114
215
  ctx.status = 200;
115
- ctx.body = fs.createReadStream(file);
216
+ ctx.body = fs.createReadStream(filePath);
116
217
  } catch (e) {
117
- console.log(e.stack);
218
+ console.error(e.stack);
118
219
  ctx.status = 400;
119
220
  ctx.body = RestHelper.prepare_response(RestHelper.FAILURE, e);
120
221
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.6.65",
3
+ "version": "2.6.67",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -41,57 +41,112 @@ export default class RestHelper {
41
41
  }
42
42
 
43
43
  static async send_download(ctx, next) {
44
- function makeDirectory(dir) {
45
- if (!fs.existsSync(dir)) {
46
- fs.mkdirSync(dir);
44
+ const makeDirectory = (dir) => {
45
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
46
+ };
47
+
48
+ const deepTrimStrings = (val) => {
49
+ if (Array.isArray(val)) return val.map(deepTrimStrings);
50
+ if (val && typeof val === "object") {
51
+ const out = {};
52
+ for (const [k, v] of Object.entries(val)) out[k] = deepTrimStrings(v);
53
+ return out;
47
54
  }
48
- }
55
+ return typeof val === "string" ? val.trim() : val;
56
+ };
57
+
58
+ const stripTopLevel = (obj, { removeId }) => {
59
+ if (Array.isArray(obj)) return obj.map((i) => stripTopLevel(i, { removeId }));
60
+ if (obj && typeof obj === "object") {
61
+ const copy = { ...obj };
62
+ if (removeId) delete copy.id;
63
+ delete copy._permissions_;
64
+ return copy;
65
+ }
66
+ return obj;
67
+ };
68
+
69
+ const normalizeEmptyArraysForCsv = (val) => {
70
+ if (Array.isArray(val)) {
71
+ if (val.length === 0) return undefined;
72
+ return val.map((v) => normalizeEmptyArraysForCsv(v));
73
+ }
74
+ if (val && typeof val === "object") {
75
+ const out = {};
76
+ for (const [k, v] of Object.entries(val)) {
77
+ const nv = normalizeEmptyArraysForCsv(v);
78
+ if (nv !== undefined) out[k] = nv;
79
+ }
80
+ return out;
81
+ }
82
+ return val;
83
+ };
84
+
85
+ const safeJSONStringify = (value, space = 2) => {
86
+ const seen = new WeakSet();
87
+ const replacer = (_k, v) => {
88
+ if (typeof v === "bigint") return v.toString();
89
+ if (v instanceof Date) return v.toISOString();
90
+ if (v && typeof v === "object") {
91
+ if (seen.has(v)) return "[Circular]";
92
+ seen.add(v);
93
+ }
94
+ return v;
95
+ };
96
+ return JSON.stringify(value, replacer, space);
97
+ };
98
+
49
99
  try {
50
- let download_type = ctx.request.query.format || "csv";
51
- let include_id = ctx.request.query.include_id;
52
- let content;
53
- if (download_type == "csv") {
100
+ const download_type = (ctx.request.query.format || "csv").toLowerCase();
101
+ const include_id = ctx.request.query.include_id;
102
+
103
+ const original = _.cloneDeep(ctx.data);
104
+ const trimmed = deepTrimStrings(original);
105
+ const cleaned = stripTopLevel(trimmed, { removeId: !include_id || include_id === "false" });
106
+
107
+ let filename, filePath;
108
+
109
+ if (download_type === "csv") {
54
110
  const { flatten } = await import("flat");
55
-
56
- let items = _.map(ctx.data, (item) => {
57
- if (!include_id || include_id == "false") {
58
- delete item.id;
59
- }
60
- delete item._permissions_;
61
- return flatten(item);
62
- });
63
- let fields = _.union(
64
- _.keys(_.first(items)),
65
- _.keys(_.last(items)),
66
- _.keys(_.nth(items.length / 2))
111
+ const rows = Array.isArray(cleaned) ? cleaned : [cleaned];
112
+
113
+ const csvReady = rows.map(normalizeEmptyArraysForCsv);
114
+ const flatRows = csvReady.map((row) => flatten(row, { safe: false, delimiter: "." }));
115
+
116
+ const fieldSet = new Set();
117
+ for (const r of flatRows) Object.keys(r).forEach((k) => fieldSet.add(k));
118
+ const fields = Array.from(fieldSet).sort((a, b) =>
119
+ a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
67
120
  );
68
- const opts = {
69
- fields,
70
- };
71
- content = parse(items, opts);
72
- ctx.set("content-type", "text/csv");
73
- } else {
74
- content = JSON.stringify(ctx.data);
75
- ctx.set("content-type", "application/json");
121
+
122
+ const csv = parse(flatRows, { fields, defaultValue: "", excelStrings: true });
123
+
124
+ makeDirectory(`./storage/exports/csv`);
125
+ filename = `${RestHelper.makeid(9)}.csv`;
126
+ filePath = path.resolve(`./storage/exports/csv/${filename}`);
127
+ await fs.promises.writeFile(filePath, csv, "utf8");
128
+
129
+
130
+ ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
131
+ ctx.type = "text/csv; charset=utf-8";
132
+ ctx.status = 200;
133
+ ctx.body = fs.createReadStream(filePath);
134
+ return;
76
135
  }
77
136
 
78
- makeDirectory(`./storage/exports`);
79
- makeDirectory(`./storage/exports/${download_type}/`);
80
- let file = path.resolve(
81
- `./storage/exports/${download_type}/${RestHelper.makeid(
82
- 9
83
- )}.${download_type}`
84
- );
85
- await fs.writeFile(file, content, function (err) {
86
- if (err) throw err;
87
- });
88
-
89
- ctx.set("content-disposition", "attachment");
90
- ctx.response.attachment(file);
137
+ const jsonText = safeJSONStringify(cleaned, 2);
138
+
139
+ makeDirectory(`./storage/exports/json`);
140
+ filename = `${RestHelper.makeid(9)}.json`;
141
+ filePath = path.resolve(`./storage/exports/json/${filename}`);
142
+ await fs.promises.writeFile(filePath, jsonText, "utf8");
143
+ ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
144
+ ctx.type = "application/json; charset=utf-8";
91
145
  ctx.status = 200;
92
- ctx.body = fs.createReadStream(file);
146
+ ctx.body = fs.createReadStream(filePath);
147
+
93
148
  } catch (e) {
94
- console.log(e.stack);
149
+ console.error(e.stack);
95
150
  ctx.status = 400;
96
151
  ctx.body = RestHelper.prepare_response(RestHelper.FAILURE, e);
97
152
  }
@@ -203,3 +258,4 @@ export default class RestHelper {
203
258
  return false;
204
259
  }
205
260
  }
261
+