spice-js 2.7.26 → 2.7.27

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.
@@ -140,6 +140,31 @@ class RestHelper {
140
140
  numeric: true,
141
141
  sensitivity: "base"
142
142
  }));
143
+
144
+ // Filter flattened fields to only include requested columns
145
+ var rawUrl = ctx.originalUrl || ctx.request.url || "";
146
+ var urlColumnsMatch = rawUrl.match(/[?&]columns=([^&]*)/);
147
+ var requestedColumns = urlColumnsMatch ? decodeURIComponent(urlColumnsMatch[1]) : null;
148
+ if (requestedColumns) {
149
+ var specs = requestedColumns.split(",").map(c => c.trim()).filter(c => c !== "");
150
+ var filtered = [];
151
+ for (var spec of specs) {
152
+ if (!spec.includes(".")) {
153
+ // Simple field: exact match
154
+ if (fields.includes(spec)) filtered.push(spec);
155
+ } else {
156
+ // Nested field: build regex that allows numeric array indices between segments
157
+ // and optionally matches all descendant fields.
158
+ var segments = spec.split(".");
159
+ var pattern = segments.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("(\\.\\d+)?\\.");
160
+ var re = new RegExp("^" + pattern + "(\\..+)?$");
161
+ for (var f of fields) {
162
+ if (re.test(f)) filtered.push(f);
163
+ }
164
+ }
165
+ }
166
+ fields = filtered.length > 0 ? filtered : fields;
167
+ }
143
168
  var csv = parse(flatRows, {
144
169
  fields,
145
170
  defaultValue: "",
@@ -154,7 +179,7 @@ class RestHelper {
154
179
  ctx.status = 200;
155
180
  var csvStream = fs.createReadStream(filePath);
156
181
  // Delete file after stream is finished
157
- csvStream.on('close', () => {
182
+ csvStream.on("close", () => {
158
183
  fs.promises.unlink(filePath).catch(() => {});
159
184
  });
160
185
  ctx.body = csvStream;
@@ -174,7 +199,7 @@ class RestHelper {
174
199
  ctx.status = 200;
175
200
  var jsonStream = fs.createReadStream(filePath);
176
201
  // Delete file after stream is finished
177
- jsonStream.on('close', () => {
202
+ jsonStream.on("close", () => {
178
203
  fs.promises.unlink(filePath).catch(() => {});
179
204
  });
180
205
  ctx.body = jsonStream;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.26",
3
+ "version": "2.7.27",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -24,7 +24,7 @@ export default class RestHelper {
24
24
  RestHelper.SUCCESS,
25
25
  ctx.data,
26
26
  ctx.errors,
27
- ctx.token
27
+ ctx.token,
28
28
  );
29
29
  }
30
30
  ctx.status = 200;
@@ -44,7 +44,7 @@ export default class RestHelper {
44
44
  const makeDirectory = (dir) => {
45
45
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
46
46
  };
47
-
47
+
48
48
  const deepTrimStrings = (val) => {
49
49
  if (Array.isArray(val)) return val.map(deepTrimStrings);
50
50
  if (val && typeof val === "object") {
@@ -54,9 +54,10 @@ export default class RestHelper {
54
54
  }
55
55
  return typeof val === "string" ? val.trim() : val;
56
56
  };
57
-
57
+
58
58
  const stripTopLevel = (obj, { removeId }) => {
59
- if (Array.isArray(obj)) return obj.map((i) => stripTopLevel(i, { removeId }));
59
+ if (Array.isArray(obj))
60
+ return obj.map((i) => stripTopLevel(i, { removeId }));
60
61
  if (obj && typeof obj === "object") {
61
62
  const copy = { ...obj };
62
63
  if (removeId) delete copy.id;
@@ -65,7 +66,7 @@ export default class RestHelper {
65
66
  }
66
67
  return obj;
67
68
  };
68
-
69
+
69
70
  const normalizeEmptyArraysForCsv = (val) => {
70
71
  if (Array.isArray(val)) {
71
72
  if (val.length === 0) return undefined;
@@ -81,7 +82,7 @@ export default class RestHelper {
81
82
  }
82
83
  return val;
83
84
  };
84
-
85
+
85
86
  const safeJSONStringify = (value, space = 2) => {
86
87
  const seen = new WeakSet();
87
88
  const replacer = (_k, v) => {
@@ -95,31 +96,72 @@ export default class RestHelper {
95
96
  };
96
97
  return JSON.stringify(value, replacer, space);
97
98
  };
98
-
99
+
99
100
  try {
100
101
  const download_type = (ctx.request.query.format || "csv").toLowerCase();
101
102
  const include_id = ctx.request.query.include_id;
102
-
103
+
103
104
  const original = _.cloneDeep(ctx.data);
104
- const trimmed = deepTrimStrings(original);
105
- const cleaned = stripTopLevel(trimmed, { removeId: !include_id || include_id === "false" });
106
-
105
+ const trimmed = deepTrimStrings(original);
106
+ const cleaned = stripTopLevel(trimmed, {
107
+ removeId: !include_id || include_id === "false",
108
+ });
109
+
107
110
  let filename, filePath;
108
-
111
+
109
112
  if (download_type === "csv") {
110
113
  const { flatten } = await import("flat");
111
114
  const rows = Array.isArray(cleaned) ? cleaned : [cleaned];
112
-
115
+
113
116
  const csvReady = rows.map(normalizeEmptyArraysForCsv);
114
- const flatRows = csvReady.map((row) => flatten(row, { safe: false, delimiter: "." }));
115
-
117
+ const flatRows = csvReady.map((row) =>
118
+ flatten(row, { safe: false, delimiter: "." }),
119
+ );
120
+
116
121
  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" })
122
+ for (const r of flatRows)
123
+ Object.keys(r).forEach((k) => fieldSet.add(k));
124
+ let fields = Array.from(fieldSet).sort((a, b) =>
125
+ a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }),
120
126
  );
121
-
122
- const csv = parse(flatRows, { fields, defaultValue: "", excelStrings: true });
127
+
128
+ // Filter flattened fields to only include requested columns
129
+ const rawUrl = ctx.originalUrl || ctx.request.url || "";
130
+ const urlColumnsMatch = rawUrl.match(/[?&]columns=([^&]*)/);
131
+ const requestedColumns = urlColumnsMatch
132
+ ? decodeURIComponent(urlColumnsMatch[1])
133
+ : null;
134
+ if (requestedColumns) {
135
+ const specs = requestedColumns
136
+ .split(",")
137
+ .map((c) => c.trim())
138
+ .filter((c) => c !== "");
139
+ const filtered = [];
140
+ for (const spec of specs) {
141
+ if (!spec.includes(".")) {
142
+ // Simple field: exact match
143
+ if (fields.includes(spec)) filtered.push(spec);
144
+ } else {
145
+ // Nested field: build regex that allows numeric array indices between segments
146
+ // and optionally matches all descendant fields.
147
+ const segments = spec.split(".");
148
+ const pattern = segments
149
+ .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
150
+ .join("(\\.\\d+)?\\.");
151
+ const re = new RegExp("^" + pattern + "(\\..+)?$");
152
+ for (const f of fields) {
153
+ if (re.test(f)) filtered.push(f);
154
+ }
155
+ }
156
+ }
157
+ fields = filtered.length > 0 ? filtered : fields;
158
+ }
159
+
160
+ const csv = parse(flatRows, {
161
+ fields,
162
+ defaultValue: "",
163
+ excelStrings: true,
164
+ });
123
165
 
124
166
  makeDirectory(`./storage/exports/csv`);
125
167
  filename = `${RestHelper.makeid(9)}.csv`;
@@ -131,7 +173,7 @@ export default class RestHelper {
131
173
  ctx.status = 200;
132
174
  const csvStream = fs.createReadStream(filePath);
133
175
  // Delete file after stream is finished
134
- csvStream.on('close', () => {
176
+ csvStream.on("close", () => {
135
177
  fs.promises.unlink(filePath).catch(() => {});
136
178
  });
137
179
  ctx.body = csvStream;
@@ -139,7 +181,7 @@ export default class RestHelper {
139
181
  }
140
182
 
141
183
  const jsonText = safeJSONStringify(cleaned, 2);
142
-
184
+
143
185
  makeDirectory(`./storage/exports/json`);
144
186
  filename = `${RestHelper.makeid(9)}.json`;
145
187
  filePath = path.resolve(`./storage/exports/json/${filename}`);
@@ -149,11 +191,10 @@ export default class RestHelper {
149
191
  ctx.status = 200;
150
192
  const jsonStream = fs.createReadStream(filePath);
151
193
  // Delete file after stream is finished
152
- jsonStream.on('close', () => {
194
+ jsonStream.on("close", () => {
153
195
  fs.promises.unlink(filePath).catch(() => {});
154
196
  });
155
197
  ctx.body = jsonStream;
156
-
157
198
  } catch (e) {
158
199
  console.error(e.stack);
159
200
  ctx.status = 400;
@@ -179,7 +220,7 @@ export default class RestHelper {
179
220
  obj = {
180
221
  data: [],
181
222
  total: null,
182
- }
223
+ },
183
224
  ) {
184
225
  var data = {
185
226
  status: status,
@@ -267,4 +308,3 @@ export default class RestHelper {
267
308
  return false;
268
309
  }
269
310
  }
270
-