Add metadatas to resolvers to know where they are created

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-01-30 10:49:42 +01:00
parent 0c6d39297f
commit 0285c7bd96
7 changed files with 299 additions and 279 deletions

View File

@ -24,7 +24,7 @@ module.exports = {
resolver: 'application::homepage.homepage.find', resolver: 'application::homepage.homepage.find',
}, },
q1: { q1: {
policies: ['homepage.test'], policies: ['test'],
resolverOf: 'application::restaurant.restaurant.find', resolverOf: 'application::restaurant.restaurant.find',
resolver(root, args, ctx) { resolver(root, args, ctx) {
return { return {

View File

@ -1,3 +1,4 @@
{ {
"amountLimit": 5 "amountLimit": 5,
"depthLimit": 10
} }

View File

@ -10,6 +10,26 @@ const { ApolloServer } = require('apollo-server-koa');
const depthLimit = require('graphql-depth-limit'); const depthLimit = require('graphql-depth-limit');
const loadConfigs = require('./load-config'); 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 => { module.exports = strapi => {
const { appPath, installedPlugins } = strapi.config; const { appPath, installedPlugins } = strapi.config;
@ -32,23 +52,32 @@ module.exports = strapi => {
/* /*
* Create a merge of all the GraphQL configuration. * Create a merge of all the GraphQL configuration.
*/ */
const apisSchemas = Object.keys(strapi.api || {}).map(key => const apisSchemas = Object.keys(strapi.api || {}).map(key => {
_.get(strapi.api[key], 'config.schema.graphql', {}) const schema = _.get(strapi.api[key], 'config.schema.graphql', {});
); return attachMetadataToResolvers(schema, { api: key });
});
const pluginsSchemas = Object.keys(strapi.plugins || {}).map(key => const pluginsSchemas = Object.keys(strapi.plugins || {}).map(key => {
_.get(strapi.plugins[key], 'config.schema.graphql', {}) const schema = _.get(strapi.plugins[key], 'config.schema.graphql', {});
); return attachMetadataToResolvers(schema, { plugin: key });
});
const extensionsSchemas = Object.keys(extensions || {}).map(key => const extensionsSchemas = Object.keys(extensions || {}).map(key => {
_.get(extensions[key], 'config.schema.graphql', {}) 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 // save the final schema in the plugin's config
_.set( _.set(
strapi, strapi,
['plugins', 'graphql', 'config', '_schema', 'graphql'], ['plugins', 'graphql', 'config', '_schema', 'graphql'],
mergeSchemas([...apisSchemas, ...pluginsSchemas, ...extensionsSchemas]) baseSchema
); );
}, },

View File

@ -15,6 +15,7 @@ const loadPluginsGraphqlConfig = async installedPlugins => {
pluginDir, pluginDir,
'config/*.graphql?(.js)' 'config/*.graphql?(.js)'
); );
_.set(root, ['plugins', pluginName], result); _.set(root, ['plugins', pluginName], result);
} }

View File

@ -7,8 +7,6 @@
const _ = require('lodash'); const _ = require('lodash');
const pluralize = require('pluralize'); const pluralize = require('pluralize');
const { convertRestQueryParams, buildQuery } = require('strapi-utils'); const { convertRestQueryParams, buildQuery } = require('strapi-utils');
const policyUtils = require('strapi-utils').policy;
const compose = require('koa-compose');
const Schema = require('./Schema.js'); const Schema = require('./Schema.js');
const GraphQLQuery = require('./Query.js'); const GraphQLQuery = require('./Query.js');
@ -204,14 +202,15 @@ const preProcessGroupByData = function({ result, fieldKey, filters, model }) {
const _result = _.toArray(result); const _result = _.toArray(result);
return _.map(_result, value => { return _.map(_result, value => {
return { return {
key: value._id, key: value._id.toString(),
connection: () => { connection: () => {
// filter by the grouped by value in next connection // filter by the grouped by value in next connection
return { return {
...filters, ...filters,
where: { where: {
...(filters.where || {}), ...(filters.where || {}),
[fieldKey]: value._id, [fieldKey]: value._id.toString(),
}, },
}; };
}, },
@ -256,7 +255,7 @@ const createGroupByFieldsResolver = function(model, fields, name) {
filters: convertRestQueryParams(params), filters: convertRestQueryParams(params),
aggregate: true, aggregate: true,
}).group({ }).group({
_id: `$${fieldKey}`, _id: `$${fieldKey === 'id' ? model.primaryKey : fieldKey}`,
}); });
return preProcessGroupByData({ return preProcessGroupByData({
@ -476,14 +475,13 @@ const formatModelConnectionsGQL = function({
fields, fields,
model, model,
name, name,
rootQuery, resolver,
plugin, plugin,
}) { }) {
const { globalId } = model; const { globalId } = model;
const _schema = strapi.plugins.graphql.config._schema.graphql;
const connectionGlobalId = `${globalId}Connection`; const connectionGlobalId = `${globalId}Connection`;
const aggregatorFormat = formatConnectionAggregator(fields, model, name); const aggregatorFormat = formatConnectionAggregator(fields, model, name);
const groupByFormat = formatConnectionGroupBy(fields, model, name); const groupByFormat = formatConnectionGroupBy(fields, model, name);
const connectionFields = { const connectionFields = {
@ -503,16 +501,9 @@ const formatModelConnectionsGQL = function({
const queryName = `${pluralName}Connection(sort: String, limit: Int, start: Int, where: JSON)`; const queryName = `${pluralName}Connection(sort: String, limit: Int, start: Int, where: JSON)`;
const policiesFn = [ const connectionResolver = Schema.buildQuery(
policyUtils.globalPolicy({ `${pluralName}Connection.values`,
controller: name, resolver
action: 'find',
plugin,
}),
];
policiesFn.push(
policyUtils.get('plugins.users-permissions.permissions', plugin, name)
); );
return { return {
@ -523,25 +514,16 @@ const formatModelConnectionsGQL = function({
}, },
resolvers: { resolvers: {
Query: { Query: {
async [`${pluralName}Connection`](obj, options, { context }) { [`${pluralName}Connection`]: {
// need to check resolverOf: resolver.resolverOf || resolver.resolver,
const ctx = context.app.createContext( resolver(obj, options, { context }) {
_.clone(context.req), return options;
_.clone(context.res) },
);
await compose(policiesFn)(ctx);
return options;
}, },
}, },
[connectionGlobalId]: { [connectionGlobalId]: {
values(obj, options, context) { values(obj, options, gqlCtx) {
// use base resolver return connectionResolver(obj, obj, gqlCtx);
return _.get(_schema, ['resolver', 'Query', rootQuery])(
obj,
obj,
context
);
}, },
groupBy(obj, options, context) { groupBy(obj, options, context) {
return obj; return obj;

View File

@ -424,6 +424,7 @@ const buildCollectionType = model => {
Query: { Query: {
[singularName]: { [singularName]: {
resolver: `${model.uid}.findOne`, resolver: `${model.uid}.findOne`,
..._.get(_schema, `resolver.Query.${singularName}`),
}, },
}, },
}, },
@ -431,7 +432,7 @@ const buildCollectionType = model => {
} }
if (isQueryEnabled(_schema, pluralName)) { if (isQueryEnabled(_schema, pluralName)) {
const resolverObj = { const resolverOpt = {
resolver: `${model.uid}.find`, resolver: `${model.uid}.find`,
..._.get(_schema, `resolver.Query.${pluralName}`), ..._.get(_schema, `resolver.Query.${pluralName}`),
}; };
@ -442,10 +443,24 @@ const buildCollectionType = model => {
}, },
resolvers: { resolvers: {
Query: { 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. // Add model Input definition.
@ -458,20 +473,6 @@ const buildCollectionType = model => {
mergeSchemas(localSchema, mutationScheam); 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; return localSchema;
}; };

View File

@ -16,169 +16,167 @@ const { mergeSchemas, createDefaultSchema } = require('./utils');
const policyUtils = require('strapi-utils').policy; const policyUtils = require('strapi-utils').policy;
const compose = require('koa-compose'); const compose = require('koa-compose');
const schemaBuilder = { /**
/** * Receive an Object and return a string which is following the GraphQL specs.
* Receive an Object and return a string which is following the GraphQL specs. *
* * @return String
* @return String */
*/
formatGQL: function(fields, description = {}, model = {}, type = 'field') { const formatGQL = (fields, description = {}, model = {}, type = 'field') => {
const typeFields = JSON.stringify(fields, null, 2).replace(/['",]+/g, ''); const typeFields = JSON.stringify(fields, null, 2).replace(/['",]+/g, '');
const lines = typeFields.split('\n'); 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');
}
// Try to add description for field.
if (type === 'field') {
return lines return lines
.map((line, index) => { .map(line => {
if ([0, lines.length - 1].includes(index)) { if (['{', '}'].includes(line)) {
return ''; 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; return line;
}) })
.join('\n'); .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('(');
* Retrieve description from variable and return a string which follow the GraphQL specs. const attribute = _.trim(split[0]);
* const info = _.get(description[attribute], 'description');
* @return String const deprecated = _.get(description[attribute], 'deprecated');
*/
getDescription: (type, model = {}) => { // Snakecase an attribute when we find a dash.
const format = '"""\n'; 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) { if (deprecated) {
return `${format}${str}\n${format}`; line = `${line} @deprecated(reason: "${deprecated}")`;
} }
return ''; return line;
}, })
.join('\n');
}
/** return lines
* Generate GraphQL schema. .map((line, index) => {
* if ([0, lines.length - 1].includes(index)) {
* @return Schema return '';
*/ }
generateSchema: function() { return line;
const shadowCRUDEnabled = })
strapi.plugins.graphql.config.shadowCRUD !== false; .join('\n');
};
// Generate type definition and query/mutation for models. /**
const shadowCRUD = shadowCRUDEnabled * Retrieve description from variable and return a string which follow the GraphQL specs.
? this.buildShadowCRUD() *
: createDefaultSchema(); * @return String
*/
const _schema = strapi.plugins.graphql.config._schema.graphql; const getDescription = (type, model = {}) => {
const format = '"""\n';
// Extract custom definition, query or resolver. const str = _.get(type, '_description') || _.get(model, 'info.description');
const { definition, query, mutation, resolver = {} } = _schema;
// Polymorphic. if (str) {
const polymorphicSchema = Types.addPolymorphicUnionType( return `${format}${str}\n${format}`;
definition + shadowCRUD.definition }
);
// Build resolvers. return '';
const resolvers = };
_.omitBy(
_.merge(shadowCRUD.resolvers, resolver, polymorphicSchema.resolvers),
_.isEmpty
) || {};
_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. // Generate type definition and query/mutation for models.
if (_.isEmpty(shadowCRUD.definition) && _.isEmpty(definition)) { const shadowCRUD = shadowCRUDEnabled
return {}; ? buildShadowCRUD()
} : createDefaultSchema();
const queryFields = this.formatGQL( const _schema = strapi.plugins.graphql.config._schema.graphql;
shadowCRUD.query,
resolver.Query,
null,
'query'
);
const mutationFields = this.formatGQL( // Extract custom definition, query or resolver.
shadowCRUD.mutation, const { definition, query, mutation, resolver = {} } = _schema;
resolver.Mutation,
null,
'mutation'
);
// Concatenate. // Polymorphic.
let typeDefs = ` 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} ${definition}
${shadowCRUD.definition} ${shadowCRUD.definition}
${polymorphicSchema.definition} ${polymorphicSchema.definition}
@ -198,93 +196,92 @@ const schemaBuilder = {
${Types.addCustomScalar(resolvers)} ${Types.addCustomScalar(resolvers)}
`; `;
// // Build schema. // // Build schema.
if (!strapi.config.currentEnvironment.server.production) { if (!strapi.config.currentEnvironment.server.production) {
// Write schema. // Write schema.
const schema = makeExecutableSchema({ const schema = makeExecutableSchema({
typeDefs, typeDefs,
resolvers, 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); return Object.keys(acc[type]).reduce((acc, resolverName) => {
typeDefs = Types.removeCustomScalar(typeDefs, resolvers); const resolverObj = acc[type][resolverName];
return { // Disabled this query.
typeDefs: gql(typeDefs), if (resolverObj === false) {
resolvers, 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 acc;
} }
return Object.keys(acc[type]).reduce((acc, resolverName) => { if (_.isFunction(resolverObj)) {
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;
}
}
return acc; 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 // TODO: implement
@ -501,19 +498,23 @@ const getActionDetails = resolver => {
*/ */
const isResolvablePath = path => _.isString(path) && !_.isEmpty(path); const isResolvablePath = path => _.isString(path) && !_.isEmpty(path);
const getPolicies = (config, { plugin, api } = {}) => { const getPolicies = config => {
const { resolver, policies = [], resolverOf } = config; const { resolver, policies = [], resolverOf } = config;
const { api, plugin } = config['_metadatas'] || {};
const policyFns = []; const policyFns = [];
const { controller, action } = isResolvablePath(resolverOf) const { controller, action, plugin: pathPlugin } = isResolvablePath(
resolverOf
)
? getActionDetails(resolverOf) ? getActionDetails(resolverOf)
: getActionDetails(resolver); : getActionDetails(resolver);
const globalPolicy = policyUtils.globalPolicy({ const globalPolicy = policyUtils.globalPolicy({
controller, controller,
action, action,
plugin, plugin: pathPlugin,
}); });
policyFns.push(globalPolicy); policyFns.push(globalPolicy);
@ -530,4 +531,9 @@ const getPolicies = (config, { plugin, api } = {}) => {
return policyFns; return policyFns;
}; };
module.exports = schemaBuilder; module.exports = {
generateSchema,
getDescription,
formatGQL,
buildQuery,
};