diff --git a/api/sample.yaml b/api/sample.yaml index c6e59a1..f527513 100644 --- a/api/sample.yaml +++ b/api/sample.yaml @@ -42,9 +42,15 @@ schema: type: boolean example: false + - name: fields + description: the fields to include in the output as array, defaults to ['_id','number','type','batch','material_id','color','condition','note_id','user_id','added'] + in: query + schema: + type: string + example: '&fields[]=number&fields[]=batch' responses: 200: - description: samples overview + description: samples overview (if the csv parameter is set, this is in CSV instead of JSON format) content: application/json: schema: diff --git a/src/db.ts b/src/db.ts index 60dadf9..cb11af5 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import cfenv from 'cfenv'; import _ from 'lodash'; import ChangelogModel from './models/changelog'; -// mongoose.set('debug', true); // enable mongoose debug +mongoose.set('debug', true); // enable mongoose debug // database urls, prod db url is retrieved automatically const TESTING_URL = 'mongodb://localhost/dfopdb_test'; diff --git a/src/index.ts b/src/index.ts index 97a080e..9af77cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,8 +48,9 @@ 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, ignore) => { + app.use('/api/:url([^]+)', (req, res) => { req.url = '/' + req.params.url; + app.handle(req, res); }); } diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index e7f6cfb..29e35b7 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -259,6 +259,33 @@ describe('/sample', () => { done(); }); }); + it('returns only the fields specified', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&page-size=1&fields[]=number&fields[]=condition&fields[]=color&fields[]=material.name&fields[]=material.mineral', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: [{number: '1', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, color: 'black', material: {name: 'Schulamid 66 GF 25 H', mineral: 0}}] + }); + }); + it('rejects an invalid fields parameter', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&page-size=1&fields=number', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"fields" must be an array'} + }); + }); + it('rejects an unknown field name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&page-size=1&fields[]=xx', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"fields[0]" must be one of [_id, color, number, type, batch, added, material.name, material.supplier, material.group, material.mineral, material.glass_fiber, material.carbon_fiber, material.number, condition, material_id, note_id, user_id, material._id, material.numbers]'} + }); + }); it('rejects a negative page size', done => { TestHelper.request(server, done, { method: 'get', @@ -302,7 +329,7 @@ describe('/sample', () => { httpStatus: 401 }); }); - }); + }); // TODO: measurement fields describe('GET /samples/{state}', () => { it('returns all new samples', done => { diff --git a/src/routes/sample.ts b/src/routes/sample.ts index 82a4eea..f356211 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -48,19 +48,17 @@ router.get('/samples', async (req, res, next) => { filters['to-page'] = 0; } - if (filters.sort[0].indexOf('material.') >= 0) { // need to populate materials, material supplier and group - query.push( - {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, - {$set: {material: { $arrayElemAt: ['$material', 0]}}}, - {$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.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]} - } + query.push( + {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, + {$set: {material: { $arrayElemAt: ['$material', 0]}}}, + {$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.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]} } - ); - } + } + ); if (filters['from-id']) { // from-id specified const fromSample = await SampleModel.findById(filters['from-id']).lean().exec().catch(err => {next(err);}); @@ -79,9 +77,9 @@ router.get('/samples', async (req, res, next) => { query.push({$sort: {[filters.sort[0]]: filters.sort[1], '_id': filters.sort[1]}}); // set _id as secondary sort } - if (filters.sort[0].indexOf('material.') >= 0) { // unpopulate materials again - query.push({$unset: 'material'}); - } + // if (filters.sort[0].indexOf('material.') >= 0) { // unpopulate materials again + // query.push({$unset: 'material'}); + // } if (filters['to-page']) { query.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + Number(filters['to-page'] < 0)}) // number to skip, if going back pages, one page has to be skipped less but on sample more @@ -90,6 +88,16 @@ router.get('/samples', async (req, res, next) => { if (filters['page-size']) { query.push({$limit: filters['page-size']}); } + console.log(filters.fields); + const projection = filters.fields.reduce((s, e) => {s[e] = true; return s; }, {}); + if (filters.fields.indexOf('added') >= 0) { // add added date + projection.added = {$toDate: '$_id'}; + } + if (!(filters.fields.indexOf('_id') >= 0)) { // disable _id explicitly + console.log('disable id'); + projection._id = false; + } + query.push({$project: projection}); SampleModel.aggregate(query).exec((err, data) => { if (err) return next(err); @@ -97,14 +105,14 @@ router.get('/samples', async (req, res, next) => { data.reverse(); } if (filters.csv) { // output as csv // TODO: csv example in OAS - csv(_.compact(data.map(e => SampleValidate.output(e))), ['_id', 'number'], (err, data) => { + csv(_.compact(data.map(e => SampleValidate.output(e, 'refs'))), ['_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 + res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs')))); // validate all and filter null values from validation errors } }) }); diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index d6c77a2..520f91b 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -51,6 +51,32 @@ export default class SampleValidate { .min('1970-01-01T00:00:00.000Z') }; + private static sortKeys = [ + '_id', + 'color', + 'number', + 'type', + 'batch', + 'added', + 'material.name', + 'material.supplier', + 'material.group', + 'material.mineral', + 'material.glass_fiber', + 'material.carbon_fiber', + 'material.number' + ]; + + private static fieldKeys = [ + ...SampleValidate.sortKeys, + 'condition', + 'material_id', + 'note_id', + 'user_id', + 'material._id', + 'material.numbers' + ]; + static input (data, param) { // validate input, set param to 'new' to make all attributes required if (param === 'new') { return Joi.object({ @@ -88,8 +114,11 @@ export default class SampleValidate { } } - static output (data, param = 'refs') { // validate output and strip unwanted properties, returns null if not valid - data.added = data._id.getTimestamp(); + static output (data, param = 'refs+added') { // validate output and strip unwanted properties, returns null if not valid + if (param === 'refs+added') { + param = 'refs'; + data.added = data._id.getTimestamp(); + } data = IdValidate.stringify(data); let joiObject; if (param === 'refs') { @@ -101,6 +130,7 @@ export default class SampleValidate { batch: this.sample.batch, condition: this.sample.condition, material_id: IdValidate.get(), + material: MaterialValidate.outputV(), note_id: IdValidate.get().allow(null), user_id: IdValidate.get(), added: this.sample.added @@ -133,8 +163,9 @@ 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|material\.number)-(asc|desc)$/m).default('_id-asc'), - csv: Joi.boolean().default(false) + sort: Joi.string().pattern(new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.') + ')-(asc|desc)$', 'm')).default('_id-asc'), + csv: Joi.boolean().default(false), + fields: Joi.array().items(Joi.string().valid(...this.fieldKeys)).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']) }).with('to-page', 'page-size').validate(data); } } \ No newline at end of file