What is the best practice for taken data generated from a db and sending it from nodejs/express to browser such that it prompts the browser to down load it as a CSV. I am thinking to send it back a line at a time, converting it to a comma delimited row as I send, either manually or via something like fast-csv.
I've put together the following function but it only sends the data back as comma delimited data to the front end without prompting the browser to download. I'd want the browser to open a download dialog.
const csvExport = (
sql: string,
params: IGetListRequest,
user: string,
filename: string,
res: Response
) => {
try {
// disable caching
res.setHeader('Expires', 'Tue, 03 Jul 2001 06:00:00 GMT');
res.setHeader(
'Cache-Control',
'max-age=0, no-cache, must-revalidate, proxy-revalidate'
);
res.setHeader('Last-Modified', `${Date()} GMT`);
// force download
res.setHeader('Content-Type', 'application/force-download');
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Type', 'application/download');
res.setHeader('Content-Type', 'text/csv');
// disposition / encoding on response body
res.setHeader(
'Content-Disposition',
`attachment; filename="${filename
.replace(' ', '-')
.toLowerCase()}.csv"`
);
res.setHeader('Content-Transfer-Encoding', 'binary');
const csvStream = csv.format({ headers: true });
csvStream.pipe(res).on('end', () => process.exit());
const stmt = db.conn.prepare(sql);
for (const rec of stmt.iterate({
user,
from: params.from.toISOString(),
to: params.to ? params.to.toISOString() : params.from.toISOString(),
})) {
csvStream.write(rec);
}
csvStream.end();
res.status(StatusCodes.OK).end();
} catch (err) {
if (err instanceof SqliteError) {
throw new AppServerError({
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
message: err.message,
});
}
throw err;
}
};
Based on a second suggestion from @gunwin I also tried this
const csvExport = (
sql: string,
params: IGetListRequest,
user: string,
filename: string,
res: Response
) => {
try {
res.type('text/csv');
res.attachment(`${filename.replace(/[.: ]/g, '-').toLowerCase()}.csv`);
const readStream = new stream.PassThrough();
readStream.pipe(res).on('end', () => process.exit());
const stmt = db.conn.prepare(sql);
let i = 0;
for (const rec of stmt.iterate({
user,
from: params.from.toISOString(),
to: params.to ? params.to.toISOString() : params.from.toISOString(),
})) {
let csvData: Buffer;
if (i < 1) {
csvData = Buffer.from(
Object.keys(rec)
.map((field) => `"${field}"`)
.join(',') + '\n'
);
readStream.write(csvData);
i++;
}
csvData = Buffer.from(
Object.values(rec)
.map((field) => `"${field}"`)
.join(',') + '\n'
);
readStream.write(csvData);
}
readStream.end();
res.status(StatusCodes.OK).end();
} catch (err) {
if (err instanceof SqliteError) {
throw new AppServerError({
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
message: err.message,
});
}
throw err;
}
};