Merge pull request #34 from rtyler/streaming-export-33

Switch the export controller to stream results directly from PostgreSQL to the response buffer
This commit is contained in:
R. Tyler Croy 2018-12-06 07:12:26 -08:00 committed by GitHub
commit 7a4b637755
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 2033 additions and 2069 deletions

3995
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -55,8 +55,10 @@
"feathers-memory": "^2.2.0",
"feathers-sequelize": "^3.1.2",
"helmet": "^3.13.0",
"jsonstream": "^1.0.3",
"passport-github": "^1.1.0",
"pg": "^7.4.3",
"pg-query-stream": "^1.1.2",
"pug": "^2.0.3",
"sequelize": "^4.38.0",
"serve-favicon": "^2.5.0",

View File

@ -5,27 +5,32 @@
import authentication from '@feathersjs/authentication';
import cookieParser from 'cookie-parser';
import db from '../models';
import QueryStream from 'pg-query-stream';
import JSONStream from 'jsonstream'
export default (app) => {
app.post('/export',
cookieParser(),
authentication.express.authenticate('jwt'),
(req, res, next) => {
app.service('/events/bulk')
.find({
query: {
type: req.body.type,
startDate: req.body.startDate,
endDate: req.body.endDate,
},
user: (req as any).user,
})
.then((result) => {
res.setHeader('Content-Disposition', `attachment; filename=${req.body.type}-${req.body.startDate}.json`);
res.setHeader('Content-Type', 'application/json');
res.send(result);
db.sequelize.connectionManager.getConnection().then((pgConn) => {
const query = new QueryStream('SELECT * FROM events WHERE type = $1 AND "createdAt" > $2 AND "createdAt" <= $3', [
req.body.type,
req.body.startDate,
req.body.endDate
]);
const stream = pgConn.query(query);
res.writeHead(200, {
'Content-Disposition' : `attachment; filename=${req.body.type}-${req.body.startDate}.json`,
'Content-Type': 'application/json',
});
stream.pipe(JSONStream.stringify(false)).pipe(res);
stream.on('end', () => {
res.end();
})
.catch(next);
});
})
.catch((err) => { console.log(err.stack); next(err); });
});
};

View File

@ -1,68 +0,0 @@
/**
* The /events/bulk service is largely responsible for streaming responses in
* the form of files to clients
*/
import { Application, HooksObject, Params, SKIP } from '@feathersjs/feathers';
import { BadRequest, NotFound } from '@feathersjs/errors';
import { QueryTypes } from 'sequelize';
import authorize from '../hooks/authorize';
import logger from '../logger';
import db from '../models';
import Event from '../models/event';
export const bulkHooks : HooksObject = {
before: {
all: [
authorize(),
(context) => {
context.params.grants = context.data.grants
return context;
},
],
},
after: {},
error: {},
};
/**
* The Bulk service class intentionally only implements the find method
*/
export class Bulk {
protected readonly app : Application;
constructor(app : Application) {
this.app = app;
}
public async find(params : Params) : Promise<any> {
if (!params.query.type) {
return Promise.reject(new BadRequest('Request must have a `type` in the URL'));
}
const grantedTypes = params.grants.filter(g => (g == '*') || (g == params.query.type));
if (grantedTypes.length == 0) {
return Promise.reject(new NotFound('No data found'));
}
/*
* This is clearly stupid. I have no idea how we'll query very large
* datasets from PostgreSQL but this at least gets us _everything_
*/
return db.sequelize.query("SELECT * FROM events WHERE type = :type AND \"createdAt\" > :startDate AND \"createdAt\" < :endDate", {
replacements: {
type: params.query.type,
startDate: params.query.startDate,
endDate: params.query.endDate,
},
type: QueryTypes.SELECT,
});
}
}
export default (app : Application) => {
app.use('/events/bulk', new Bulk(app));
app.service('events/bulk').hooks(bulkHooks);
};

View File

@ -1,11 +1,9 @@
import bulk from './bulk';
import events from './events';
import grants from './grants';
import types from './types';
import users from './users';
export default (app) => {
app.configure(bulk);
app.configure(events);
app.configure(grants);
app.configure(types);