Support pagination with JSON API

This commit is contained in:
Aurélien Georget 2016-03-02 16:20:36 +01:00
parent f08409d336
commit 93572c4e02
6 changed files with 81 additions and 63 deletions

View File

@ -150,8 +150,11 @@ module.exports = {
fields: {}
};
ctx.state.url = ctx.request.url.replace(ctx.request.search, '');
ctx.state.query = ctx.request.query;
_.forEach(ctx.query, function (value, key) {
if (_.includes(['include', 'sort', 'page', 'filter'], key)) {
if (_.includes(['include', 'sort', 'filter'], key)) {
throw {
status: 400,
body: {
@ -172,6 +175,12 @@ module.exports = {
}
ctx.state.filter.fields[type] = value.split(',');
} else if (key === 'page[number]' && _.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.hasOwnProperty('paginate') && strapi.config.jsonapi.paginate === parseInt(strapi.config.jsonapi.paginate, 10)) {
_.set(ctx.request.query, 'limit', strapi.config.jsonapi.paginate);
_.set(ctx.request.query, 'offset', strapi.config.jsonapi.paginate * (value - 1));
// Remove JSON API page strategy
ctx.request.query = _.omit(ctx.request.query, 'page[number]');
}
});
},

View File

@ -10,6 +10,7 @@ const JSONAPI = require('jsonapi-serializer');
// Local Strapi dependencies.
const utils = require('../utils/utils');
let utilsORM;
/**
* JSON API helper
@ -21,10 +22,13 @@ module.exports = {
* Set response
*/
set: function (ctx, matchedRoute, actionRoute) {
set: function * (ctx, matchedRoute, actionRoute) {
const object = utils.getObject(matchedRoute);
const type = utils.getType(ctx, actionRoute.controller);
// Load right ORM utils
utilsORM = require('../utils/' + strapi.models[type].orm);
// Fetch a relationship that does not exist
// Reject related request with `include` parameter
if (_.isUndefined(type) || (type === 'related' && ctx.params.hasOwnProperty('include'))) {
@ -43,7 +47,7 @@ module.exports = {
const value = this.fetchValue(ctx, object).toJSON() || this.fetchValue(ctx, object);
if (!_.isEmpty(value)) {
ctx.response.body = this.serialize(ctx, type, object, value, matchedRoute);
ctx.response.body = yield this.serialize(ctx, type, object, value, matchedRoute);
}
},
@ -51,9 +55,9 @@ module.exports = {
* Serialize response with JSON API specification
*/
serialize: function (ctx, type, object, value, matchedRoute) {
serialize: function * (ctx, type, object, value) {
const toSerialize = {
topLevelLinks: {self: ctx.request.origin + ctx.request.url},
topLevelLinks: {self: ctx.request.origin + ctx.request.originalUrl},
keyForAttribute: 'dash-case',
pluralizeType: false,
included: true,
@ -71,7 +75,7 @@ module.exports = {
_.assign(toSerialize, _.pick(strapi.config.jsonapi, 'keyForAttribute'));
}
const PK = utils.getPK(type);
const PK = utilsORM.getPK(type);
if (_.isArray(value) && !_.isEmpty(value)) {
// Array
@ -86,7 +90,7 @@ module.exports = {
toSerialize.dataLinks = {
self: function (record) {
if (record.hasOwnProperty(PK)) {
return ctx.request.origin + ctx.request.url + '/' + record[PK];
return ctx.request.origin + ctx.state.url + '/' + record[PK];
}
}
};
@ -124,15 +128,10 @@ module.exports = {
}
// Display JSON API pagination
// TODO:
// - Only enabled this feature for BookShelf ORM.
if (_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.hasOwnProperty('pagination') && strapi.config.jsonapi.pagination === true) {
this.includePagination(ctx, toSerialize, object, type, matchedRoute);
if (_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.hasOwnProperty('paginate') && strapi.config.jsonapi.paginate === parseInt(strapi.config.jsonapi.paginate, 10) && object === 'collection') {
yield this.includePagination(ctx, toSerialize, object, type);
}
console.log(toSerialize);
console.log(value);
const serialized = new JSONAPI.Serializer(type, value, toSerialize);
// Display JSON API version support
@ -151,32 +150,52 @@ module.exports = {
* Include pagination links to the object
*/
includePagination: function (ctx, toSerialize, object, type, matchedRoute) {
const links = {
first: null,
last: null,
prev: null,
next: null
};
includePagination: function * (ctx, toSerialize, object, type) {
return new Promise(function (resolve, reject) {
if (strapi.models.hasOwnProperty(type) && strapi.hasOwnProperty(strapi.models[type].orm) && strapi[strapi.models[type].orm].hasOwnProperty('collections')) {
// We force page-based strategy for now.
utilsORM.getCount(type).then(function (count) {
const links = {};
const pageNumber = Math.ceil(count / strapi.config.jsonapi.paginate);
let index = 1;
const currentParameters = ctx.request.url.match(matchedRoute.regexp);
const data = _.mapValues(_.indexBy(matchedRoute.paramNames, 'name'), function () {
return currentParameters[index++];
// Get current page number
const value = _.first(_.values(_.pick(ctx.state.query, 'page[number]')));
const currentPage = _.isEmpty(value) ? 1 : value;
// Verify integer
if (currentPage.toString() === parseInt(currentPage, 10).toString()) {
links.first = ctx.request.origin + ctx.state.url;
links.prev = ctx.request.origin + ctx.state.url + '?page[number]=' + (parseInt(currentPage, 10) - 1);
links.next = ctx.request.origin + ctx.state.url + '?page[number]=' + (parseInt(currentPage, 10) + 1);
links.last = ctx.request.origin + ctx.state.url + '?page[number]=' + pageNumber;
if ((parseInt(currentPage, 10) - 1) === 0) {
links.prev = links.first;
}
// Last page
if (ctx.request.url === ctx.state.url + '?page[number]=' + pageNumber) {
// Don't display useless
links.last = null;
links.next = null;
} else if (ctx.request.url === ctx.state.url) {
// First page
links.first = null;
links.prev = null;
}
}
_.assign(toSerialize.topLevelLinks, _.omit(links, _.isNull));
resolve();
})
.catch(function (err) {
reject(err);
});
} else {
resolve();
}
});
// TODO:
// - Call request to get first, latest, previous and next record
switch (object) {
default:
_.assign(toSerialize.topLevelLinks, _.mapValues(links, function () {
return ctx.request.origin + matchedRoute.path.replace(/(:[a-z]+)/g, function (match, token) {
return data[token.substr(1)];
});
}));
break;
}
},
/**
@ -186,7 +205,7 @@ module.exports = {
includedRelationShips: function (ctx, toSerialize, type) {
if (strapi.models.hasOwnProperty(type)) {
_.forEach(strapi.models[type].associations, function (relation) {
const PK = utils.getPK(relation.model) || utils.getPK(relation.collection);
const PK = utilsORM.getPK(relation.model) || utilsORM.getPK(relation.collection);
// TODO:
// - Use matched route
const availableRoutes = {

View File

@ -48,7 +48,7 @@ module.exports = function (strapi) {
const actionRoute = strapi.config.routes[this.request.method.toUpperCase() + ' ' + matchedRoute.path];
if (!_.isUndefined(actionRoute)) {
response.set(this, matchedRoute, actionRoute);
yield response.set(this, matchedRoute, actionRoute);
}
}
} else {

View File

@ -19,6 +19,15 @@ module.exports = {
getPK: function (type) {
return global[_.capitalize(type)].idAttribute || 'id';
}
},
/**
* Find primary key
*/
getCount: function (type) {
return strapi.bookshelf.collections[type].forge().count().then(function (count) {
return count;
});
}
};

View File

@ -6,8 +6,6 @@
// Public node modules.
const _ = require('lodash');
const utilsBookShelf = require('./bookshelf');
const utilsWaterline = require('./waterline');
/**
* JSON API utils
@ -78,32 +76,13 @@ module.exports = {
return undefined;
},
/**
* Find primary key
*/
getPK: function (type) {
if (!strapi.models.hasOwnProperty(type)) {
return null;
}
switch (strapi.models[type].orm.toLowerCase()) {
case 'bookshelf':
return utilsBookShelf.getPK(type);
case 'waterline':
return utilsWaterline.getPK(type);
default:
return null;
}
},
/**
* Find router object for matched route
*/
matchedRoute: function (ctx) {
return _.find(strapi.router.stack, function (stack) {
if (new RegExp(stack.regexp).test(ctx.request.url) && _.includes(stack.methods, ctx.request.method.toUpperCase())) {
if (new RegExp(stack.regexp).test(ctx.request.url.replace(ctx.request.search, '')) && _.includes(stack.methods, ctx.request.method.toUpperCase())) {
return stack;
}
});

View File

@ -39,6 +39,8 @@ module.exports = function (strapi) {
// Set up config defaults.
return {
collections: {},
// Core (default) hooks.
hooks: _.reduce(DEFAULT_HOOKS, function (memo, hookBundled, hookIdentity) {
memo[hookIdentity] = require('./hooks/' + hookIdentity);