Disable subscriptions by default, change how to handle the type in wrapResolvers

This commit is contained in:
Convly 2021-11-10 11:18:35 +01:00
parent cf5e4078b5
commit 69d8b20f1b
6 changed files with 120 additions and 80 deletions

View File

@ -31,9 +31,9 @@ module.exports = async ({ strapi }) => {
return; return;
} }
const { config } = strapi.plugin('graphql'); const { config } = strapi.plugin('graphql').service('utils');
const path = config('endpoint', '/graphql'); const path = config.endpoint;
const defaultServerConfig = { const defaultServerConfig = {
// Schema // Schema
@ -46,7 +46,7 @@ module.exports = async ({ strapi }) => {
}), }),
// Validation // Validation
validationRules: [depthLimit(config('depthLimit'))], validationRules: [depthLimit(config.depthLimit)],
// Errors // Errors
formatError: formatGraphqlError, formatError: formatGraphqlError,
@ -63,10 +63,10 @@ module.exports = async ({ strapi }) => {
], ],
}; };
const serverConfig = merge(defaultServerConfig, config('apolloServer', {})); const serverConfig = merge(defaultServerConfig, config.apolloServer);
// Handle subscriptions // Handle subscriptions
if (config('subscriptions', true)) { if (config.subscriptions) {
const subscriptionServer = SubscriptionServer.create( const subscriptionServer = SubscriptionServer.create(
{ schema, execute, subscribe }, { schema, execute, subscribe },
{ server: strapi.server.httpServer, path } { server: strapi.server.httpServer, path }

View File

@ -95,7 +95,7 @@ module.exports = ({ strapi }) => {
* Apply basic transform to GQL args * Apply basic transform to GQL args
*/ */
transformArgs(args, { contentType, usePagination = false } = {}) { transformArgs(args, { contentType, usePagination = false } = {}) {
const { mappers } = getService('utils'); const { mappers, config } = getService('utils');
const { pagination = {}, filters = {} } = args; const { pagination = {}, filters = {} } = args;
// Init // Init
@ -103,8 +103,7 @@ module.exports = ({ strapi }) => {
// Pagination // Pagination
if (usePagination) { if (usePagination) {
const defaultLimit = strapi.plugin('graphql').config('defaultLimit'); const { defaultLimit, maxLimit } = config;
const maxLimit = strapi.plugin('graphql').config('maxLimit', -1);
Object.assign( Object.assign(
newArgs, newArgs,

View File

@ -5,6 +5,7 @@ const {
makeExecutableSchema, makeExecutableSchema,
addResolversToSchema, addResolversToSchema,
} = require('@graphql-tools/schema'); } = require('@graphql-tools/schema');
const { pruneSchema } = require('@graphql-tools/utils');
const { makeSchema } = require('nexus'); const { makeSchema } = require('nexus');
const { prop, startsWith } = require('lodash/fp'); const { prop, startsWith } = require('lodash/fp');
@ -26,7 +27,7 @@ const {
module.exports = ({ strapi }) => { module.exports = ({ strapi }) => {
const { service: getGraphQLService } = strapi.plugin('graphql'); const { service: getGraphQLService } = strapi.plugin('graphql');
const { config } = strapi.plugin('graphql'); const { config } = getGraphQLService('utils');
const { KINDS, GENERIC_MORPH_TYPENAME } = getGraphQLService('constants'); const { KINDS, GENERIC_MORPH_TYPENAME } = getGraphQLService('constants');
const extensionService = getGraphQLService('extension'); const extensionService = getGraphQLService('extension');
@ -37,7 +38,7 @@ module.exports = ({ strapi }) => {
let builders; let builders;
const buildSchema = () => { const buildSchema = () => {
const isShadowCRUDEnabled = !!config('shadowCRUD', true); const isShadowCRUDEnabled = !!config.shadowCRUD;
// Create a new empty type registry // Create a new empty type registry
registry = getGraphQLService('type-registry').new(); registry = getGraphQLService('type-registry').new();
@ -71,7 +72,11 @@ module.exports = ({ strapi }) => {
// Wrap resolvers if needed (auth, middlewares, policies...) as configured in the extension // Wrap resolvers if needed (auth, middlewares, policies...) as configured in the extension
const wrappedSchema = wrapResolvers({ schema, strapi, 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 }) => { const buildSchemas = ({ registry }) => {

View File

@ -25,100 +25,96 @@ const introspectionQueries = [
* @return {GraphQLSchema} * @return {GraphQLSchema}
*/ */
const wrapResolvers = ({ schema, strapi, extension = {} }) => { const wrapResolvers = ({ schema, strapi, extension = {} }) => {
const { config: graphQLConfig } = strapi.plugin('graphql').service('utils');
// Get all the registered resolvers configuration // Get all the registered resolvers configuration
const { resolversConfig = {} } = extension; const { resolversConfig = {} } = extension;
// Fields filters // Fields filters
const isValidFieldName = ([field]) => !field.startsWith('__'); const isValidFieldName = ([field]) => !field.startsWith('__');
const typesMaps = [schema.getTypeMap()]; const typeMap = schema.getTypeMap();
const subscriptionType = schema.getSubscriptionType(); if (!graphQLConfig.subscriptions) {
if (subscriptionType) { delete typeMap.Subscription;
typesMaps.push(subscriptionType.getFields());
} }
typesMaps.forEach(typeMap => Object.entries(typeMap).forEach(([type, definition]) => {
// Iterate over every field from every type within the const isGraphQLObjectType = definition instanceof GraphQLObjectType;
// schema's type map and wrap its resolve attribute if needed const isIgnoredType = introspectionQueries.includes(type);
Object.entries(typeMap).forEach(([type, definition]) => {
const isGraphQLObjectType = definition instanceof GraphQLObjectType;
const isIgnoredType = introspectionQueries.includes(type);
if (!isGraphQLObjectType || isIgnoredType) { if (!isGraphQLObjectType || isIgnoredType) {
return; return;
} }
const fields = definition.getFields(); const fields = definition.getFields();
const fieldsToProcess = Object.entries(fields).filter(isValidFieldName); const fieldsToProcess = Object.entries(fields).filter(isValidFieldName);
for (const [fieldName, fieldDefinition] of fieldsToProcess) { for (const [fieldName, fieldDefinition] of fieldsToProcess) {
const defaultResolver = get(fieldName); const defaultResolver = get(fieldName);
const path = `${type}.${fieldName}`; const path = `${type}.${fieldName}`;
const resolverConfig = getOr({}, path, resolversConfig); const resolverConfig = getOr({}, path, resolversConfig);
const { resolve: baseResolver = defaultResolver } = fieldDefinition; const { resolve: baseResolver = defaultResolver } = fieldDefinition;
// Parse & initialize the middlewares // Parse & initialize the middlewares
const middlewares = parseMiddlewares(resolverConfig, strapi); const middlewares = parseMiddlewares(resolverConfig, strapi);
// Generate the policy middleware // Generate the policy middleware
const policyMiddleware = createPoliciesMiddleware(resolverConfig, { strapi }); const policyMiddleware = createPoliciesMiddleware(resolverConfig, { strapi });
// Add the policyMiddleware at the end of the middlewares collection // Add the policyMiddleware at the end of the middlewares collection
middlewares.push(policyMiddleware); middlewares.push(policyMiddleware);
// Bind every middleware to the next one // Bind every middleware to the next one
const boundMiddlewares = middlewares.map((middleware, index, collection) => { const boundMiddlewares = middlewares.map((middleware, index, collection) => {
return (...args) => return (...args) =>
middleware( middleware(
// Make sure the last middleware in the list calls the baseResolver // Make sure the last middleware in the list calls the baseResolver
index >= collection.length - 1 ? baseResolver : boundMiddlewares[index + 1], index >= collection.length - 1 ? baseResolver : boundMiddlewares[index + 1],
...args ...args
); );
}); });
/** /**
* GraphQL authorization flow * GraphQL authorization flow
* @param {object} context * @param {object} context
* @return {Promise<void>} * @return {Promise<void>}
*/ */
const authorize = async ({ context }) => { const authorize = async ({ context }) => {
const authConfig = get('auth', resolverConfig); const authConfig = get('auth', resolverConfig);
const authContext = get('state.auth', context); const authContext = get('state.auth', context);
const isValidType = ['Mutation', 'Query', 'Subscription'].includes(type); const isValidType = ['Mutation', 'Query', 'Subscription'].includes(type);
const hasConfig = !isNil(authConfig); const hasConfig = !isNil(authConfig);
const isAuthDisabled = authConfig === false; const isAuthDisabled = authConfig === false;
if ((isValidType || hasConfig) && !isAuthDisabled) { if ((isValidType || hasConfig) && !isAuthDisabled) {
try { try {
await strapi.auth.verify(authContext, authConfig); await strapi.auth.verify(authContext, authConfig);
} catch (error) { } catch (error) {
throw new ForbiddenError(); throw new ForbiddenError();
}
} }
}; }
};
/** /**
* Base resolver wrapper that handles authorization, middlewares & policies * Base resolver wrapper that handles authorization, middlewares & policies
* @param {object} parent * @param {object} parent
* @param {object} args * @param {object} args
* @param {object} context * @param {object} context
* @param {object} info * @param {object} info
* @return {Promise<any>} * @return {Promise<any>}
*/ */
fieldDefinition.resolve = async (parent, args, context, info) => { fieldDefinition.resolve = async (parent, args, context, info) => {
await authorize({ context }); await authorize({ context });
// Execute middlewares (including the policy middleware which will always be included) // Execute middlewares (including the policy middleware which will always be included)
return first(boundMiddlewares).call(null, parent, args, context, info); return first(boundMiddlewares).call(null, parent, args, context, info);
}; };
} }
}) });
);
return schema; return schema;
}; };

View File

@ -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', {});
},
};
};

View File

@ -3,9 +3,11 @@
const mappers = require('./mappers'); const mappers = require('./mappers');
const attributes = require('./attributes'); const attributes = require('./attributes');
const naming = require('./naming'); const naming = require('./naming');
const config = require('./config');
module.exports = context => ({ module.exports = context => ({
naming: naming(context), naming: naming(context),
attributes: attributes(context), attributes: attributes(context),
mappers: mappers(context), mappers: mappers(context),
config: config(context),
}); });