diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 8c8dc5998b..88c4d78552 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -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]'); } }); }, diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 7ddaf07a25..3028870e6c 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -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 = { diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index 923cfeee2b..9966ae666b 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -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 { diff --git a/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js b/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js index 53b0eefb72..6dc7e9c232 100644 --- a/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js +++ b/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js @@ -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; + }); + } }; diff --git a/lib/configuration/hooks/jsonapi/utils/utils.js b/lib/configuration/hooks/jsonapi/utils/utils.js index a8d14a7ffd..a9e841170d 100644 --- a/lib/configuration/hooks/jsonapi/utils/utils.js +++ b/lib/configuration/hooks/jsonapi/utils/utils.js @@ -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; } }); diff --git a/lib/configuration/index.js b/lib/configuration/index.js index fc2d02c224..590962243b 100755 --- a/lib/configuration/index.js +++ b/lib/configuration/index.js @@ -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);