From 69d8b20f1b8d550661fb1be7e40db209aaed1f9d Mon Sep 17 00:00:00 2001 From: Convly Date: Wed, 10 Nov 2021 11:18:35 +0100 Subject: [PATCH] Disable subscriptions by default, change how to handle the type in wrapResolvers --- packages/plugins/graphql/server/bootstrap.js | 10 +- .../graphql/server/services/builders/utils.js | 5 +- .../server/services/content-api/index.js | 11 +- .../services/content-api/wrap-resolvers.js | 134 +++++++++--------- .../graphql/server/services/utils/config.js | 38 +++++ .../graphql/server/services/utils/index.js | 2 + 6 files changed, 120 insertions(+), 80 deletions(-) create mode 100644 packages/plugins/graphql/server/services/utils/config.js diff --git a/packages/plugins/graphql/server/bootstrap.js b/packages/plugins/graphql/server/bootstrap.js index 67699afa6d..f93fdd917e 100644 --- a/packages/plugins/graphql/server/bootstrap.js +++ b/packages/plugins/graphql/server/bootstrap.js @@ -31,9 +31,9 @@ module.exports = async ({ strapi }) => { return; } - const { config } = strapi.plugin('graphql'); + const { config } = strapi.plugin('graphql').service('utils'); - const path = config('endpoint', '/graphql'); + const path = config.endpoint; const defaultServerConfig = { // Schema @@ -46,7 +46,7 @@ module.exports = async ({ strapi }) => { }), // Validation - validationRules: [depthLimit(config('depthLimit'))], + validationRules: [depthLimit(config.depthLimit)], // Errors formatError: formatGraphqlError, @@ -63,10 +63,10 @@ module.exports = async ({ strapi }) => { ], }; - const serverConfig = merge(defaultServerConfig, config('apolloServer', {})); + const serverConfig = merge(defaultServerConfig, config.apolloServer); // Handle subscriptions - if (config('subscriptions', true)) { + if (config.subscriptions) { const subscriptionServer = SubscriptionServer.create( { schema, execute, subscribe }, { server: strapi.server.httpServer, path } diff --git a/packages/plugins/graphql/server/services/builders/utils.js b/packages/plugins/graphql/server/services/builders/utils.js index 7f5c43930e..6a584e7697 100644 --- a/packages/plugins/graphql/server/services/builders/utils.js +++ b/packages/plugins/graphql/server/services/builders/utils.js @@ -95,7 +95,7 @@ module.exports = ({ strapi }) => { * Apply basic transform to GQL args */ transformArgs(args, { contentType, usePagination = false } = {}) { - const { mappers } = getService('utils'); + const { mappers, config } = getService('utils'); const { pagination = {}, filters = {} } = args; // Init @@ -103,8 +103,7 @@ module.exports = ({ strapi }) => { // Pagination if (usePagination) { - const defaultLimit = strapi.plugin('graphql').config('defaultLimit'); - const maxLimit = strapi.plugin('graphql').config('maxLimit', -1); + const { defaultLimit, maxLimit } = config; Object.assign( newArgs, diff --git a/packages/plugins/graphql/server/services/content-api/index.js b/packages/plugins/graphql/server/services/content-api/index.js index ad486cb87d..db25d0d256 100644 --- a/packages/plugins/graphql/server/services/content-api/index.js +++ b/packages/plugins/graphql/server/services/content-api/index.js @@ -5,6 +5,7 @@ const { makeExecutableSchema, addResolversToSchema, } = require('@graphql-tools/schema'); +const { pruneSchema } = require('@graphql-tools/utils'); const { makeSchema } = require('nexus'); const { prop, startsWith } = require('lodash/fp'); @@ -26,7 +27,7 @@ const { module.exports = ({ strapi }) => { const { service: getGraphQLService } = strapi.plugin('graphql'); - const { config } = strapi.plugin('graphql'); + const { config } = getGraphQLService('utils'); const { KINDS, GENERIC_MORPH_TYPENAME } = getGraphQLService('constants'); const extensionService = getGraphQLService('extension'); @@ -37,7 +38,7 @@ module.exports = ({ strapi }) => { let builders; const buildSchema = () => { - const isShadowCRUDEnabled = !!config('shadowCRUD', true); + const isShadowCRUDEnabled = !!config.shadowCRUD; // Create a new empty type registry registry = getGraphQLService('type-registry').new(); @@ -71,7 +72,11 @@ module.exports = ({ strapi }) => { // Wrap resolvers if needed (auth, middlewares, policies...) as configured in the extension const wrappedSchema = wrapResolvers({ schema, strapi, extension }); - return wrappedSchema; + // Prune schema, remove unused types + // eg: removes registered subscriptions if they're disabled in the config) + const prunedSchema = pruneSchema(wrappedSchema); + + return prunedSchema; }; const buildSchemas = ({ registry }) => { diff --git a/packages/plugins/graphql/server/services/content-api/wrap-resolvers.js b/packages/plugins/graphql/server/services/content-api/wrap-resolvers.js index b7405d16fb..8efb1fb968 100644 --- a/packages/plugins/graphql/server/services/content-api/wrap-resolvers.js +++ b/packages/plugins/graphql/server/services/content-api/wrap-resolvers.js @@ -25,100 +25,96 @@ const introspectionQueries = [ * @return {GraphQLSchema} */ const wrapResolvers = ({ schema, strapi, extension = {} }) => { + const { config: graphQLConfig } = strapi.plugin('graphql').service('utils'); // Get all the registered resolvers configuration const { resolversConfig = {} } = extension; // Fields filters const isValidFieldName = ([field]) => !field.startsWith('__'); - const typesMaps = [schema.getTypeMap()]; + const typeMap = schema.getTypeMap(); - const subscriptionType = schema.getSubscriptionType(); - if (subscriptionType) { - typesMaps.push(subscriptionType.getFields()); + if (!graphQLConfig.subscriptions) { + delete typeMap.Subscription; } - typesMaps.forEach(typeMap => - // Iterate over every field from every type within the - // schema's type map and wrap its resolve attribute if needed - Object.entries(typeMap).forEach(([type, definition]) => { - const isGraphQLObjectType = definition instanceof GraphQLObjectType; - const isIgnoredType = introspectionQueries.includes(type); + Object.entries(typeMap).forEach(([type, definition]) => { + const isGraphQLObjectType = definition instanceof GraphQLObjectType; + const isIgnoredType = introspectionQueries.includes(type); - if (!isGraphQLObjectType || isIgnoredType) { - return; - } + if (!isGraphQLObjectType || isIgnoredType) { + return; + } - const fields = definition.getFields(); - const fieldsToProcess = Object.entries(fields).filter(isValidFieldName); + const fields = definition.getFields(); + const fieldsToProcess = Object.entries(fields).filter(isValidFieldName); - for (const [fieldName, fieldDefinition] of fieldsToProcess) { - const defaultResolver = get(fieldName); + for (const [fieldName, fieldDefinition] of fieldsToProcess) { + const defaultResolver = get(fieldName); - const path = `${type}.${fieldName}`; - const resolverConfig = getOr({}, path, resolversConfig); + const path = `${type}.${fieldName}`; + const resolverConfig = getOr({}, path, resolversConfig); - const { resolve: baseResolver = defaultResolver } = fieldDefinition; + const { resolve: baseResolver = defaultResolver } = fieldDefinition; - // Parse & initialize the middlewares - const middlewares = parseMiddlewares(resolverConfig, strapi); + // Parse & initialize the middlewares + const middlewares = parseMiddlewares(resolverConfig, strapi); - // Generate the policy middleware - const policyMiddleware = createPoliciesMiddleware(resolverConfig, { strapi }); + // Generate the policy middleware + const policyMiddleware = createPoliciesMiddleware(resolverConfig, { strapi }); - // Add the policyMiddleware at the end of the middlewares collection - middlewares.push(policyMiddleware); + // Add the policyMiddleware at the end of the middlewares collection + middlewares.push(policyMiddleware); - // Bind every middleware to the next one - const boundMiddlewares = middlewares.map((middleware, index, collection) => { - return (...args) => - middleware( - // Make sure the last middleware in the list calls the baseResolver - index >= collection.length - 1 ? baseResolver : boundMiddlewares[index + 1], - ...args - ); - }); + // Bind every middleware to the next one + const boundMiddlewares = middlewares.map((middleware, index, collection) => { + return (...args) => + middleware( + // Make sure the last middleware in the list calls the baseResolver + index >= collection.length - 1 ? baseResolver : boundMiddlewares[index + 1], + ...args + ); + }); - /** - * GraphQL authorization flow - * @param {object} context - * @return {Promise} - */ - const authorize = async ({ context }) => { - const authConfig = get('auth', resolverConfig); - const authContext = get('state.auth', context); + /** + * GraphQL authorization flow + * @param {object} context + * @return {Promise} + */ + const authorize = async ({ context }) => { + const authConfig = get('auth', resolverConfig); + const authContext = get('state.auth', context); - const isValidType = ['Mutation', 'Query', 'Subscription'].includes(type); - const hasConfig = !isNil(authConfig); + const isValidType = ['Mutation', 'Query', 'Subscription'].includes(type); + const hasConfig = !isNil(authConfig); - const isAuthDisabled = authConfig === false; + const isAuthDisabled = authConfig === false; - if ((isValidType || hasConfig) && !isAuthDisabled) { - try { - await strapi.auth.verify(authContext, authConfig); - } catch (error) { - throw new ForbiddenError(); - } + if ((isValidType || hasConfig) && !isAuthDisabled) { + try { + await strapi.auth.verify(authContext, authConfig); + } catch (error) { + throw new ForbiddenError(); } - }; + } + }; - /** - * Base resolver wrapper that handles authorization, middlewares & policies - * @param {object} parent - * @param {object} args - * @param {object} context - * @param {object} info - * @return {Promise} - */ - fieldDefinition.resolve = async (parent, args, context, info) => { - await authorize({ context }); + /** + * Base resolver wrapper that handles authorization, middlewares & policies + * @param {object} parent + * @param {object} args + * @param {object} context + * @param {object} info + * @return {Promise} + */ + fieldDefinition.resolve = async (parent, args, context, info) => { + await authorize({ context }); - // Execute middlewares (including the policy middleware which will always be included) - return first(boundMiddlewares).call(null, parent, args, context, info); - }; - } - }) - ); + // Execute middlewares (including the policy middleware which will always be included) + return first(boundMiddlewares).call(null, parent, args, context, info); + }; + } + }); return schema; }; diff --git a/packages/plugins/graphql/server/services/utils/config.js b/packages/plugins/graphql/server/services/utils/config.js new file mode 100644 index 0000000000..2fb1b66547 --- /dev/null +++ b/packages/plugins/graphql/server/services/utils/config.js @@ -0,0 +1,38 @@ +'use strict'; + +/** + * GraphQL config helper with consistent defaults values + */ +module.exports = ({ strapi }) => { + const { config: graphQLConfig } = strapi.plugin('graphql'); + + return { + get shadowCRUD() { + return graphQLConfig('shadowCRUD', true); + }, + + get subscriptions() { + return graphQLConfig('subscriptions', false); + }, + + get endpoint() { + return graphQLConfig('endpoint', '/graphql'); + }, + + get defaultLimit() { + return graphQLConfig('defaultLimit'); + }, + + get maxLimit() { + return graphQLConfig('maxLimit', -1); + }, + + get depthLimit() { + return graphQLConfig('depthLimit'); + }, + + get apolloServer() { + return graphQLConfig('apolloServer', {}); + }, + }; +}; diff --git a/packages/plugins/graphql/server/services/utils/index.js b/packages/plugins/graphql/server/services/utils/index.js index db4bbeda9b..795b3153d3 100644 --- a/packages/plugins/graphql/server/services/utils/index.js +++ b/packages/plugins/graphql/server/services/utils/index.js @@ -3,9 +3,11 @@ const mappers = require('./mappers'); const attributes = require('./attributes'); const naming = require('./naming'); +const config = require('./config'); module.exports = context => ({ naming: naming(context), attributes: attributes(context), mappers: mappers(context), + config: config(context), });