Merge branch 'master' into patch-content-manager

This commit is contained in:
Jim LAURIE 2018-05-31 13:53:07 +02:00 committed by GitHub
commit 1d44f6c271
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 143 additions and 41 deletions

View File

@ -22,7 +22,8 @@ By default, the [Shadow CRUD](#shadow-crud) feature is enabled and the GraphQL i
``` ```
{ {
"endpoint": "/graphql", "endpoint": "/graphql",
"shadowCRUD": true "shadowCRUD": true,
"depthLimit": 7
} }
``` ```
@ -268,7 +269,26 @@ module.exports = {
Query: { Query: {
person: { person: {
description: 'Return a single person', description: 'Return a single person',
resolver: 'Person.findOne' // It will use the action `findOne` located in the `Person.js` controller. resolver: 'Person.findOne' // It will use the action `findOne` located in the `Person.js` controller*.
}
}
}
};
```
>The resolver parameter also accepts an object as a value to target a controller located in a plugin.
```js
module.exports = {
...
resolver: {
Query: {
person: {
description: 'Return a single person',
resolver: {
plugin: 'users-permissions',
handler: 'User.findOne' // It will use the action `findOne` located in the `Person.js` controller inside the plugin `Users & Permissions`.
}
} }
} }
} }

View File

@ -39,7 +39,8 @@ module.exports = function (strapi) {
port: 27017, port: 27017,
database: 'strapi', database: 'strapi',
authenticationDatabase: '', authenticationDatabase: '',
ssl: false ssl: false,
debug: false
}, },
/** /**
@ -50,10 +51,11 @@ module.exports = function (strapi) {
_.forEach(_.pickBy(strapi.config.connections, {connector: 'strapi-mongoose'}), (connection, connectionName) => { _.forEach(_.pickBy(strapi.config.connections, {connector: 'strapi-mongoose'}), (connection, connectionName) => {
const instance = new Mongoose(); const instance = new Mongoose();
const { uri, host, port, username, password, database } = _.defaults(connection.settings, strapi.config.hook.settings.mongoose); const { uri, host, port, username, password, database } = _.defaults(connection.settings, strapi.config.hook.settings.mongoose);
const { authenticationDatabase, ssl } = _.defaults(connection.options, strapi.config.hook.settings.mongoose); const { authenticationDatabase, ssl, debug } = _.defaults(connection.options, strapi.config.hook.settings.mongoose);
// Connect to mongo database // Connect to mongo database
const connectOptions = {}; const connectOptions = {};
const options = {};
if (!_.isEmpty(username)) { if (!_.isEmpty(username)) {
connectOptions.user = username; connectOptions.user = username;
@ -67,10 +69,16 @@ module.exports = function (strapi) {
connectOptions.authSource = authenticationDatabase; connectOptions.authSource = authenticationDatabase;
} }
connectOptions.ssl = ssl === true || ssl === 'true'; connectOptions.ssl = Boolean(ssl);
options.debug = Boolean(debug);
instance.connect(uri || `mongodb://${host}:${port}/${database}`, connectOptions); instance.connect(uri || `mongodb://${host}:${port}/${database}`, connectOptions);
for (let key in options) {
instance.set(key, options[key])
}
// Handle error // Handle error
instance.connection.on('error', error => { instance.connection.on('error', error => {
if (error.message.indexOf(`:${port}`)) { if (error.message.indexOf(`:${port}`)) {

View File

@ -44,7 +44,7 @@ module.exports = {
withRelated: populate || this.associations.map(x => x.alias) withRelated: populate || this.associations.map(x => x.alias)
}); });
const data = record ? record.toJSON() : record; const data = record.toJSON ? record.toJSON() : record;
// Retrieve data manually. // Retrieve data manually.
if (_.isEmpty(populate)) { if (_.isEmpty(populate)) {

View File

@ -1,4 +1,5 @@
{ {
"endpoint": "/graphql", "endpoint": "/graphql",
"shadowCRUD": true "shadowCRUD": true,
"depthLimit": 7
} }

View File

@ -10,6 +10,7 @@ const path = require('path');
const glob = require('glob'); const glob = require('glob');
const { graphqlKoa } = require('apollo-server-koa'); const { graphqlKoa } = require('apollo-server-koa');
const koaPlayground = require('graphql-playground-middleware-koa').default; const koaPlayground = require('graphql-playground-middleware-koa').default;
const depthLimit = require('graphql-depth-limit');
module.exports = strapi => { module.exports = strapi => {
return { return {
@ -109,8 +110,17 @@ module.exports = strapi => {
const router = strapi.koaMiddlewares.routerJoi(); const router = strapi.koaMiddlewares.routerJoi();
router.post(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({ schema, context: ctx })(ctx, next)); router.post(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({
router.get(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({ schema, context: ctx })(ctx, next)); schema,
context: ctx,
validationRules: [ depthLimit(strapi.plugins.graphql.config.depthLimit) ]
})(ctx, next));
router.get(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({
schema,
context: ctx,
validationRules: [ depthLimit(strapi.plugins.graphql.config.depthLimit) ]
})(ctx, next));
// Disable GraphQL Playground in production environment. // Disable GraphQL Playground in production environment.
if (strapi.config.environment !== 'production') { if (strapi.config.environment !== 'production') {

View File

@ -24,6 +24,7 @@
"apollo-server-koa": "^1.3.3", "apollo-server-koa": "^1.3.3",
"glob": "^7.1.2", "glob": "^7.1.2",
"graphql": "^0.13.2", "graphql": "^0.13.2",
"graphql-depth-limit": "^1.1.0",
"graphql-playground-middleware-koa": "^1.5.1", "graphql-playground-middleware-koa": "^1.5.1",
"graphql-tools": "^2.23.1", "graphql-tools": "^2.23.1",
"graphql-type-json": "^0.2.0", "graphql-type-json": "^0.2.0",

View File

@ -126,6 +126,22 @@ module.exports = {
}, {}); }, {});
}, },
/**
* Security to avoid infinite limit.
*
* @return String
*/
amountLimiting: (params) => {
if (params.limit && params.limit < 0) {
params.limit = 0;
} else if (params.limit && params.limit > 100) {
params.limit = 100;
}
return params;
},
/** /**
* Convert Strapi type to GraphQL type. * Convert Strapi type to GraphQL type.
* *
@ -135,19 +151,29 @@ module.exports = {
convertType: (definition = {}) => { convertType: (definition = {}) => {
// Type. // Type.
if (definition.type) { if (definition.type) {
let type = 'String';
switch (definition.type) { switch (definition.type) {
case 'string': case 'string':
case 'text': case 'text':
return 'String'; type = 'String';
break;
case 'boolean': case 'boolean':
return 'Boolean'; type = 'Boolean';
break;
case 'integer': case 'integer':
return 'Int'; type = 'Int';
break;
case 'float': case 'float':
return 'Float'; type = 'Float';
default: break;
return 'String';
} }
if (definition.required) {
type += '!';
}
return type;
} }
const ref = definition.model || definition.collection; const ref = definition.model || definition.collection;
@ -193,19 +219,21 @@ module.exports = {
// Extract custom resolver or type description. // Extract custom resolver or type description.
const { resolver: handler = {} } = _schema; const { resolver: handler = {} } = _schema;
const queryName = isSingular ? let queryName;
if (isSingular === 'force') {
queryName = name;
} else {
queryName = isSingular ?
pluralize.singular(name): pluralize.singular(name):
pluralize.plural(name); pluralize.plural(name);
}
// Retrieve policies. // Retrieve policies.
const policies = isSingular ? const policies = _.get(handler, `Query.${queryName}.policies`, []);
_.get(handler, `Query.${pluralize.singular(name)}.policies`, []):
_.get(handler, `Query.${pluralize.plural(name)}.policies`, []);
// Retrieve resolverOf. // Retrieve resolverOf.
const resolverOf = isSingular ? const resolverOf = _.get(handler, `Query.${queryName}.resolverOf`, '');
_.get(handler, `Query.${pluralize.singular(name)}.resolverOf`, ''):
_.get(handler, `Query.${pluralize.plural(name)}.resolverOf`, '');
const policiesFn = []; const policiesFn = [];
@ -216,13 +244,13 @@ module.exports = {
// or the shadow CRUD resolver (aka Content-Manager). // or the shadow CRUD resolver (aka Content-Manager).
const resolver = (() => { const resolver = (() => {
// Try to retrieve custom resolver. // Try to retrieve custom resolver.
const resolver = isSingular ? const resolver = _.get(handler, `Query.${queryName}.resolver`);
_.get(handler, `Query.${pluralize.singular(name)}.resolver`):
_.get(handler, `Query.${pluralize.plural(name)}.resolver`); if (_.isString(resolver) || _.isPlainObject(resolver)) {
const { handler = resolver } = _.isPlainObject(resolver) ? resolver : {};
if (_.isString(resolver)) {
// Retrieve the controller's action to be executed. // Retrieve the controller's action to be executed.
const [ name, action ] = resolver.split('.'); const [ name, action ] = handler.split('.');
const controller = plugin ? const controller = plugin ?
_.get(strapi.plugins, `${plugin}.controllers.${_.toLower(name)}.${action}`): _.get(strapi.plugins, `${plugin}.controllers.${_.toLower(name)}.${action}`):
@ -287,6 +315,7 @@ module.exports = {
// Plural. // Plural.
return async (ctx, next) => { return async (ctx, next) => {
ctx.params = this.amountLimiting(ctx.params);
ctx.query = Object.assign( ctx.query = Object.assign(
this.convertToParams(_.omit(ctx.params, 'where')), this.convertToParams(_.omit(ctx.params, 'where')),
ctx.params.where ctx.params.where
@ -328,7 +357,7 @@ module.exports = {
return async (obj, options, context) => { return async (obj, options, context) => {
// Hack to be able to handle permissions for each query. // Hack to be able to handle permissions for each query.
const ctx = Object.assign(context, { const ctx = Object.assign(_.clone(context), {
request: Object.assign(_.clone(context.request), { request: Object.assign(_.clone(context.request), {
graphql: null graphql: null
}) })
@ -350,7 +379,7 @@ module.exports = {
// Resolver can be a function. Be also a native resolver or a controller's action. // Resolver can be a function. Be also a native resolver or a controller's action.
if (_.isFunction(resolver)) { if (_.isFunction(resolver)) {
context.query = this.convertToParams(options); context.query = this.convertToParams(options);
context.params = options; context.params = this.amountLimiting(options);
if (isController) { if (isController) {
const values = await resolver.call(null, context); const values = await resolver.call(null, context);
@ -362,6 +391,7 @@ module.exports = {
return values && values.toJSON ? values.toJSON() : values; return values && values.toJSON ? values.toJSON() : values;
} }
return resolver.call(null, obj, options, context); return resolver.call(null, obj, options, context);
} }
@ -394,7 +424,7 @@ module.exports = {
// Setup initial state with default attribute that should be displayed // Setup initial state with default attribute that should be displayed
// but these attributes are not properly defined in the models. // but these attributes are not properly defined in the models.
const initialState = { const initialState = {
[model.primaryKey]: 'String' [model.primaryKey]: 'String!'
}; };
const globalId = model.globalId; const globalId = model.globalId;
@ -407,8 +437,8 @@ module.exports = {
// Add timestamps attributes. // Add timestamps attributes.
if (_.get(model, 'options.timestamps') === true) { if (_.get(model, 'options.timestamps') === true) {
Object.assign(initialState, { Object.assign(initialState, {
createdAt: 'String', createdAt: 'String!',
updatedAt: 'String' updatedAt: 'String!'
}); });
Object.assign(acc.resolver[globalId], { Object.assign(acc.resolver[globalId], {
@ -426,6 +456,7 @@ module.exports = {
// Convert our layer Model to the GraphQL DL. // Convert our layer Model to the GraphQL DL.
const attributes = Object.keys(model.attributes) const attributes = Object.keys(model.attributes)
.filter(attribute => model.attributes[attribute].private !== true)
.reduce((acc, attribute) => { .reduce((acc, attribute) => {
// Convert our type to the GraphQL type. // Convert our type to the GraphQL type.
acc[attribute] = this.convertType(model.attributes[attribute]); acc[attribute] = this.convertType(model.attributes[attribute]);
@ -495,6 +526,21 @@ module.exports = {
// Build associations queries. // Build associations queries.
(model.associations || []).forEach(association => { (model.associations || []).forEach(association => {
switch (association.nature) { switch (association.nature) {
case 'oneToManyMorph':
return _.merge(acc.resolver[globalId], {
[association.alias]: async (obj) => {
const withRelated = await resolvers.fetch({
id: obj[model.primaryKey],
model: name
}, plugin, [association.alias], false);
const entry = withRelated && withRelated.toJSON ? withRelated.toJSON() : withRelated;
entry[association.alias]._type = _.upperFirst(association.model);
return entry[association.alias];
}
});
case 'manyMorphToOne': case 'manyMorphToOne':
case 'manyMorphToMany': case 'manyMorphToMany':
case 'manyToManyMorph': case 'manyToManyMorph':
@ -513,6 +559,8 @@ module.exports = {
const entry = withRelated && withRelated.toJSON ? withRelated.toJSON() : withRelated; const entry = withRelated && withRelated.toJSON ? withRelated.toJSON() : withRelated;
// TODO:
// - Handle sort, limit and start (lodash or inside the query)
entry[association.alias].map((entry, index) => { entry[association.alias].map((entry, index) => {
const type = _.get(withoutRelated, `${association.alias}.${index}.kind`) || const type = _.get(withoutRelated, `${association.alias}.${index}.kind`) ||
_.upperFirst(_.camelCase(_.get(withoutRelated, `${association.alias}.${index}.${association.alias}_type`))) || _.upperFirst(_.camelCase(_.get(withoutRelated, `${association.alias}.${index}.${association.alias}_type`))) ||
@ -549,7 +597,7 @@ module.exports = {
strapi.models[params.model]; strapi.models[params.model];
// Apply optional arguments to make more precise nested request. // Apply optional arguments to make more precise nested request.
const convertedParams = strapi.utils.models.convertParams(name, this.convertToParams(options)); const convertedParams = strapi.utils.models.convertParams(name, this.convertToParams(this.amountLimiting(options)));
const where = strapi.utils.models.convertParams(name, options.where || {}); const where = strapi.utils.models.convertParams(name, options.where || {});
// Limit, order, etc. // Limit, order, etc.
@ -560,7 +608,7 @@ module.exports = {
switch (association.nature) { switch (association.nature) {
case 'manyToMany': { case 'manyToMany': {
const arrayOfIds = obj[association.alias].map(related => { const arrayOfIds = (obj[association.alias] || []).map(related => {
return related[ref.primaryKey] || related; return related[ref.primaryKey] || related;
}); });
@ -642,9 +690,20 @@ module.exports = {
return acc; return acc;
} }
acc[type][resolver] = _.isFunction(acc[type][resolver]) ? if (!_.isFunction(acc[type][resolver])) {
acc[type][resolver]: acc[type][resolver] = acc[type][resolver].resolver;
acc[type][resolver].resolver; }
if (_.isString(acc[type][resolver]) || _.isPlainObject(acc[type][resolver])) {
const { plugin = '' } = _.isPlainObject(acc[type][resolver]) ? acc[type][resolver] : {};
acc[type][resolver] = this.composeResolver(
strapi.plugins.graphql.config._schema.graphql,
plugin,
resolver,
'force' // Avoid singular/pluralize and force query name.
);
}
return acc; return acc;
}, acc); }, acc);

View File

@ -39,9 +39,11 @@ module.exports = async (ctx, next) => {
}, []); }, []);
if (!permission) { if (!permission) {
ctx.forbidden(); if (ctx.request.graphql === null) {
return ctx.request.graphql = strapi.errors.forbidden();
}
return ctx.request.graphql = ctx.body; ctx.forbidden();
} }
// Execute the policies. // Execute the policies.

View File

@ -19,6 +19,7 @@ module.exports = strapi => {
this.delegator = delegate(strapi.app.context, 'response'); this.delegator = delegate(strapi.app.context, 'response');
this.createResponses(); this.createResponses();
strapi.errors = Boom;
strapi.app.use(async (ctx, next) => { strapi.app.use(async (ctx, next) => {
try { try {
// App logic. // App logic.