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;
}
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 }

View File

@ -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,

View File

@ -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 }) => {

View File

@ -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<void>}
*/
const authorize = async ({ context }) => {
const authConfig = get('auth', resolverConfig);
const authContext = get('state.auth', context);
/**
* GraphQL authorization flow
* @param {object} context
* @return {Promise<void>}
*/
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<any>}
*/
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<any>}
*/
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;
};

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