Skip to content

Commit ed6af03

Browse files
authored
perf(export): improve memory usage when exporting data (#1029)
1 parent 72002f9 commit ed6af03

File tree

6 files changed

+91
-95
lines changed

6 files changed

+91
-95
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"body-parser": "^1.20.1",
3636
"compose-middleware": "5.0.1",
3737
"cors": "2.8.5",
38-
"csv-stringify": "1.0.4",
38+
"csv-stringify": "6.5.0",
3939
"express": "^4.18.2",
4040
"express-jwt": "8.4.1",
4141
"forest-ip-utils": "1.0.1",

src/routes/associations.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ module.exports = function Associations(app, model, Implementation, integrator, o
7777
.catch(next);
7878
}
7979

80-
function exportCSV(request, response, next) {
80+
async function exportCSV(request, response, next) {
8181
const { params, associationModel } = getContext(request);
8282

8383
const recordsExporter = new Implementation.ResourcesExporter(

src/routes/resources.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ module.exports = function Resources(app, model, { configStore } = inject()) {
4848
.catch(next);
4949
};
5050

51-
this.exportCSV = (request, response, next) => {
51+
this.exportCSV = async (request, response, next) => {
5252
const params = request.query;
5353
const recordsExporter = new Implementation.ResourcesExporter(
5454
model,

src/services/csv-exporter.js

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,40 @@
1-
const P = require('bluebird');
21
const moment = require('moment');
3-
const stringify = require('csv-stringify');
2+
// eslint-disable-next-line import/no-unresolved
3+
const { stringify } = require('csv-stringify/sync');
44
const { inject } = require('@forestadmin/context');
55
const ParamsFieldsDeserializer = require('../deserializers/params-fields');
66
const SmartFieldsValuesInjector = require('./smart-fields-values-injector');
77

88
// NOTICE: Prevent bad date formatting into timestamps.
99
const CSV_OPTIONS = {
10-
formatters: {
10+
cast: {
1111
date: (value) => moment(value).format(),
1212
},
1313
};
1414

1515
function CSVExporter(params, response, modelName, recordsExporter) {
1616
const { configStore } = inject();
1717

18-
this.perform = () => {
18+
function getValueForAttribute(record, attribute) {
19+
let value;
20+
if (params.fields[attribute]) {
21+
if (record[attribute]) {
22+
if (params.fields[attribute] && record[attribute][params.fields[attribute]]) {
23+
value = record[attribute][params.fields[attribute]];
24+
} else {
25+
// eslint-disable-next-line
26+
value = record[attribute].id || record[attribute]._id;
27+
}
28+
}
29+
} else {
30+
value = record[attribute];
31+
}
32+
33+
return value || '';
34+
}
35+
36+
// eslint-disable-next-line sonarjs/cognitive-complexity
37+
this.perform = async () => {
1938
const filename = `${params.filename}.csv`;
2039
response.setHeader('Content-Type', 'text/csv; charset=utf-8');
2140
response.setHeader('Content-disposition', `attachment; filename=${filename}`);
@@ -32,47 +51,24 @@ function CSVExporter(params, response, modelName, recordsExporter) {
3251

3352
const fieldsPerModel = new ParamsFieldsDeserializer(params.fields).perform();
3453

35-
return recordsExporter
36-
.perform((records) => P
37-
.map(records, (record) =>
38-
new SmartFieldsValuesInjector(record, modelName, fieldsPerModel).perform())
39-
.then((recordsWithSmartFieldsValues) =>
40-
new P((resolve) => {
41-
if (configStore.Implementation.Flattener) {
42-
recordsWithSmartFieldsValues = configStore.Implementation.Flattener
43-
.flattenRecordsForExport(modelName, recordsWithSmartFieldsValues);
44-
}
54+
await recordsExporter
55+
.perform(async (records) => {
56+
await Promise.all(
57+
// eslint-disable-next-line max-len
58+
records.map((record) => new SmartFieldsValuesInjector(record, modelName, fieldsPerModel).perform()),
59+
);
4560

46-
const CSVLines = [];
47-
recordsWithSmartFieldsValues.forEach((record) => {
48-
const CSVLine = [];
49-
CSVAttributes.forEach((attribute) => {
50-
let value;
51-
if (params.fields[attribute]) {
52-
if (record[attribute]) {
53-
if (params.fields[attribute] && record[attribute][params.fields[attribute]]) {
54-
value = record[attribute][params.fields[attribute]];
55-
} else {
56-
// eslint-disable-next-line
57-
value = record[attribute].id || record[attribute]._id;
58-
}
59-
}
60-
} else {
61-
value = record[attribute];
62-
}
63-
CSVLine.push(value || '');
64-
});
65-
CSVLines.push(CSVLine);
66-
});
61+
if (configStore.Implementation.Flattener) {
62+
records = configStore.Implementation.Flattener
63+
.flattenRecordsForExport(modelName, records);
64+
}
6765

68-
stringify(CSVLines, CSV_OPTIONS, (error, csv) => {
69-
response.write(csv);
70-
resolve();
71-
});
72-
})))
73-
.then(() => {
74-
response.end();
66+
records.forEach((record) => {
67+
// eslint-disable-next-line max-len
68+
response.write(stringify([CSVAttributes.map((attribute) => getValueForAttribute(record, attribute, params))], CSV_OPTIONS));
69+
});
7570
});
71+
response.end();
7672
};
7773
}
7874

src/services/smart-fields-values-injector.js

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const _ = require('lodash');
2-
const P = require('bluebird');
32
const logger = require('./logger');
43
const Schemas = require('../generators/schemas');
54

@@ -74,60 +73,63 @@ function SmartFieldsValuesInjector(
7473
&& fieldsPerModel[modelNameToCheck].indexOf(fieldName) !== -1;
7574
}
7675

77-
this.perform = () =>
78-
P.each(schema.fields, (field) => {
79-
const fieldWasRequested = isRequestedField(requestedField || modelName, field.field);
76+
// eslint-disable-next-line sonarjs/cognitive-complexity
77+
function injectSmartFieldValue(field) {
78+
const fieldWasRequested = isRequestedField(requestedField || modelName, field.field);
8079

81-
if (record && field.isVirtual && (field.get || field.value)) {
82-
if (fieldsPerModel && !fieldWasRequested) {
83-
return null;
84-
}
85-
86-
return setSmartFieldValue(record, field, modelName);
80+
if (record && field.isVirtual && (field.get || field.value)) {
81+
if (fieldsPerModel && !fieldWasRequested) {
82+
return null;
8783
}
8884

89-
if (
90-
!record[field.field]
85+
return setSmartFieldValue(record, field, modelName);
86+
}
87+
88+
if (
89+
!record[field.field]
9190
&& _.isArray(field.type)
9291
&& (field.relationship || field.isVirtual)) {
93-
// Add empty arrays on relation fields so that JsonApiSerializer add the relevant
94-
// `data.x.relationships` section in the response.
95-
//
96-
// The field must match the following condition
97-
// - field is a real or a smart HasMany / BelongsToMany relation
98-
// - field is NOT an 'embedded' relationship (@see mongoose)
99-
100-
record[field.field] = [];
101-
} else if (field.reference && !_.isArray(field.type)) {
102-
// NOTICE: Set Smart Fields values to "belongsTo" associated records.
103-
const modelNameAssociation = getReferencedModelName(field);
104-
const schemaAssociation = Schemas.schemas[modelNameAssociation];
105-
106-
if (schemaAssociation && !_.isArray(field.type)) {
107-
return P.each(schemaAssociation.fields, (fieldAssociation) => {
108-
if (record
92+
// Add empty arrays on relation fields so that JsonApiSerializer add the relevant
93+
// `data.x.relationships` section in the response.
94+
//
95+
// The field must match the following condition
96+
// - field is a real or a smart HasMany / BelongsToMany relation
97+
// - field is NOT an 'embedded' relationship (@see mongoose)
98+
99+
record[field.field] = [];
100+
} else if (field.reference && !_.isArray(field.type)) {
101+
// NOTICE: Set Smart Fields values to "belongsTo" associated records.
102+
const modelNameAssociation = getReferencedModelName(field);
103+
const schemaAssociation = Schemas.schemas[modelNameAssociation];
104+
105+
if (schemaAssociation && !_.isArray(field.type)) {
106+
return Promise.all(schemaAssociation.fields.map((fieldAssociation) => {
107+
if (record
109108
&& record[field.field]
110109
&& fieldAssociation.isVirtual
111110
&& (fieldAssociation.get || fieldAssociation.value)) {
112-
if (fieldsPerModel && !isRequestedField(field.field, fieldAssociation.field)) {
113-
return null;
114-
}
115-
116-
return setSmartFieldValue(
117-
record[field.field],
118-
fieldAssociation,
119-
modelNameAssociation,
120-
);
111+
if (fieldsPerModel && !isRequestedField(field.field, fieldAssociation.field)) {
112+
return null;
121113
}
122114

123-
return null;
124-
});
125-
}
115+
return setSmartFieldValue(
116+
record[field.field],
117+
fieldAssociation,
118+
modelNameAssociation,
119+
);
120+
}
121+
122+
return null;
123+
}));
126124
}
125+
}
126+
127+
return null;
128+
}
127129

128-
return null;
129-
})
130-
.thenReturn(record);
130+
this.perform = async () => Promise.all(
131+
schema.fields.map((field) => injectSmartFieldValue(field)),
132+
);
131133
}
132134

133135
module.exports = SmartFieldsValuesInjector;

yarn.lock

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3746,12 +3746,10 @@ cssesc@^3.0.0:
37463746
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
37473747
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
37483748

3749-
csv-stringify@1.0.4:
3750-
version "1.0.4"
3751-
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-1.0.4.tgz#bc18bab9ad4cef3195fd257980b58b479c42d3e5"
3752-
integrity sha1-vBi6ua1M7zGV/SV5gLWLR5xC0+U=
3753-
dependencies:
3754-
lodash.get "^4.0.0"
3749+
csv-stringify@6.5.0:
3750+
version "6.5.0"
3751+
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-6.5.0.tgz#7b1491893c917e018a97de9bf9604e23b88647c2"
3752+
integrity sha512-edlXFVKcUx7r8Vx5zQucsuMg4wb/xT6qyz+Sr1vnLrdXqlLD1+UKyWNyZ9zn6mUW1ewmGxrpVwAcChGF0HQ/2Q==
37553753

37563754
dargs@^7.0.0:
37573755
version "7.0.0"
@@ -6570,7 +6568,7 @@ lodash.escaperegexp@^4.1.2:
65706568
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
65716569
integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==
65726570

6573-
lodash.get@^4.0.0, lodash.get@^4.4.2:
6571+
lodash.get@^4.4.2:
65746572
version "4.4.2"
65756573
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
65766574
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=

0 commit comments

Comments
 (0)