diff --git a/api/material.yaml b/api/material.yaml index 548bce8..0bdd996 100644 --- a/api/material.yaml +++ b/api/material.yaml @@ -20,7 +20,13 @@ schema: type: array items: - $ref: 'api.yaml#/components/schemas/Material' + allOf: + - $ref: 'api.yaml#/components/schemas/Material' + properties: + status: + type: string + description: can be deleted/new/validated + example: new 401: $ref: 'api.yaml#/components/responses/401' 500: diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index 1bbae13..657cb7c 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -24,11 +24,12 @@ describe('/material', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'validated').length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); + should(material).have.property('status', 'validated'); should(material.properties).have.property('material_template').be.type('string'); should(material.numbers).be.instanceof(Array); }); @@ -46,11 +47,12 @@ describe('/material', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'validated').length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); + should(material).have.property('status', 'validated'); should(material.properties).have.property('material_template').be.type('string'); should(material.numbers).be.instanceof(Array); }); @@ -60,7 +62,7 @@ describe('/material', () => { it('allows filtering by state', done => { TestHelper.request(server, done, { method: 'get', - url: '/materials?status=new', + url: '/materials?status[]=new', auth: {basic: 'janedoe'}, httpStatus: 200 }).end((err, res) => { @@ -68,24 +70,57 @@ describe('/material', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'new').length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); + should(material).have.property('status', 'new'); should(material.properties).have.property('material_template').be.type('string'); should(material.numbers).be.instanceof(Array); }); done(); }); }); + it('allows filtering by deleted state for admins', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials?status[]=deleted', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'deleted').length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('name').be.type('string'); + should(material).have.property('supplier').be.type('string'); + should(material).have.property('group').be.type('string'); + should(material).have.property('status', 'deleted'); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); + }); + done(); + }); + }); + it('rejects filtering by deleted state for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials?status[]=deleted', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} + }); + }); it('rejects an invalid state name', done => { TestHelper.request(server, done, { method: 'get', - url: '/materials?status=xxx', + url: '/materials?status[]=xxx', auth: {basic: 'janedoe'}, httpStatus: 400, - res: {status: 'Invalid body format', details: '"status" must be one of [validated, new, all]'} + res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} }); }); it('rejects unauthorized requests', done => { diff --git a/src/routes/material.ts b/src/routes/material.ts index a1cf1e5..4fb4bad 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -21,7 +21,8 @@ const router = express.Router(); router.get('/materials', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - const {error, value: filters} = MaterialValidate.query(req.query); + const {error, value: filters} = + MaterialValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0); if (error) return res400(error, res); let conditions; @@ -38,11 +39,12 @@ router.get('/materials', (req, res, next) => { conditions = {status: globals.status.val}; } - MaterialModel.find(conditions).populate('group_id').populate('supplier_id').lean().exec((err, data) => { + MaterialModel.find(conditions).sort({name: 1}).populate('group_id').populate('supplier_id') + .lean().exec((err, data) => { if (err) return next(err); // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => MaterialValidate.output(e)))); + res.json(_.compact(data.map(e => MaterialValidate.output(e, true)))); }); }); diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts index b4532e8..cf5782d 100644 --- a/src/routes/validate/material.ts +++ b/src/routes/validate/material.ts @@ -20,7 +20,10 @@ export default class MaterialValidate { // validate input for material .items( Joi.string() .max(64) - ) + ), + + status: Joi.string() + .valid(...Object.values(globals.status)) }; static input (data, param) { // validate input, set param to 'new' to make all attributes required @@ -47,18 +50,22 @@ export default class MaterialValidate { // validate input for material } } - static output (data) { // validate output and strip unwanted properties, returns null if not valid + static output (data, status = false) { // validate output and strip unwanted properties, returns null if not valid data = IdValidate.stringify(data); data.group = data.group_id.name; data.supplier = data.supplier_id.name; - const {value, error} = Joi.object({ + const validate: any = { _id: IdValidate.get(), name: this.material.name, supplier: this.material.supplier, group: this.material.group, properties: this.material.properties, numbers: this.material.numbers - }).validate(data, {stripUnknown: true}); + }; + if (status) { + validate.status = this.material.status; + } + const {value, error} = Joi.object(validate).validate(data, {stripUnknown: true}); return error !== undefined? null : value; } @@ -83,9 +90,13 @@ export default class MaterialValidate { // validate input for material }); } - static query (data) { + static query (data, dev = false) { + const acceptedStatuses = [globals.status.val, globals.status.new]; + if (dev) { // dev and admin can also access deleted samples + acceptedStatuses.push(globals.status.del) + } return Joi.object({ - status: Joi.string().valid(globals.status.val, globals.status.new, 'all') + status: Joi.array().items(Joi.string().valid(...acceptedStatuses)).default([globals.status.val]) }).validate(data); } } \ No newline at end of file