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

View File

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

View File

@ -19,6 +19,15 @@ module.exports = {
getPK: function (type) { getPK: function (type) {
return global[_.capitalize(type)].idAttribute || 'id'; 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. // Public node modules.
const _ = require('lodash'); const _ = require('lodash');
const utilsBookShelf = require('./bookshelf');
const utilsWaterline = require('./waterline');
/** /**
* JSON API utils * JSON API utils
@ -78,32 +76,13 @@ module.exports = {
return undefined; 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 * Find router object for matched route
*/ */
matchedRoute: function (ctx) { matchedRoute: function (ctx) {
return _.find(strapi.router.stack, function (stack) { 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; return stack;
} }
}); });

View File

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