mirror of
https://github.com/strapi/strapi.git
synced 2025-09-15 11:36:17 +00:00
commit
f4d2ec43a9
@ -144,16 +144,19 @@ module.exports = function (strapi) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Merge API controllers with the main ones.
|
// 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.
|
// 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.
|
// 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.
|
// 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,5 +26,6 @@ module.exports = {
|
|||||||
router: true,
|
router: true,
|
||||||
static: true,
|
static: true,
|
||||||
websockets: true,
|
websockets: true,
|
||||||
studio: true
|
studio: true,
|
||||||
|
jsonapi: true
|
||||||
};
|
};
|
||||||
|
290
lib/configuration/hooks/jsonapi/helpers/request.js
Normal file
290
lib/configuration/hooks/jsonapi/helpers/request.js
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public node modules.
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
// Local Strapi dependencies.
|
||||||
|
const utils = require('../utils/');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON API helper
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 (this.default.method) {
|
||||||
|
case 'GET':
|
||||||
|
try {
|
||||||
|
yield this.fetchQuery(ctx);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'PATCH':
|
||||||
|
case 'POST':
|
||||||
|
try {
|
||||||
|
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':
|
||||||
|
if (utils.getObject(utils.matchedRoute(ctx)) === 'relationships') {
|
||||||
|
try {
|
||||||
|
yield this.fetchRelationRequest(ctx);
|
||||||
|
yield this.formatRelationBody(ctx);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: {
|
||||||
|
message: 'Invalid HTTP method'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
formatBody: function * (ctx) {
|
||||||
|
const body = ctx.request.body;
|
||||||
|
const values = _.assign({}, body.data.attributes, _.mapValues(body.data.relationships, function (relation) {
|
||||||
|
return _.isArray(relation.data) ? _.map(relation.data, 'id') : relation.data.id;
|
||||||
|
}));
|
||||||
|
|
||||||
|
ctx.request.body = values;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch query parameters
|
||||||
|
*/
|
||||||
|
|
||||||
|
fetchQuery: function * (ctx) {
|
||||||
|
// Use context namespace for passing information throug middleware
|
||||||
|
ctx.state.filter = {
|
||||||
|
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', '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(',');
|
||||||
|
} 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]');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch attributes schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
fetchSchema: function * (ctx) {
|
||||||
|
const body = ctx.request.body;
|
||||||
|
|
||||||
|
if (!body.hasOwnProperty('data')) {
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: {
|
||||||
|
message: 'Missing `data` member'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (!utils.isRessourceObject(body.data) && this.default.method !== 'POST') {
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: {
|
||||||
|
message: 'Invalid ressource object'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (!body.data.hasOwnProperty('type') && this.default.method === 'POST') {
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: {
|
||||||
|
message: 'Invalid ressource object'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (!strapi.models.hasOwnProperty(body.data.type)) {
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: {
|
||||||
|
message: 'Unknow `type` ' + body.data.type
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract required attributes
|
||||||
|
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 = body.data.hasOwnProperty('attributes') ? _.difference(_.keys(requiredAttributes), _.keys(body.data.attributes)) : null;
|
||||||
|
|
||||||
|
if (!_.isEmpty(missingAttributes)) {
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: {
|
||||||
|
message: 'Missing required attributes (' + missingAttributes.toString() + ')'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract required 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 = body.data.hasOwnProperty('relationships') ? _.difference(_.keys(requiredRelationships), _.keys(body.data.relationships)) : null;
|
||||||
|
|
||||||
|
if (!_.isEmpty(missingRelationships)) {
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: {
|
||||||
|
message: 'Missing required relationships (' + missingRelationships.toString() + ')'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build array of errors
|
||||||
|
if (_.size(requiredRelationships)) {
|
||||||
|
const errors = _.remove(_.flattenDeep(_.map(body.data.relationships, function (relation, key) {
|
||||||
|
if (!relation.hasOwnProperty('data')) {
|
||||||
|
return {
|
||||||
|
message: 'Missing `data` member for relationships ' + relation
|
||||||
|
};
|
||||||
|
} else if (_.isArray(relation.data)) {
|
||||||
|
return _.map(relation.data, function (ressource, position) {
|
||||||
|
if (!utils.isRessourceObject(ressource)) {
|
||||||
|
return {
|
||||||
|
position: position,
|
||||||
|
message: 'Invalid ressource object in relationships ' + key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!utils.isRessourceObject(relation.data)) {
|
||||||
|
return {
|
||||||
|
message: 'Invalid ressource object for relationships ' + key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})), function (n) {
|
||||||
|
return !_.isUndefined(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!_.isEmpty(errors)) {
|
||||||
|
throw {
|
||||||
|
status: 403,
|
||||||
|
body: errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
344
lib/configuration/hooks/jsonapi/helpers/response.js
Normal file
344
lib/configuration/hooks/jsonapi/helpers/response.js
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public node modules.
|
||||||
|
const _ = require('lodash');
|
||||||
|
const JSONAPI = require('jsonapi-serializer');
|
||||||
|
|
||||||
|
// Local Strapi dependencies.
|
||||||
|
const utils = require('../utils/');
|
||||||
|
let utilsORM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON API helper
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set response
|
||||||
|
*/
|
||||||
|
|
||||||
|
set: function * (ctx, matchedRoute, actionRoute) {
|
||||||
|
const object = utils.getObject(matchedRoute);
|
||||||
|
const type = utils.getType(ctx, actionRoute.controller);
|
||||||
|
|
||||||
|
// Load right ORM utils
|
||||||
|
utilsORM = require('strapi-' + strapi.models[type].orm + '/lib/helpers/');
|
||||||
|
|
||||||
|
// Fetch a relationship that does not exist
|
||||||
|
// Reject related request with `include` parameter
|
||||||
|
if (_.isUndefined(type) || (type === 'related' && ctx.params.hasOwnProperty('include'))) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (ctx.method === 'DELETE') {
|
||||||
|
// Request successful and responds with only top-level meta data or nothing.
|
||||||
|
ctx.response.body = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and format value
|
||||||
|
const value = this.fetchValue(ctx, object);
|
||||||
|
|
||||||
|
if (!_.isNull(value)) {
|
||||||
|
ctx.response.body = yield this.serialize(ctx, type, object, value, matchedRoute);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize response with JSON API specification
|
||||||
|
*/
|
||||||
|
|
||||||
|
serialize: function * (ctx, type, object, value) {
|
||||||
|
const toSerialize = {
|
||||||
|
topLevelLinks: {self: ctx.request.origin + ctx.request.originalUrl},
|
||||||
|
keyForAttribute: 'dash-case',
|
||||||
|
pluralizeType: false,
|
||||||
|
included: true,
|
||||||
|
typeForAttribute: function (currentType) {
|
||||||
|
if (strapi.models.hasOwnProperty(type)) {
|
||||||
|
return _.first(_.reject(_.map(strapi.models[type].associations, function (relation) {
|
||||||
|
return (relation.alias === currentType) ? relation.model || relation.collection : undefined;
|
||||||
|
}), _.isUndefined)) || currentType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assign custom configurations
|
||||||
|
if (_.isPlainObject(strapi.config.jsonapi) && !_.isEmpty(strapi.config.jsonapi)) {
|
||||||
|
_.assign(toSerialize, _.pick(strapi.config.jsonapi, 'keyForAttribute'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const PK = utilsORM.getPK(type);
|
||||||
|
|
||||||
|
if (_.isArray(value) && !_.isEmpty(value)) {
|
||||||
|
// Array
|
||||||
|
if (!_.isNull(PK)) {
|
||||||
|
_.forEach(value, function (record) {
|
||||||
|
if (record.hasOwnProperty(PK)) {
|
||||||
|
record[PK] = record[PK].toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toSerialize.dataLinks = {
|
||||||
|
self: function (record) {
|
||||||
|
if (record.hasOwnProperty(PK)) {
|
||||||
|
return ctx.request.origin + ctx.state.url + '/' + record[PK];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = ctx.state.filter.fields[type] || _.keys(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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':
|
||||||
|
this.includedRelationShips(ctx, toSerialize, type);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display JSON API pagination
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
_.assign(serialized, {
|
||||||
|
jsonapi: {
|
||||||
|
version: '1.0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialized;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include pagination links to the object
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Get current page number
|
||||||
|
const value = _.first(_.values(_.pick(ctx.state.query, 'page[number]')));
|
||||||
|
const currentPage = _.isEmpty(value) || parseInt(value, 10) === 0 ? 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;
|
||||||
|
|
||||||
|
// 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 (parseInt(currentPage, 10) === pageNumber) {
|
||||||
|
links.last = null;
|
||||||
|
links.next = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First page
|
||||||
|
if (parseInt(currentPage, 10) === 1) {
|
||||||
|
links.first = null;
|
||||||
|
links.prev = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_.assign(toSerialize.topLevelLinks, _.omit(links, _.isNull));
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include relationships values to the object
|
||||||
|
*/
|
||||||
|
|
||||||
|
includedRelationShips: function (ctx, toSerialize, type) {
|
||||||
|
if (strapi.models.hasOwnProperty(type)) {
|
||||||
|
_.forEach(strapi.models[type].associations, function (relation) {
|
||||||
|
const PK = utilsORM.getPK(relation.model) || utilsORM.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':
|
||||||
|
case 'manyToOne':
|
||||||
|
// Object
|
||||||
|
toSerialize[relation.alias] = {
|
||||||
|
ref: PK,
|
||||||
|
included: strapi.config.jsonapi.included || false,
|
||||||
|
ignoreRelationshipData: strapi.config.jsonapi.ignoreRelationshipData || false,
|
||||||
|
attributes: ctx.state.filter.fields[relation.model] || _.keys(_.omit(strapi.models[type].attributes, _.isFunction)),
|
||||||
|
relationshipLinks: {
|
||||||
|
self: function (record) {
|
||||||
|
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) && availableRoutes.relSlRelated) {
|
||||||
|
return ctx.request.origin + '/' + type + '/' + record[PK];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
includedLinks: {
|
||||||
|
self: function (data, record) {
|
||||||
|
if (!_.isUndefined(record) && record.hasOwnProperty(PK) && availableRoutes.incSelf) {
|
||||||
|
return ctx.request.origin + '/' + relation.model + '/' + record[PK];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'oneToMany':
|
||||||
|
case 'manyToMany':
|
||||||
|
// Array
|
||||||
|
toSerialize[relation.alias] = {
|
||||||
|
ref: PK,
|
||||||
|
included: strapi.config.jsonapi.included || false,
|
||||||
|
ignoreRelationshipData: strapi.config.jsonapi.ignoreRelationshipData || false,
|
||||||
|
typeForAttribute: relation.collection,
|
||||||
|
attributes: ctx.state.filter.fields[relation.collection] || _.keys(_.omit(strapi.models[type].attributes, _.isFunction)),
|
||||||
|
relationshipLinks: {
|
||||||
|
self: function (record) {
|
||||||
|
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) && availableRoutes.relSlRelated) {
|
||||||
|
return ctx.request.origin + '/' + type + '/' + record[PK];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
includedLinks: {
|
||||||
|
self: function (data, record) {
|
||||||
|
if (record.hasOwnProperty(PK) && availableRoutes.incSelf) {
|
||||||
|
return ctx.request.origin + '/' + relation.collection + '/' + record[PK];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and format value
|
||||||
|
*/
|
||||||
|
|
||||||
|
fetchValue: function (ctx, object) {
|
||||||
|
const data = _.isFunction(_.get(ctx.body, 'toJSON')) ? ctx.body.toJSON() : ctx.body;
|
||||||
|
|
||||||
|
switch (object) {
|
||||||
|
case 'collection':
|
||||||
|
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]) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
case 'ressource':
|
||||||
|
if (_.isObject(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
case 'related':
|
||||||
|
case 'relationships':
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[ctx.params.relation] || _.first(data[ctx.params.relation]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return 'collection';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
100
lib/configuration/hooks/jsonapi/index.js
Normal file
100
lib/configuration/hooks/jsonapi/index.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public node modules.
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
// Local Strapi dependencies.
|
||||||
|
const request = require('./helpers/request');
|
||||||
|
const response = require('./helpers/response');
|
||||||
|
const utils = require('./utils/');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON API hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function (strapi) {
|
||||||
|
const hook = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
initialize: function (cb) {
|
||||||
|
function * _interceptor(next) {
|
||||||
|
// Wait for downstream middleware/handlers to execute to build the response
|
||||||
|
yield next;
|
||||||
|
|
||||||
|
// Exclude administration routes
|
||||||
|
if (this.request.url.indexOf('admin') === -1) {
|
||||||
|
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
|
||||||
|
const matchedRoute = utils.matchedRoute(this);
|
||||||
|
|
||||||
|
if (!_.isUndefined(matchedRoute)) {
|
||||||
|
// Handlers set the response body
|
||||||
|
const actionRoute = strapi.config.routes[this.request.method.toUpperCase() + ' ' + matchedRoute.path];
|
||||||
|
|
||||||
|
if (!_.isUndefined(actionRoute)) {
|
||||||
|
yield response.set(this, matchedRoute, actionRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
};
|
||||||
|
} 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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_.isPlainObject(strapi.config.jsonapi) && strapi.config.jsonapi.enabled === true) || (_.isBoolean(strapi.config.jsonapi) && strapi.config.jsonapi === true)) {
|
||||||
|
strapi.app.use(_interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
cb();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse request and attributes
|
||||||
|
*/
|
||||||
|
|
||||||
|
parse: function () {
|
||||||
|
return function * (next) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return hook;
|
||||||
|
};
|
91
lib/configuration/hooks/jsonapi/utils/index.js
Normal file
91
lib/configuration/hooks/jsonapi/utils/index.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
'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');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if the route exists
|
||||||
|
*/
|
||||||
|
|
||||||
|
isRoute: function (link) {
|
||||||
|
return strapi.config.routes.hasOwnProperty(link);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
// Relationships or related ressource
|
||||||
|
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));
|
||||||
|
} 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;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.replace(ctx.request.search, '')) && _.includes(stack.methods, ctx.request.method.toUpperCase())) {
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -12,6 +12,7 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
// Local utilities.
|
// Local utilities.
|
||||||
const regex = require('../../../../util/regex');
|
const regex = require('../../../../util/regex');
|
||||||
|
const JSONAPI = require('../jsonapi')();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router hook
|
* Router hook
|
||||||
@ -26,7 +27,13 @@ module.exports = function (strapi) {
|
|||||||
|
|
||||||
defaults: {
|
defaults: {
|
||||||
prefix: '',
|
prefix: '',
|
||||||
routes: {}
|
routes: {},
|
||||||
|
graphql: {
|
||||||
|
enabled: true,
|
||||||
|
route: '/graphql',
|
||||||
|
graphiql: false,
|
||||||
|
pretty: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,6 +90,11 @@ module.exports = function (strapi) {
|
|||||||
// Add the `globalPolicy`.
|
// Add the `globalPolicy`.
|
||||||
policies.push(globalPolicy(endpoint, value, route));
|
policies.push(globalPolicy(endpoint, value, route));
|
||||||
|
|
||||||
|
// 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)) {
|
if (_.isArray(value.policies) && !_.isEmpty(value.policies)) {
|
||||||
_.forEach(value.policies, function (policy) {
|
_.forEach(value.policies, function (policy) {
|
||||||
if (strapi.policies[policy]) {
|
if (strapi.policies[policy]) {
|
||||||
@ -99,6 +111,24 @@ 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 (this.defaults.graphql.enabled === true) {
|
||||||
|
strapi.app.use(strapi.middlewares.mount(this.defaults.graphql.route, strapi.middlewares.graphql((request, context) => ({
|
||||||
|
schema: strapi.schemas,
|
||||||
|
pretty: this.defaults.graphql.pretty,
|
||||||
|
rootValue: {
|
||||||
|
context: context
|
||||||
|
},
|
||||||
|
graphiql: this.defaults.graphql.graphiql
|
||||||
|
}))));
|
||||||
|
} else {
|
||||||
|
global.graphql = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Let the router use our routes and allowed methods.
|
// Let the router use our routes and allowed methods.
|
||||||
strapi.app.use(strapi.router.routes());
|
strapi.app.use(strapi.router.routes());
|
||||||
strapi.app.use(strapi.router.allowedMethods());
|
strapi.app.use(strapi.router.allowedMethods());
|
||||||
|
@ -39,6 +39,8 @@ module.exports = function (strapi) {
|
|||||||
// Set up config defaults.
|
// Set up config defaults.
|
||||||
return {
|
return {
|
||||||
|
|
||||||
|
collections: {},
|
||||||
|
|
||||||
// Core (default) hooks.
|
// Core (default) hooks.
|
||||||
hooks: _.reduce(DEFAULT_HOOKS, function (memo, hookBundled, hookIdentity) {
|
hooks: _.reduce(DEFAULT_HOOKS, function (memo, hookBundled, hookIdentity) {
|
||||||
memo[hookIdentity] = require('./hooks/' + hookIdentity);
|
memo[hookIdentity] = require('./hooks/' + hookIdentity);
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"auth",
|
"auth",
|
||||||
"framework",
|
"framework",
|
||||||
"grant",
|
"grant",
|
||||||
|
"graphql",
|
||||||
"koa",
|
"koa",
|
||||||
"koajs",
|
"koajs",
|
||||||
"lusca",
|
"lusca",
|
||||||
@ -35,11 +36,13 @@
|
|||||||
"herd": "~1.0.0",
|
"herd": "~1.0.0",
|
||||||
"include-all": "~0.1.6",
|
"include-all": "~0.1.6",
|
||||||
"json-stringify-safe": "~5.0.1",
|
"json-stringify-safe": "~5.0.1",
|
||||||
|
"jsonapi-serializer": "seyz/jsonapi-serializer",
|
||||||
"koa": "~1.1.2",
|
"koa": "~1.1.2",
|
||||||
"koa-bodyparser": "~2.0.1",
|
"koa-bodyparser": "~2.0.1",
|
||||||
"koa-compose": "~2.3.0",
|
"koa-compose": "~2.3.0",
|
||||||
"koa-cors": "~0.0.16",
|
"koa-cors": "~0.0.16",
|
||||||
"koa-favicon": "~1.2.0",
|
"koa-favicon": "~1.2.0",
|
||||||
|
"koa-graphql": "^0.4.4",
|
||||||
"koa-gzip": "~0.1.0",
|
"koa-gzip": "~0.1.0",
|
||||||
"koa-i18n": "~1.2.0",
|
"koa-i18n": "~1.2.0",
|
||||||
"koa-ip": "~0.1.0",
|
"koa-ip": "~0.1.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user