From 03400e82f5bbd376aad75390d88a5f1cc388bb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 22 Jan 2016 15:40:43 +0100 Subject: [PATCH 01/40] Build interceptor and JSON API helpers --- lib/configuration/hooks/defaultHooks.js | 3 +- .../hooks/jsonapi/helpers/response.js | 19 ++++++ lib/configuration/hooks/jsonapi/index.js | 62 +++++++++++++++++++ package.json | 1 + 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 lib/configuration/hooks/jsonapi/helpers/response.js create mode 100644 lib/configuration/hooks/jsonapi/index.js diff --git a/lib/configuration/hooks/defaultHooks.js b/lib/configuration/hooks/defaultHooks.js index 93db13464b..c753dbf610 100755 --- a/lib/configuration/hooks/defaultHooks.js +++ b/lib/configuration/hooks/defaultHooks.js @@ -27,5 +27,6 @@ module.exports = { router: true, static: true, websockets: true, - studio: true + studio: true, + jsonapi: true }; diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js new file mode 100644 index 0000000000..09435d32ad --- /dev/null +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -0,0 +1,19 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); +const JSONAPISerializer = require('jsonapi-serializer'); + +/** + * JSON API helper + */ + +module.exports = { + set: function(ctx, actionRoute) { + console.log(actionRoute); + } +}; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js new file mode 100644 index 0000000000..2d0870d646 --- /dev/null +++ b/lib/configuration/hooks/jsonapi/index.js @@ -0,0 +1,62 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); +const regex = require('../../../../util/regex'); +const response = require('./helpers/response'); + +/** + * JSON API hook + */ + +module.exports = function (strapi) { + const hook = { + + /** + * Default options + */ + + defaults: { + jsonapi: {} + }, + + /** + * Initialize the hook + */ + + initialize: function (cb) { + function *interceptor(next) { + const self = this; + + // Wait for downstream middleware/handlers to execute to build the response + yield next; + + // Detect route + const matchedRoute = _.find(strapi.router.stack, function(stack) { + if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { + return stack; + } + }); + + // Handlers set the response body + if (!_.isUndefined(matchedRoute)) { + const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; + + if (!_.isUndefined(actionRoute)) { + response.set(this, actionRoute); + } + } + }; + + strapi.app.use(interceptor); + + cb(); + } + }; + + return hook; +}; diff --git a/package.json b/package.json index 4216bceb88..241a29c1a8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "herd": "~1.0.0", "include-all": "~0.1.6", "json-stringify-safe": "~5.0.1", + "jsonapi-serializer": "^2.0.4", "koa": "~1.1.2", "koa-bodyparser": "~2.0.1", "koa-compose": "~2.3.0", From 684acb6f7d4f5772d684b24a6d197e6031550880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 22 Jan 2016 17:59:46 +0100 Subject: [PATCH 02/40] Serialize data for collections and ressources --- .../hooks/jsonapi/helpers/response.js | 148 +++++++++++++++++- lib/configuration/hooks/jsonapi/index.js | 8 +- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 09435d32ad..2a546708b8 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -13,7 +13,151 @@ const JSONAPISerializer = require('jsonapi-serializer'); */ module.exports = { - set: function(ctx, actionRoute) { - console.log(actionRoute); + + default: {}, + + /** + * Initialize the hook + */ + + set: function(ctx, matchedRoute, actionRoute) { + const type = actionRoute.controller.toLowerCase(); + const kind = this.whichKind(matchedRoute); + const value = this.verifyAndSetValue(ctx, kind); + + ctx.response.body = this.serialize(ctx, type, kind, value); + }, + + /** + * Verify type of the value + */ + + serialize: function(ctx, type, kind, value) { + const toSerialize = { + topLevelLinks: { self: ctx.request.origin + ctx.request.url } + }; + + switch (kind) { + case 'collection': + if (!_.isEmpty(value) && _.isArray(value)) { + toSerialize.dataLinks = { + self: function (record) { + return ctx.request.origin + ctx.request.url + '/' + record.id + } + }; + } + + if (true) { + toSerialize.attributes = ['id']; + } + + return new JSONAPISerializer(type, value, toSerialize); + break; + case 'ressource': + if (true) { + toSerialize.attributes = ['id']; + } + + return new JSONAPISerializer(type, value, toSerialize); + break; + case 'relationships': + + break; + case 'related': + + break; + default: + + } + }, + + /** + * Verify type of the value + */ + + verifyAndSetValue: function(ctx, kind) { + const data = ctx.body; + + switch (ctx.response.status) { + case 404: + ctx.status = 200; + ctx.body = null; + break; + } + + switch (kind) { + case 'collection': + // Collection + if (_.isArray(data) && _.size(data) > 1) { + return data; + } else if (_.isArray(data) && (_.size(data) === 1 || _.size(data) === 0)) { + return _.isObject(data[0]) ? data[0] : []; + } else { + return null; + } + break; + case 'ressource': + // Ressource + if (_.isObject(data)) { + return data; + } else { + return null; + } + break; + case 'relationships': + // TODO: + // - Detect kind of relation + // - MtM, OtM: array + // - OtO, MtO: object + + + // Relationships + if (_.isObject(data) || _.isArray(data)) { + return data; + } else { + return null; + } + break; + case 'related': + // TODO: + // - Detect kind of relation + // - MtM, OtM: array + // - OtO, MtO: object + + + // Related + if (_.isObject(data) || _.isArray(data)) { + return data; + } else { + return null; + } + break; + default: + return 'collection' + } + }, + + /** + * Return kind of ressources + */ + + whichKind: function(matchedRoute) { + // Top level route + switch (_.size(matchedRoute.regexp.keys)) { + case 0: + // Collection + return 'collection'; + break; + case 1: + // Ressource + return 'ressource'; + break; + case 2: + // Relationships or related ressource + return (matchedRoute.path.indexOf('relationships')) ? 'relationships' : 'related'; + break; + default: + return 'collection' + } } }; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index 2d0870d646..93e6865a95 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -35,6 +35,12 @@ module.exports = function (strapi) { // Wait for downstream middleware/handlers to execute to build the response yield next; + // Verify Content-Type header + if (this.request.type !== 'application/vnd.api+json') { + this.status = 406; + this.body = ''; + } + // Detect route const matchedRoute = _.find(strapi.router.stack, function(stack) { if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { @@ -47,7 +53,7 @@ module.exports = function (strapi) { const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; if (!_.isUndefined(actionRoute)) { - response.set(this, actionRoute); + response.set(this, matchedRoute, actionRoute); } } }; From 2868ce08ee1c7a796289c31f3ad0b6429a89eb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 22 Jan 2016 18:36:15 +0100 Subject: [PATCH 03/40] Writting some TODOs, adding support for single ressource fetch --- .../hooks/jsonapi/helpers/response.js | 87 +++++++++++-------- lib/configuration/hooks/jsonapi/index.js | 11 ++- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 2a546708b8..0a63b71eec 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -17,57 +17,63 @@ module.exports = { default: {}, /** - * Initialize the hook + * Set response */ - set: function(ctx, matchedRoute, actionRoute) { - const type = actionRoute.controller.toLowerCase(); - const kind = this.whichKind(matchedRoute); - const value = this.verifyAndSetValue(ctx, kind); + set: function (ctx, matchedRoute, actionRoute) { + const type = this.getType(actionRoute.controller); + const object = this.getObject(matchedRoute); + const value = this.verifyAndSetValue(ctx, object); - ctx.response.body = this.serialize(ctx, type, kind, value); + ctx.response.body = this.serialize(ctx, type, object, value); }, /** - * Verify type of the value + * Serialize response with JSON API specification */ - serialize: function(ctx, type, kind, value) { + serialize: function (ctx, type, object, value) { const toSerialize = { - topLevelLinks: { self: ctx.request.origin + ctx.request.url } + topLevelLinks: {self: ctx.request.origin + ctx.request.url} }; - switch (kind) { + switch (object) { case 'collection': if (!_.isEmpty(value) && _.isArray(value)) { + + // TODO : + // - Detect PK and stringify the value + _.forEach(value, function (value) { + value.id = value.id.toString(); + }); + toSerialize.dataLinks = { self: function (record) { - return ctx.request.origin + ctx.request.url + '/' + record.id + return ctx.request.origin + ctx.request.url + '/' + record.id; } }; + } else if (!_.isEmpty(value) && _.isObject(value)) { + // TODO : + // - Detect PK and stringify the value } - if (true) { - toSerialize.attributes = ['id']; - } + // TODO : + // - Parse the model based on the type value + // - Displayed attributes but also consider query parameters + + toSerialize.attributes = ['id']; return new JSONAPISerializer(type, value, toSerialize); - break; case 'ressource': - if (true) { - toSerialize.attributes = ['id']; - } + toSerialize.attributes = ['id']; return new JSONAPISerializer(type, value, toSerialize); - break; case 'relationships': - break; case 'related': - break; - default: - + default: + return new JSONAPISerializer(type, value, toSerialize); } }, @@ -75,7 +81,7 @@ module.exports = { * Verify type of the value */ - verifyAndSetValue: function(ctx, kind) { + verifyAndSetValue: function (ctx, object) { const data = ctx.body; switch (ctx.response.status) { @@ -83,9 +89,11 @@ module.exports = { ctx.status = 200; ctx.body = null; break; + default: + break; } - switch (kind) { + switch (object) { case 'collection': // Collection if (_.isArray(data) && _.size(data) > 1) { @@ -106,11 +114,10 @@ module.exports = { break; case 'relationships': // TODO: - // - Detect kind of relation + // - Detect object of relation // - MtM, OtM: array // - OtO, MtO: object - // Relationships if (_.isObject(data) || _.isArray(data)) { return data; @@ -120,11 +127,10 @@ module.exports = { break; case 'related': // TODO: - // - Detect kind of relation + // - Detect object of relation // - MtM, OtM: array // - OtO, MtO: object - // Related if (_.isObject(data) || _.isArray(data)) { return data; @@ -133,31 +139,40 @@ module.exports = { } break; default: - return 'collection' + return 'collection'; } }, /** - * Return kind of ressources + * Find data object */ - whichKind: function(matchedRoute) { + getObject: function (matchedRoute) { // Top level route switch (_.size(matchedRoute.regexp.keys)) { case 0: // Collection return 'collection'; - break; case 1: // Ressource return 'ressource'; - break; case 2: // Relationships or related ressource return (matchedRoute.path.indexOf('relationships')) ? 'relationships' : 'related'; - break; default: - return 'collection' + return 'collection'; + } + }, + + /** + * Find data type + */ + + getType: function (supposedType) { + if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { + return supposedType.toLowerCase(); + } else { + return 'Unknow'; } } }; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index 93e6865a95..73b47c7689 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -6,7 +6,6 @@ // Public node modules. const _ = require('lodash'); -const regex = require('../../../../util/regex'); const response = require('./helpers/response'); /** @@ -29,7 +28,11 @@ module.exports = function (strapi) { */ initialize: function (cb) { - function *interceptor(next) { + // TODO: + // - Force or not the routes? + // - Add middleware before called the controller action to check parameters structure + + function * interceptor(next) { const self = this; // Wait for downstream middleware/handlers to execute to build the response @@ -42,7 +45,7 @@ module.exports = function (strapi) { } // Detect route - const matchedRoute = _.find(strapi.router.stack, function(stack) { + const matchedRoute = _.find(strapi.router.stack, function (stack) { if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { return stack; } @@ -56,7 +59,7 @@ module.exports = function (strapi) { response.set(this, matchedRoute, actionRoute); } } - }; + } strapi.app.use(interceptor); From 23b306c5768b2659401f533087bdc11bff41932f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 25 Jan 2016 16:48:15 +0100 Subject: [PATCH 04/40] Support relationships for collection and ressource --- .../hooks/jsonapi/helpers/response.js | 155 +++++++++++++----- 1 file changed, 117 insertions(+), 38 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 0a63b71eec..9bb01fb113 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -25,55 +25,110 @@ module.exports = { const object = this.getObject(matchedRoute); const value = this.verifyAndSetValue(ctx, object); - ctx.response.body = this.serialize(ctx, type, object, value); + ctx.response.body = this.serialize(ctx, type, value); }, /** * Serialize response with JSON API specification */ - serialize: function (ctx, type, object, value) { + serialize: function (ctx, type, value) { + // TODO: + // - Handle configuration with a file to improve flexibility of JSON API support const toSerialize = { - topLevelLinks: {self: ctx.request.origin + ctx.request.url} + topLevelLinks: {self: ctx.request.origin + ctx.request.url}, + keyForAttribute: 'camelCase', + pluralizeType: false, + typeForAttribute: function (currentType) { + if (strapi.models.hasOwnProperty(type)) { + const tryFindType = _.first(_.reject(_.map(strapi.models[type].associations, function (relation) { + return (relation.alias === currentType) ? relation.model || relation.collection : undefined; + }), _.isUndefined)); + + return _.isUndefined(tryFindType) ? currentType : tryFindType; + } + } }; - switch (object) { - case 'collection': - if (!_.isEmpty(value) && _.isArray(value)) { + const PK = this.getPK(type); - // TODO : - // - Detect PK and stringify the value - _.forEach(value, function (value) { - value.id = value.id.toString(); - }); + if (_.isArray(value) && !_.isEmpty(value)) { + // Array + if (!_.isNull(PK)) { + _.forEach(value, function (record) { + record[PK] = record[PK].toString(); + }); + } - toSerialize.dataLinks = { - self: function (record) { - return ctx.request.origin + ctx.request.url + '/' + record.id; - } - }; - } else if (!_.isEmpty(value) && _.isObject(value)) { - // TODO : - // - Detect PK and stringify the value + toSerialize.dataLinks = { + self: function (record) { + return ctx.request.origin + ctx.request.url + '/' + record.id; } + }; - // TODO : - // - Parse the model based on the type value - // - Displayed attributes but also consider query parameters + toSerialize.attributes = _.keys(value[0]); + } else if (_.isObject(value) && !_.isEmpty(value)) { + // Object + if (!_.isNull(PK)) { + value[PK] = value[PK].toString(); + } - toSerialize.attributes = ['id']; + toSerialize.attributes = _.keys(value); + } - return new JSONAPISerializer(type, value, toSerialize); - case 'ressource': - toSerialize.attributes = ['id']; + this.includedRelationShips(ctx, toSerialize, type); - return new JSONAPISerializer(type, value, toSerialize); - case 'relationships': - break; - case 'related': - break; - default: - return new JSONAPISerializer(type, value, toSerialize); + return new JSONAPISerializer(type, value, toSerialize); + }, + + /** + * Include relationships values to the object + */ + + includedRelationShips: function (ctx, toSerialize, type) { + const self = this; + + if (strapi.models.hasOwnProperty(type)) { + _.forEach(strapi.models[type].associations, function (relation) { + switch (relation.nature) { + case 'oneToOne': + case 'manyToOne': + // Object + toSerialize[relation.alias] = { + ref: self.getPK(relation.model), + attributes: _.keys(strapi.models[type].attributes), + relationshipLinks: { + self: ctx.request.origin + ctx.request.url + '/relationships/' + relation.alias, + related: ctx.request.origin + ctx.request.url + }, + includedLinks: { + self: function (data, record) { + return ctx.request.origin + '/' + relation.model + '/' + record.id; + } + } + }; + break; + case 'oneToMany': + case 'manyToMany': + // Array + toSerialize[relation.alias] = { + ref: self.getPK(relation.collection), + typeForAttribute: relation.collection, + attributes: _.keys(strapi.models[type].attributes), + relationshipLinks: { + self: ctx.request.origin + ctx.request.url + '/relationships/' + relation.alias, + related: ctx.request.origin + ctx.request.url + }, + includedLinks: { + self: function (data, record) { + return ctx.request.origin + '/' + relation.collection + '/' + record.id; + } + } + }; + break; + default: + } + }); } }, @@ -148,16 +203,15 @@ module.exports = { */ getObject: function (matchedRoute) { - // Top level route + // TODO: + // - Improve way to detect collection/ressource/relationships/related + switch (_.size(matchedRoute.regexp.keys)) { case 0: - // Collection return 'collection'; case 1: - // Ressource return 'ressource'; case 2: - // Relationships or related ressource return (matchedRoute.path.indexOf('relationships')) ? 'relationships' : 'related'; default: return 'collection'; @@ -169,10 +223,35 @@ module.exports = { */ getType: function (supposedType) { + // TODO: + // - Parse the URL and try to extract useful information to find the type + if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { return supposedType.toLowerCase(); } else { - return 'Unknow'; + return null; } + }, + + /** + * Find primary key + */ + + getPK: function (type) { + if (!strapi.models.hasOwnProperty(type)) { + return null; + } + + const PK = _.findKey(strapi.models[type].attributes, {primaryKey: true}); + + if (!_.isUndefined(PK)) { + return PK; + } else if (strapi.models[type].attributes.hasOwnProperty('id')) { + return 'id'; + } else if (strapi.models[type].attributes.hasOwnProperty('uuid')) { + return 'uuid'; + } + + return null; } }; From fc970e6538a9e0a7c5af6535b635b310f5d1ed1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Tue, 26 Jan 2016 15:35:25 +0100 Subject: [PATCH 05/40] Handle relationships requests --- .../hooks/jsonapi/helpers/response.js | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 9bb01fb113..64973bf833 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -21,18 +21,18 @@ module.exports = { */ set: function (ctx, matchedRoute, actionRoute) { - const type = this.getType(actionRoute.controller); const object = this.getObject(matchedRoute); + const type = this.getType(ctx, actionRoute.controller, object).toLowerCase(); const value = this.verifyAndSetValue(ctx, object); - ctx.response.body = this.serialize(ctx, type, value); + ctx.response.body = this.serialize(ctx, type, object, value); }, /** * Serialize response with JSON API specification */ - serialize: function (ctx, type, value) { + serialize: function (ctx, type, object, value) { // TODO: // - Handle configuration with a file to improve flexibility of JSON API support const toSerialize = { @@ -41,11 +41,9 @@ module.exports = { pluralizeType: false, typeForAttribute: function (currentType) { if (strapi.models.hasOwnProperty(type)) { - const tryFindType = _.first(_.reject(_.map(strapi.models[type].associations, function (relation) { + return _.first(_.reject(_.map(strapi.models[type].associations, function (relation) { return (relation.alias === currentType) ? relation.model || relation.collection : undefined; - }), _.isUndefined)); - - return _.isUndefined(tryFindType) ? currentType : tryFindType; + }), _.isUndefined)) || currentType; } } }; @@ -76,7 +74,26 @@ module.exports = { toSerialize.attributes = _.keys(value); } - this.includedRelationShips(ctx, toSerialize, type); + switch (object) { + case 'collection': + this.includedRelationShips(ctx, toSerialize, type); + break; + case 'ressource': + this.includedRelationShips(ctx, toSerialize, type); + break; + case 'relationships': + // Remove data key + delete toSerialize.dataLinks; + delete toSerialize.attributes; + + // Dirty way to set related URL + toSerialize.topLevelLinks.related = toSerialize.topLevelLinks.self.replace('relationships/', ''); + break; + case 'related': + break; + default: + break; + } return new JSONAPISerializer(type, value, toSerialize); }, @@ -98,8 +115,12 @@ module.exports = { ref: self.getPK(relation.model), attributes: _.keys(strapi.models[type].attributes), relationshipLinks: { - self: ctx.request.origin + ctx.request.url + '/relationships/' + relation.alias, - related: ctx.request.origin + ctx.request.url + self: function (record) { + return ctx.request.origin + '/' + type + '/' + record.id + '/relationships/' + relation.alias; + }, + related: function (record) { + return ctx.request.origin + '/' + type + '/' + record.id; + } }, includedLinks: { self: function (data, record) { @@ -116,8 +137,12 @@ module.exports = { typeForAttribute: relation.collection, attributes: _.keys(strapi.models[type].attributes), relationshipLinks: { - self: ctx.request.origin + ctx.request.url + '/relationships/' + relation.alias, - related: ctx.request.origin + ctx.request.url + self: function (record) { + return ctx.request.origin + '/' + type + '/' + record.id + '/relationships/' + relation.alias; + }, + related: function (record) { + return ctx.request.origin + '/' + type + '/' + record.id; + } }, includedLinks: { self: function (data, record) { @@ -174,8 +199,8 @@ module.exports = { // - OtO, MtO: object // Relationships - if (_.isObject(data) || _.isArray(data)) { - return data; + if (_.isObject(data) || _.isArray(data) && data.hasOwnProperty(ctx.params.relation)) { + return data[ctx.params.relation]; } else { return null; } @@ -205,7 +230,6 @@ module.exports = { getObject: function (matchedRoute) { // TODO: // - Improve way to detect collection/ressource/relationships/related - switch (_.size(matchedRoute.regexp.keys)) { case 0: return 'collection'; @@ -222,15 +246,24 @@ module.exports = { * Find data type */ - getType: function (supposedType) { + getType: function (ctx, supposedType, object) { // TODO: // - Parse the URL and try to extract useful information to find the type if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { - return supposedType.toLowerCase(); - } else { - return null; + switch (object) { + case 'relationships': + return _.first(_.reject(_.map(strapi.models[supposedType.toLowerCase()].associations, function (relation) { + return (ctx.params.hasOwnProperty('relation') && ctx.params.relation === relation.alias) ? relation.model || relation.collection : undefined; + }), _.isUndefined)) || supposedType.toLowerCase(); + case 'related': + return supposedType.toLowerCase(); + default: + return supposedType.toLowerCase(); + } } + + return null; }, /** From 38f9239ec9a1d22b57cbbfd0f3dcb3456514cce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Tue, 26 Jan 2016 16:24:43 +0100 Subject: [PATCH 06/40] Handle related ressource without include parameter --- .../hooks/jsonapi/helpers/response.js | 90 ++++++++----------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 64973bf833..9766a5ea9e 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -22,9 +22,19 @@ module.exports = { set: function (ctx, matchedRoute, actionRoute) { const object = this.getObject(matchedRoute); - const type = this.getType(ctx, actionRoute.controller, object).toLowerCase(); - const value = this.verifyAndSetValue(ctx, object); + const type = this.getType(ctx, actionRoute.controller); + // Fetch a relationship that does not exist + if (_.isUndefined(type)) { + ctx.response.status = 404; + ctx.response.body = ''; + + return false; + } + + // Fetch and format value + const value = this.fetchValue(ctx, object); + ctx.response.body = this.serialize(ctx, type, object, value); }, @@ -90,6 +100,7 @@ module.exports = { toSerialize.topLevelLinks.related = toSerialize.topLevelLinks.self.replace('relationships/', ''); break; case 'related': + this.includedRelationShips(ctx, toSerialize, type); break; default: break; @@ -158,66 +169,43 @@ module.exports = { }, /** - * Verify type of the value + * Fetch and format value */ - verifyAndSetValue: function (ctx, object) { + fetchValue: function (ctx, object) { const data = ctx.body; - switch (ctx.response.status) { - case 404: - ctx.status = 200; - ctx.body = null; - break; - default: - break; - } - switch (object) { case 'collection': - // Collection if (_.isArray(data) && _.size(data) > 1) { return data; } else if (_.isArray(data) && (_.size(data) === 1 || _.size(data) === 0)) { - return _.isObject(data[0]) ? data[0] : []; - } else { - return null; + return _.isObject(_.first(data)) ? _.first(data[0]) : []; } - break; + + return null; case 'ressource': - // Ressource if (_.isObject(data)) { return data; - } else { - return null; } - break; + + return null; + case 'related': case 'relationships': // TODO: // - Detect object of relation // - MtM, OtM: array // - OtO, MtO: object - // Relationships if (_.isObject(data) || _.isArray(data) && data.hasOwnProperty(ctx.params.relation)) { - return data[ctx.params.relation]; - } else { - return null; - } - break; - case 'related': - // TODO: - // - Detect object of relation - // - MtM, OtM: array - // - OtO, MtO: object + if (_.isArray(data[ctx.params.relation]) && _.size(data[ctx.params.relation]) > 1) { + return data[ctx.params.relation]; + } - // Related - if (_.isObject(data) || _.isArray(data)) { - return data; - } else { - return null; + return _.first(data[ctx.params.relation]) || data[ctx.params.relation]; } - break; + + return null; default: return 'collection'; } @@ -236,7 +224,7 @@ module.exports = { case 1: return 'ressource'; case 2: - return (matchedRoute.path.indexOf('relationships')) ? 'relationships' : 'related'; + return (matchedRoute.path.indexOf('relationships') !== -1) ? 'relationships' : 'related'; default: return 'collection'; } @@ -246,24 +234,20 @@ module.exports = { * Find data type */ - getType: function (ctx, supposedType, object) { + getType: function (ctx, supposedType) { // TODO: // - Parse the URL and try to extract useful information to find the type - if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { - switch (object) { - case 'relationships': - return _.first(_.reject(_.map(strapi.models[supposedType.toLowerCase()].associations, function (relation) { - return (ctx.params.hasOwnProperty('relation') && ctx.params.relation === relation.alias) ? relation.model || relation.collection : undefined; - }), _.isUndefined)) || supposedType.toLowerCase(); - case 'related': - return supposedType.toLowerCase(); - default: - return supposedType.toLowerCase(); - } + // Relationships or related ressource + if (strapi.models.hasOwnProperty(supposedType.toLowerCase()) && ctx.params.hasOwnProperty('relation')) { + return _.first(_.reject(_.map(strapi.models[supposedType.toLowerCase()].associations, function (relation) { + return (ctx.params.hasOwnProperty('relation') && ctx.params.relation === relation.alias) ? relation.model || relation.collection : undefined; + }), _.isUndefined)); + } else if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { + return supposedType.toLowerCase(); } - return null; + return undefined; }, /** From 8408538f21957c8022e6f3616de09fee15b12e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Tue, 26 Jan 2016 16:30:31 +0100 Subject: [PATCH 07/40] Reject related request with include parameter --- lib/configuration/hooks/jsonapi/helpers/response.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 9766a5ea9e..e437ac1536 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -25,7 +25,8 @@ module.exports = { const type = this.getType(ctx, actionRoute.controller); // Fetch a relationship that does not exist - if (_.isUndefined(type)) { + // Reject related request with `include` parameter + if (_.isUndefined(type) || (type === 'related' && ctx.params.hasOwnProperty('include'))) { ctx.response.status = 404; ctx.response.body = ''; @@ -34,7 +35,7 @@ module.exports = { // Fetch and format value const value = this.fetchValue(ctx, object); - + ctx.response.body = this.serialize(ctx, type, object, value); }, From 9dd94e72a22de1be7f035d485010473d7cd80f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Tue, 26 Jan 2016 18:03:52 +0100 Subject: [PATCH 08/40] Add middleware to parse data request --- .../hooks/jsonapi/helpers/request.js | 23 +++++++++++++++++++ lib/configuration/hooks/jsonapi/index.js | 13 +++++++++++ lib/configuration/hooks/router/index.js | 2 ++ 3 files changed, 38 insertions(+) create mode 100644 lib/configuration/hooks/jsonapi/helpers/request.js diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js new file mode 100644 index 0000000000..f73420f101 --- /dev/null +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -0,0 +1,23 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); + +/** + * JSON API helper + */ + +module.exports = { + + default: {}, + + /** + * Parse request + */ + + parse: function (ctx) {} +}; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index 73b47c7689..f81821e2d7 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -6,6 +6,7 @@ // Public node modules. const _ = require('lodash'); +const request = require('./helpers/request'); const response = require('./helpers/response'); /** @@ -64,6 +65,18 @@ module.exports = function (strapi) { strapi.app.use(interceptor); cb(); + }, + + parse: function * (next) { + // Verify Content-Type header + if (this.request.type !== 'application/vnd.api+json') { + this.status = 406; + this.body = ''; + } + + request.parse(this); + + yield next; } }; diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index ad2a5bdd91..5bda18178e 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -12,6 +12,7 @@ const _ = require('lodash'); // Local utilities. const regex = require('../../../../util/regex'); +const JSONAPI = require('../jsonapi')(); /** * Router hook @@ -86,6 +87,7 @@ module.exports = function (strapi) { // Add the `globalPolicy`. policies.push(globalPolicy(endpoint, value, route)); + policies.push(JSONAPI.parse); if (_.isArray(value.policies) && !_.isEmpty(value.policies)) { _.forEach(value.policies, function (policy) { From fad6c2af6d168a3a3471353deebe915db6a5cb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 27 Jan 2016 14:20:09 +0100 Subject: [PATCH 09/40] Apply on GET routes and dirty way to verify request body object --- .../hooks/jsonapi/helpers/request.js | 60 ++++++++++++++++++- lib/configuration/hooks/jsonapi/index.js | 40 +++++++------ 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index f73420f101..e633aa3d49 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -19,5 +19,63 @@ module.exports = { * Parse request */ - parse: function (ctx) {} + parse: function (ctx, cb) { + switch (ctx.method.toUpperCase()) { + case 'GET': + console.log('GET'); + break; + case 'PUT': + case 'POST': + this.fetchSchema(ctx, function (err) { + cb(err); + }); + console.log('POST|PUT'); + break; + case 'DELETE': + console.log('DELETE'); + break; + default: + } + }, + + /** + * Fetch attributes schema + */ + + fetchSchema : function (ctx, cb) { + const attributes = ctx.request.body; + + if (!attributes.hasOwnProperty('data')) { + ctx.response.status = 404; + ctx.response.body = 'Missing `data` member'; + } else if (!attributes.data.hasOwnProperty('type')) { + ctx.response.status = 404; + ctx.response.body = 'Missing `type` member'; + } else if (!strapi.models.hasOwnProperty(attributes.data.type)) { + ctx.response.status = 404; + ctx.response.body = 'Unknow `type` ' + attributes.data.type; + } else { + // Extract required attributes + const requiredAttributes = _.filter(strapi.models[attributes.data.type].attributes, {required:true}); + // Identify required relationships + const relationships = _.indexBy(strapi.models[attributes.data.type].associations, 'alias'); + // Extract required relationships + const requiredRelationships = _.intersection(_.keys(requiredAttributes), _.keys(relationships)); + + console.log(requiredAttributes); + console.log(requiredRelationships); + + if (_.size(requiredAttributes) > 0 && _.isEmpty(attributes.data.attributes)) { + ctx.response.status = 404; + ctx.response.body = 'Missing required attributes'; + } else if (!_.isEmpty(_.difference(_.keys(requiredRelationships), _.keys(attributes.data.relationships)))) { + ctx.response.status = 404; + ctx.response.body = 'Missing required relationships'; + } else { + cb(); + console.log("coucou"); + // Looks good + } + } + } }; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index f81821e2d7..2f04ff124e 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -43,21 +43,23 @@ module.exports = function (strapi) { if (this.request.type !== 'application/vnd.api+json') { this.status = 406; this.body = ''; - } + } else if (this.request.method === 'GET') { + // Intercept only GET request - // Detect route - const matchedRoute = _.find(strapi.router.stack, function (stack) { - if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { - return stack; - } - }); + // Detect route + const matchedRoute = _.find(strapi.router.stack, function (stack) { + if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { + return stack; + } + }); - // Handlers set the response body - if (!_.isUndefined(matchedRoute)) { - const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; + // Handlers set the response body + if (!_.isUndefined(matchedRoute)) { + const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; - if (!_.isUndefined(actionRoute)) { - response.set(this, matchedRoute, actionRoute); + if (!_.isUndefined(actionRoute)) { + response.set(this, matchedRoute, actionRoute); + } } } } @@ -70,13 +72,15 @@ module.exports = function (strapi) { parse: function * (next) { // Verify Content-Type header if (this.request.type !== 'application/vnd.api+json') { - this.status = 406; - this.body = ''; + this.response.status = 406; + this.response.body = ''; + } else { + request.parse(this, function * (err) { + if (!err) { + yield next; + } + }); } - - request.parse(this); - - yield next; } }; From c7049253d6d71c2722f29531d964e54101ab4f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 27 Jan 2016 16:45:44 +0100 Subject: [PATCH 10/40] Use generators, send strapi instance in parser and optimizations --- .../hooks/jsonapi/helpers/request.js | 81 +++++++++++-------- lib/configuration/hooks/jsonapi/index.js | 69 +++++++++------- lib/configuration/hooks/router/index.js | 2 +- 3 files changed, 91 insertions(+), 61 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index e633aa3d49..6828cc4db5 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -19,17 +19,16 @@ module.exports = { * Parse request */ - parse: function (ctx, cb) { + parse: function * (ctx, cb) { switch (ctx.method.toUpperCase()) { case 'GET': console.log('GET'); break; case 'PUT': case 'POST': - this.fetchSchema(ctx, function (err) { - cb(err); + yield this.fetchSchema(ctx, function * (err) { + yield cb(err); }); - console.log('POST|PUT'); break; case 'DELETE': console.log('DELETE'); @@ -42,40 +41,56 @@ module.exports = { * Fetch attributes schema */ - fetchSchema : function (ctx, cb) { + fetchSchema: function * (ctx, cb) { const attributes = ctx.request.body; if (!attributes.hasOwnProperty('data')) { - ctx.response.status = 404; - ctx.response.body = 'Missing `data` member'; + return yield cb({ + status: 403, + body: 'Missing `data` member' + }); } else if (!attributes.data.hasOwnProperty('type')) { - ctx.response.status = 404; - ctx.response.body = 'Missing `type` member'; + return yield cb({ + status: 403, + body: 'Missing `type` member' + }); } else if (!strapi.models.hasOwnProperty(attributes.data.type)) { - ctx.response.status = 404; - ctx.response.body = 'Unknow `type` ' + attributes.data.type; - } else { - // Extract required attributes - const requiredAttributes = _.filter(strapi.models[attributes.data.type].attributes, {required:true}); - // Identify required relationships - const relationships = _.indexBy(strapi.models[attributes.data.type].associations, 'alias'); - // Extract required relationships - const requiredRelationships = _.intersection(_.keys(requiredAttributes), _.keys(relationships)); - - console.log(requiredAttributes); - console.log(requiredRelationships); - - if (_.size(requiredAttributes) > 0 && _.isEmpty(attributes.data.attributes)) { - ctx.response.status = 404; - ctx.response.body = 'Missing required attributes'; - } else if (!_.isEmpty(_.difference(_.keys(requiredRelationships), _.keys(attributes.data.relationships)))) { - ctx.response.status = 404; - ctx.response.body = 'Missing required relationships'; - } else { - cb(); - console.log("coucou"); - // Looks good - } + return yield cb({ + status: 403, + body: 'Unknow `type` ' + attributes.data.type + }); } + + // Extract required attributes + const requiredAttributes = _.omit(_.mapValues(strapi.models[attributes.data.type].attributes, function (attr) { + return attr.required ? attr : undefined; + }), _.isUndefined); + // Identify missing attributes + const missingAttributes = _.difference(_.keys(requiredAttributes), _.keys(attributes.data.attributes)); + + if (!_.isEmpty(missingAttributes)) { + return yield cb({ + status: 403, + body: 'Missing required attributes (' + missingAttributes.toString() + ')' + }); + } + + // Identify required relationships + const relationships = _.indexBy(strapi.models[attributes.data.type].associations, 'alias'); + // Extract required relationships + const requiredRelationships = _.intersection(_.keys(requiredAttributes), _.keys(relationships)); + // Identify missing relationships + const missingRelationships = _.difference(_.keys(requiredRelationships), _.keys(attributes.data.relationships)); + + if (!_.isEmpty(missingRelationships)) { + return yield cb({ + status: 403, + body: 'Missing required relationships (' + missingRelationships.toString() + ')' + }); + } + + // Looks good + yield cb(); + } }; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index 2f04ff124e..41fcfa7033 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -39,26 +39,29 @@ module.exports = function (strapi) { // Wait for downstream middleware/handlers to execute to build the response yield next; - // Verify Content-Type header - if (this.request.type !== 'application/vnd.api+json') { - this.status = 406; - this.body = ''; - } else if (this.request.method === 'GET') { - // Intercept only GET request + // Exclude administration routes + if (!strapi.api.admin.config.routes.hasOwnProperty(this.request.method + ' ' + this.request.url)) { + // Verify Content-Type header + if (this.request.type !== 'application/vnd.api+json') { + this.status = 406; + this.body = ''; + } else if (this.request.method === 'GET') { + // Intercept only GET request - // Detect route - const matchedRoute = _.find(strapi.router.stack, function (stack) { - if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { - return stack; - } - }); + // Detect route + const matchedRoute = _.find(strapi.router.stack, function (stack) { + if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { + return stack; + } + }); - // Handlers set the response body - if (!_.isUndefined(matchedRoute)) { - const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; + // Handlers set the response body + if (!_.isUndefined(matchedRoute)) { + const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; - if (!_.isUndefined(actionRoute)) { - response.set(this, matchedRoute, actionRoute); + if (!_.isUndefined(actionRoute)) { + response.set(this, matchedRoute, actionRoute); + } } } } @@ -69,17 +72,29 @@ module.exports = function (strapi) { cb(); }, - parse: function * (next) { - // Verify Content-Type header - if (this.request.type !== 'application/vnd.api+json') { - this.response.status = 406; - this.response.body = ''; - } else { - request.parse(this, function * (err) { - if (!err) { + /** + * Parse request and attributes + */ + + parse: function (strapi) { + return function * (next) { + const self = this; + + // Verify Content-Type header and exclude administration routes + if (strapi.api.admin.config.routes.hasOwnProperty(this.request.method + ' ' + this.request.url)) { + yield next; + } else if (this.request.type !== 'application/vnd.api+json') { + this.response.status = 406; + this.response.body = ''; + } else { + yield request.parse(this, function * (err) { + if (err) { + return _.assign(self.response, err); + } + yield next; - } - }); + }); + } } } }; diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index 5bda18178e..1ca8e9ed8c 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -87,7 +87,7 @@ module.exports = function (strapi) { // Add the `globalPolicy`. policies.push(globalPolicy(endpoint, value, route)); - policies.push(JSONAPI.parse); + policies.push(JSONAPI.parse(strapi)); if (_.isArray(value.policies) && !_.isEmpty(value.policies)) { _.forEach(value.policies, function (policy) { From 37b937a4de396993311eb29f4312018694178a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Thu, 28 Jan 2016 16:12:16 +0100 Subject: [PATCH 11/40] Handle POST method with JSON API request and response --- .../hooks/jsonapi/helpers/request.js | 112 +++++++++++++----- .../hooks/jsonapi/helpers/response.js | 2 +- lib/configuration/hooks/jsonapi/index.js | 15 ++- .../hooks/jsonapi/utils/utils.js | 24 ++++ 4 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 lib/configuration/hooks/jsonapi/utils/utils.js diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 6828cc4db5..bc6c3d7fc3 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -6,6 +6,7 @@ // Public node modules. const _ = require('lodash'); +const utils = require('../utils/utils'); /** * JSON API helper @@ -19,78 +20,127 @@ module.exports = { * Parse request */ - parse: function * (ctx, cb) { + parse: function * (ctx) { switch (ctx.method.toUpperCase()) { case 'GET': console.log('GET'); break; - case 'PUT': + case 'PATCH': case 'POST': - yield this.fetchSchema(ctx, function * (err) { - yield cb(err); - }); + try { + yield this.fetchSchema(ctx); + yield this.formatBody(ctx); + } catch (err) { + throw err; + } break; case 'DELETE': console.log('DELETE'); break; default: + throw { + status: 403, + body: 'Invalid HTTP method' + }; } }, + /** + * Format attributes for more simple API + */ + + formatBody: function * (ctx) { + const body = ctx.request.body; + const values = _.assign({}, body.data.attributes); + + _.forEach(body.data.relationships, function (relation, key) { + values[key] = _.isArray(relation.data) ? _.map(relation.data, 'id') : relation.data.id; + }); + + ctx.request.body = values; + }, + /** * Fetch attributes schema */ - fetchSchema: function * (ctx, cb) { - const attributes = ctx.request.body; + fetchSchema: function * (ctx) { + const body = ctx.request.body; - if (!attributes.hasOwnProperty('data')) { - return yield cb({ + if (!body.hasOwnProperty('data')) { + throw { status: 403, body: 'Missing `data` member' - }); - } else if (!attributes.data.hasOwnProperty('type')) { - return yield cb({ + }; + } else if (!utils.isRessourceObject(body.data) && ctx.method !== 'POST') { + throw { status: 403, - body: 'Missing `type` member' - }); - } else if (!strapi.models.hasOwnProperty(attributes.data.type)) { - return yield cb({ + body: 'Invalid ressource object' + }; + } else if (!body.data.hasOwnProperty('type') && ctx.method === 'POST') { + throw { status: 403, - body: 'Unknow `type` ' + attributes.data.type - }); + body: 'Invalid ressource object' + }; + } else if (!strapi.models.hasOwnProperty(body.data.type)) { + throw { + status: 403, + body: 'Unknow `type` ' + body.data.type + }; } // Extract required attributes - const requiredAttributes = _.omit(_.mapValues(strapi.models[attributes.data.type].attributes, function (attr) { - return attr.required ? attr : undefined; + const requiredAttributes = _.omit(_.mapValues(strapi.models[body.data.type].attributes, function (attr) { + return (attr.required && attr.type) ? attr : undefined; }), _.isUndefined); // Identify missing attributes - const missingAttributes = _.difference(_.keys(requiredAttributes), _.keys(attributes.data.attributes)); + const missingAttributes = body.data.hasOwnProperty('attributes') ? _.difference(_.keys(requiredAttributes), _.keys(body.data.attributes)) : null; if (!_.isEmpty(missingAttributes)) { - return yield cb({ + throw { status: 403, body: 'Missing required attributes (' + missingAttributes.toString() + ')' - }); + }; } - // Identify required relationships - const relationships = _.indexBy(strapi.models[attributes.data.type].associations, 'alias'); // Extract required relationships - const requiredRelationships = _.intersection(_.keys(requiredAttributes), _.keys(relationships)); + const requiredRelationships = _.omit(_.mapValues(strapi.models[body.data.type].attributes, function (attr) { + return (attr.required && (attr.model || attr.collection)) ? attr : undefined; + }), _.isUndefined); // Identify missing relationships - const missingRelationships = _.difference(_.keys(requiredRelationships), _.keys(attributes.data.relationships)); + const missingRelationships = body.data.hasOwnProperty('relationships') ? _.difference(_.keys(requiredRelationships), _.keys(body.data.relationships)) : null; if (!_.isEmpty(missingRelationships)) { - return yield cb({ + throw { status: 403, body: 'Missing required relationships (' + missingRelationships.toString() + ')' - }); + }; } - // Looks good - yield cb(); + // Build array of errors + if (_.size(requiredRelationships)) { + const errors = _.remove(_.flattenDeep(_.map(body.data.relationships, function (relation, key) { + if (!relation.hasOwnProperty('data')) { + return 'Missing `data` member for relationships ' + relation; + } else if (_.isArray(relation.data)) { + return _.map(relation.data, function (ressource) { + if (!utils.isRessourceObject(ressource)) { + return 'Invalid ressource object in relationships ' + key; + } + }); + } else if (!utils.isRessourceObject(relation.data)) { + return 'Invalid ressource object for relationships ' + key; + } + })), function (n) { + return !_.isUndefined(n); + }); + if (!_.isEmpty(errors)) { + throw { + status: 403, + body: errors.toString() + }; + } + } } }; diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index e437ac1536..40a252d738 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -178,7 +178,7 @@ module.exports = { switch (object) { case 'collection': - if (_.isArray(data) && _.size(data) > 1) { + if ((_.isArray(data) && _.size(data) > 1) || _.isObject(data)) { return data; } else if (_.isArray(data) && (_.size(data) === 1 || _.size(data) === 0)) { return _.isObject(_.first(data)) ? _.first(data[0]) : []; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index 41fcfa7033..ef9feb2340 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -45,7 +45,7 @@ module.exports = function (strapi) { if (this.request.type !== 'application/vnd.api+json') { this.status = 406; this.body = ''; - } else if (this.request.method === 'GET') { + } else { // Intercept only GET request // Detect route @@ -87,15 +87,14 @@ module.exports = function (strapi) { this.response.status = 406; this.response.body = ''; } else { - yield request.parse(this, function * (err) { - if (err) { - return _.assign(self.response, err); - } - + try { + yield request.parse(this); yield next; - }); + } catch (err) { + _.assign(self.response, err); + } } - } + }; } }; diff --git a/lib/configuration/hooks/jsonapi/utils/utils.js b/lib/configuration/hooks/jsonapi/utils/utils.js new file mode 100644 index 0000000000..2294e9093d --- /dev/null +++ b/lib/configuration/hooks/jsonapi/utils/utils.js @@ -0,0 +1,24 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); + +/** + * JSON API utils + */ + +module.exports = { + + /** + * Verify ressource object + */ + + isRessourceObject: function (object) { + return _.isObject(object) && object.hasOwnProperty('id') && object.hasOwnProperty('type'); + } + +}; From 42c06950e2adaa68bd8e2a86a4ee4339d0714fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Thu, 28 Jan 2016 16:21:25 +0100 Subject: [PATCH 12/40] Migrate utils functions --- .../hooks/jsonapi/helpers/response.js | 72 ++----------------- .../hooks/jsonapi/utils/utils.js | 61 ++++++++++++++++ 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 40a252d738..4d6684c0f0 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -7,6 +7,7 @@ // Public node modules. const _ = require('lodash'); const JSONAPISerializer = require('jsonapi-serializer'); +const utils = require('../utils/utils'); /** * JSON API helper @@ -21,8 +22,8 @@ module.exports = { */ set: function (ctx, matchedRoute, actionRoute) { - const object = this.getObject(matchedRoute); - const type = this.getType(ctx, actionRoute.controller); + const object = utils.getObject(matchedRoute); + const type = utils.getType(ctx, actionRoute.controller); // Fetch a relationship that does not exist // Reject related request with `include` parameter @@ -59,7 +60,7 @@ module.exports = { } }; - const PK = this.getPK(type); + const PK = utils.getPK(type); if (_.isArray(value) && !_.isEmpty(value)) { // Array @@ -124,7 +125,7 @@ module.exports = { case 'manyToOne': // Object toSerialize[relation.alias] = { - ref: self.getPK(relation.model), + ref: utils.getPK(relation.model), attributes: _.keys(strapi.models[type].attributes), relationshipLinks: { self: function (record) { @@ -145,7 +146,7 @@ module.exports = { case 'manyToMany': // Array toSerialize[relation.alias] = { - ref: self.getPK(relation.collection), + ref: utils.getPK(relation.collection), typeForAttribute: relation.collection, attributes: _.keys(strapi.models[type].attributes), relationshipLinks: { @@ -210,66 +211,5 @@ module.exports = { default: return 'collection'; } - }, - - /** - * Find data object - */ - - getObject: function (matchedRoute) { - // TODO: - // - Improve way to detect collection/ressource/relationships/related - switch (_.size(matchedRoute.regexp.keys)) { - case 0: - return 'collection'; - case 1: - return 'ressource'; - case 2: - return (matchedRoute.path.indexOf('relationships') !== -1) ? 'relationships' : 'related'; - default: - return 'collection'; - } - }, - - /** - * Find data type - */ - - getType: function (ctx, supposedType) { - // TODO: - // - Parse the URL and try to extract useful information to find the type - - // Relationships or related ressource - if (strapi.models.hasOwnProperty(supposedType.toLowerCase()) && ctx.params.hasOwnProperty('relation')) { - return _.first(_.reject(_.map(strapi.models[supposedType.toLowerCase()].associations, function (relation) { - return (ctx.params.hasOwnProperty('relation') && ctx.params.relation === relation.alias) ? relation.model || relation.collection : undefined; - }), _.isUndefined)); - } else if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { - return supposedType.toLowerCase(); - } - - return undefined; - }, - - /** - * Find primary key - */ - - getPK: function (type) { - if (!strapi.models.hasOwnProperty(type)) { - return null; - } - - const PK = _.findKey(strapi.models[type].attributes, {primaryKey: true}); - - if (!_.isUndefined(PK)) { - return PK; - } else if (strapi.models[type].attributes.hasOwnProperty('id')) { - return 'id'; - } else if (strapi.models[type].attributes.hasOwnProperty('uuid')) { - return 'uuid'; - } - - return null; } }; diff --git a/lib/configuration/hooks/jsonapi/utils/utils.js b/lib/configuration/hooks/jsonapi/utils/utils.js index 2294e9093d..40ac201b3d 100644 --- a/lib/configuration/hooks/jsonapi/utils/utils.js +++ b/lib/configuration/hooks/jsonapi/utils/utils.js @@ -19,6 +19,67 @@ module.exports = { isRessourceObject: function (object) { return _.isObject(object) && object.hasOwnProperty('id') && object.hasOwnProperty('type'); + }, + + /** + * Find data object + */ + + getObject: function (matchedRoute) { + // TODO: + // - Improve way to detect collection/ressource/relationships/related + switch (_.size(matchedRoute.regexp.keys)) { + case 0: + return 'collection'; + case 1: + return 'ressource'; + case 2: + return (matchedRoute.path.indexOf('relationships') !== -1) ? 'relationships' : 'related'; + default: + return 'collection'; + } + }, + + /** + * Find data type + */ + + getType: function (ctx, supposedType) { + // TODO: + // - Parse the URL and try to extract useful information to find the type + + // Relationships or related ressource + if (strapi.models.hasOwnProperty(supposedType.toLowerCase()) && ctx.params.hasOwnProperty('relation')) { + return _.first(_.reject(_.map(strapi.models[supposedType.toLowerCase()].associations, function (relation) { + return (ctx.params.hasOwnProperty('relation') && ctx.params.relation === relation.alias) ? relation.model || relation.collection : undefined; + }), _.isUndefined)); + } else if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { + return supposedType.toLowerCase(); + } + + return undefined; + }, + + /** + * Find primary key + */ + + getPK: function (type) { + if (!strapi.models.hasOwnProperty(type)) { + return null; + } + + const PK = _.findKey(strapi.models[type].attributes, {primaryKey: true}); + + if (!_.isUndefined(PK)) { + return PK; + } else if (strapi.models[type].attributes.hasOwnProperty('id')) { + return 'id'; + } else if (strapi.models[type].attributes.hasOwnProperty('uuid')) { + return 'uuid'; + } + + return null; } }; From bd2ae138593c9d801718c2a5508ce7daf15859c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 29 Jan 2016 13:17:38 +0100 Subject: [PATCH 13/40] Remove logs, handle DELETE method and add some verifications --- .../hooks/jsonapi/helpers/request.js | 5 +- .../hooks/jsonapi/helpers/response.js | 48 ++++++++++++++----- lib/configuration/hooks/jsonapi/index.js | 9 +++- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index bc6c3d7fc3..d7547b19b2 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -21,9 +21,10 @@ module.exports = { */ parse: function * (ctx) { + // HTTP methods allowed switch (ctx.method.toUpperCase()) { case 'GET': - console.log('GET'); + // Nothing to do break; case 'PATCH': case 'POST': @@ -35,7 +36,7 @@ module.exports = { } break; case 'DELETE': - console.log('DELETE'); + // Nothing to do break; default: throw { diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 4d6684c0f0..cd8aa78bd3 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -31,6 +31,11 @@ module.exports = { ctx.response.status = 404; ctx.response.body = ''; + return false; + } else if (ctx.method === 'DELETE') { + // Request successful and responds with only top-level meta data or nothing. + ctx.response.body = ''; + return false; } @@ -45,6 +50,7 @@ module.exports = { */ serialize: function (ctx, type, object, value) { + // TODO: // - Handle configuration with a file to improve flexibility of JSON API support const toSerialize = { @@ -66,20 +72,24 @@ module.exports = { // Array if (!_.isNull(PK)) { _.forEach(value, function (record) { - record[PK] = record[PK].toString(); + if (record.hasOwnProperty(PK)) { + record[PK] = record[PK].toString(); + } }); } toSerialize.dataLinks = { self: function (record) { - return ctx.request.origin + ctx.request.url + '/' + record.id; + if (record.hasOwnProperty(PK)) { + return ctx.request.origin + ctx.request.url + '/' + record[PK]; + } } }; - toSerialize.attributes = _.keys(value[0]); + toSerialize.attributes = _.keys(_.last(value)); } else if (_.isObject(value) && !_.isEmpty(value)) { // Object - if (!_.isNull(PK)) { + if (!_.isNull(PK) && value.hasOwnProperty(PK)) { value[PK] = value[PK].toString(); } @@ -120,24 +130,32 @@ module.exports = { if (strapi.models.hasOwnProperty(type)) { _.forEach(strapi.models[type].associations, function (relation) { + let PK = utils.getPK(relation.model) || utils.getPK(relation.collection); + switch (relation.nature) { case 'oneToOne': case 'manyToOne': // Object toSerialize[relation.alias] = { - ref: utils.getPK(relation.model), + ref: PK, attributes: _.keys(strapi.models[type].attributes), relationshipLinks: { self: function (record) { - return ctx.request.origin + '/' + type + '/' + record.id + '/relationships/' + relation.alias; + if (record.hasOwnProperty(PK)) { + return ctx.request.origin + '/' + type + '/' + record[PK] + '/relationships/' + relation.alias; + } }, related: function (record) { - return ctx.request.origin + '/' + type + '/' + record.id; + if (record.hasOwnProperty(PK)) { + return ctx.request.origin + '/' + type + '/' + record[PK]; + } } }, includedLinks: { self: function (data, record) { - return ctx.request.origin + '/' + relation.model + '/' + record.id; + if (!_.isUndefined(record) && record.hasOwnProperty(PK)) { + return ctx.request.origin + '/' + relation.model + '/' + record[PK]; + } } } }; @@ -146,20 +164,26 @@ module.exports = { case 'manyToMany': // Array toSerialize[relation.alias] = { - ref: utils.getPK(relation.collection), + ref: PK, typeForAttribute: relation.collection, attributes: _.keys(strapi.models[type].attributes), relationshipLinks: { self: function (record) { - return ctx.request.origin + '/' + type + '/' + record.id + '/relationships/' + relation.alias; + if (record.hasOwnProperty(PK)) { + return ctx.request.origin + '/' + type + '/' + record[PK] + '/relationships/' + relation.alias; + } }, related: function (record) { - return ctx.request.origin + '/' + type + '/' + record.id; + if (record.hasOwnProperty(PK)) { + return ctx.request.origin + '/' + type + '/' + record[PK]; + } } }, includedLinks: { self: function (data, record) { - return ctx.request.origin + '/' + relation.collection + '/' + record.id; + if (record.hasOwnProperty(PK)) { + return ctx.request.origin + '/' + relation.collection + '/' + record[PK]; + } } } }; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index ef9feb2340..ce43aa7845 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -45,8 +45,8 @@ module.exports = function (strapi) { if (this.request.type !== 'application/vnd.api+json') { this.status = 406; this.body = ''; - } else { - // Intercept only GET request + } else if (_.startsWith(this.status, '2')) { + // Intercept success requests // Detect route const matchedRoute = _.find(strapi.router.stack, function (stack) { @@ -63,6 +63,11 @@ module.exports = function (strapi) { response.set(this, matchedRoute, actionRoute); } } + } else { + // Intercept error requests + this.body = { + error: this.body + }; } } } From 9ba51377777b8a4b8562b0f95e7699ebb9368994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 29 Jan 2016 14:52:11 +0100 Subject: [PATCH 14/40] Clean code and improve administration routes exclusion --- .../hooks/jsonapi/helpers/request.js | 12 ++++----- .../hooks/jsonapi/helpers/response.js | 9 ++----- lib/configuration/hooks/jsonapi/index.js | 26 +++++++------------ 3 files changed, 16 insertions(+), 31 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index d7547b19b2..48dedb1105 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -6,6 +6,8 @@ // Public node modules. const _ = require('lodash'); + +// Local Strapi dependencies. const utils = require('../utils/utils'); /** @@ -14,8 +16,6 @@ const utils = require('../utils/utils'); module.exports = { - default: {}, - /** * Parse request */ @@ -52,11 +52,9 @@ module.exports = { formatBody: function * (ctx) { const body = ctx.request.body; - const values = _.assign({}, body.data.attributes); - - _.forEach(body.data.relationships, function (relation, key) { - values[key] = _.isArray(relation.data) ? _.map(relation.data, 'id') : relation.data.id; - }); + const values = _.assign({}, body.data.attributes, _.mapValues(body.data.relationships, function (relation, key) { + return _.isArray(relation.data) ? _.map(relation.data, 'id') : relation.data.id; + })); ctx.request.body = values; }, diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index cd8aa78bd3..7219911764 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -7,6 +7,8 @@ // Public node modules. const _ = require('lodash'); const JSONAPISerializer = require('jsonapi-serializer'); + +// Local Strapi dependencies. const utils = require('../utils/utils'); /** @@ -15,8 +17,6 @@ const utils = require('../utils/utils'); module.exports = { - default: {}, - /** * Set response */ @@ -218,11 +218,6 @@ module.exports = { return null; case 'related': case 'relationships': - // TODO: - // - Detect object of relation - // - MtM, OtM: array - // - OtO, MtO: object - if (_.isObject(data) || _.isArray(data) && data.hasOwnProperty(ctx.params.relation)) { if (_.isArray(data[ctx.params.relation]) && _.size(data[ctx.params.relation]) > 1) { return data[ctx.params.relation]; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index ce43aa7845..b096cabec5 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -6,6 +6,8 @@ // Public node modules. const _ = require('lodash'); + +// Local Strapi dependencies. const request = require('./helpers/request'); const response = require('./helpers/response'); @@ -16,14 +18,6 @@ const response = require('./helpers/response'); module.exports = function (strapi) { const hook = { - /** - * Default options - */ - - defaults: { - jsonapi: {} - }, - /** * Initialize the hook */ @@ -33,14 +27,14 @@ module.exports = function (strapi) { // - Force or not the routes? // - Add middleware before called the controller action to check parameters structure - function * interceptor(next) { + function * _interceptor(next) { const self = this; // Wait for downstream middleware/handlers to execute to build the response yield next; // Exclude administration routes - if (!strapi.api.admin.config.routes.hasOwnProperty(this.request.method + ' ' + this.request.url)) { + if (this.request.url.indexOf('admin') === -1) { // Verify Content-Type header if (this.request.type !== 'application/vnd.api+json') { this.status = 406; @@ -55,8 +49,8 @@ module.exports = function (strapi) { } }); - // Handlers set the response body if (!_.isUndefined(matchedRoute)) { + // Handlers set the response body const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; if (!_.isUndefined(actionRoute)) { @@ -72,7 +66,7 @@ module.exports = function (strapi) { } } - strapi.app.use(interceptor); + strapi.app.use(_interceptor); cb(); }, @@ -83,10 +77,8 @@ module.exports = function (strapi) { parse: function (strapi) { return function * (next) { - const self = this; - - // Verify Content-Type header and exclude administration routes - if (strapi.api.admin.config.routes.hasOwnProperty(this.request.method + ' ' + this.request.url)) { + // Verify Content-Type header and exclude administration and user routes + if (this.request.url.indexOf('admin') !== -1) { yield next; } else if (this.request.type !== 'application/vnd.api+json') { this.response.status = 406; @@ -96,7 +88,7 @@ module.exports = function (strapi) { yield request.parse(this); yield next; } catch (err) { - _.assign(self.response, err); + _.assign(this.response, err); } } }; From 744bf740691b1e73faee2a577080aa2bbbd95a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 29 Jan 2016 16:01:10 +0100 Subject: [PATCH 15/40] Enabled or not JSON API support thanks to configuration --- .../hooks/jsonapi/helpers/request.js | 2 +- .../hooks/jsonapi/helpers/response.js | 19 ++++++++++--------- lib/configuration/hooks/jsonapi/index.js | 10 ++++------ lib/configuration/hooks/router/index.js | 6 +++++- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 48dedb1105..dd1903859b 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -52,7 +52,7 @@ module.exports = { formatBody: function * (ctx) { const body = ctx.request.body; - const values = _.assign({}, body.data.attributes, _.mapValues(body.data.relationships, function (relation, key) { + const values = _.assign({}, body.data.attributes, _.mapValues(body.data.relationships, function (relation) { return _.isArray(relation.data) ? _.map(relation.data, 'id') : relation.data.id; })); diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 7219911764..5428b4a887 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -50,13 +50,11 @@ module.exports = { */ serialize: function (ctx, type, object, value) { - - // TODO: - // - Handle configuration with a file to improve flexibility of JSON API support const toSerialize = { topLevelLinks: {self: ctx.request.origin + ctx.request.url}, keyForAttribute: 'camelCase', pluralizeType: false, + included: true, typeForAttribute: function (currentType) { if (strapi.models.hasOwnProperty(type)) { return _.first(_.reject(_.map(strapi.models[type].associations, function (relation) { @@ -66,15 +64,20 @@ module.exports = { } }; + // Assign custom configurations + if (_.isPlainObject(strapi.config.jsonapi) && !_.isEmpty(strapi.config.jsonapi)) { + _.assign(toSerialize, _.omit(strapi.config.jsonapi, 'enabled')); + } + const PK = utils.getPK(type); if (_.isArray(value) && !_.isEmpty(value)) { // Array if (!_.isNull(PK)) { _.forEach(value, function (record) { - if (record.hasOwnProperty(PK)) { - record[PK] = record[PK].toString(); - } + if (record.hasOwnProperty(PK)) { + record[PK] = record[PK].toString(); + } }); } @@ -126,11 +129,9 @@ module.exports = { */ includedRelationShips: function (ctx, toSerialize, type) { - const self = this; - if (strapi.models.hasOwnProperty(type)) { _.forEach(strapi.models[type].associations, function (relation) { - let PK = utils.getPK(relation.model) || utils.getPK(relation.collection); + const PK = utils.getPK(relation.model) || utils.getPK(relation.collection); switch (relation.nature) { case 'oneToOne': diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index b096cabec5..ef48b7a5df 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -23,10 +23,6 @@ module.exports = function (strapi) { */ initialize: function (cb) { - // TODO: - // - Force or not the routes? - // - Add middleware before called the controller action to check parameters structure - function * _interceptor(next) { const self = this; @@ -66,7 +62,9 @@ module.exports = function (strapi) { } } - strapi.app.use(_interceptor); + if ((_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.enabled === true) || (_.isBoolean(strapi.config.jsonapi) && strapi.config.jsonapi === true)) { + strapi.app.use(_interceptor); + } cb(); }, @@ -78,7 +76,7 @@ module.exports = function (strapi) { parse: function (strapi) { return function * (next) { // Verify Content-Type header and exclude administration and user routes - if (this.request.url.indexOf('admin') !== -1) { + if (this.request.url.indexOf('admin') !== -1 && !(_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.enabled === true)) { yield next; } else if (this.request.type !== 'application/vnd.api+json') { this.response.status = 406; diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index 1ca8e9ed8c..c3dd8b1874 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -87,7 +87,11 @@ module.exports = function (strapi) { // Add the `globalPolicy`. policies.push(globalPolicy(endpoint, value, route)); - policies.push(JSONAPI.parse(strapi)); + + // Enabled JSON API support + if ((_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.enabled === true) || (_.isBoolean(strapi.config.jsonapi) && strapi.config.jsonapi === true)) { + policies.push(JSONAPI.parse(strapi)); + } if (_.isArray(value.policies) && !_.isEmpty(value.policies)) { _.forEach(value.policies, function (policy) { From 06549e6bb7b6fb4c297c6d76da00fe763ae397b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 29 Jan 2016 17:56:57 +0100 Subject: [PATCH 16/40] Improve errors display and display only existing link --- .../hooks/jsonapi/helpers/request.js | 45 ++++++++++++++----- .../hooks/jsonapi/helpers/response.js | 41 ++++++++++++----- lib/configuration/hooks/jsonapi/index.js | 5 ++- .../hooks/jsonapi/utils/utils.js | 8 ++++ 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index dd1903859b..dcb2c50f93 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -41,7 +41,9 @@ module.exports = { default: throw { status: 403, - body: 'Invalid HTTP method' + body: { + message: 'Invalid HTTP method' + } }; } }, @@ -69,22 +71,30 @@ module.exports = { if (!body.hasOwnProperty('data')) { throw { status: 403, - body: 'Missing `data` member' + body: { + message: 'Missing `data` member' + } }; } else if (!utils.isRessourceObject(body.data) && ctx.method !== 'POST') { throw { status: 403, - body: 'Invalid ressource object' + body: { + message: 'Invalid ressource object' + } }; } else if (!body.data.hasOwnProperty('type') && ctx.method === 'POST') { throw { status: 403, - body: 'Invalid ressource object' + body: { + message: 'Invalid ressource object' + } }; } else if (!strapi.models.hasOwnProperty(body.data.type)) { throw { status: 403, - body: 'Unknow `type` ' + body.data.type + body: { + message: 'Unknow `type` ' + body.data.type + } }; } @@ -98,7 +108,9 @@ module.exports = { if (!_.isEmpty(missingAttributes)) { throw { status: 403, - body: 'Missing required attributes (' + missingAttributes.toString() + ')' + body: { + message: 'Missing required attributes (' + missingAttributes.toString() + ')' + } }; } @@ -112,7 +124,9 @@ module.exports = { if (!_.isEmpty(missingRelationships)) { throw { status: 403, - body: 'Missing required relationships (' + missingRelationships.toString() + ')' + body: { + message: 'Missing required relationships (' + missingRelationships.toString() + ')' + } }; } @@ -120,15 +134,22 @@ module.exports = { if (_.size(requiredRelationships)) { const errors = _.remove(_.flattenDeep(_.map(body.data.relationships, function (relation, key) { if (!relation.hasOwnProperty('data')) { - return 'Missing `data` member for relationships ' + relation; + return { + message: 'Missing `data` member for relationships ' + relation + }; } else if (_.isArray(relation.data)) { - return _.map(relation.data, function (ressource) { + return _.map(relation.data, function (ressource, position) { if (!utils.isRessourceObject(ressource)) { - return 'Invalid ressource object in relationships ' + key; + return { + position: position, + message: 'Invalid ressource object in relationships ' + key + }; } }); } else if (!utils.isRessourceObject(relation.data)) { - return 'Invalid ressource object for relationships ' + key; + return { + message: 'Invalid ressource object for relationships ' + key + }; } })), function (n) { return !_.isUndefined(n); @@ -137,7 +158,7 @@ module.exports = { if (!_.isEmpty(errors)) { throw { status: 403, - body: errors.toString() + body: errors }; } } diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 5428b4a887..630b6fa5f5 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -31,18 +31,20 @@ module.exports = { ctx.response.status = 404; ctx.response.body = ''; - return false; + return; } else if (ctx.method === 'DELETE') { // Request successful and responds with only top-level meta data or nothing. ctx.response.body = ''; - return false; + return; } // Fetch and format value const value = this.fetchValue(ctx, object); - ctx.response.body = this.serialize(ctx, type, object, value); + if (!_.isEmpty(value)) { + ctx.response.body = this.serialize(ctx, type, object, value); + } }, /** @@ -132,6 +134,11 @@ module.exports = { if (strapi.models.hasOwnProperty(type)) { _.forEach(strapi.models[type].associations, function (relation) { const PK = utils.getPK(relation.model) || utils.getPK(relation.collection); + const availableRoutes = { + relSlSelf: utils.isRoute('GET /' + type + '/:' + PK + '/relationships/:relation'), + relSlRelated: utils.isRoute('GET /' + type + '/:' + PK), + incSelf: relation.model ? utils.isRoute('GET /' + relation.model + '/:' + PK) : utils.isRoute('GET /' + relation.collection + '/:' + PK) + }; switch (relation.nature) { case 'oneToOne': @@ -139,24 +146,30 @@ module.exports = { // Object toSerialize[relation.alias] = { ref: PK, - attributes: _.keys(strapi.models[type].attributes), + attributes: _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), relationshipLinks: { self: function (record) { - if (record.hasOwnProperty(PK)) { + if (record.hasOwnProperty(PK) && availableRoutes.relSlSelf) { return ctx.request.origin + '/' + type + '/' + record[PK] + '/relationships/' + relation.alias; } + + return undefined; }, related: function (record) { - if (record.hasOwnProperty(PK)) { + if (record.hasOwnProperty(PK) && availableRoutes.relSlRelated) { return ctx.request.origin + '/' + type + '/' + record[PK]; } + + return undefined; } }, includedLinks: { self: function (data, record) { - if (!_.isUndefined(record) && record.hasOwnProperty(PK)) { + if (!_.isUndefined(record) && record.hasOwnProperty(PK) && availableRoutes.incSelf) { return ctx.request.origin + '/' + relation.model + '/' + record[PK]; } + + return undefined; } } }; @@ -167,24 +180,30 @@ module.exports = { toSerialize[relation.alias] = { ref: PK, typeForAttribute: relation.collection, - attributes: _.keys(strapi.models[type].attributes), + attributes: _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), relationshipLinks: { self: function (record) { - if (record.hasOwnProperty(PK)) { + if (record.hasOwnProperty(PK) && availableRoutes.relSlSelf) { return ctx.request.origin + '/' + type + '/' + record[PK] + '/relationships/' + relation.alias; } + + return undefined; }, related: function (record) { - if (record.hasOwnProperty(PK)) { + if (record.hasOwnProperty(PK) && availableRoutes.relSlRelated) { return ctx.request.origin + '/' + type + '/' + record[PK]; } + + return undefined; } }, includedLinks: { self: function (data, record) { - if (record.hasOwnProperty(PK)) { + if (record.hasOwnProperty(PK) && availableRoutes.incSelf) { return ctx.request.origin + '/' + relation.collection + '/' + record[PK]; } + + return undefined; } } }; diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index ef48b7a5df..a9354fc7da 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -31,6 +31,9 @@ module.exports = function (strapi) { // Exclude administration routes if (this.request.url.indexOf('admin') === -1) { + // Set the required header + this.response.type = 'application/vnd.api+json'; + // Verify Content-Type header if (this.request.type !== 'application/vnd.api+json') { this.status = 406; @@ -56,7 +59,7 @@ module.exports = function (strapi) { } else { // Intercept error requests this.body = { - error: this.body + errors: this.body }; } } diff --git a/lib/configuration/hooks/jsonapi/utils/utils.js b/lib/configuration/hooks/jsonapi/utils/utils.js index 40ac201b3d..df5a6267ce 100644 --- a/lib/configuration/hooks/jsonapi/utils/utils.js +++ b/lib/configuration/hooks/jsonapi/utils/utils.js @@ -21,6 +21,14 @@ module.exports = { return _.isObject(object) && object.hasOwnProperty('id') && object.hasOwnProperty('type'); }, + /** + * Verify if the route exists + */ + + isRoute: function (link) { + return strapi.config.routes.hasOwnProperty(link); + }, + /** * Find data object */ From da68e196884f570be7e76e61a936d7369c1f1d6e Mon Sep 17 00:00:00 2001 From: SylvainLap Date: Mon, 1 Feb 2016 08:43:17 +0100 Subject: [PATCH 17/40] fix function names --- lib/configuration/hooks/blueprints/actions/add.js | 2 +- lib/configuration/hooks/blueprints/actions/findOne.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/configuration/hooks/blueprints/actions/add.js b/lib/configuration/hooks/blueprints/actions/add.js index 82d5339da9..d7254ab74e 100644 --- a/lib/configuration/hooks/blueprints/actions/add.js +++ b/lib/configuration/hooks/blueprints/actions/add.js @@ -15,7 +15,7 @@ const actionUtil = require('../actionUtil'); * Add an entry to a specific parent entry */ -module.exports = function destroy(_ctx) { +module.exports = function add(_ctx) { const deferred = Promise.defer(); // Ensure a model and alias can be deduced from the request. diff --git a/lib/configuration/hooks/blueprints/actions/findOne.js b/lib/configuration/hooks/blueprints/actions/findOne.js index 4009ad2a86..a7999de3ee 100644 --- a/lib/configuration/hooks/blueprints/actions/findOne.js +++ b/lib/configuration/hooks/blueprints/actions/findOne.js @@ -11,7 +11,7 @@ const actionUtil = require('../actionUtil'); * Find a specific entry */ -module.exports = function destroy(_ctx) { +module.exports = function findOne(_ctx) { const deferred = Promise.defer(); // Return the model used. From 348428132c1114880595330d95ea1de4791b34be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 1 Feb 2016 14:22:29 +0100 Subject: [PATCH 18/40] Support X-HTTP-Method-Override for old clients --- .../hooks/jsonapi/helpers/request.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index dcb2c50f93..952ed7a3b9 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -16,13 +16,24 @@ const utils = require('../utils/utils'); module.exports = { + default: {}, + /** * Parse request */ parse: function * (ctx) { + // Assign used method + this.default.method = ctx.method.toUpperCase(); + + // Detect X-HTTP-Method-Override for some clients + // which don't support PATCH method + if (ctx.header.hasOwnProperty('x-http-method-override') && ctx.header['x-http-method-override'] === 'PATCH') { + this.default.method = 'POST'; + } + // HTTP methods allowed - switch (ctx.method.toUpperCase()) { + switch (this.default.method) { case 'GET': // Nothing to do break; @@ -75,14 +86,14 @@ module.exports = { message: 'Missing `data` member' } }; - } else if (!utils.isRessourceObject(body.data) && ctx.method !== 'POST') { + } else if (!utils.isRessourceObject(body.data) && this.default.method !== 'POST') { throw { status: 403, body: { message: 'Invalid ressource object' } }; - } else if (!body.data.hasOwnProperty('type') && ctx.method === 'POST') { + } else if (!body.data.hasOwnProperty('type') && this.default.method === 'POST') { throw { status: 403, body: { From 3d0f801b3ac0240400c2ad639b5b1973071d99a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 1 Feb 2016 14:38:55 +0100 Subject: [PATCH 19/40] Handle display JSON API version support --- lib/configuration/hooks/jsonapi/helpers/response.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 630b6fa5f5..1b68d7076b 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -123,6 +123,15 @@ module.exports = { break; } + // Display JSON API version support + if (_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.hasOwnProperty('showVersion') && strapi.config.jsonapi.showVersion === true) { + return _.assign(new JSONAPISerializer(type, value, toSerialize), { + jsonapi: { + version: '1.0' + } + }); + } + return new JSONAPISerializer(type, value, toSerialize); }, From 3fa303fa8bc57f19a36117e93ed29234d5faa198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 1 Feb 2016 14:54:31 +0100 Subject: [PATCH 20/40] Fire error when query parameter is not supported --- .../hooks/jsonapi/helpers/request.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 952ed7a3b9..cc81fa71fc 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -36,6 +36,11 @@ module.exports = { switch (this.default.method) { case 'GET': // Nothing to do + try { + yield this.fetchQuery(ctx); + } catch (err) { + throw err; + } break; case 'PATCH': case 'POST': @@ -72,6 +77,23 @@ module.exports = { ctx.request.body = values; }, + /** + * Fetch query parameters + */ + + fetchQuery: function * (ctx) { + _.forEach(_.keys(ctx.query), function (key) { + if (_.includes(['include', 'fields', 'sort'], key)) { + throw { + status: 400, + body: { + message: 'Parameter `' + key + '` is not supported yet' + } + }; + } + }); + }, + /** * Fetch attributes schema */ From 4725e87cc4853eafc041b76a4d33d57ec745d621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 1 Feb 2016 14:56:17 +0100 Subject: [PATCH 21/40] Update list of unsupported query parameters --- lib/configuration/hooks/jsonapi/helpers/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index cc81fa71fc..5d81e347ac 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -83,7 +83,7 @@ module.exports = { fetchQuery: function * (ctx) { _.forEach(_.keys(ctx.query), function (key) { - if (_.includes(['include', 'fields', 'sort'], key)) { + if (_.includes(['include', 'fields', 'sort', 'page', 'filter'], key)) { throw { status: 400, body: { From 5995bccf8d472f996b8015442ae03d92197fe0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 1 Feb 2016 16:00:54 +0100 Subject: [PATCH 22/40] Update jsonapi-serializer and consider configuration on relationships --- lib/configuration/hooks/jsonapi/helpers/response.js | 12 ++++++++---- package.json | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 1b68d7076b..60f40d9d1a 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -6,7 +6,7 @@ // Public node modules. const _ = require('lodash'); -const JSONAPISerializer = require('jsonapi-serializer'); +const JSONAPI = require('jsonapi-serializer'); // Local Strapi dependencies. const utils = require('../utils/utils'); @@ -68,7 +68,7 @@ module.exports = { // Assign custom configurations if (_.isPlainObject(strapi.config.jsonapi) && !_.isEmpty(strapi.config.jsonapi)) { - _.assign(toSerialize, _.omit(strapi.config.jsonapi, 'enabled')); + _.assign(toSerialize, _.pick(strapi.config.jsonapi, 'keyForAttribute')); } const PK = utils.getPK(type); @@ -125,14 +125,14 @@ module.exports = { // Display JSON API version support if (_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.hasOwnProperty('showVersion') && strapi.config.jsonapi.showVersion === true) { - return _.assign(new JSONAPISerializer(type, value, toSerialize), { + return _.assign(new JSONAPI(type, value, toSerialize), { jsonapi: { version: '1.0' } }); } - return new JSONAPISerializer(type, value, toSerialize); + return new JSONAPI.Serializer(type, value, toSerialize); }, /** @@ -155,6 +155,8 @@ module.exports = { // Object toSerialize[relation.alias] = { ref: PK, + included: strapi.config.jsonapi.included || false, + ignoreRelationshipData: strapi.config.jsonapi.ignoreRelationshipData || false, attributes: _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), relationshipLinks: { self: function (record) { @@ -188,6 +190,8 @@ module.exports = { // Array toSerialize[relation.alias] = { ref: PK, + included: strapi.config.jsonapi.included || false, + ignoreRelationshipData: strapi.config.jsonapi.ignoreRelationshipData || false, typeForAttribute: relation.collection, attributes: _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), relationshipLinks: { diff --git a/package.json b/package.json index 241a29c1a8..3b4e4b4091 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "herd": "~1.0.0", "include-all": "~0.1.6", "json-stringify-safe": "~5.0.1", - "jsonapi-serializer": "^2.0.4", + "jsonapi-serializer": "seyz/jsonapi-serializer", "koa": "~1.1.2", "koa-bodyparser": "~2.0.1", "koa-compose": "~2.3.0", From d318f1cc65c3a61f6e5caaebdf936a7ef3f9dc1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 1 Feb 2016 17:38:08 +0100 Subject: [PATCH 23/40] Handle fields selection in query --- .../hooks/jsonapi/helpers/request.js | 37 ++++++++++++++----- .../hooks/jsonapi/helpers/response.js | 10 ++--- .../hooks/jsonapi/utils/utils.js | 14 +++++-- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 5d81e347ac..7dcaa7df51 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -82,15 +82,34 @@ module.exports = { */ fetchQuery: function * (ctx) { - _.forEach(_.keys(ctx.query), function (key) { - if (_.includes(['include', 'fields', 'sort', 'page', 'filter'], key)) { - throw { - status: 400, - body: { - message: 'Parameter `' + key + '` is not supported yet' - } - }; - } + // Use context namespace for passing information throug middleware + ctx.state.filter = { + fields: {} + }; + + _.forEach(ctx.query, function (value, key) { + if (_.includes(['include', 'sort', 'page', 'filter'], key)) { + throw { + status: 400, + body: { + message: 'Parameter `' + key + '` is not supported yet' + } + }; + } else if (key.indexOf('fields') !== -1) { + const alias = key.match(/\[(.*?)\]/)[1]; + const type = utils.getType(ctx, alias); + + if (_.isUndefined(type)) { + throw { + status: 403, + body: { + message: 'Wrong `type` in fields parameters' + } + }; + } + + ctx.state.filter.fields[type] = value.split(','); + } }); }, diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 60f40d9d1a..cc4e37a68d 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -91,14 +91,14 @@ module.exports = { } }; - toSerialize.attributes = _.keys(_.last(value)); + toSerialize.attributes = ctx.state.filter.fields[type] || _.keys(_.last(value)); } else if (_.isObject(value) && !_.isEmpty(value)) { // Object if (!_.isNull(PK) && value.hasOwnProperty(PK)) { value[PK] = value[PK].toString(); } - toSerialize.attributes = _.keys(value); + toSerialize.attributes = ctx.state.filter.fields[type] || _.keys(value); } switch (object) { @@ -125,7 +125,7 @@ module.exports = { // Display JSON API version support if (_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.hasOwnProperty('showVersion') && strapi.config.jsonapi.showVersion === true) { - return _.assign(new JSONAPI(type, value, toSerialize), { + return _.assign(new JSONAPI.Serializer(type, value, toSerialize), { jsonapi: { version: '1.0' } @@ -157,7 +157,7 @@ module.exports = { ref: PK, included: strapi.config.jsonapi.included || false, ignoreRelationshipData: strapi.config.jsonapi.ignoreRelationshipData || false, - attributes: _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), + attributes: ctx.state.filter.fields[relation.model] || _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), relationshipLinks: { self: function (record) { if (record.hasOwnProperty(PK) && availableRoutes.relSlSelf) { @@ -193,7 +193,7 @@ module.exports = { included: strapi.config.jsonapi.included || false, ignoreRelationshipData: strapi.config.jsonapi.ignoreRelationshipData || false, typeForAttribute: relation.collection, - attributes: _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), + attributes: ctx.state.filter.fields[relation.collection] || _.keys(_.omit(strapi.models[type].attributes, _.isFunction)), relationshipLinks: { self: function (record) { if (record.hasOwnProperty(PK) && availableRoutes.relSlSelf) { diff --git a/lib/configuration/hooks/jsonapi/utils/utils.js b/lib/configuration/hooks/jsonapi/utils/utils.js index df5a6267ce..802f47ece0 100644 --- a/lib/configuration/hooks/jsonapi/utils/utils.js +++ b/lib/configuration/hooks/jsonapi/utils/utils.js @@ -53,9 +53,6 @@ module.exports = { */ getType: function (ctx, supposedType) { - // TODO: - // - Parse the URL and try to extract useful information to find the type - // Relationships or related ressource if (strapi.models.hasOwnProperty(supposedType.toLowerCase()) && ctx.params.hasOwnProperty('relation')) { return _.first(_.reject(_.map(strapi.models[supposedType.toLowerCase()].associations, function (relation) { @@ -63,6 +60,17 @@ module.exports = { }), _.isUndefined)); } else if (strapi.models.hasOwnProperty(supposedType.toLowerCase())) { return supposedType.toLowerCase(); + } else if (!strapi.models.hasOwnProperty(supposedType.toLowerCase())) { + // Deep search based on the relation alias + const tryFindType = _.reject(_.flattenDeep(_.map(strapi.models, function (model) { + return _.map(model.associations, function (relation) { + return (supposedType.toLowerCase() === relation.alias) ? relation.model || relation.collection : undefined; + }); + })), _.isUndefined); + + if (!_.isUndefined(tryFindType)) { + return _.first(tryFindType); + } } return undefined; From ab4416c2b2ad6a34ff0d9131ece178a5e2b5611b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Tue, 2 Feb 2016 16:15:21 +0100 Subject: [PATCH 24/40] Add select attribute in blueprints --- .../hooks/blueprints/actionUtil.js | 26 ++++++++++++++++++- .../hooks/blueprints/actions/find.js | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/configuration/hooks/blueprints/actionUtil.js b/lib/configuration/hooks/blueprints/actionUtil.js index 1fbf8cee95..6f806aaedd 100644 --- a/lib/configuration/hooks/blueprints/actionUtil.js +++ b/lib/configuration/hooks/blueprints/actionUtil.js @@ -21,8 +21,20 @@ module.exports = { */ populateEach: function (query, _ctx, model) { + const selectedAttributes = _.get(this.parseSelect(_ctx), 'select'); let shouldPopulate = strapi.config.blueprints.populate; let aliasFilter = (_ctx.request.query && _ctx.request.query.populate) || (_ctx.request.body && _ctx.request.body.populate); + let associationToPopulate; + + if (!_.isUndefined(selectedAttributes)) { + associationToPopulate = _.remove(_.map(model.associations, function (association) { + return _.includes(selectedAttributes, association.alias) ? association : undefined; + }), function (n) { + return !_.isUndefined(n); + }); + } else { + associationToPopulate = model.associations; + } // Convert the string representation of the filter list to an array. We // need this to provide flexibility in the request param. This way both @@ -34,7 +46,7 @@ module.exports = { aliasFilter = (aliasFilter) ? aliasFilter.split(',') : []; } - return _(model.associations).reduce(function populateEachAssociation(query, association) { + return _(associationToPopulate).reduce(function populateEachAssociation(query, association) { // If an alias filter was provided, override the blueprint config. if (aliasFilter) { @@ -236,5 +248,17 @@ module.exports = { skip = +skip; } return skip; + }, + + /** + * Parse select params. + * + * @param {Object} _ctx + */ + + parseSelect: function (_ctx) { + _ctx.options = _ctx.options || {}; + const select = _ctx.request.query.hasOwnProperty('select') ? _.pick(_ctx.request.query, 'select') : {}; + return select; } }; diff --git a/lib/configuration/hooks/blueprints/actions/find.js b/lib/configuration/hooks/blueprints/actions/find.js index 812bb96560..0a89d09466 100644 --- a/lib/configuration/hooks/blueprints/actions/find.js +++ b/lib/configuration/hooks/blueprints/actions/find.js @@ -23,7 +23,7 @@ module.exports = function find(_ctx) { const Model = actionUtil.parseModel(_ctx); // Init the query. - let query = Model.find() + let query = Model.find(actionUtil.parseSelect(_ctx)) .where(actionUtil.parseCriteria(_ctx)) .limit(actionUtil.parseLimit(_ctx)) .skip(actionUtil.parseSkip(_ctx)) From 53518623e44a4bbe105be72085b599e804972f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Tue, 2 Feb 2016 16:31:11 +0100 Subject: [PATCH 25/40] Add select for findOne method --- lib/configuration/hooks/blueprints/actions/findOne.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/configuration/hooks/blueprints/actions/findOne.js b/lib/configuration/hooks/blueprints/actions/findOne.js index a7999de3ee..fde7cc2218 100644 --- a/lib/configuration/hooks/blueprints/actions/findOne.js +++ b/lib/configuration/hooks/blueprints/actions/findOne.js @@ -20,8 +20,14 @@ module.exports = function findOne(_ctx) { // Locate and validate the required `id` parameter. const pk = actionUtil.requirePk(_ctx); + // Build criteria object + const criteria = {}; + criteria[Model.primaryKey] = pk; + + _.merge(criteria, actionUtil.parseSelect(_ctx)); + // Init the query. - let query = Model.findOne(pk); + let query = Model.findOne(criteria); query = actionUtil.populateEach(query, _ctx, Model); query.exec(function found(err, matchingRecord) { if (err) { From eee53028de923ec749add3dbe61cfade0f8d50c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 3 Feb 2016 16:53:30 +0100 Subject: [PATCH 26/40] Draft for JSON API pagination support --- .../hooks/jsonapi/helpers/response.js | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index cc4e37a68d..96f65ca16f 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -43,7 +43,7 @@ module.exports = { const value = this.fetchValue(ctx, object); if (!_.isEmpty(value)) { - ctx.response.body = this.serialize(ctx, type, object, value); + ctx.response.body = this.serialize(ctx, type, object, value, matchedRoute); } }, @@ -51,7 +51,7 @@ module.exports = { * Serialize response with JSON API specification */ - serialize: function (ctx, type, object, value) { + serialize: function (ctx, type, object, value, matchedRoute) { const toSerialize = { topLevelLinks: {self: ctx.request.origin + ctx.request.url}, keyForAttribute: 'camelCase', @@ -123,16 +123,57 @@ module.exports = { break; } + // 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); + } + + const serialized = new JSONAPI.Serializer(type, value, toSerialize); + // Display JSON API version support if (_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.hasOwnProperty('showVersion') && strapi.config.jsonapi.showVersion === true) { - return _.assign(new JSONAPI.Serializer(type, value, toSerialize), { + _.assign(serialized, { jsonapi: { version: '1.0' } }); } - return new JSONAPI.Serializer(type, value, toSerialize); + return serialized; + }, + + /** + * Include pagination links to the object + */ + + includePagination: function (ctx, toSerialize, object, type, matchedRoute) { + const links = { + first: null, + last: null, + prev: null, + next: null + }; + + let index = 1; + const currentParameters = ctx.request.url.match(matchedRoute.regexp); + const data = _.mapValues(_.indexBy(matchedRoute.paramNames, 'name'), function () { + return currentParameters[index++]; + }); + + // TODO: + // - Execute some requests to get first, lastest, 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; + } }, /** @@ -143,6 +184,8 @@ module.exports = { if (strapi.models.hasOwnProperty(type)) { _.forEach(strapi.models[type].associations, function (relation) { const PK = utils.getPK(relation.model) || utils.getPK(relation.collection); + // TODO: + // - Use matched route const availableRoutes = { relSlSelf: utils.isRoute('GET /' + type + '/:' + PK + '/relationships/:relation'), relSlRelated: utils.isRoute('GET /' + type + '/:' + PK), From 047f68383f267e63553d802f85d7f3038411517b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Fri, 5 Feb 2016 18:00:47 +0100 Subject: [PATCH 27/40] Write some TODOs --- lib/configuration/hooks/jsonapi/helpers/request.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 7dcaa7df51..386d9da049 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -44,6 +44,9 @@ module.exports = { break; case 'PATCH': case 'POST': + // TODO: + // - Detect relationships edition or addition + // - Fetch and format body try { yield this.fetchSchema(ctx); yield this.formatBody(ctx); @@ -52,7 +55,9 @@ module.exports = { } break; case 'DELETE': - // Nothing to do + // TODO: + // - Detect relationships deletion + // - Fetch and format body break; default: throw { From 2da3adc1bed6a439eb325c6954f305787eac9530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 8 Feb 2016 12:07:58 +0100 Subject: [PATCH 28/40] Handle relationships update --- .../hooks/jsonapi/helpers/request.js | 76 ++++++++++++++++--- .../hooks/jsonapi/helpers/response.js | 2 +- lib/configuration/hooks/jsonapi/index.js | 11 +-- .../hooks/jsonapi/utils/utils.js | 14 +++- 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 386d9da049..8c8dc5998b 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -35,7 +35,6 @@ module.exports = { // HTTP methods allowed switch (this.default.method) { case 'GET': - // Nothing to do try { yield this.fetchQuery(ctx); } catch (err) { @@ -44,20 +43,27 @@ module.exports = { break; case 'PATCH': case 'POST': - // TODO: - // - Detect relationships edition or addition - // - Fetch and format body try { - yield this.fetchSchema(ctx); - yield this.formatBody(ctx); + if (utils.getObject(utils.matchedRoute(ctx)) === 'relationships') { + yield this.fetchRelationRequest(ctx); + yield this.formatRelationBody(ctx); + } else { + yield this.fetchSchema(ctx); + yield this.formatBody(ctx); + } } catch (err) { throw err; } break; case 'DELETE': - // TODO: - // - Detect relationships deletion - // - Fetch and format body + if (utils.getObject(utils.matchedRoute(ctx)) === 'relationships') { + try { + yield this.fetchRelationRequest(ctx); + yield this.formatRelationBody(ctx); + } catch (err) { + throw err; + } + } break; default: throw { @@ -69,6 +75,58 @@ module.exports = { } }, + /** + * Format attributes for relationships request to get more simple API + */ + + formatRelationBody: function * (ctx) { + ctx.request.body = _.map(ctx.request.body.data, 'id'); + }, + + /** + * Fetch request for relationships + */ + + fetchRelationRequest: function * (ctx) { + const body = ctx.request.body; + + if (!body.hasOwnProperty('data')) { + throw { + status: 403, + body: { + message: 'Missing `data` member' + } + }; + } else if (_.isPlainObject(body.data) && !utils.isRessourceObject(body.data)) { + throw { + status: 403, + body: { + message: 'Invalid ressource object' + } + }; + } else if (_.isArray(body.data)) { + const errors = _.remove(_.map(body.data, function (n, position) { + if (!utils.isRessourceObject(n) || _.isUndefined(utils.getType(ctx, _.get(n, 'type')))) { + return { + position: position, + message: 'Invalid ressource object or unknow type' + }; + } + }), function (n) { + return !_.isUndefined(n); + }); + + if (!_.isEmpty(errors)) { + throw { + status: 403, + body: { + message: errors + } + }; + } + } + }, + /** * Format attributes for more simple API */ diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 96f65ca16f..a285b8db8c 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -163,7 +163,7 @@ module.exports = { }); // TODO: - // - Execute some requests to get first, lastest, previous and next record + // - Call request to get first, latest, previous and next record switch (object) { default: diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index a9354fc7da..923cfeee2b 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -10,6 +10,7 @@ const _ = require('lodash'); // Local Strapi dependencies. const request = require('./helpers/request'); const response = require('./helpers/response'); +const utils = require('./utils/utils'); /** * JSON API hook @@ -24,8 +25,6 @@ module.exports = function (strapi) { initialize: function (cb) { function * _interceptor(next) { - const self = this; - // Wait for downstream middleware/handlers to execute to build the response yield next; @@ -42,15 +41,11 @@ module.exports = function (strapi) { // Intercept success requests // Detect route - const matchedRoute = _.find(strapi.router.stack, function (stack) { - if (new RegExp(stack.regexp).test(self.request.url) && _.includes(stack.methods, self.request.method.toUpperCase())) { - return stack; - } - }); + const matchedRoute = utils.matchedRoute(this); if (!_.isUndefined(matchedRoute)) { // Handlers set the response body - const actionRoute = strapi.config.routes[self.request.method.toUpperCase() + ' ' + matchedRoute.path]; + const actionRoute = strapi.config.routes[this.request.method.toUpperCase() + ' ' + matchedRoute.path]; if (!_.isUndefined(actionRoute)) { response.set(this, matchedRoute, actionRoute); diff --git a/lib/configuration/hooks/jsonapi/utils/utils.js b/lib/configuration/hooks/jsonapi/utils/utils.js index 802f47ece0..95d6aa3357 100644 --- a/lib/configuration/hooks/jsonapi/utils/utils.js +++ b/lib/configuration/hooks/jsonapi/utils/utils.js @@ -54,7 +54,7 @@ module.exports = { getType: function (ctx, supposedType) { // Relationships or related ressource - if (strapi.models.hasOwnProperty(supposedType.toLowerCase()) && ctx.params.hasOwnProperty('relation')) { + if (strapi.models.hasOwnProperty(supposedType.toLowerCase()) && ctx.params.hasOwnProperty('relation') && ctx.method === 'GET') { return _.first(_.reject(_.map(strapi.models[supposedType.toLowerCase()].associations, function (relation) { return (ctx.params.hasOwnProperty('relation') && ctx.params.relation === relation.alias) ? relation.model || relation.collection : undefined; }), _.isUndefined)); @@ -96,6 +96,18 @@ module.exports = { } 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())) { + return stack; + } + }); } }; From 9a329072820d151b20516fe23f72398d05e02520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 22 Feb 2016 17:36:58 +0100 Subject: [PATCH 29/40] Split JSON API utils to support multiple ORM --- .../hooks/jsonapi/helpers/response.js | 7 ++-- .../hooks/jsonapi/utils/bookshelf/index.js | 24 +++++++++++++ .../hooks/jsonapi/utils/utils.js | 19 +++++------ .../hooks/jsonapi/utils/waterline/index.js | 34 +++++++++++++++++++ 4 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 lib/configuration/hooks/jsonapi/utils/bookshelf/index.js create mode 100644 lib/configuration/hooks/jsonapi/utils/waterline/index.js diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index a285b8db8c..7ddaf07a25 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -40,7 +40,7 @@ module.exports = { } // Fetch and format value - const value = this.fetchValue(ctx, object); + const value = this.fetchValue(ctx, object).toJSON() || this.fetchValue(ctx, object); if (!_.isEmpty(value)) { ctx.response.body = this.serialize(ctx, type, object, value, matchedRoute); @@ -54,7 +54,7 @@ module.exports = { serialize: function (ctx, type, object, value, matchedRoute) { const toSerialize = { topLevelLinks: {self: ctx.request.origin + ctx.request.url}, - keyForAttribute: 'camelCase', + keyForAttribute: 'dash-case', pluralizeType: false, included: true, typeForAttribute: function (currentType) { @@ -130,6 +130,9 @@ module.exports = { this.includePagination(ctx, toSerialize, object, type, matchedRoute); } + console.log(toSerialize); + console.log(value); + const serialized = new JSONAPI.Serializer(type, value, toSerialize); // Display JSON API version support diff --git a/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js b/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js new file mode 100644 index 0000000000..53b0eefb72 --- /dev/null +++ b/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js @@ -0,0 +1,24 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); + +/** + * JSON API utils for BookShelf + */ + +module.exports = { + + /** + * Find primary key + */ + + getPK: function (type) { + return global[_.capitalize(type)].idAttribute || 'id'; + } + +}; diff --git a/lib/configuration/hooks/jsonapi/utils/utils.js b/lib/configuration/hooks/jsonapi/utils/utils.js index 95d6aa3357..a8d14a7ffd 100644 --- a/lib/configuration/hooks/jsonapi/utils/utils.js +++ b/lib/configuration/hooks/jsonapi/utils/utils.js @@ -6,6 +6,8 @@ // Public node modules. const _ = require('lodash'); +const utilsBookShelf = require('./bookshelf'); +const utilsWaterline = require('./waterline'); /** * JSON API utils @@ -85,17 +87,14 @@ module.exports = { return null; } - const PK = _.findKey(strapi.models[type].attributes, {primaryKey: true}); - - if (!_.isUndefined(PK)) { - return PK; - } else if (strapi.models[type].attributes.hasOwnProperty('id')) { - return 'id'; - } else if (strapi.models[type].attributes.hasOwnProperty('uuid')) { - return 'uuid'; + switch (strapi.models[type].orm.toLowerCase()) { + case 'bookshelf': + return utilsBookShelf.getPK(type); + case 'waterline': + return utilsWaterline.getPK(type); + default: + return null; } - - return null; }, /** diff --git a/lib/configuration/hooks/jsonapi/utils/waterline/index.js b/lib/configuration/hooks/jsonapi/utils/waterline/index.js new file mode 100644 index 0000000000..07249c1e1e --- /dev/null +++ b/lib/configuration/hooks/jsonapi/utils/waterline/index.js @@ -0,0 +1,34 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); + +/** + * JSON API utils for Waterline + */ + +module.exports = { + + /** + * Find primary key + */ + + getPK: function (type) { + const PK = _.findKey(strapi.models[type].attributes, {primaryKey: true}); + + if (!_.isUndefined(PK)) { + return PK; + } else if (strapi.models[type].attributes.hasOwnProperty('id')) { + return 'id'; + } else if (strapi.models[type].attributes.hasOwnProperty('uuid')) { + return 'uuid'; + } + + return null; + } + +}; From f5a72e11982b01961ec102d5435f6a19ab3e32c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Tue, 23 Feb 2016 19:01:22 +0100 Subject: [PATCH 30/40] Start GraphQL server in the router --- lib/configuration/hooks/router/index.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index 3aa3cd7dae..a46ab380b7 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -105,6 +105,20 @@ module.exports = function (strapi) { } }); + // Define GraphQL route to GraphQL schema + // or disable the global variable + if (strapi.config.graphql.enabled === true) { + // Wait GraphQL schemas generation + strapi.once('orm:graphql:ready', function () { + strapi.router.get(strapi.config.graphql.route, strapi.middlewares.graphql({ + schema: strapi.schemas, + pretty: true + })); + }); + } else { + global.graphql = undefined; + } + // Let the router use our routes and allowed methods. strapi.app.use(strapi.router.routes()); strapi.app.use(strapi.router.allowedMethods()); From c89bad49497fa4ea68144dd2faa94c2d9eb9a42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 24 Feb 2016 16:33:42 +0100 Subject: [PATCH 31/40] Add koa-graphql && fix orm load with GraphQL server --- lib/configuration/hooks/router/index.js | 22 +++++++++++++--------- package.json | 2 ++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index a46ab380b7..30f48ffc60 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -27,7 +27,11 @@ module.exports = function (strapi) { defaults: { prefix: '', - routes: {} + routes: {}, + graphql: { + enabled: true, + route: '/graphql' + } }, /** @@ -105,16 +109,16 @@ module.exports = function (strapi) { } }); + // Override default configuration for GraphQL + _.assign(this.defaults.graphql, strapi.config.graphql); + // Define GraphQL route to GraphQL schema // or disable the global variable - if (strapi.config.graphql.enabled === true) { - // Wait GraphQL schemas generation - strapi.once('orm:graphql:ready', function () { - strapi.router.get(strapi.config.graphql.route, strapi.middlewares.graphql({ - schema: strapi.schemas, - pretty: true - })); - }); + if (this.defaults.graphql.enabled === true) { + strapi.router.get(this.defaults.graphql.route, strapi.middlewares.graphql({ + schema: strapi.schemas, + pretty: true + })); } else { global.graphql = undefined; } diff --git a/package.json b/package.json index 420fe12a35..c6c865e120 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "auth", "framework", "grant", + "graphql", "koa", "koajs", "lusca", @@ -41,6 +42,7 @@ "koa-compose": "~2.3.0", "koa-cors": "~0.0.16", "koa-favicon": "~1.2.0", + "koa-graphql": "^0.4.4", "koa-gzip": "~0.1.0", "koa-i18n": "~1.2.0", "koa-ip": "~0.1.0", From f08409d336071512a9353bfec084fa0ce67ed2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 29 Feb 2016 15:41:31 +0100 Subject: [PATCH 32/40] Allow POST requests with GraphQL --- lib/configuration/hooks/router/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index 30f48ffc60..bc06a6867f 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -115,10 +115,10 @@ module.exports = function (strapi) { // Define GraphQL route to GraphQL schema // or disable the global variable if (this.defaults.graphql.enabled === true) { - strapi.router.get(this.defaults.graphql.route, strapi.middlewares.graphql({ + strapi.app.use(strapi.middlewares.mount(this.defaults.graphql.route, strapi.middlewares.graphql({ schema: strapi.schemas, pretty: true - })); + }))); } else { global.graphql = undefined; } From 93572c4e024e998d6d38ce0892860143b026c5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 2 Mar 2016 16:20:36 +0100 Subject: [PATCH 33/40] Support pagination with JSON API --- .../hooks/jsonapi/helpers/request.js | 11 ++- .../hooks/jsonapi/helpers/response.js | 95 +++++++++++-------- lib/configuration/hooks/jsonapi/index.js | 2 +- .../hooks/jsonapi/utils/bookshelf/index.js | 11 ++- .../hooks/jsonapi/utils/utils.js | 23 +---- lib/configuration/index.js | 2 + 6 files changed, 81 insertions(+), 63 deletions(-) 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); From 0e26dc48e93f045e508b778a387ae5ba2c78f065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 2 Mar 2016 16:44:38 +0100 Subject: [PATCH 34/40] Remove useless todo --- lib/configuration/hooks/jsonapi/helpers/response.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 3028870e6c..3c17f4640b 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -206,8 +206,6 @@ module.exports = { if (strapi.models.hasOwnProperty(type)) { _.forEach(strapi.models[type].associations, function (relation) { const PK = utilsORM.getPK(relation.model) || utilsORM.getPK(relation.collection); - // TODO: - // - Use matched route const availableRoutes = { relSlSelf: utils.isRoute('GET /' + type + '/:' + PK + '/relationships/:relation'), relSlRelated: utils.isRoute('GET /' + type + '/:' + PK), From 112a956babc61dc54d5e6c274a24bfa7c358add6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 2 Mar 2016 17:07:59 +0100 Subject: [PATCH 35/40] Fix relationships request --- lib/configuration/hooks/jsonapi/helpers/response.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 3c17f4640b..6fc59f4a05 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -44,7 +44,7 @@ module.exports = { } // Fetch and format value - const value = this.fetchValue(ctx, object).toJSON() || this.fetchValue(ctx, object); + const value = this.fetchValue(ctx, object); if (!_.isEmpty(value)) { ctx.response.body = yield this.serialize(ctx, type, object, value, matchedRoute); @@ -295,7 +295,7 @@ module.exports = { */ fetchValue: function (ctx, object) { - const data = ctx.body; + const data = ctx.body.toJSON() || ctx.body; switch (object) { case 'collection': @@ -319,7 +319,7 @@ module.exports = { return data[ctx.params.relation]; } - return _.first(data[ctx.params.relation]) || data[ctx.params.relation]; + return data[ctx.params.relation] || _.first(data[ctx.params.relation]); } return null; From 8c2f7cf8a608668a19637bc9c49938621e246882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 2 Mar 2016 17:53:07 +0100 Subject: [PATCH 36/40] Send context in rootValue variable --- lib/configuration/hooks/router/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index bc06a6867f..65bf2f8a31 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -115,10 +115,13 @@ module.exports = function (strapi) { // Define GraphQL route to GraphQL schema // or disable the global variable if (this.defaults.graphql.enabled === true) { - strapi.app.use(strapi.middlewares.mount(this.defaults.graphql.route, strapi.middlewares.graphql({ + strapi.app.use(strapi.middlewares.mount(this.defaults.graphql.route, strapi.middlewares.graphql((request, context) => ({ schema: strapi.schemas, - pretty: true - }))); + pretty: true, + rootValue: { + context: context + } + })))); } else { global.graphql = undefined; } From 6221cd48bca34b3561c306c9cad008e37889f1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 7 Mar 2016 17:35:07 +0100 Subject: [PATCH 37/40] Add strapi.services & prevent merging issue --- lib/configuration/hooks/_api/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/configuration/hooks/_api/index.js b/lib/configuration/hooks/_api/index.js index 1f1d5c6ee4..8af0ac00fa 100644 --- a/lib/configuration/hooks/_api/index.js +++ b/lib/configuration/hooks/_api/index.js @@ -144,16 +144,19 @@ module.exports = function (strapi) { } // Merge API controllers with the main ones. - strapi.controllers = _.merge(strapi.controllers, strapi.api[api.name].controllers); + strapi.controllers = _.merge({}, strapi.controllers, strapi.api[api.name].controllers); + + // Merge API services with the main ones. + strapi.services = _.merge({}, strapi.services, strapi.api[api.name].services); // Merge API models with the main ones. - strapi.models = _.merge(strapi.models, strapi.api[api.name].models); + strapi.models = _.merge({}, strapi.models, strapi.api[api.name].models); // Merge API policies with the main ones. - strapi.policies = _.merge(strapi.policies, strapi.api[api.name].policies); + strapi.policies = _.merge({}, strapi.policies, strapi.api[api.name].policies); // Merge API routes with the main ones. - strapi.config.routes = _.merge(strapi.config.routes, strapi.api[api.name].config.routes); + strapi.config.routes = _.merge({}, strapi.config.routes, strapi.api[api.name].config.routes); }); }); From 47f0731df603ed9bbb6e288e1089404427742f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Mon, 7 Mar 2016 18:34:27 +0100 Subject: [PATCH 38/40] Improve JSON API pagination --- .../hooks/jsonapi/helpers/response.js | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 6fc59f4a05..17c0f374f7 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -46,7 +46,7 @@ module.exports = { // Fetch and format value const value = this.fetchValue(ctx, object); - if (!_.isEmpty(value)) { + if (!_.isNull(value)) { ctx.response.body = yield this.serialize(ctx, type, object, value, matchedRoute); } }, @@ -160,7 +160,7 @@ module.exports = { // Get current page number const value = _.first(_.values(_.pick(ctx.state.query, 'page[number]'))); - const currentPage = _.isEmpty(value) ? 1 : value; + const currentPage = _.isEmpty(value) || parseInt(value, 10) === 0 ? 1 : value; // Verify integer if (currentPage.toString() === parseInt(currentPage, 10).toString()) { @@ -169,17 +169,31 @@ module.exports = { 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; + // Second page if ((parseInt(currentPage, 10) - 1) === 0) { links.prev = links.first; } + // Before last page + if ((parseInt(currentPage, 10) - 1) === pageNumber) { + links.next = links.last; + } + + // No data + if (pageNumber === 0) { + links.prev = null; + links.next = null; + links.last = null; + } + // Last page - if (ctx.request.url === ctx.state.url + '?page[number]=' + pageNumber) { - // Don't display useless + if (parseInt(currentPage, 10) === pageNumber) { links.last = null; links.next = null; - } else if (ctx.request.url === ctx.state.url) { - // First page + } + + // First page + if (parseInt(currentPage, 10) === 1) { links.first = null; links.prev = null; } @@ -295,7 +309,7 @@ module.exports = { */ fetchValue: function (ctx, object) { - const data = ctx.body.toJSON() || ctx.body; + const data = _.isFunction(_.get(ctx.body, 'toJSON')) ? ctx.body.toJSON() : ctx.body; switch (object) { case 'collection': From 351b59eab2ddc6c69bd1e517ef2566d8fd6fb132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 9 Mar 2016 16:13:19 +0100 Subject: [PATCH 39/40] Enabled JSON API only with application/vnd.api+json content-type header & open GraphiQL server --- lib/configuration/hooks/jsonapi/index.js | 32 +++++++++++++----------- lib/configuration/hooks/router/index.js | 9 ++++--- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index 9966ae666b..af2d638c1c 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -30,14 +30,10 @@ module.exports = function (strapi) { // Exclude administration routes if (this.request.url.indexOf('admin') === -1) { - // Set the required header + // Set required respoonse header this.response.type = 'application/vnd.api+json'; - // Verify Content-Type header - if (this.request.type !== 'application/vnd.api+json') { - this.status = 406; - this.body = ''; - } else if (_.startsWith(this.status, '2')) { + if (this.request.type === 'application/vnd.api+json' && _.startsWith(this.status, '2')) { // Intercept success requests // Detect route @@ -51,11 +47,15 @@ module.exports = function (strapi) { yield response.set(this, matchedRoute, actionRoute); } } - } else { + } else if (this.request.type === 'application/vnd.api+json') { // Intercept error requests this.body = { errors: this.body }; + } else if (this.request.type.indexOf('application/vnd.api+json') !== -1) { + // Right header detected but there are others header too. + this.status = 406; + this.body = ''; } } } @@ -71,21 +71,23 @@ module.exports = function (strapi) { * Parse request and attributes */ - parse: function (strapi) { + parse: function () { return function * (next) { - // Verify Content-Type header and exclude administration and user routes - if (this.request.url.indexOf('admin') !== -1 && !(_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.enabled === true)) { - yield next; - } else if (this.request.type !== 'application/vnd.api+json') { - this.response.status = 406; - this.response.body = ''; - } else { + // Verify Content-Type header + if (this.request.type === 'application/vnd.api+json') { + // Only one and right header detected. try { yield request.parse(this); yield next; } catch (err) { _.assign(this.response, err); } + } else if (this.request.type.indexOf('application/vnd.api+json') !== -1) { + // Right header detected but there are others header too. + this.response.status = 406; + this.response.body = ''; + } else { + yield next; } }; } diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index 65bf2f8a31..2cf60d9ee9 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -30,7 +30,9 @@ module.exports = function (strapi) { routes: {}, graphql: { enabled: true, - route: '/graphql' + route: '/graphql', + graphiql: false, + pretty: true } }, @@ -117,10 +119,11 @@ module.exports = function (strapi) { if (this.defaults.graphql.enabled === true) { strapi.app.use(strapi.middlewares.mount(this.defaults.graphql.route, strapi.middlewares.graphql((request, context) => ({ schema: strapi.schemas, - pretty: true, + pretty: this.defaults.graphql.pretty, rootValue: { context: context - } + }, + graphiql: this.defaults.graphql.graphiql })))); } else { global.graphql = undefined; From fc2f037e81ee22a07cbac6d29f30b8355680ac02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Georget?= Date: Wed, 9 Mar 2016 17:57:39 +0100 Subject: [PATCH 40/40] Use index file instead of dedicated file per ORM & fix some issues --- .../hooks/jsonapi/helpers/request.js | 2 +- .../hooks/jsonapi/helpers/response.js | 4 +-- lib/configuration/hooks/jsonapi/index.js | 11 +++--- .../hooks/jsonapi/utils/bookshelf/index.js | 33 ------------------ .../jsonapi/utils/{utils.js => index.js} | 0 .../hooks/jsonapi/utils/waterline/index.js | 34 ------------------- 6 files changed, 10 insertions(+), 74 deletions(-) delete mode 100644 lib/configuration/hooks/jsonapi/utils/bookshelf/index.js rename lib/configuration/hooks/jsonapi/utils/{utils.js => index.js} (100%) delete mode 100644 lib/configuration/hooks/jsonapi/utils/waterline/index.js diff --git a/lib/configuration/hooks/jsonapi/helpers/request.js b/lib/configuration/hooks/jsonapi/helpers/request.js index 88c4d78552..c53a2641b7 100644 --- a/lib/configuration/hooks/jsonapi/helpers/request.js +++ b/lib/configuration/hooks/jsonapi/helpers/request.js @@ -8,7 +8,7 @@ const _ = require('lodash'); // Local Strapi dependencies. -const utils = require('../utils/utils'); +const utils = require('../utils/'); /** * JSON API helper diff --git a/lib/configuration/hooks/jsonapi/helpers/response.js b/lib/configuration/hooks/jsonapi/helpers/response.js index 17c0f374f7..78edfcac07 100644 --- a/lib/configuration/hooks/jsonapi/helpers/response.js +++ b/lib/configuration/hooks/jsonapi/helpers/response.js @@ -9,7 +9,7 @@ const _ = require('lodash'); const JSONAPI = require('jsonapi-serializer'); // Local Strapi dependencies. -const utils = require('../utils/utils'); +const utils = require('../utils/'); let utilsORM; /** @@ -27,7 +27,7 @@ module.exports = { const type = utils.getType(ctx, actionRoute.controller); // Load right ORM utils - utilsORM = require('../utils/' + strapi.models[type].orm); + utilsORM = require('strapi-' + strapi.models[type].orm + '/lib/helpers/'); // Fetch a relationship that does not exist // Reject related request with `include` parameter diff --git a/lib/configuration/hooks/jsonapi/index.js b/lib/configuration/hooks/jsonapi/index.js index af2d638c1c..30d445fc85 100644 --- a/lib/configuration/hooks/jsonapi/index.js +++ b/lib/configuration/hooks/jsonapi/index.js @@ -10,7 +10,7 @@ const _ = require('lodash'); // Local Strapi dependencies. const request = require('./helpers/request'); const response = require('./helpers/response'); -const utils = require('./utils/utils'); +const utils = require('./utils/'); /** * JSON API hook @@ -30,10 +30,10 @@ module.exports = function (strapi) { // Exclude administration routes if (this.request.url.indexOf('admin') === -1) { - // Set required respoonse header - this.response.type = 'application/vnd.api+json'; - if (this.request.type === 'application/vnd.api+json' && _.startsWith(this.status, '2')) { + // Set required response header + this.response.type = 'application/vnd.api+json'; + // Intercept success requests // Detect route @@ -48,6 +48,9 @@ module.exports = function (strapi) { } } } else if (this.request.type === 'application/vnd.api+json') { + // Set required response header + this.response.type = 'application/vnd.api+json'; + // Intercept error requests this.body = { errors: this.body diff --git a/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js b/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js deleted file mode 100644 index 6dc7e9c232..0000000000 --- a/lib/configuration/hooks/jsonapi/utils/bookshelf/index.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -/** - * Module dependencies - */ - -// Public node modules. -const _ = require('lodash'); - -/** - * JSON API utils for BookShelf - */ - -module.exports = { - - /** - * Find primary key - */ - - 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/index.js similarity index 100% rename from lib/configuration/hooks/jsonapi/utils/utils.js rename to lib/configuration/hooks/jsonapi/utils/index.js diff --git a/lib/configuration/hooks/jsonapi/utils/waterline/index.js b/lib/configuration/hooks/jsonapi/utils/waterline/index.js deleted file mode 100644 index 07249c1e1e..0000000000 --- a/lib/configuration/hooks/jsonapi/utils/waterline/index.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -/** - * Module dependencies - */ - -// Public node modules. -const _ = require('lodash'); - -/** - * JSON API utils for Waterline - */ - -module.exports = { - - /** - * Find primary key - */ - - getPK: function (type) { - const PK = _.findKey(strapi.models[type].attributes, {primaryKey: true}); - - if (!_.isUndefined(PK)) { - return PK; - } else if (strapi.models[type].attributes.hasOwnProperty('id')) { - return 'id'; - } else if (strapi.models[type].attributes.hasOwnProperty('uuid')) { - return 'uuid'; - } - - return null; - } - -};