Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(export): improve memory usage when exporting data #1029

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"body-parser": "^1.20.1",
"compose-middleware": "5.0.1",
"cors": "2.8.5",
"csv-stringify": "1.0.4",
"csv-stringify": "6.5.0",
"express": "^4.18.2",
"express-jwt": "8.4.1",
"forest-ip-utils": "1.0.1",
Expand Down
2 changes: 1 addition & 1 deletion src/routes/associations.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ module.exports = function Associations(app, model, Implementation, integrator, o
.catch(next);
}

function exportCSV(request, response, next) {
async function exportCSV(request, response, next) {
const { params, associationModel } = getContext(request);

const recordsExporter = new Implementation.ResourcesExporter(
Expand Down
2 changes: 1 addition & 1 deletion src/routes/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ module.exports = function Resources(app, model, { configStore } = inject()) {
.catch(next);
};

this.exportCSV = (request, response, next) => {
this.exportCSV = async (request, response, next) => {
const params = request.query;
const recordsExporter = new Implementation.ResourcesExporter(
model,
Expand Down
80 changes: 38 additions & 42 deletions src/services/csv-exporter.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
const P = require('bluebird');
const moment = require('moment');
const stringify = require('csv-stringify');
// eslint-disable-next-line import/no-unresolved
const { stringify } = require('csv-stringify/sync');
const { inject } = require('@forestadmin/context');
const ParamsFieldsDeserializer = require('../deserializers/params-fields');
const SmartFieldsValuesInjector = require('./smart-fields-values-injector');

// NOTICE: Prevent bad date formatting into timestamps.
const CSV_OPTIONS = {
formatters: {
cast: {
date: (value) => moment(value).format(),
},
};

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

this.perform = () => {
function getValueForAttribute(record, attribute) {
let value;
if (params.fields[attribute]) {
if (record[attribute]) {
if (params.fields[attribute] && record[attribute][params.fields[attribute]]) {
value = record[attribute][params.fields[attribute]];
} else {
// eslint-disable-next-line
value = record[attribute].id || record[attribute]._id;
}
}
} else {
value = record[attribute];
}

return value || '';
}

// eslint-disable-next-line sonarjs/cognitive-complexity
this.perform = async () => {
const filename = `${params.filename}.csv`;
response.setHeader('Content-Type', 'text/csv; charset=utf-8');
response.setHeader('Content-disposition', `attachment; filename=${filename}`);
Expand All @@ -32,47 +51,24 @@ function CSVExporter(params, response, modelName, recordsExporter) {

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

return recordsExporter
.perform((records) => P
.map(records, (record) =>
new SmartFieldsValuesInjector(record, modelName, fieldsPerModel).perform())
.then((recordsWithSmartFieldsValues) =>
new P((resolve) => {
if (configStore.Implementation.Flattener) {
recordsWithSmartFieldsValues = configStore.Implementation.Flattener
.flattenRecordsForExport(modelName, recordsWithSmartFieldsValues);
}
await recordsExporter
.perform(async (records) => {
await Promise.all(
// eslint-disable-next-line max-len
records.map((record) => new SmartFieldsValuesInjector(record, modelName, fieldsPerModel).perform()),
);

const CSVLines = [];
recordsWithSmartFieldsValues.forEach((record) => {
const CSVLine = [];
CSVAttributes.forEach((attribute) => {
let value;
if (params.fields[attribute]) {
if (record[attribute]) {
if (params.fields[attribute] && record[attribute][params.fields[attribute]]) {
value = record[attribute][params.fields[attribute]];
} else {
// eslint-disable-next-line
value = record[attribute].id || record[attribute]._id;
}
}
} else {
value = record[attribute];
}
CSVLine.push(value || '');
});
CSVLines.push(CSVLine);
});
if (configStore.Implementation.Flattener) {
records = configStore.Implementation.Flattener
.flattenRecordsForExport(modelName, records);
}

stringify(CSVLines, CSV_OPTIONS, (error, csv) => {
response.write(csv);
resolve();
});
})))
.then(() => {
response.end();
records.forEach((record) => {
// eslint-disable-next-line max-len
response.write(stringify([CSVAttributes.map((attribute) => getValueForAttribute(record, attribute, params))], CSV_OPTIONS));
});
});
response.end();
};
}

Expand Down
88 changes: 45 additions & 43 deletions src/services/smart-fields-values-injector.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const _ = require('lodash');
const P = require('bluebird');
const logger = require('./logger');
const Schemas = require('../generators/schemas');

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

this.perform = () =>
P.each(schema.fields, (field) => {
const fieldWasRequested = isRequestedField(requestedField || modelName, field.field);
// eslint-disable-next-line sonarjs/cognitive-complexity
function injectSmartFieldValue(field) {
Thenkei marked this conversation as resolved.
Show resolved Hide resolved
const fieldWasRequested = isRequestedField(requestedField || modelName, field.field);

if (record && field.isVirtual && (field.get || field.value)) {
if (fieldsPerModel && !fieldWasRequested) {
return null;
}

return setSmartFieldValue(record, field, modelName);
if (record && field.isVirtual && (field.get || field.value)) {
if (fieldsPerModel && !fieldWasRequested) {
return null;
}

if (
!record[field.field]
return setSmartFieldValue(record, field, modelName);
}

if (
!record[field.field]
&& _.isArray(field.type)
&& (field.relationship || field.isVirtual)) {
// Add empty arrays on relation fields so that JsonApiSerializer add the relevant
// `data.x.relationships` section in the response.
//
// The field must match the following condition
// - field is a real or a smart HasMany / BelongsToMany relation
// - field is NOT an 'embedded' relationship (@see mongoose)

record[field.field] = [];
} else if (field.reference && !_.isArray(field.type)) {
// NOTICE: Set Smart Fields values to "belongsTo" associated records.
const modelNameAssociation = getReferencedModelName(field);
const schemaAssociation = Schemas.schemas[modelNameAssociation];

if (schemaAssociation && !_.isArray(field.type)) {
return P.each(schemaAssociation.fields, (fieldAssociation) => {
if (record
// Add empty arrays on relation fields so that JsonApiSerializer add the relevant
// `data.x.relationships` section in the response.
//
// The field must match the following condition
// - field is a real or a smart HasMany / BelongsToMany relation
// - field is NOT an 'embedded' relationship (@see mongoose)

record[field.field] = [];
} else if (field.reference && !_.isArray(field.type)) {
// NOTICE: Set Smart Fields values to "belongsTo" associated records.
const modelNameAssociation = getReferencedModelName(field);
const schemaAssociation = Schemas.schemas[modelNameAssociation];

if (schemaAssociation && !_.isArray(field.type)) {
return Promise.all(schemaAssociation.fields.map((fieldAssociation) => {
if (record
&& record[field.field]
&& fieldAssociation.isVirtual
&& (fieldAssociation.get || fieldAssociation.value)) {
if (fieldsPerModel && !isRequestedField(field.field, fieldAssociation.field)) {
return null;
}

return setSmartFieldValue(
record[field.field],
fieldAssociation,
modelNameAssociation,
);
if (fieldsPerModel && !isRequestedField(field.field, fieldAssociation.field)) {
return null;
}

return null;
});
}
return setSmartFieldValue(
record[field.field],
fieldAssociation,
modelNameAssociation,
);
}

return null;
}));
}
}

return null;
}

return null;
})
.thenReturn(record);
this.perform = async () => Promise.all(
schema.fields.map((field) => injectSmartFieldValue(field)),
);
}

module.exports = SmartFieldsValuesInjector;
12 changes: 5 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3746,12 +3746,10 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==

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

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

lodash.get@^4.0.0, lodash.get@^4.4.2:
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
Expand Down
Loading