diff --git a/api/sample.yaml b/api/sample.yaml index bb5d9be..5f07b78 100644 --- a/api/sample.yaml +++ b/api/sample.yaml @@ -30,6 +30,12 @@ schema: type: string example: 30 + - name: sort + description: sorting of results, in format 'key-asc/desc' + in: query + schema: + type: string + example: color-asc responses: 200: description: samples overview diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index bdda00e..5a32356 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -177,6 +177,59 @@ describe('/sample', () => { done(); }); }); + it('sorts the samples ascending', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&sort=color-asc', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('color', 'black'); + should(res.body[res.body.length - 1]).have.property('color', 'natural'); + done(); + }); + }); + it('sorts the samples descending', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&sort=number-desc', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('number', 'Rng36'); + should(res.body[1]).have.property('number', '33'); + should(res.body[res.body.length - 1]).have.property('number', '1'); + done(); + }); + }); + it('sorts the samples correctly in combination with paging', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&sort=color-asc&page-size=2&from-id=400000000000000000000006', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000006'); + should(res.body[1]).have.property('_id', '400000000000000000000002'); + done(); + }); + }); + it('sorts the samples correctly in combination with going pages backward', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&sort=color-desc&page-size=2&from-id=400000000000000000000004&to-page=-1', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000002'); + should(res.body[1]).have.property('_id', '400000000000000000000006'); + 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 19ec993..7bc1b67 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -19,7 +19,7 @@ import db from '../db'; const router = express.Router(); -router.get('/samples', (req, res, next) => { +router.get('/samples', async (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; const {error, value: filters} = SampleValidate.query(req.query); @@ -37,29 +37,55 @@ router.get('/samples', (req, res, next) => { else { // default status = {status: globals.status.validated}; } - const query = SampleModel.find(status); + + + const sort = []; + let paging = {} + + // sorting + filters.sort = filters.sort.split('-'); + filters.sort[0] = filters.sort[0] === 'added' ? '_id' : filters.sort[0]; // route added sorting criteria to _id + filters.sort[1] = filters.sort[1] === 'desc' ? -1 : 1; + + if (!filters['to-page']) { // set to-page default + filters['to-page'] = 0; + } + + if (filters['from-id']) { // from-id specified + const fromSample = SampleValidate.output(await SampleModel.findById(filters['from-id']).lean().exec().catch(err => {next(err);})); + if (fromSample instanceof Error) return; + + if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) { // asc + paging = {$or: [{[filters.sort[0]]: {$gt: fromSample[filters.sort[0]]}}, {$and: [{[filters.sort[0]]: fromSample[filters.sort[0]]}, {_id: {$gte: mongoose.Types.ObjectId(filters['from-id'])}}]}]}; + sort.push([filters.sort[0], 1]); + sort.push(['_id', 1]); + } + else { + paging = {$or: [{[filters.sort[0]]: {$lt: fromSample[filters.sort[0]]}}, {$and: [{[filters.sort[0]]: fromSample[filters.sort[0]]}, {_id: {$lte: mongoose.Types.ObjectId(filters['from-id'])}}]}]}; + sort.push([filters.sort[0], -1]); + sort.push(['_id', -1]); + } + } + else { // sort from beginning + sort.push([filters.sort[0], filters.sort[1]]); // set _id as secondary sort + sort.push(['_id', filters.sort[1]]); // set _id as secondary sort + } + + const query = SampleModel.find({$and: [status, paging]}); if (filters['page-size']) { query.limit(filters['page-size']); } - if (filters['from-id']) { - if (filters['to-page'] && filters['to-page'] < 0) { - query.lt('_id', mongoose.Types.ObjectId(filters['from-id'])); // TODO: consider sorting - query.sort({_id: -1}); - } - else { - query.gte('_id', mongoose.Types.ObjectId(filters['from-id'])); // TODO: consider sorting - } + if (filters['to-page']) { + query.skip(Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + Number(filters['to-page'] < 0)); } - if (filters['to-page']) { - query.skip(Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size']); // TODO: check order for negative numbers - } + query.sort(sort); query.lean().exec((err, data) => { if (err) return next(err); - if (filters['to-page'] && filters['to-page'] < 0) { + 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 diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index 4b10a7c..7a706c2 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -132,7 +132,8 @@ export default class SampleValidate { status: Joi.string().valid('validated', 'new', 'all'), 'from-id': IdValidate.get(), '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)-(asc|desc)$/m).default('_id-asc') // TODO: material keys }).with('to-page', 'page-size').validate(data); } } \ No newline at end of file