2018-03-27 17:15:28 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* GraphQL.js service
|
|
|
|
*
|
|
|
|
* @description: A set of functions similar to controller's actions to avoid code duplication.
|
|
|
|
*/
|
|
|
|
|
2018-03-30 17:05:24 +02:00
|
|
|
const policyUtils = require('strapi-utils').policy;
|
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
const fs = require('fs');
|
|
|
|
const path = require('path');
|
|
|
|
const _ = require('lodash');
|
2018-03-29 14:03:09 +02:00
|
|
|
const pluralize = require('pluralize');
|
2018-03-27 19:02:04 +02:00
|
|
|
const { makeExecutableSchema } = require('graphql-tools');
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
2018-03-30 17:05:24 +02:00
|
|
|
/**
|
|
|
|
* Receive an Object and return a string which is following the GraphQL specs.
|
|
|
|
*
|
|
|
|
* @return String
|
|
|
|
*/
|
|
|
|
|
2018-03-30 17:33:04 +02:00
|
|
|
formatGQL: function (fields, description = {}, type = 'field') {
|
2018-03-30 17:05:24 +02:00
|
|
|
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, index) => {
|
|
|
|
if ([0, lines.length - 1].includes(index)) {
|
|
|
|
return line;
|
|
|
|
}
|
|
|
|
|
|
|
|
const split = line.split(':');
|
|
|
|
const attribute = _.trim(split[0]);
|
|
|
|
const info = description[attribute];
|
|
|
|
|
|
|
|
if (info) {
|
|
|
|
return ` """\n ${info}\n """\n${line}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return line;
|
|
|
|
})
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
return typeFields;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve description from variable and return a string which follow the GraphQL specs.
|
|
|
|
*
|
|
|
|
* @return String
|
|
|
|
*/
|
|
|
|
|
|
|
|
getDescription: (description) => {
|
|
|
|
const format = `"""\n`;
|
|
|
|
|
|
|
|
const str = _.get(description, `_description`) || description;
|
|
|
|
|
|
|
|
if (str) {
|
|
|
|
return `${format}${str}\n${format}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ``;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert Strapi type to GraphQL type.
|
|
|
|
*
|
|
|
|
* @return String
|
|
|
|
*/
|
2018-03-28 18:40:59 +02:00
|
|
|
|
2018-03-27 19:02:04 +02:00
|
|
|
convertType: (type) => {
|
|
|
|
switch (type) {
|
|
|
|
case 'string':
|
|
|
|
case 'text':
|
|
|
|
return 'String';
|
|
|
|
case 'boolean':
|
|
|
|
return 'Boolean';
|
2018-03-30 17:05:24 +02:00
|
|
|
case 'integer':
|
|
|
|
return 'Int';
|
2018-03-27 19:02:04 +02:00
|
|
|
default:
|
|
|
|
return 'String';
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-03-30 17:05:24 +02:00
|
|
|
/**
|
|
|
|
* Execute policies before the specified resolver.
|
|
|
|
*
|
|
|
|
* @return Promise or Error.
|
|
|
|
*/
|
|
|
|
|
|
|
|
composeResolver: async (context, plugin, policies = [], resolver) => {
|
|
|
|
const policiesFn = [];
|
|
|
|
|
|
|
|
// Populate policies.
|
|
|
|
policies.forEach(policy => policyUtils.get(policy, plugin, policiesFn, 'GraphQL error'));
|
|
|
|
|
|
|
|
// Execute policies stack.
|
|
|
|
const policy = await strapi.koaMiddlewares.compose(policiesFn)(context);
|
|
|
|
|
|
|
|
// Policy doesn't always return errors but they update the current context.
|
|
|
|
if (_.isError(context.response.body) || _.get(context.response.body, 'isBoom')) {
|
|
|
|
return context.response.body;
|
|
|
|
}
|
|
|
|
|
|
|
|
// When everything is okay, the policy variable should be undefined
|
|
|
|
// so it will return the resolver instead.
|
|
|
|
return policy || resolver;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct the GraphQL query & definition and apply the right resolvers.
|
|
|
|
*
|
|
|
|
* @return Object
|
|
|
|
*/
|
|
|
|
|
2018-03-30 17:33:04 +02:00
|
|
|
shadowCRUD: function (models, plugin) {
|
2018-03-29 14:03:09 +02:00
|
|
|
const initialState = { definition: ``, query: {}, resolver: {} };
|
2018-03-27 19:02:04 +02:00
|
|
|
// Retrieve generic service from the Content Manager plugin.
|
|
|
|
const resolvers = strapi.plugins['content-manager'].services['contentmanager'];
|
|
|
|
|
2018-03-29 14:03:09 +02:00
|
|
|
if (_.isEmpty(models)) {
|
|
|
|
return initialState;
|
|
|
|
}
|
|
|
|
|
2018-03-30 17:33:04 +02:00
|
|
|
return models.reduce((acc, name) => {
|
|
|
|
const model = plugin ?
|
|
|
|
strapi.plugins[plugin].models[name]:
|
|
|
|
strapi.models[name];
|
2018-03-28 18:40:59 +02:00
|
|
|
const params = {
|
2018-03-30 17:33:04 +02:00
|
|
|
model: name
|
2018-03-28 18:40:59 +02:00
|
|
|
};
|
2018-03-27 19:02:04 +02:00
|
|
|
|
2018-03-30 17:33:04 +02:00
|
|
|
const queryOpts = plugin ? { source: plugin } : {};
|
2018-03-27 19:02:04 +02:00
|
|
|
|
2018-03-29 14:03:09 +02:00
|
|
|
// Setup initial state with default attribute that should be displayed
|
|
|
|
// but these attributes are not properly defined in the models.
|
|
|
|
const initialState = {
|
2018-03-30 17:33:04 +02:00
|
|
|
[model.primaryKey]: 'String'
|
2018-03-29 14:03:09 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Add timestamps attributes.
|
2018-03-30 17:33:04 +02:00
|
|
|
if (_.get(model, 'options.timestamps') === true) {
|
2018-03-29 14:03:09 +02:00
|
|
|
Object.assign(initialState, {
|
|
|
|
created_at: 'String',
|
|
|
|
updated_at: 'String'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-03-30 17:33:04 +02:00
|
|
|
const globalId = model.globalId;
|
2018-03-30 17:05:24 +02:00
|
|
|
|
|
|
|
// Retrieve user customisation.
|
2018-03-30 17:33:04 +02:00
|
|
|
const { resolver = {}, query, definition, _type = {} } = plugin ?
|
|
|
|
_.get(strapi.plugins, `${plugin}.config.schema.graphql`, {}) :
|
|
|
|
_.get(strapi.api, `${model}.config.schema.graphql`, {});
|
2018-03-30 17:05:24 +02:00
|
|
|
|
2018-03-27 19:02:04 +02:00
|
|
|
// Convert our layer Model to the GraphQL DL.
|
2018-03-30 17:33:04 +02:00
|
|
|
const attributes = Object.keys(model.attributes)
|
2018-03-27 19:02:04 +02:00
|
|
|
.reduce((acc, attribute) => {
|
|
|
|
// Convert our type to the GraphQL type.
|
2018-03-30 17:33:04 +02:00
|
|
|
acc[attribute] = this.convertType(model.attributes[attribute].type);
|
2018-03-27 19:02:04 +02:00
|
|
|
|
|
|
|
return acc;
|
2018-03-29 14:03:09 +02:00
|
|
|
}, initialState);
|
2018-03-27 19:02:04 +02:00
|
|
|
|
2018-03-30 17:05:24 +02:00
|
|
|
acc.definition += `${this.getDescription(_type[globalId])}type ${globalId} ${this.formatGQL(attributes, _type[globalId])}\n\n`;
|
2018-03-27 19:02:04 +02:00
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
Object.assign(acc.query, {
|
2018-03-30 17:33:04 +02:00
|
|
|
[`${pluralize.plural(name)}`]: `[${model.globalId}]`,
|
|
|
|
[`${pluralize.singular(name)}(id: String!)`]: model.globalId
|
2018-03-28 18:40:59 +02:00
|
|
|
});
|
2018-03-27 19:02:04 +02:00
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
// TODO
|
|
|
|
// - Handle mutations.
|
|
|
|
Object.assign(acc.resolver, {
|
2018-03-30 17:33:04 +02:00
|
|
|
[pluralize.plural(name)]: (obj, options, context) => this.composeResolver(
|
2018-03-30 17:05:24 +02:00
|
|
|
context,
|
|
|
|
plugin,
|
2018-03-30 17:33:04 +02:00
|
|
|
_.get(resolver, `Query.${pluralize.plural(name)}.policy`),
|
2018-03-30 17:05:24 +02:00
|
|
|
resolvers.fetchAll(params, {...queryOpts, ...options})
|
|
|
|
),
|
2018-03-30 17:33:04 +02:00
|
|
|
[pluralize.singular(name)]: (obj, { id }, context) => this.composeResolver(
|
2018-03-30 17:05:24 +02:00
|
|
|
context,
|
|
|
|
plugin,
|
2018-03-30 17:33:04 +02:00
|
|
|
_.get(resolver, `Query.${pluralize.singular(name)}.policy`),
|
2018-03-30 17:05:24 +02:00
|
|
|
resolvers.fetch({ ...params, id }, queryOpts)
|
|
|
|
)
|
2018-03-27 19:02:04 +02:00
|
|
|
});
|
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
return acc;
|
2018-03-29 14:03:09 +02:00
|
|
|
}, initialState);
|
2018-03-28 18:40:59 +02:00
|
|
|
},
|
|
|
|
|
2018-03-30 17:05:24 +02:00
|
|
|
/**
|
|
|
|
* Generate GraphQL schema.
|
|
|
|
*
|
|
|
|
* @return Schema
|
|
|
|
*/
|
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
generateSchema: function () {
|
|
|
|
// Generate type definition and query/mutation for models.
|
2018-03-30 17:33:04 +02:00
|
|
|
const shadowCRUD = strapi.plugins.graphql.config.shadowCRUD !== false ? (() => {
|
|
|
|
// Exclude core models.
|
|
|
|
const models = Object.keys(strapi.models).filter(model => model !== 'core_store');
|
|
|
|
|
|
|
|
// Reproduce the same pattern for each plugin.
|
|
|
|
return Object.keys(strapi.plugins).reduce((acc, plugin) => {
|
|
|
|
const { definition, query, resolver } = this.shadowCRUD(Object.keys(strapi.plugins[plugin].models), plugin);
|
|
|
|
|
|
|
|
// We cannot put this in the merge because it's a string.
|
|
|
|
acc.definition += definition;
|
|
|
|
|
|
|
|
return _.merge(acc, {
|
|
|
|
query,
|
|
|
|
resolver
|
|
|
|
});
|
|
|
|
}, this.shadowCRUD(models));
|
|
|
|
})() : {};
|
2018-03-27 19:02:04 +02:00
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
// Build resolvers.
|
|
|
|
const resolvers = {
|
|
|
|
Query: shadowCRUD.resolver || {}
|
|
|
|
};
|
2018-03-27 19:02:04 +02:00
|
|
|
|
2018-03-29 14:03:09 +02:00
|
|
|
// Return empty schema when there is no model.
|
|
|
|
if (_.isEmpty(shadowCRUD.definition)) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2018-03-27 19:02:04 +02:00
|
|
|
// Concatenate.
|
2018-03-30 17:05:24 +02:00
|
|
|
const typeDefs = shadowCRUD.definition + `type Query ${this.formatGQL(shadowCRUD.query, {}, 'query')}`;
|
|
|
|
|
|
|
|
console.log(typeDefs);
|
2018-03-28 18:40:59 +02:00
|
|
|
|
|
|
|
// Write schema.
|
|
|
|
this.writeGenerateSchema(typeDefs);
|
|
|
|
|
2018-03-27 19:02:04 +02:00
|
|
|
// Build schema.
|
|
|
|
const schema = makeExecutableSchema({
|
|
|
|
typeDefs,
|
|
|
|
resolvers,
|
|
|
|
});
|
|
|
|
|
|
|
|
return schema;
|
2018-03-28 18:40:59 +02:00
|
|
|
},
|
|
|
|
|
2018-03-30 17:05:24 +02:00
|
|
|
/**
|
|
|
|
* Save into a file the readable GraphQL schema.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
writeGenerateSchema(schema) {
|
2018-03-28 20:13:09 +02:00
|
|
|
// Disable auto-reload.
|
|
|
|
strapi.reload.isWatching = false;
|
|
|
|
|
2018-03-28 18:40:59 +02:00
|
|
|
const generatedFolder = path.resolve(strapi.config.appPath, 'plugins', 'graphql', 'config', 'generated');
|
|
|
|
|
|
|
|
// Create folder if necessary.
|
|
|
|
try {
|
|
|
|
fs.accessSync(generatedFolder, fs.constants.R_OK | fs.constants.W_OK);
|
|
|
|
} catch (err) {
|
|
|
|
if (err && err.code === 'ENOENT') {
|
|
|
|
fs.mkdirSync(generatedFolder);
|
|
|
|
} else {
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(generatedFolder, 'schema.graphql'), schema);
|
2018-03-28 20:13:09 +02:00
|
|
|
|
|
|
|
strapi.reload.isWatching = true;
|
2018-03-27 19:02:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
};
|