first implementation of fields
This commit is contained in:
		@@ -42,9 +42,15 @@
 | 
				
			|||||||
        schema:
 | 
					        schema:
 | 
				
			||||||
          type: boolean
 | 
					          type: boolean
 | 
				
			||||||
        example: false
 | 
					        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:
 | 
					    responses:
 | 
				
			||||||
      200:
 | 
					      200:
 | 
				
			||||||
        description: samples overview
 | 
					        description: samples overview (if the csv parameter is set, this is in CSV instead of JSON format)
 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
          application/json:
 | 
					          application/json:
 | 
				
			||||||
            schema:
 | 
					            schema:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ import cfenv from 'cfenv';
 | 
				
			|||||||
import _ from 'lodash';
 | 
					import _ from 'lodash';
 | 
				
			||||||
import ChangelogModel from './models/changelog';
 | 
					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
 | 
					// database urls, prod db url is retrieved automatically
 | 
				
			||||||
const TESTING_URL = 'mongodb://localhost/dfopdb_test';
 | 
					const TESTING_URL = 'mongodb://localhost/dfopdb_test';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,8 +48,9 @@ app.use(require('./helpers/authorize'));  // handle authentication
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// redirect /api routes for Angular proxy in development
 | 
					// redirect /api routes for Angular proxy in development
 | 
				
			||||||
if (process.env.NODE_ENV !== 'production') {
 | 
					if (process.env.NODE_ENV !== 'production') {
 | 
				
			||||||
  app.use('/api/:url([^]+)', (req, ignore) => {
 | 
					  app.use('/api/:url([^]+)', (req, res) => {
 | 
				
			||||||
    req.url = '/' + req.params.url;
 | 
					    req.url = '/' + req.params.url;
 | 
				
			||||||
 | 
					    app.handle(req, res);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -259,6 +259,33 @@ describe('/sample', () => {
 | 
				
			|||||||
        done();
 | 
					        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 => {
 | 
					    it('rejects a negative page size', done => {
 | 
				
			||||||
      TestHelper.request(server, done, {
 | 
					      TestHelper.request(server, done, {
 | 
				
			||||||
        method: 'get',
 | 
					        method: 'get',
 | 
				
			||||||
@@ -302,7 +329,7 @@ describe('/sample', () => {
 | 
				
			|||||||
        httpStatus: 401
 | 
					        httpStatus: 401
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });  // TODO: measurement fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('GET /samples/{state}', () => {
 | 
					  describe('GET /samples/{state}', () => {
 | 
				
			||||||
    it('returns all new samples', done => {
 | 
					    it('returns all new samples', done => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,7 +48,6 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
    filters['to-page'] = 0;
 | 
					    filters['to-page'] = 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (filters.sort[0].indexOf('material.') >= 0) {  // need to populate materials, material supplier and group
 | 
					 | 
				
			||||||
  query.push(
 | 
					  query.push(
 | 
				
			||||||
    {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
 | 
					    {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
 | 
				
			||||||
    {$set: {material: { $arrayElemAt: ['$material', 0]}}},
 | 
					    {$set: {material: { $arrayElemAt: ['$material', 0]}}},
 | 
				
			||||||
@@ -60,7 +59,6 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (filters['from-id']) {  // from-id specified
 | 
					  if (filters['from-id']) {  // from-id specified
 | 
				
			||||||
    const fromSample = await SampleModel.findById(filters['from-id']).lean().exec().catch(err => {next(err);});
 | 
					    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
 | 
					    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
 | 
					  // if (filters.sort[0].indexOf('material.') >= 0) {  // unpopulate materials again
 | 
				
			||||||
    query.push({$unset: 'material'});
 | 
					  //   query.push({$unset: 'material'});
 | 
				
			||||||
  }
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (filters['to-page']) {
 | 
					  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
 | 
					    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']) {
 | 
					  if (filters['page-size']) {
 | 
				
			||||||
    query.push({$limit: 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) => {
 | 
					  SampleModel.aggregate(query).exec((err, data) => {
 | 
				
			||||||
    if (err) return next(err);
 | 
					    if (err) return next(err);
 | 
				
			||||||
@@ -97,14 +105,14 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
      data.reverse();
 | 
					      data.reverse();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (filters.csv) {  // output as csv  // TODO: csv example in OAS
 | 
					    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);
 | 
					        if (err) return next(err);
 | 
				
			||||||
        res.set('Content-Type', 'text/csv');
 | 
					        res.set('Content-Type', 'text/csv');
 | 
				
			||||||
        res.send(data);
 | 
					        res.send(data);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    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
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,6 +51,32 @@ export default class SampleValidate {
 | 
				
			|||||||
      .min('1970-01-01T00:00:00.000Z')
 | 
					      .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
 | 
					  static input (data, param) {  // validate input, set param to 'new' to make all attributes required
 | 
				
			||||||
    if (param === 'new') {
 | 
					    if (param === 'new') {
 | 
				
			||||||
      return Joi.object({
 | 
					      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
 | 
					  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.added = data._id.getTimestamp();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    data = IdValidate.stringify(data);
 | 
					    data = IdValidate.stringify(data);
 | 
				
			||||||
    let joiObject;
 | 
					    let joiObject;
 | 
				
			||||||
    if (param === 'refs') {
 | 
					    if (param === 'refs') {
 | 
				
			||||||
@@ -101,6 +130,7 @@ export default class SampleValidate {
 | 
				
			|||||||
        batch: this.sample.batch,
 | 
					        batch: this.sample.batch,
 | 
				
			||||||
        condition: this.sample.condition,
 | 
					        condition: this.sample.condition,
 | 
				
			||||||
        material_id: IdValidate.get(),
 | 
					        material_id: IdValidate.get(),
 | 
				
			||||||
 | 
					        material: MaterialValidate.outputV(),
 | 
				
			||||||
        note_id: IdValidate.get().allow(null),
 | 
					        note_id: IdValidate.get().allow(null),
 | 
				
			||||||
        user_id: IdValidate.get(),
 | 
					        user_id: IdValidate.get(),
 | 
				
			||||||
        added: this.sample.added
 | 
					        added: this.sample.added
 | 
				
			||||||
@@ -133,8 +163,9 @@ export default class SampleValidate {
 | 
				
			|||||||
      'from-id': IdValidate.get(),
 | 
					      'from-id': IdValidate.get(),
 | 
				
			||||||
      'to-page': Joi.number().integer(),
 | 
					      'to-page': Joi.number().integer(),
 | 
				
			||||||
      'page-size': Joi.number().integer().min(1),
 | 
					      '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'),
 | 
					      sort: Joi.string().pattern(new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.') + ')-(asc|desc)$', 'm')).default('_id-asc'),
 | 
				
			||||||
      csv: Joi.boolean().default(false)
 | 
					      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);
 | 
					    }).with('to-page', 'page-size').validate(data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user