Merge pull request #6019 from vdemedes/api-order

Add order parameter
This commit is contained in:
Hannah Wolfe 2015-11-01 15:04:24 +00:00
commit a78ee06848
9 changed files with 189 additions and 6 deletions

View File

@ -23,7 +23,7 @@ utils = {
// ### Manual Default Options // ### Manual Default Options
// These must be provided by the endpoint // These must be provided by the endpoint
// browseDefaultOptions - valid for all browse api endpoints // browseDefaultOptions - valid for all browse api endpoints
browseDefaultOptions: ['page', 'limit', 'fields', 'filter'], browseDefaultOptions: ['page', 'limit', 'fields', 'filter', 'order'],
// idDefaultOptions - valid whenever an id is valid // idDefaultOptions - valid whenever an id is valid
idDefaultOptions: ['id'], idDefaultOptions: ['id'],
@ -114,6 +114,7 @@ utils = {
page: {matches: /^\d+$/}, page: {matches: /^\d+$/},
limit: {matches: /^\d+|all$/}, limit: {matches: /^\d+|all$/},
fields: {matches: /^[\w, ]+$/}, fields: {matches: /^[\w, ]+$/},
order: {matches: /^[a-z0-9_,\. ]+$/i},
name: {} name: {}
}, },
// these values are sanitised/validated separately // these values are sanitised/validated separately

View File

@ -301,7 +301,11 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
// TODO: this should just be done for all methods @ the API level // TODO: this should just be done for all methods @ the API level
options.withRelated = _.union(options.withRelated, options.include); options.withRelated = _.union(options.withRelated, options.include);
options.order = self.orderDefaultOptions(); if (options.order) {
options.order = self.parseOrderOption(options.order);
} else {
options.order = self.orderDefaultOptions();
}
return itemCollection.fetchPage(options).then(function formatResponse(response) { return itemCollection.fetchPage(options).then(function formatResponse(response) {
var data = {}; var data = {};
@ -455,6 +459,36 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
// Test for duplicate slugs. // Test for duplicate slugs.
return checkIfSlugExists(slug); return checkIfSlugExists(slug);
}); });
},
parseOrderOption: function (order) {
var permittedAttributes, result, rules;
permittedAttributes = this.prototype.permittedAttributes();
result = {};
rules = order.split(',');
_.each(rules, function (rule) {
var match, field, direction;
match = /^([a-z0-9_\.]+)\s+(asc|desc)$/i.exec(rule.trim());
// invalid order syntax
if (!match) {
return;
}
field = match[1].toLowerCase();
direction = match[2].toUpperCase();
if (permittedAttributes.indexOf(field) === -1) {
return;
}
result[field] = direction;
});
return result;
} }
}); });

View File

@ -388,7 +388,7 @@ Post = ghostBookshelf.Model.extend({
// these are the only options that can be passed to Bookshelf / Knex. // these are the only options that can be passed to Bookshelf / Knex.
validOptions = { validOptions = {
findOne: ['importing', 'withRelated'], findOne: ['importing', 'withRelated'],
findPage: ['page', 'limit', 'columns', 'filter', 'status', 'staticPages'], findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status', 'staticPages'],
add: ['importing'] add: ['importing']
}; };

View File

@ -79,7 +79,7 @@ Tag = ghostBookshelf.Model.extend({
// whitelists for the `options` hash argument on methods, by method name. // whitelists for the `options` hash argument on methods, by method name.
// these are the only options that can be passed to Bookshelf / Knex. // these are the only options that can be passed to Bookshelf / Knex.
validOptions = { validOptions = {
findPage: ['page', 'limit', 'columns'] findPage: ['page', 'limit', 'columns', 'order']
}; };
if (validOptions[methodName]) { if (validOptions[methodName]) {

View File

@ -219,7 +219,7 @@ User = ghostBookshelf.Model.extend({
findOne: ['withRelated', 'status'], findOne: ['withRelated', 'status'],
setup: ['id'], setup: ['id'],
edit: ['withRelated', 'id'], edit: ['withRelated', 'id'],
findPage: ['page', 'limit', 'columns', 'status'] findPage: ['page', 'limit', 'columns', 'order', 'status']
}; };
if (validOptions[methodName]) { if (validOptions[methodName]) {

View File

@ -330,6 +330,54 @@ describe('Post API', function () {
done(); done();
}).catch(done); }).catch(done);
}); });
it('can order posts using asc', function (done) {
var posts, expectedTitles;
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
expectedTitles = _(posts).pluck('title').sortBy().value();
PostAPI.browse({context: {user: 1}, status: 'all', order: 'title asc', fields: 'title'}).then(function (results) {
should.exist(results.posts);
var titles = _.pluck(results.posts, 'title');
titles.should.eql(expectedTitles);
done();
}).catch(done);
});
it('can order posts using desc', function (done) {
var posts, expectedTitles;
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
expectedTitles = _(posts).pluck('title').sortBy().reverse().value();
PostAPI.browse({context: {user: 1}, status: 'all', order: 'title DESC', fields: 'title'}).then(function (results) {
should.exist(results.posts);
var titles = _.pluck(results.posts, 'title');
titles.should.eql(expectedTitles);
done();
}).catch(done);
});
it('can order posts and filter disallowed attributes', function (done) {
var posts, expectedTitles;
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
expectedTitles = _(posts).pluck('title').sortBy().value();
PostAPI.browse({context: {user: 1}, status: 'all', order: 'bunny DESC, title ASC', fields: 'title'}).then(function (results) {
should.exist(results.posts);
var titles = _.pluck(results.posts, 'title');
titles.should.eql(expectedTitles);
done();
}).catch(done);
});
}); });
describe('Read', function () { describe('Read', function () {

View File

@ -9,6 +9,14 @@ var testUtils = require('../../utils'),
TagAPI = require('../../../server/api/tags'); TagAPI = require('../../../server/api/tags');
// there are some random generated tags in test database
// which can't be sorted easily using _.sortBy()
// so we filter them out and leave only pre-built fixtures
// usage: tags.filter(onlyFixtures)
function onlyFixtures(slug) {
return testUtils.DataGenerator.Content.tags.indexOf(slug) >= 0;
}
describe('Tags API', function () { describe('Tags API', function () {
// Keep the DB clean // Keep the DB clean
before(testUtils.teardown); before(testUtils.teardown);
@ -259,6 +267,52 @@ describe('Tags API', function () {
done(); done();
}).catch(done); }).catch(done);
}); });
it('can browse and order by slug using asc', function (done) {
var expectedTags;
TagAPI.browse({context: {user: 1}})
.then(function (results) {
should.exist(results);
expectedTags = _(results.tags).pluck('slug').filter(onlyFixtures).sortBy().value();
return TagAPI.browse({context: {user: 1}, order: 'slug asc'});
})
.then(function (results) {
var tags;
should.exist(results);
tags = _(results.tags).pluck('slug').filter(onlyFixtures).value();
tags.should.eql(expectedTags);
})
.then(done)
.catch(done);
});
it('can browse and order by slug using desc', function (done) {
var expectedTags;
TagAPI.browse({context: {user: 1}})
.then(function (results) {
should.exist(results);
expectedTags = _(results.tags).pluck('slug').filter(onlyFixtures).sortBy().reverse().value();
return TagAPI.browse({context: {user: 1}, order: 'slug desc'});
})
.then(function (results) {
var tags;
should.exist(results);
tags = _(results.tags).pluck('slug').filter(onlyFixtures).value();
tags.should.eql(expectedTags);
})
.then(done)
.catch(done);
});
}); });
describe('Read', function () { describe('Read', function () {

View File

@ -139,6 +139,52 @@ describe('Users API', function () {
done(); done();
}).catch(done); }).catch(done);
}); });
it('can browse and order by name using asc', function (done) {
var expectedUsers;
UserAPI.browse(testUtils.context.admin)
.then(function (results) {
should.exist(results);
expectedUsers = _(results.users).pluck('slug').sortBy().value();
return UserAPI.browse(_.extend({}, testUtils.context.admin, {order: 'slug asc'}));
})
.then(function (results) {
var users;
should.exist(results);
users = _.pluck(results.users, 'slug');
users.should.eql(expectedUsers);
})
.then(done)
.catch(done);
});
it('can browse and order by name using desc', function (done) {
var expectedUsers;
UserAPI.browse(testUtils.context.admin)
.then(function (results) {
should.exist(results);
expectedUsers = _(results.users).pluck('slug').sortBy().reverse().value();
return UserAPI.browse(_.extend({}, testUtils.context.admin, {order: 'slug desc'}));
})
.then(function (results) {
var users;
should.exist(results);
users = _.pluck(results.users, 'slug');
users.should.eql(expectedUsers);
})
.then(done)
.catch(done);
});
}); });
describe('Read', function () { describe('Read', function () {

View File

@ -18,7 +18,7 @@ describe('API Utils', function () {
describe('Default Options', function () { describe('Default Options', function () {
it('should provide a set of default options', function () { it('should provide a set of default options', function () {
apiUtils.globalDefaultOptions.should.eql(['context', 'include']); apiUtils.globalDefaultOptions.should.eql(['context', 'include']);
apiUtils.browseDefaultOptions.should.eql(['page', 'limit', 'fields', 'filter']); apiUtils.browseDefaultOptions.should.eql(['page', 'limit', 'fields', 'filter', 'order']);
apiUtils.dataDefaultOptions.should.eql(['data']); apiUtils.dataDefaultOptions.should.eql(['data']);
apiUtils.idDefaultOptions.should.eql(['id']); apiUtils.idDefaultOptions.should.eql(['id']);
}); });