diff --git a/examples/getstarted/api/homepage/config/schema.graphql.js b/examples/getstarted/api/homepage/config/schema.graphql.js index 06faf510b4..5f82ca4b67 100644 --- a/examples/getstarted/api/homepage/config/schema.graphql.js +++ b/examples/getstarted/api/homepage/config/schema.graphql.js @@ -24,7 +24,7 @@ module.exports = { resolver: 'application::homepage.homepage.find', }, q1: { - policies: ['homepage.test'], + policies: ['test'], resolverOf: 'application::restaurant.restaurant.find', resolver(root, args, ctx) { return { diff --git a/examples/getstarted/extensions/graphql/config/settings.json b/examples/getstarted/extensions/graphql/config/settings.json index 1bb1e6080e..f1f492b708 100644 --- a/examples/getstarted/extensions/graphql/config/settings.json +++ b/examples/getstarted/extensions/graphql/config/settings.json @@ -1,3 +1,4 @@ { - "amountLimit": 5 + "amountLimit": 5, + "depthLimit": 10 } diff --git a/packages/strapi-plugin-graphql/hooks/graphql/index.js b/packages/strapi-plugin-graphql/hooks/graphql/index.js index b24eae6bcd..329ffe5111 100644 --- a/packages/strapi-plugin-graphql/hooks/graphql/index.js +++ b/packages/strapi-plugin-graphql/hooks/graphql/index.js @@ -10,6 +10,26 @@ const { ApolloServer } = require('apollo-server-koa'); const depthLimit = require('graphql-depth-limit'); const loadConfigs = require('./load-config'); +const attachMetadataToResolvers = (schema, { api, plugin }) => { + const { resolver = {} } = schema; + if (_.isEmpty(resolver)) return schema; + + Object.keys(resolver).forEach(type => { + if (!_.isPlainObject(resolver[type])) return; + + Object.keys(resolver[type]).forEach(resolverName => { + if (!_.isPlainObject(resolver[type][resolverName])) return; + + resolver[type][resolverName]['_metadatas'] = { + api, + plugin, + }; + }); + }); + + return schema; +}; + module.exports = strapi => { const { appPath, installedPlugins } = strapi.config; @@ -32,23 +52,32 @@ module.exports = strapi => { /* * Create a merge of all the GraphQL configuration. */ - const apisSchemas = Object.keys(strapi.api || {}).map(key => - _.get(strapi.api[key], 'config.schema.graphql', {}) - ); + const apisSchemas = Object.keys(strapi.api || {}).map(key => { + const schema = _.get(strapi.api[key], 'config.schema.graphql', {}); + return attachMetadataToResolvers(schema, { api: key }); + }); - const pluginsSchemas = Object.keys(strapi.plugins || {}).map(key => - _.get(strapi.plugins[key], 'config.schema.graphql', {}) - ); + const pluginsSchemas = Object.keys(strapi.plugins || {}).map(key => { + const schema = _.get(strapi.plugins[key], 'config.schema.graphql', {}); + return attachMetadataToResolvers(schema, { plugin: key }); + }); - const extensionsSchemas = Object.keys(extensions || {}).map(key => - _.get(extensions[key], 'config.schema.graphql', {}) - ); + const extensionsSchemas = Object.keys(extensions || {}).map(key => { + const schema = _.get(extensions[key], 'config.schema.graphql', {}); + return attachMetadataToResolvers(schema, { plugin: key }); + }); + + const baseSchema = mergeSchemas([ + ...apisSchemas, + ...pluginsSchemas, + ...extensionsSchemas, + ]); // save the final schema in the plugin's config _.set( strapi, ['plugins', 'graphql', 'config', '_schema', 'graphql'], - mergeSchemas([...apisSchemas, ...pluginsSchemas, ...extensionsSchemas]) + baseSchema ); }, diff --git a/packages/strapi-plugin-graphql/hooks/graphql/load-config.js b/packages/strapi-plugin-graphql/hooks/graphql/load-config.js index 59381d6668..78e2bff5f1 100644 --- a/packages/strapi-plugin-graphql/hooks/graphql/load-config.js +++ b/packages/strapi-plugin-graphql/hooks/graphql/load-config.js @@ -15,6 +15,7 @@ const loadPluginsGraphqlConfig = async installedPlugins => { pluginDir, 'config/*.graphql?(.js)' ); + _.set(root, ['plugins', pluginName], result); } diff --git a/packages/strapi-plugin-graphql/services/Aggregator.js b/packages/strapi-plugin-graphql/services/Aggregator.js index 0f2c05c3ed..48026ef9dd 100644 --- a/packages/strapi-plugin-graphql/services/Aggregator.js +++ b/packages/strapi-plugin-graphql/services/Aggregator.js @@ -7,8 +7,6 @@ const _ = require('lodash'); const pluralize = require('pluralize'); const { convertRestQueryParams, buildQuery } = require('strapi-utils'); -const policyUtils = require('strapi-utils').policy; -const compose = require('koa-compose'); const Schema = require('./Schema.js'); const GraphQLQuery = require('./Query.js'); @@ -204,14 +202,15 @@ const preProcessGroupByData = function({ result, fieldKey, filters, model }) { const _result = _.toArray(result); return _.map(_result, value => { return { - key: value._id, + key: value._id.toString(), connection: () => { // filter by the grouped by value in next connection + return { ...filters, where: { ...(filters.where || {}), - [fieldKey]: value._id, + [fieldKey]: value._id.toString(), }, }; }, @@ -256,7 +255,7 @@ const createGroupByFieldsResolver = function(model, fields, name) { filters: convertRestQueryParams(params), aggregate: true, }).group({ - _id: `$${fieldKey}`, + _id: `$${fieldKey === 'id' ? model.primaryKey : fieldKey}`, }); return preProcessGroupByData({ @@ -476,14 +475,13 @@ const formatModelConnectionsGQL = function({ fields, model, name, - rootQuery, + resolver, plugin, }) { const { globalId } = model; - const _schema = strapi.plugins.graphql.config._schema.graphql; - const connectionGlobalId = `${globalId}Connection`; + const aggregatorFormat = formatConnectionAggregator(fields, model, name); const groupByFormat = formatConnectionGroupBy(fields, model, name); const connectionFields = { @@ -503,16 +501,9 @@ const formatModelConnectionsGQL = function({ const queryName = `${pluralName}Connection(sort: String, limit: Int, start: Int, where: JSON)`; - const policiesFn = [ - policyUtils.globalPolicy({ - controller: name, - action: 'find', - plugin, - }), - ]; - - policiesFn.push( - policyUtils.get('plugins.users-permissions.permissions', plugin, name) + const connectionResolver = Schema.buildQuery( + `${pluralName}Connection.values`, + resolver ); return { @@ -523,25 +514,16 @@ const formatModelConnectionsGQL = function({ }, resolvers: { Query: { - async [`${pluralName}Connection`](obj, options, { context }) { - // need to check - const ctx = context.app.createContext( - _.clone(context.req), - _.clone(context.res) - ); - - await compose(policiesFn)(ctx); - return options; + [`${pluralName}Connection`]: { + resolverOf: resolver.resolverOf || resolver.resolver, + resolver(obj, options, { context }) { + return options; + }, }, }, [connectionGlobalId]: { - values(obj, options, context) { - // use base resolver - return _.get(_schema, ['resolver', 'Query', rootQuery])( - obj, - obj, - context - ); + values(obj, options, gqlCtx) { + return connectionResolver(obj, obj, gqlCtx); }, groupBy(obj, options, context) { return obj; diff --git a/packages/strapi-plugin-graphql/services/Resolvers.js b/packages/strapi-plugin-graphql/services/Resolvers.js index 298c9dcf22..a186722847 100644 --- a/packages/strapi-plugin-graphql/services/Resolvers.js +++ b/packages/strapi-plugin-graphql/services/Resolvers.js @@ -424,6 +424,7 @@ const buildCollectionType = model => { Query: { [singularName]: { resolver: `${model.uid}.findOne`, + ..._.get(_schema, `resolver.Query.${singularName}`), }, }, }, @@ -431,7 +432,7 @@ const buildCollectionType = model => { } if (isQueryEnabled(_schema, pluralName)) { - const resolverObj = { + const resolverOpt = { resolver: `${model.uid}.find`, ..._.get(_schema, `resolver.Query.${pluralName}`), }; @@ -442,10 +443,24 @@ const buildCollectionType = model => { }, resolvers: { Query: { - [pluralName]: resolverObj, + [pluralName]: resolverOpt, }, }, }); + + // TODO: Add support for Graphql Aggregation in Bookshelf ORM + if (model.orm === 'mongoose') { + // Generation the aggregation for the given model + const aggregationSchema = Aggregator.formatModelConnectionsGQL({ + fields: typeDefObj, + model, + name: modelName, + resolver: resolverOpt, + plugin, + }); + + mergeSchemas(localSchema, aggregationSchema); + } } // Add model Input definition. @@ -458,20 +473,6 @@ const buildCollectionType = model => { mergeSchemas(localSchema, mutationScheam); }); - // TODO: Add support for Graphql Aggregation in Bookshelf ORM - if (model.orm === 'mongoose') { - // Generation the aggregation for the given model - const aggregationSchema = Aggregator.formatModelConnectionsGQL({ - fields: typeDefObj, - model, - name: modelName, - rootQuery: pluralName, - plugin, - }); - - mergeSchemas(localSchema, aggregationSchema); - } - return localSchema; }; diff --git a/packages/strapi-plugin-graphql/services/Schema.js b/packages/strapi-plugin-graphql/services/Schema.js index d8c9dde141..fd59680902 100644 --- a/packages/strapi-plugin-graphql/services/Schema.js +++ b/packages/strapi-plugin-graphql/services/Schema.js @@ -16,169 +16,167 @@ const { mergeSchemas, createDefaultSchema } = require('./utils'); const policyUtils = require('strapi-utils').policy; const compose = require('koa-compose'); -const schemaBuilder = { - /** - * Receive an Object and return a string which is following the GraphQL specs. - * - * @return String - */ +/** + * Receive an Object and return a string which is following the GraphQL specs. + * + * @return String + */ - formatGQL: function(fields, description = {}, model = {}, type = 'field') { - const typeFields = JSON.stringify(fields, null, 2).replace(/['",]+/g, ''); +const formatGQL = (fields, description = {}, model = {}, type = 'field') => { + const typeFields = JSON.stringify(fields, null, 2).replace(/['",]+/g, ''); - const lines = typeFields.split('\n'); - - // Try to add description for field. - if (type === 'field') { - return lines - .map(line => { - if (['{', '}'].includes(line)) { - return ''; - } - - const split = line.split(':'); - const attribute = _.trim(split[0]); - const info = - (_.isString(description[attribute]) - ? description[attribute] - : _.get(description[attribute], 'description')) || - _.get(model, `attributes.${attribute}.description`); - const deprecated = - _.get(description[attribute], 'deprecated') || - _.get(model, `attributes.${attribute}.deprecated`); - - // Snakecase an attribute when we find a dash. - if (attribute.indexOf('-') !== -1) { - line = ` ${_.snakeCase(attribute)}: ${_.trim(split[1])}`; - } - - if (info) { - line = ` """\n ${info}\n """\n${line}`; - } - - if (deprecated) { - line = `${line} @deprecated(reason: "${deprecated}")`; - } - - return line; - }) - .join('\n'); - } else if (type === 'query' || type === 'mutation') { - return lines - .map((line, index) => { - if (['{', '}'].includes(line)) { - return ''; - } - - const split = Object.keys(fields)[index - 1].split('('); - const attribute = _.trim(split[0]); - const info = _.get(description[attribute], 'description'); - const deprecated = _.get(description[attribute], 'deprecated'); - - // Snakecase an attribute when we find a dash. - if (attribute.indexOf('-') !== -1) { - line = ` ${_.snakeCase(attribute)}(${_.trim(split[1])}`; - } - - if (info) { - line = ` """\n ${info}\n """\n${line}`; - } - - if (deprecated) { - line = `${line} @deprecated(reason: "${deprecated}")`; - } - - return line; - }) - .join('\n'); - } + const lines = typeFields.split('\n'); + // Try to add description for field. + if (type === 'field') { return lines - .map((line, index) => { - if ([0, lines.length - 1].includes(index)) { + .map(line => { + if (['{', '}'].includes(line)) { return ''; } + const split = line.split(':'); + const attribute = _.trim(split[0]); + const info = + (_.isString(description[attribute]) + ? description[attribute] + : _.get(description[attribute], 'description')) || + _.get(model, `attributes.${attribute}.description`); + const deprecated = + _.get(description[attribute], 'deprecated') || + _.get(model, `attributes.${attribute}.deprecated`); + + // Snakecase an attribute when we find a dash. + if (attribute.indexOf('-') !== -1) { + line = ` ${_.snakeCase(attribute)}: ${_.trim(split[1])}`; + } + + if (info) { + line = ` """\n ${info}\n """\n${line}`; + } + + if (deprecated) { + line = `${line} @deprecated(reason: "${deprecated}")`; + } + return line; }) .join('\n'); - }, + } else if (type === 'query' || type === 'mutation') { + return lines + .map((line, index) => { + if (['{', '}'].includes(line)) { + return ''; + } - /** - * Retrieve description from variable and return a string which follow the GraphQL specs. - * - * @return String - */ + const split = Object.keys(fields)[index - 1].split('('); + const attribute = _.trim(split[0]); + const info = _.get(description[attribute], 'description'); + const deprecated = _.get(description[attribute], 'deprecated'); - getDescription: (type, model = {}) => { - const format = '"""\n'; + // Snakecase an attribute when we find a dash. + if (attribute.indexOf('-') !== -1) { + line = ` ${_.snakeCase(attribute)}(${_.trim(split[1])}`; + } - const str = _.get(type, '_description') || _.get(model, 'info.description'); + if (info) { + line = ` """\n ${info}\n """\n${line}`; + } - if (str) { - return `${format}${str}\n${format}`; - } + if (deprecated) { + line = `${line} @deprecated(reason: "${deprecated}")`; + } - return ''; - }, + return line; + }) + .join('\n'); + } - /** - * Generate GraphQL schema. - * - * @return Schema - */ + return lines + .map((line, index) => { + if ([0, lines.length - 1].includes(index)) { + return ''; + } - generateSchema: function() { - const shadowCRUDEnabled = - strapi.plugins.graphql.config.shadowCRUD !== false; + return line; + }) + .join('\n'); +}; - // Generate type definition and query/mutation for models. - const shadowCRUD = shadowCRUDEnabled - ? this.buildShadowCRUD() - : createDefaultSchema(); +/** + * Retrieve description from variable and return a string which follow the GraphQL specs. + * + * @return String + */ - const _schema = strapi.plugins.graphql.config._schema.graphql; +const getDescription = (type, model = {}) => { + const format = '"""\n'; - // Extract custom definition, query or resolver. - const { definition, query, mutation, resolver = {} } = _schema; + const str = _.get(type, '_description') || _.get(model, 'info.description'); - // Polymorphic. - const polymorphicSchema = Types.addPolymorphicUnionType( - definition + shadowCRUD.definition - ); + if (str) { + return `${format}${str}\n${format}`; + } - // Build resolvers. - const resolvers = - _.omitBy( - _.merge(shadowCRUD.resolvers, resolver, polymorphicSchema.resolvers), - _.isEmpty - ) || {}; + return ''; +}; - _schema.resolver = resolvers; +/** + * Generate GraphQL schema. + * + * @return Schema + */ - this.buildResolvers(resolvers); +const generateSchema = () => { + const shadowCRUDEnabled = strapi.plugins.graphql.config.shadowCRUD !== false; - // Return empty schema when there is no model. - if (_.isEmpty(shadowCRUD.definition) && _.isEmpty(definition)) { - return {}; - } + // Generate type definition and query/mutation for models. + const shadowCRUD = shadowCRUDEnabled + ? buildShadowCRUD() + : createDefaultSchema(); - const queryFields = this.formatGQL( - shadowCRUD.query, - resolver.Query, - null, - 'query' - ); + const _schema = strapi.plugins.graphql.config._schema.graphql; - const mutationFields = this.formatGQL( - shadowCRUD.mutation, - resolver.Mutation, - null, - 'mutation' - ); + // Extract custom definition, query or resolver. + const { definition, query, mutation, resolver = {} } = _schema; - // Concatenate. - let typeDefs = ` + // Polymorphic. + const polymorphicSchema = Types.addPolymorphicUnionType( + definition + shadowCRUD.definition + ); + + // Build resolvers. + const resolvers = + _.omitBy( + _.merge(shadowCRUD.resolvers, resolver, polymorphicSchema.resolvers), + _.isEmpty + ) || {}; + + _schema.resolver = resolvers; + + buildResolvers(resolvers); + + // Return empty schema when there is no model. + if (_.isEmpty(shadowCRUD.definition) && _.isEmpty(definition)) { + return {}; + } + + const queryFields = formatGQL( + shadowCRUD.query, + resolver.Query, + null, + 'query' + ); + + const mutationFields = formatGQL( + shadowCRUD.mutation, + resolver.Mutation, + null, + 'mutation' + ); + + // Concatenate. + let typeDefs = ` ${definition} ${shadowCRUD.definition} ${polymorphicSchema.definition} @@ -198,93 +196,92 @@ const schemaBuilder = { ${Types.addCustomScalar(resolvers)} `; - // // Build schema. - if (!strapi.config.currentEnvironment.server.production) { - // Write schema. - const schema = makeExecutableSchema({ - typeDefs, - resolvers, - }); + // // Build schema. + if (!strapi.config.currentEnvironment.server.production) { + // Write schema. + const schema = makeExecutableSchema({ + typeDefs, + resolvers, + }); - this.writeGenerateSchema(graphql.printSchema(schema)); + writeGenerateSchema(graphql.printSchema(schema)); + } + + // Remove custom scalar (like Upload); + typeDefs = Types.removeCustomScalar(typeDefs, resolvers); + + return { + typeDefs: gql(typeDefs), + resolvers, + }; +}; + +/** + * Save into a file the readable GraphQL schema. + * + * @return void + */ + +const writeGenerateSchema = schema => { + return strapi.fs.writeAppFile('exports/graphql/schema.graphql', schema); +}; + +const buildShadowCRUD = () => { + const modelSchema = Resolvers.buildShadowCRUD( + _.omitBy(strapi.models, model => model.internal === true) + ); + + const pluginSchemas = Object.keys(strapi.plugins).reduce((acc, plugin) => { + const schemas = Resolvers.buildShadowCRUD(strapi.plugins[plugin].models); + return acc.concat(schemas); + }, []); + + const componentSchemas = Object.values(strapi.components).map(compo => + Resolvers.buildComponent(compo) + ); + + const schema = { definition: '', resolvers: {}, query: {}, mutation: {} }; + mergeSchemas(schema, modelSchema, ...pluginSchemas, ...componentSchemas); + + return schema; +}; + +const buildResolvers = resolvers => { + // Transform object to only contain function. + Object.keys(resolvers).reduce((acc, type) => { + if (graphql.isScalarType(acc[type])) { + return acc; } - // Remove custom scalar (like Upload); - typeDefs = Types.removeCustomScalar(typeDefs, resolvers); + return Object.keys(acc[type]).reduce((acc, resolverName) => { + const resolverObj = acc[type][resolverName]; - return { - typeDefs: gql(typeDefs), - resolvers, - }; - }, + // Disabled this query. + if (resolverObj === false) { + delete acc[type][resolverName]; - /** - * Save into a file the readable GraphQL schema. - * - * @return void - */ - - writeGenerateSchema: schema => { - return strapi.fs.writeAppFile('exports/graphql/schema.graphql', schema); - }, - - buildShadowCRUD() { - const modelSchema = Resolvers.buildShadowCRUD( - _.omitBy(strapi.models, model => model.internal === true) - ); - - const pluginSchemas = Object.keys(strapi.plugins).reduce((acc, plugin) => { - const schemas = Resolvers.buildShadowCRUD(strapi.plugins[plugin].models); - return acc.concat(schemas); - }, []); - - const componentSchemas = Object.values(strapi.components).map(compo => - Resolvers.buildComponent(compo) - ); - - const schema = { definition: '', resolvers: {}, query: {}, mutation: {} }; - mergeSchemas(schema, modelSchema, ...pluginSchemas, ...componentSchemas); - - return schema; - }, - - buildResolvers(resolvers) { - // Transform object to only contain function. - Object.keys(resolvers).reduce((acc, type) => { - if (graphql.isScalarType(acc[type])) { return acc; } - return Object.keys(acc[type]).reduce((acc, resolverName) => { - const resolverObj = acc[type][resolverName]; - - // Disabled this query. - if (resolverObj === false) { - delete acc[type][resolverName]; - - return acc; - } - - if (_.isFunction(resolverObj)) { - return acc; - } - - switch (type) { - case 'Mutation': { - acc[type][resolverName] = buildMutation(resolverName, resolverObj); - break; - } - case 'Query': - default: { - acc[type][resolverName] = buildQuery(resolverName, resolverObj); - break; - } - } - + if (_.isFunction(resolverObj)) { return acc; - }, acc); - }, resolvers); - }, + } + + switch (type) { + case 'Mutation': { + acc[type][resolverName] = buildMutation(resolverName, resolverObj); + break; + } + case 'Query': + default: { + acc[type][resolverName] = buildQuery(resolverName, resolverObj); + break; + } + } + + return acc; + }, acc); + }, resolvers); }; // TODO: implement @@ -501,19 +498,23 @@ const getActionDetails = resolver => { */ const isResolvablePath = path => _.isString(path) && !_.isEmpty(path); -const getPolicies = (config, { plugin, api } = {}) => { +const getPolicies = config => { const { resolver, policies = [], resolverOf } = config; + const { api, plugin } = config['_metadatas'] || {}; + const policyFns = []; - const { controller, action } = isResolvablePath(resolverOf) + const { controller, action, plugin: pathPlugin } = isResolvablePath( + resolverOf + ) ? getActionDetails(resolverOf) : getActionDetails(resolver); const globalPolicy = policyUtils.globalPolicy({ controller, action, - plugin, + plugin: pathPlugin, }); policyFns.push(globalPolicy); @@ -530,4 +531,9 @@ const getPolicies = (config, { plugin, api } = {}) => { return policyFns; }; -module.exports = schemaBuilder; +module.exports = { + generateSchema, + getDescription, + formatGQL, + buildQuery, +};