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(
|
2019-12-23 16:38:42 +01:00
|
|
|
_.omitBy(strapi.models, model => model.internal === true)
|
2019-07-17 15:45:26 +02:00
|
|
|
);
|
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;
|