Add support for exporting all of the events based on a type

This is certain to crush the node app for big queries, but we'll see what
happens
This commit is contained in:
R. Tyler Croy 2018-09-11 16:02:02 -07:00
parent c9454f641d
commit 5260c44037
No known key found for this signature in database
GPG Key ID: 1426C7DC3F51E16F
9 changed files with 174 additions and 31 deletions

View File

@ -22,6 +22,9 @@ import services from './services';
import { appHooks } from './app.hooks';
import channels from './channels';
import Dashboard from './controllers/dashboard';
import Export from './controllers/export';
const app = express(feathers());
const settings = configuration();
@ -64,27 +67,10 @@ app.configure(oauth2(Object.assign(githubSettings, {
})));
app.set('view engine', 'pug');
/*
* Render the dashboard view with authentication
*/
app.get('/dashboard',
cookieParser(),
authentication.express.authenticate('jwt'),
(req, res, next) => {
let query = Object.assign({
$sort: {
createdAt: -1,
}
}, req.query);
app.service('events')
.find({ query: query })
.then(result =>
res.render('dashboard', {
events: result,
user: (req as any).user,
query: req.query,
}));
});
Dashboard(app);
Export(app);
app.get('/logout',
cookieParser(),
(req, res, next) => {

View File

@ -0,0 +1,30 @@
/**
* The Dashboard controller is responsible for stiching the /dashboard data
* together
*/
import authentication from '@feathersjs/authentication';
import cookieParser from 'cookie-parser';
export default (app) => {
app.get('/dashboard',
cookieParser(),
authentication.express.authenticate('jwt'),
(req, res, next) => {
let query = Object.assign({
$sort: {
createdAt: -1,
}
}, req.query);
app.service('events')
.find({ query: query })
.then(result =>
res.render('dashboard', {
events: result,
user: (req as any).user,
query: req.query,
})
);
});
};

23
src/controllers/export.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* The Export controller is responsible for generating a file download
* containing entries matching a specific type
*/
import authentication from '@feathersjs/authentication';
import cookieParser from 'cookie-parser';
export default (app) => {
app.get('/export/:type',
cookieParser(),
authentication.express.authenticate('jwt'),
(req, res, next) => {
app.service('/events/bulk')
.find({ type: req.params.type })
.then((result) => {
res.setHeader('Content-Disposition', `attachment; filename=${req.params.type}.json`);
res.setHeader('Content-Type', 'application/json');
res.send(result);
res.end();
});
});
};

64
src/services/bulk.ts Normal file
View File

@ -0,0 +1,64 @@
/**
* The /events/bulk service is largely responsible for streaming responses in
* the form of files to clients
*/
import authentication from '@feathersjs/authentication';
import { Application, HooksObject, Params, SKIP } from '@feathersjs/feathers';
import { QueryTypes } from 'sequelize';
import logger from '../logger';
import db from '../models';
import Event from '../models/event';
export const bulkHooks : HooksObject = {
before: {
all: [
(context) => {
/*
* Allow skipping for our tests
*/
if (process.env.NODE_ENV != 'production') {
return SKIP;
}
return context;
},
authentication.hooks.authenticate(['jwt']),
],
},
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.type) {
return Promise.resolve([]);
}
/*
* 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", {
replacements: {
type: params.type,
},
type: QueryTypes.SELECT,
});
}
}
export default (app : Application) => {
app.use('/events/bulk', new Bulk(app));
app.service('events/bulk').hooks(bulkHooks);
};

View File

@ -1,5 +1,5 @@
/*
* The /receive service is responsible receiving the data POSTed from the
* The /events service is responsible receiving the data POSTed from the
* Jenkins instances and storing it in the database
*/
@ -8,6 +8,7 @@ import { Application, HooksObject } from '@feathersjs/feathers';
import service from 'feathers-sequelize';
import { DataTypes } from 'sequelize';
import logger from '../logger';
import db from '../models';
import Event from '../models/event';

View File

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

34
test/bulk.test.ts Normal file
View File

@ -0,0 +1,34 @@
import url from 'url';
import request from 'request-promise';
import app from '../src/app';
import { Bulk } from '../src/service/bulk';
// Offsetting a bit to ensure that we can watch and run at the same time
const port = (app.get('port') || 3030) + 10;
const getUrl = pathname => url.format({
hostname: app.get('host') || 'localhost',
protocol: 'http',
port,
pathname
});
describe('Acceptance tests for /evetns/bulk', () => {
beforeEach((done) => {
this.server = app.listen(port);
this.server.once('listening', () => done());
});
afterEach(done => this.server.close(done));
describe('GET on /events/bulk', () => {
it('without a type parameter should be empty', () => {
return request(getUrl('/events/bulk'), {
json: true,
resolveWithFullResponse: true,
}).then((response) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toHaveLength(0);
});
});
});
});

View File

@ -2,6 +2,7 @@ import url from 'url';
import request from 'request-promise';
import app from '../src/app';
import events from '../src/service/events';
// Offsetting a bit to ensure that we can watch and run at the same time
const port = (app.get('port') || 3030) + 10;
@ -17,10 +18,7 @@ describe('Acceptance tests for /evetns', () => {
this.server = app.listen(port);
this.server.once('listening', () => done());
});
afterEach((done) => {
this.server.close(done);
});
afterEach(done => this.server.close(done));
it('responds to GET /events', () => {
return request(getUrl('/events'), {

View File

@ -20,11 +20,11 @@ html(lang="en")
div
ul.list-inline
li.list-inline-item
a(href='/dashboard').
a(href='/dashboard', title='Reset the filter query').
Reset query
if events.total > events.limit
li.list-inline-item
a(href='?id[$lt]=' + events.data[events.data.length - 1].id).
a(href='?id[$lt]=' + events.data[events.data.length - 1].id, title='View the next page of results').
Next
table.table-dark.table
tr
@ -50,9 +50,14 @@ html(lang="en")
td.text-center
textarea(cols=80, rows=4, readonly=true).
#{JSON.stringify(e.payload)}
td.text-center
a(href='/events/' + e.id, target='_blank').
View Raw
td
ul.list
li.list-item
a(href='/events/' + e.id, target='_blank', title='View the raw JSON of this event').
View Raw
li.list-item
a(href='/export/' + e.type, title='Export a .json file of all `' + e.type + '` records').
Export all of this type
// vim: ft=haml