diff --git a/packages/apollo-server-core/CHANGELOG.md b/packages/apollo-server-core/CHANGELOG.md index a188c3d0e47..7150838125e 100644 --- a/packages/apollo-server-core/CHANGELOG.md +++ b/packages/apollo-server-core/CHANGELOG.md @@ -2,6 +2,7 @@ ### vNEXT +* `apollo-server-core`: Added a function wrapper to `validationRules` for dynamic validation [PR#2232](https://github.com/apollographql/apollo-server/pull/2232) * `apollo-server-core`: Add persisted queries [PR#1149](https://github.com/apollographql/apollo-server/pull/1149) * `apollo-server-core`: added `BadUserInputError` * `apollo-server-core`: **breaking** gql is exported from gql-tag and ApolloServer requires a DocumentNode [PR#1146](https://github.com/apollographql/apollo-server/pull/1146) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 8d81e2f0794..53971e9f181 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -1,4 +1,5 @@ import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; +import { ValidationRule } from 'graphql/validation/ValidationContext'; import { Server as HttpServer } from 'http'; import { execute, @@ -9,6 +10,7 @@ import { GraphQLFieldResolver, ValidationContext, FieldDefinitionNode, + ASTVisitor, } from 'graphql'; import { GraphQLExtension } from 'graphql-extensions'; import { EngineReportingAgent } from 'apollo-engine-reporting'; @@ -25,6 +27,7 @@ import { formatApolloErrors } from 'apollo-server-errors'; import { GraphQLServerOptions as GraphQLOptions, PersistedQueryOptions, + ValidationRulesFunction, } from './graphqlOptions'; import { @@ -55,18 +58,19 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; -const NoIntrospection = (context: ValidationContext) => ({ - Field(node: FieldDefinitionNode) { - if (node.name.value === '__schema' || node.name.value === '__type') { - context.reportError( - new GraphQLError( - 'GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production', - [node], - ), - ); - } - }, -}); +const NoIntrospection: ValidationRule = (context: ValidationContext) => + ({ + Field(node: FieldDefinitionNode) { + if (node.name.value === '__schema' || node.name.value === '__type') { + context.reportError( + new GraphQLError( + 'GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production', + [node], + ), + ); + } + }, + } as ASTVisitor); function getEngineServiceId(engine: Config['engine']): string | undefined { const keyFromEnv = process.env.ENGINE_API_KEY || ''; @@ -154,10 +158,19 @@ export class ApolloServerBase { (typeof introspection === 'boolean' && !introspection) || (introspection === undefined && !isDev) ) { - const noIntro = [NoIntrospection]; - requestOptions.validationRules = requestOptions.validationRules - ? requestOptions.validationRules.concat(noIntro) - : noIntro; + let rules: ValidationRule[] | ValidationRulesFunction = [NoIntrospection]; + if (requestOptions.validationRules) { + if (typeof requestOptions.validationRules === 'function') { + rules = req => [ + ...(rules as ValidationRule[]), + ...(requestOptions.validationRules as ValidationRulesFunction)(req), + ]; + } else { + rules = [...rules, ...requestOptions.validationRules]; + } + } + + requestOptions.validationRules = rules; } if (requestOptions.cacheControl !== false) { diff --git a/packages/apollo-server-core/src/graphqlOptions.ts b/packages/apollo-server-core/src/graphqlOptions.ts index afed78ab98a..c3661dbfb02 100644 --- a/packages/apollo-server-core/src/graphqlOptions.ts +++ b/packages/apollo-server-core/src/graphqlOptions.ts @@ -1,14 +1,10 @@ -import { - GraphQLSchema, - ValidationContext, - GraphQLFieldResolver, - DocumentNode, -} from 'graphql'; +import { GraphQLSchema, GraphQLFieldResolver, DocumentNode } from 'graphql'; import { GraphQLExtension } from 'graphql-extensions'; import { CacheControlExtensionOptions } from 'apollo-cache-control'; import { KeyValueCache } from 'apollo-server-caching'; import { DataSource } from 'apollo-datasource'; -import { ApolloServerPlugin } from 'apollo-server-plugin-base'; +import { ApolloServerPlugin, GraphQLRequest } from 'apollo-server-plugin-base'; +import { ValidationRule } from 'graphql/validation/ValidationContext'; /* * GraphQLServerOptions @@ -24,6 +20,10 @@ import { ApolloServerPlugin } from 'apollo-server-plugin-base'; * - (optional) extensions: an array of functions which create GraphQLExtensions (each GraphQLExtension object is used for one request) * */ +export type ValidationRulesFunction = ( + request: GraphQLRequest, +) => ValidationRule[]; + export interface GraphQLServerOptions< TContext = Record, TRootValue = any @@ -32,7 +32,7 @@ export interface GraphQLServerOptions< formatError?: Function; rootValue?: ((parsedQuery: DocumentNode) => TRootValue) | TRootValue; context?: TContext | (() => never); - validationRules?: Array<(context: ValidationContext) => any>; + validationRules?: ValidationRule[] | ValidationRulesFunction; formatResponse?: Function; fieldResolver?: GraphQLFieldResolver; debug?: boolean; diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 03ca7128715..a09350060c7 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -63,7 +63,9 @@ export interface GraphQLRequestPipelineConfig { schema: GraphQLSchema; rootValue?: ((document: DocumentNode) => any) | any; - validationRules?: ValidationRule[]; + validationRules?: + | ValidationRule[] + | ((request: GraphQLRequest) => ValidationRule[]); fieldResolver?: GraphQLFieldResolver; dataSources?: () => DataSources; @@ -190,7 +192,7 @@ export async function processGraphQLRequest( requestContext as WithRequired, ); - const validationErrors = validate(document); + const validationErrors = validate(document, request); if (validationErrors.length === 0) { validationDidEnd(); @@ -278,10 +280,18 @@ export async function processGraphQLRequest( } } - function validate(document: DocumentNode): ReadonlyArray { + function validate( + document: DocumentNode, + request: GraphQLRequest, + ): ReadonlyArray { let rules = specifiedRules; if (config.validationRules) { - rules = rules.concat(config.validationRules); + rules = [ + ...rules, + ...(typeof config.validationRules === 'function' + ? config.validationRules(request) + : config.validationRules), + ]; } const validationDidEnd = extensionStack.validationDidStart();