| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * GraphQL.js service | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @description: A set of functions similar to controller's actions to avoid code duplication. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const { gql, makeExecutableSchema } = require('apollo-server-koa'); | 
					
						
							|  |  |  | const _ = require('lodash'); | 
					
						
							|  |  |  | const graphql = require('graphql'); | 
					
						
							|  |  |  | const Query = require('./Query.js'); | 
					
						
							|  |  |  | const Mutation = require('./Mutation.js'); | 
					
						
							|  |  |  | const Types = require('./Types.js'); | 
					
						
							|  |  |  | const Resolvers = require('./Resolvers.js'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-13 19:27:18 +01:00
										 |  |  | const schemaBuilder = { | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * 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 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'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return lines | 
					
						
							|  |  |  |       .map((line, index) => { | 
					
						
							|  |  |  |         if ([0, lines.length - 1].includes(index)) { | 
					
						
							|  |  |  |           return ''; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return line; | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .join('\n'); | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Retrieve description from variable and return a string which follow the GraphQL specs. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @return String | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getDescription: (description, model = {}) => { | 
					
						
							|  |  |  |     const format = '"""\n'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const str = | 
					
						
							|  |  |  |       _.get(description, '_description') || _.isString(description) | 
					
						
							|  |  |  |         ? description | 
					
						
							|  |  |  |         : undefined || _.get(model, 'info.description'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (str) { | 
					
						
							|  |  |  |       return `${format}${str}\n${format}`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return ''; | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Generate GraphQL schema. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @return Schema | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   generateSchema: function() { | 
					
						
							|  |  |  |     // Generate type definition and query/mutation for models.
 | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |     let shadowCRUD = { definition: '', query: '', mutation: '', resolvers: {} }; | 
					
						
							| 
									
										
										
										
											2019-03-13 19:27:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // build defaults schemas if shadowCRUD is enabled
 | 
					
						
							|  |  |  |     if (strapi.plugins.graphql.config.shadowCRUD !== false) { | 
					
						
							| 
									
										
										
										
											2019-07-17 15:45:26 +02:00
										 |  |  |       const modelCruds = Resolvers.buildShadowCRUD( | 
					
						
							|  |  |  |         _.omit(strapi.models, ['core_store']) | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2019-07-18 10:55:13 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-13 19:27:18 +01:00
										 |  |  |       shadowCRUD = Object.keys(strapi.plugins).reduce((acc, plugin) => { | 
					
						
							| 
									
										
										
										
											2019-04-09 21:51:28 +02:00
										 |  |  |         const { | 
					
						
							|  |  |  |           definition, | 
					
						
							|  |  |  |           query, | 
					
						
							|  |  |  |           mutation, | 
					
						
							| 
									
										
										
										
											2019-12-10 16:21:21 +01:00
										 |  |  |           resolvers, | 
					
						
							| 
									
										
										
										
											2019-07-17 15:45:26 +02:00
										 |  |  |         } = Resolvers.buildShadowCRUD(strapi.plugins[plugin].models, plugin); | 
					
						
							| 
									
										
										
										
											2019-03-13 19:27:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // We cannot put this in the merge because it's a string.
 | 
					
						
							|  |  |  |         acc.definition += definition || ''; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return _.merge(acc, { | 
					
						
							|  |  |  |           query, | 
					
						
							| 
									
										
										
										
											2019-12-10 16:21:21 +01:00
										 |  |  |           resolvers, | 
					
						
							| 
									
										
										
										
											2019-03-13 19:27:18 +01:00
										 |  |  |           mutation, | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }, modelCruds); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |     const componentsSchema = { | 
					
						
							|  |  |  |       definition: '', | 
					
						
							|  |  |  |       resolvers: {}, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Object.keys(strapi.components).forEach(key => | 
					
						
							| 
									
										
										
										
											2019-12-10 16:21:21 +01:00
										 |  |  |       Resolvers.buildModel(strapi.components[key], { | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |         isComponent: true, | 
					
						
							|  |  |  |         schema: componentsSchema, | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2019-07-17 13:13:07 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |     // Extract custom definition, query or resolver.
 | 
					
						
							|  |  |  |     const { | 
					
						
							|  |  |  |       definition, | 
					
						
							|  |  |  |       query, | 
					
						
							|  |  |  |       mutation, | 
					
						
							|  |  |  |       resolver = {}, | 
					
						
							|  |  |  |     } = strapi.plugins.graphql.config._schema.graphql; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Polymorphic.
 | 
					
						
							| 
									
										
										
										
											2019-04-09 21:51:28 +02:00
										 |  |  |     const { | 
					
						
							|  |  |  |       polymorphicDef, | 
					
						
							|  |  |  |       polymorphicResolver, | 
					
						
							|  |  |  |     } = Types.addPolymorphicUnionType(definition, shadowCRUD.definition); | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Build resolvers.
 | 
					
						
							|  |  |  |     const resolvers = | 
					
						
							| 
									
										
										
										
											2019-04-09 21:51:28 +02:00
										 |  |  |       _.omitBy( | 
					
						
							| 
									
										
										
										
											2019-07-17 15:45:26 +02:00
										 |  |  |         _.merge( | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |           shadowCRUD.resolvers, | 
					
						
							|  |  |  |           componentsSchema.resolvers, | 
					
						
							| 
									
										
										
										
											2019-07-17 15:45:26 +02:00
										 |  |  |           resolver, | 
					
						
							|  |  |  |           polymorphicResolver | 
					
						
							|  |  |  |         ), | 
					
						
							| 
									
										
										
										
											2019-04-09 21:51:28 +02:00
										 |  |  |         _.isEmpty | 
					
						
							|  |  |  |       ) || {}; | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Transform object to only contain function.
 | 
					
						
							|  |  |  |     Object.keys(resolvers).reduce((acc, type) => { | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |       if (graphql.isScalarType(acc[type])) { | 
					
						
							|  |  |  |         return acc; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  |       return Object.keys(acc[type]).reduce((acc, resolverName) => { | 
					
						
							|  |  |  |         const resolverObj = acc[type][resolverName]; | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |         // Disabled this query.
 | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  |         if (resolverObj === false) { | 
					
						
							|  |  |  |           delete acc[type][resolverName]; | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |           return acc; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  |         if (_.isFunction(resolverObj)) { | 
					
						
							|  |  |  |           return acc; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         let plugin; | 
					
						
							|  |  |  |         if (_.has(resolverObj, ['plugin'])) { | 
					
						
							|  |  |  |           plugin = resolverObj.plugin; | 
					
						
							|  |  |  |         } else if (_.has(resolverObj, ['resolver', 'plugin'])) { | 
					
						
							|  |  |  |           plugin = resolverObj.resolver.plugin; | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  |         switch (type) { | 
					
						
							|  |  |  |           case 'Mutation': { | 
					
						
							|  |  |  |             let name, action; | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |               _.has(resolverObj, ['resolver']) && | 
					
						
							|  |  |  |               _.isString(resolverObj.resolver) | 
					
						
							|  |  |  |             ) { | 
					
						
							|  |  |  |               [name, action] = resolverObj.resolver.split('.'); | 
					
						
							|  |  |  |             } else if ( | 
					
						
							|  |  |  |               _.has(resolverObj, ['resolver', 'handler']) && | 
					
						
							| 
									
										
										
										
											2019-10-08 10:39:25 +02:00
										 |  |  |               _.isString(resolverObj.resolver.handler) | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  |             ) { | 
					
						
							|  |  |  |               [name, action] = resolverObj.resolver.handler.split('.'); | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |               name = null; | 
					
						
							|  |  |  |               action = resolverName; | 
					
						
							| 
									
										
										
										
											2019-08-13 10:55:12 +02:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |             const mutationResolver = Mutation.composeMutationResolver({ | 
					
						
							|  |  |  |               _schema: strapi.plugins.graphql.config._schema.graphql, | 
					
						
							|  |  |  |               plugin, | 
					
						
							|  |  |  |               name: _.toLower(name), | 
					
						
							|  |  |  |               action, | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             acc[type][resolverName] = mutationResolver; | 
					
						
							|  |  |  |             break; | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  |           case 'Query': | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |           default: { | 
					
						
							| 
									
										
										
										
											2019-09-18 12:07:45 +02:00
										 |  |  |             acc[type][resolverName] = Query.composeQueryResolver({ | 
					
						
							|  |  |  |               _schema: strapi.plugins.graphql.config._schema.graphql, | 
					
						
							|  |  |  |               plugin, | 
					
						
							|  |  |  |               name: resolverName, | 
					
						
							|  |  |  |               isSingular: 'force', // Avoid singular/pluralize and force query name.
 | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |             break; | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return acc; | 
					
						
							|  |  |  |       }, acc); | 
					
						
							|  |  |  |     }, resolvers); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Return empty schema when there is no model.
 | 
					
						
							|  |  |  |     if (_.isEmpty(shadowCRUD.definition) && _.isEmpty(definition)) { | 
					
						
							|  |  |  |       return {}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Concatenate.
 | 
					
						
							|  |  |  |     let typeDefs = `
 | 
					
						
							|  |  |  |       ${definition} | 
					
						
							|  |  |  |       ${shadowCRUD.definition} | 
					
						
							| 
									
										
										
										
											2019-11-28 15:33:37 +01:00
										 |  |  |       ${componentsSchema.definition} | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |       type Query {${shadowCRUD.query && | 
					
						
							| 
									
										
										
										
											2019-04-09 21:51:28 +02:00
										 |  |  |         this.formatGQL( | 
					
						
							|  |  |  |           shadowCRUD.query, | 
					
						
							|  |  |  |           resolver.Query, | 
					
						
							|  |  |  |           null, | 
					
						
							|  |  |  |           'query' | 
					
						
							|  |  |  |         )}${query}} | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |       type Mutation {${shadowCRUD.mutation && | 
					
						
							| 
									
										
										
										
											2019-04-09 21:51:28 +02:00
										 |  |  |         this.formatGQL( | 
					
						
							|  |  |  |           shadowCRUD.mutation, | 
					
						
							|  |  |  |           resolver.Mutation, | 
					
						
							|  |  |  |           null, | 
					
						
							|  |  |  |           'mutation' | 
					
						
							|  |  |  |         )}${mutation}} | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |       ${Types.addCustomScalar(resolvers)} | 
					
						
							|  |  |  |       ${Types.addInput()} | 
					
						
							|  |  |  |       ${polymorphicDef} | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-09 21:51:28 +02:00
										 |  |  |     // // Build schema.
 | 
					
						
							| 
									
										
										
										
											2019-04-10 18:11:55 +02:00
										 |  |  |     if (!strapi.config.currentEnvironment.server.production) { | 
					
						
							|  |  |  |       // Write schema.
 | 
					
						
							|  |  |  |       const schema = makeExecutableSchema({ | 
					
						
							|  |  |  |         typeDefs, | 
					
						
							|  |  |  |         resolvers, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       this.writeGenerateSchema(graphql.printSchema(schema)); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-17 13:13:07 +02:00
										 |  |  |     // Remove custom scalar (like Upload);
 | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  |     typeDefs = Types.removeCustomScalar(typeDefs, resolvers); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       typeDefs: gql(typeDefs), | 
					
						
							|  |  |  |       resolvers, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Save into a file the readable GraphQL schema. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @return void | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   writeGenerateSchema: schema => { | 
					
						
							| 
									
										
										
										
											2019-04-12 18:50:25 +02:00
										 |  |  |     return strapi.fs.writeAppFile('exports/graphql/schema.graphql', schema); | 
					
						
							| 
									
										
										
										
											2019-05-29 16:09:19 +02:00
										 |  |  |   }, | 
					
						
							| 
									
										
										
										
											2018-09-10 16:05:00 +08:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2019-03-13 19:27:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | module.exports = schemaBuilder; |