From e5cc661928f7b844f631c5b228513835329710d8 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 29 Jun 2020 12:27:39 +0200 Subject: [PATCH] base for csv export --- api/sample.yaml | 6 ++++++ package-lock.json | 22 ++++++++++++++++++++++ package.json | 1 + src/helpers/authorize.ts | 4 +++- src/helpers/csv.ts | 7 +++++++ src/index.ts | 3 +-- src/routes/sample.spec.ts | 15 +++++++++++++++ src/routes/sample.ts | 17 +++++++++++++++-- src/routes/validate/sample.ts | 3 ++- src/test/helper.ts | 26 +++++++++++++++----------- 10 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 src/helpers/csv.ts diff --git a/api/sample.yaml b/api/sample.yaml index 5f07b78..c6e59a1 100644 --- a/api/sample.yaml +++ b/api/sample.yaml @@ -36,6 +36,12 @@ schema: type: string example: color-asc + - name: csv + description: output as csv + in: query + schema: + type: boolean + example: false responses: 200: description: samples overview diff --git a/package-lock.json b/package-lock.json index 93fdea0..5478eef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2203,6 +2203,23 @@ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz", "integrity": "sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ==" }, + "json2csv": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.1.tgz", + "integrity": "sha512-QFMifUX1y8W2tKi2TwZpnzf2rHdZvzdmgZUMEMDF46F90f4a9mUeWfx/qg4kzXSZYJYc3cWA5O+eLXk5lj9g8g==", + "requires": { + "commander": "^5.0.0", + "jsonparse": "^1.3.1", + "lodash.get": "^4.4.2" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + } + } + }, "json5": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", @@ -2212,6 +2229,11 @@ "minimist": "^1.2.5" } }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, "jszip": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", diff --git a/package.json b/package.json index e5ca620..4b04218 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "express": "^4.17.1", "helmet": "^3.22.0", "json-schema": "^0.2.5", + "json2csv": "^5.0.1", "lodash": "^4.17.15", "mongo-sanitize": "^1.1.0", "mongoose": "^5.8.7", diff --git a/src/helpers/authorize.ts b/src/helpers/authorize.ts index 71a42c2..03d344b 100644 --- a/src/helpers/authorize.ts +++ b/src/helpers/authorize.ts @@ -89,7 +89,9 @@ function key (req, next): any { // checks API key and returns changed user obje if (err) return next(err); if (data.length === 1) { // one user found resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location}); - delete req.query.key; // delete query parameter to avoid interference with later validation + if (!/^\/api/m.test(req.url)){ + delete req.query.key; // delete query parameter to avoid interference with later validation + } } else { resolve(null); diff --git a/src/helpers/csv.ts b/src/helpers/csv.ts new file mode 100644 index 0000000..dbeb213 --- /dev/null +++ b/src/helpers/csv.ts @@ -0,0 +1,7 @@ +import {parseAsync} from 'json2csv'; + +export default function csv(input: object, fields: string[], f: (err, data) => void) { + parseAsync(input) + .then(csv => f(null, csv)) + .catch(err => f(err, null)); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9af77cf..97a080e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,9 +48,8 @@ app.use(require('./helpers/authorize')); // handle authentication // redirect /api routes for Angular proxy in development if (process.env.NODE_ENV !== 'production') { - app.use('/api/:url([^]+)', (req, res) => { + app.use('/api/:url([^]+)', (req, ignore) => { req.url = '/' + req.params.url; - app.handle(req, res); }); } diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index 0aa1c1f..e7f6cfb 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -244,6 +244,21 @@ describe('/sample', () => { done(); }); }); + it('returns a correct csv file if specified', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&page-size=2&csv=true', + contentType: /text\/csv/, + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.text).be.eql('"_id","number","type","color","batch","condition","material_id","note_id","user_id","added"\r\n' + + '"400000000000000000000001","1","granulate","black","","{""material"":""copper"",""weeks"":3,""condition_template"":""200000000000000000000001""}","100000000000000000000004",,"000000000000000000000002","2004-01-10T13:37:04.000Z"\r\n' + + '"400000000000000000000002","21","granulate","natural","1560237365","{""material"":""copper"",""weeks"":3,""condition_template"":""200000000000000000000001""}","100000000000000000000001","500000000000000000000001","000000000000000000000002","2004-01-10T13:37:04.000Z"'); + done(); + }); + }); it('rejects a negative page size', done => { TestHelper.request(server, done, { method: 'get', diff --git a/src/routes/sample.ts b/src/routes/sample.ts index 4a04fc3..82a4eea 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -15,6 +15,7 @@ import ConditionTemplateModel from '../models/condition_template'; import ParametersValidate from './validate/parameters'; import globals from '../globals'; import db from '../db'; +import csv from '../helpers/csv'; const router = express.Router(); @@ -54,7 +55,10 @@ router.get('/samples', async (req, res, next) => { {$lookup: { from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }}, {$set: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}}, {$lookup: { from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}}, - {$set: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} + {$set: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}, + {$set: {'material.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]} + } + } ); } @@ -92,7 +96,16 @@ router.get('/samples', async (req, res, next) => { if (filters['to-page'] < 0) { data.reverse(); } - res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors + if (filters.csv) { // output as csv // TODO: csv example in OAS + csv(_.compact(data.map(e => SampleValidate.output(e))), ['_id', 'number'], (err, data) => { + if (err) return next(err); + res.set('Content-Type', 'text/csv'); + res.send(data); + }); + } + else { + res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors + } }) }); diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index b55b847..d6c77a2 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -133,7 +133,8 @@ export default class SampleValidate { 'from-id': IdValidate.get(), 'to-page': Joi.number().integer(), 'page-size': Joi.number().integer().min(1), - sort: Joi.string().pattern(/^(_id|color|number|type|batch|added|material\.name|material\.supplier|material\.group|material\.mineral|material\.glass_fiber|material\.carbon_fiber)-(asc|desc)$/m).default('_id-asc') // TODO: material keys + sort: Joi.string().pattern(/^(_id|color|number|type|batch|added|material\.name|material\.supplier|material\.group|material\.mineral|material\.glass_fiber|material\.carbon_fiber|material\.number)-(asc|desc)$/m).default('_id-asc'), + csv: Joi.boolean().default(false) }).with('to-page', 'page-size').validate(data); } } \ No newline at end of file diff --git a/src/test/helper.ts b/src/test/helper.ts index e1e8eec..44085f7 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -38,15 +38,7 @@ export default class TestHelper { return server } - static afterEach (server, done) { - server.close(done); - } - - static after(done) { - db.disconnect(done); - } - - static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, default (set to false if you want to dismiss default .end handling)} + static request (server, done, options) { // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, default (set to false if you want to dismiss default .end handling)} let st = supertest(server); if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { // resolve API key options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key); @@ -79,8 +71,12 @@ export default class TestHelper { st = st.auth(options.auth.basic.name, options.auth.basic.pass) } } - st = st.expect('Content-type', /json/) - .expect(options.httpStatus); + if (options.hasOwnProperty('contentType')) { + st = st.expect('Content-type', options.contentType).expect(options.httpStatus); + } + else { + st = st.expect('Content-type', /json/).expect(options.httpStatus); + } if (options.hasOwnProperty('res')) { // evaluate result return st.end((err, res) => { if (err) return done (err); @@ -128,4 +124,12 @@ export default class TestHelper { return st; } } + + static afterEach (server, done) { + server.close(done); + } + + static after(done) { + db.disconnect(done); + } } \ No newline at end of file