diff --git a/docs/3.x.x/en/guides/graphql.md b/docs/3.x.x/en/guides/graphql.md index 845d0b3beb..6449538a25 100644 --- a/docs/3.x.x/en/guides/graphql.md +++ b/docs/3.x.x/en/guides/graphql.md @@ -22,7 +22,8 @@ By default, the [Shadow CRUD](#shadow-crud) feature is enabled and the GraphQL i ``` { "endpoint": "/graphql", - "shadowCRUD": true + "shadowCRUD": true, + "depthLimit": 7 } ``` @@ -268,7 +269,26 @@ module.exports = { Query: { 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`. + } } } } diff --git a/packages/strapi-mongoose/lib/index.js b/packages/strapi-mongoose/lib/index.js index 51469672b4..ffa26506de 100755 --- a/packages/strapi-mongoose/lib/index.js +++ b/packages/strapi-mongoose/lib/index.js @@ -39,7 +39,8 @@ module.exports = function (strapi) { port: 27017, database: 'strapi', 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) => { const instance = new 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 const connectOptions = {}; + const options = {}; if (!_.isEmpty(username)) { connectOptions.user = username; @@ -67,10 +69,16 @@ module.exports = function (strapi) { 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); + for (let key in options) { + instance.set(key, options[key]) + } + // Handle error instance.connection.on('error', error => { if (error.message.indexOf(`:${port}`)) { diff --git a/packages/strapi-plugin-content-manager/config/queries/bookshelf.js b/packages/strapi-plugin-content-manager/config/queries/bookshelf.js index dcec12fb83..e2fe854281 100755 --- a/packages/strapi-plugin-content-manager/config/queries/bookshelf.js +++ b/packages/strapi-plugin-content-manager/config/queries/bookshelf.js @@ -44,7 +44,7 @@ module.exports = { withRelated: populate || this.associations.map(x => x.alias) }); - const data = record ? record.toJSON() : record; + const data = record.toJSON ? record.toJSON() : record; // Retrieve data manually. if (_.isEmpty(populate)) { diff --git a/packages/strapi-plugin-graphql/config/settings.json b/packages/strapi-plugin-graphql/config/settings.json index 4fa039be1e..1501513aee 100644 --- a/packages/strapi-plugin-graphql/config/settings.json +++ b/packages/strapi-plugin-graphql/config/settings.json @@ -1,4 +1,5 @@ { "endpoint": "/graphql", - "shadowCRUD": true + "shadowCRUD": true, + "depthLimit": 7 } diff --git a/packages/strapi-plugin-graphql/hooks/graphql/index.js b/packages/strapi-plugin-graphql/hooks/graphql/index.js index 63128595b6..a8e0586432 100644 --- a/packages/strapi-plugin-graphql/hooks/graphql/index.js +++ b/packages/strapi-plugin-graphql/hooks/graphql/index.js @@ -10,6 +10,7 @@ const path = require('path'); const glob = require('glob'); const { graphqlKoa } = require('apollo-server-koa'); const koaPlayground = require('graphql-playground-middleware-koa').default; +const depthLimit = require('graphql-depth-limit'); module.exports = strapi => { return { @@ -109,8 +110,17 @@ module.exports = strapi => { const router = strapi.koaMiddlewares.routerJoi(); - router.post(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({ schema, context: ctx })(ctx, next)); - router.get(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({ + 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. if (strapi.config.environment !== 'production') { diff --git a/packages/strapi-plugin-graphql/package.json b/packages/strapi-plugin-graphql/package.json index 9eae9edbbe..292d939dae 100644 --- a/packages/strapi-plugin-graphql/package.json +++ b/packages/strapi-plugin-graphql/package.json @@ -24,6 +24,7 @@ "apollo-server-koa": "^1.3.3", "glob": "^7.1.2", "graphql": "^0.13.2", + "graphql-depth-limit": "^1.1.0", "graphql-playground-middleware-koa": "^1.5.1", "graphql-tools": "^2.23.1", "graphql-type-json": "^0.2.0", diff --git a/packages/strapi-plugin-graphql/services/GraphQL.js b/packages/strapi-plugin-graphql/services/GraphQL.js index e1b7cda05c..291b85dab1 100644 --- a/packages/strapi-plugin-graphql/services/GraphQL.js +++ b/packages/strapi-plugin-graphql/services/GraphQL.js @@ -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. * @@ -135,19 +151,29 @@ module.exports = { convertType: (definition = {}) => { // Type. if (definition.type) { + let type = 'String'; + switch (definition.type) { case 'string': case 'text': - return 'String'; + type = 'String'; + break; case 'boolean': - return 'Boolean'; + type = 'Boolean'; + break; case 'integer': - return 'Int'; + type = 'Int'; + break; case 'float': - return 'Float'; - default: - return 'String'; + type = 'Float'; + break; } + + if (definition.required) { + type += '!'; + } + + return type; } const ref = definition.model || definition.collection; @@ -193,19 +219,21 @@ module.exports = { // Extract custom resolver or type description. const { resolver: handler = {} } = _schema; - const queryName = isSingular ? - pluralize.singular(name): - pluralize.plural(name); + let queryName; + + if (isSingular === 'force') { + queryName = name; + } else { + queryName = isSingular ? + pluralize.singular(name): + pluralize.plural(name); + } // Retrieve policies. - const policies = isSingular ? - _.get(handler, `Query.${pluralize.singular(name)}.policies`, []): - _.get(handler, `Query.${pluralize.plural(name)}.policies`, []); + const policies = _.get(handler, `Query.${queryName}.policies`, []); // Retrieve resolverOf. - const resolverOf = isSingular ? - _.get(handler, `Query.${pluralize.singular(name)}.resolverOf`, ''): - _.get(handler, `Query.${pluralize.plural(name)}.resolverOf`, ''); + const resolverOf = _.get(handler, `Query.${queryName}.resolverOf`, ''); const policiesFn = []; @@ -216,13 +244,13 @@ module.exports = { // or the shadow CRUD resolver (aka Content-Manager). const resolver = (() => { // Try to retrieve custom resolver. - const resolver = isSingular ? - _.get(handler, `Query.${pluralize.singular(name)}.resolver`): - _.get(handler, `Query.${pluralize.plural(name)}.resolver`); + const resolver = _.get(handler, `Query.${queryName}.resolver`); + + if (_.isString(resolver) || _.isPlainObject(resolver)) { + const { handler = resolver } = _.isPlainObject(resolver) ? resolver : {}; - if (_.isString(resolver)) { // Retrieve the controller's action to be executed. - const [ name, action ] = resolver.split('.'); + const [ name, action ] = handler.split('.'); const controller = plugin ? _.get(strapi.plugins, `${plugin}.controllers.${_.toLower(name)}.${action}`): @@ -287,6 +315,7 @@ module.exports = { // Plural. return async (ctx, next) => { + ctx.params = this.amountLimiting(ctx.params); ctx.query = Object.assign( this.convertToParams(_.omit(ctx.params, 'where')), ctx.params.where @@ -328,7 +357,7 @@ module.exports = { return async (obj, options, context) => { // 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), { graphql: null }) @@ -350,7 +379,7 @@ module.exports = { // Resolver can be a function. Be also a native resolver or a controller's action. if (_.isFunction(resolver)) { context.query = this.convertToParams(options); - context.params = options; + context.params = this.amountLimiting(options); if (isController) { const values = await resolver.call(null, context); @@ -362,6 +391,7 @@ module.exports = { return values && values.toJSON ? values.toJSON() : values; } + return resolver.call(null, obj, options, context); } @@ -394,7 +424,7 @@ module.exports = { // Setup initial state with default attribute that should be displayed // but these attributes are not properly defined in the models. const initialState = { - [model.primaryKey]: 'String' + [model.primaryKey]: 'String!' }; const globalId = model.globalId; @@ -407,8 +437,8 @@ module.exports = { // Add timestamps attributes. if (_.get(model, 'options.timestamps') === true) { Object.assign(initialState, { - createdAt: 'String', - updatedAt: 'String' + createdAt: 'String!', + updatedAt: 'String!' }); Object.assign(acc.resolver[globalId], { @@ -426,6 +456,7 @@ module.exports = { // Convert our layer Model to the GraphQL DL. const attributes = Object.keys(model.attributes) + .filter(attribute => model.attributes[attribute].private !== true) .reduce((acc, attribute) => { // Convert our type to the GraphQL type. acc[attribute] = this.convertType(model.attributes[attribute]); @@ -495,6 +526,21 @@ module.exports = { // Build associations queries. (model.associations || []).forEach(association => { 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 'manyMorphToMany': case 'manyToManyMorph': @@ -513,6 +559,8 @@ module.exports = { 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) => { const type = _.get(withoutRelated, `${association.alias}.${index}.kind`) || _.upperFirst(_.camelCase(_.get(withoutRelated, `${association.alias}.${index}.${association.alias}_type`))) || @@ -549,7 +597,7 @@ module.exports = { strapi.models[params.model]; // 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 || {}); // Limit, order, etc. @@ -560,7 +608,7 @@ module.exports = { switch (association.nature) { case 'manyToMany': { - const arrayOfIds = obj[association.alias].map(related => { + const arrayOfIds = (obj[association.alias] || []).map(related => { return related[ref.primaryKey] || related; }); @@ -642,9 +690,20 @@ module.exports = { return acc; } - acc[type][resolver] = _.isFunction(acc[type][resolver]) ? - acc[type][resolver]: - acc[type][resolver].resolver; + if (!_.isFunction(acc[type][resolver])) { + acc[type][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; }, acc); diff --git a/packages/strapi-plugin-users-permissions/config/policies/permissions.js b/packages/strapi-plugin-users-permissions/config/policies/permissions.js index 87ee1c59e0..3407b5ee41 100644 --- a/packages/strapi-plugin-users-permissions/config/policies/permissions.js +++ b/packages/strapi-plugin-users-permissions/config/policies/permissions.js @@ -39,9 +39,11 @@ module.exports = async (ctx, next) => { }, []); 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. diff --git a/packages/strapi/lib/middlewares/boom/index.js b/packages/strapi/lib/middlewares/boom/index.js index ad2940835a..ed2d590349 100644 --- a/packages/strapi/lib/middlewares/boom/index.js +++ b/packages/strapi/lib/middlewares/boom/index.js @@ -19,6 +19,7 @@ module.exports = strapi => { this.delegator = delegate(strapi.app.context, 'response'); this.createResponses(); + strapi.errors = Boom; strapi.app.use(async (ctx, next) => { try { // App logic.