diff --git a/.changeset/validate-options.md b/.changeset/validate-options.md new file mode 100644 index 00000000000..06cef98d532 --- /dev/null +++ b/.changeset/validate-options.md @@ -0,0 +1,12 @@ +--- +'@apollo/server': minor +--- + +Expose `graphql` validation options. + +``` +const server = new ApolloServer({ + typeDefs, + resolvers, + validateOptions: { maxErrors: 10 }, +}); diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 4a9c8d97c9f..a597900ab4f 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -14,6 +14,7 @@ import { assertValidSchema, print, printSchema, + type validate, type DocumentNode, type FormattedExecutionResult, type GraphQLFieldResolver, @@ -152,6 +153,8 @@ type ServerState = stopError: Error | null; }; +export type ValidateOptions = NonNullable[3]>; + export interface ApolloServerInternals { state: ServerState; gatewayExecutor: GatewayExecutor | null; @@ -167,6 +170,7 @@ export interface ApolloServerInternals { apolloConfig: ApolloConfig; plugins: ApolloServerPlugin[]; parseOptions: ParseOptions; + validationOptions: ValidateOptions; // `undefined` means we figure out what to do during _start (because // the default depends on whether or not we used the background version // of start). @@ -304,6 +308,7 @@ export class ApolloServer { hideSchemaDetailsFromClientErrors, dangerouslyDisableValidation: config.dangerouslyDisableValidation ?? false, + validationOptions: config.validationOptions ?? {}, fieldResolver: config.fieldResolver, includeStacktraceInErrorResponses: config.includeStacktraceInErrorResponses ?? diff --git a/packages/server/src/__tests__/ApolloServer.test.ts b/packages/server/src/__tests__/ApolloServer.test.ts index 460a7d5cc76..089e0c0a1c6 100644 --- a/packages/server/src/__tests__/ApolloServer.test.ts +++ b/packages/server/src/__tests__/ApolloServer.test.ts @@ -425,7 +425,9 @@ describe('ApolloServer start', () => { }); }); -function singleResult(body: GraphQLResponseBody): FormattedExecutionResult { +export function singleResult( + body: GraphQLResponseBody, +): FormattedExecutionResult { if (body.kind === 'single') { return body.singleResult; } diff --git a/packages/server/src/__tests__/runQuery.test.ts b/packages/server/src/__tests__/runQuery.test.ts index 854bddfc4e9..85ad6e4b8a5 100644 --- a/packages/server/src/__tests__/runQuery.test.ts +++ b/packages/server/src/__tests__/runQuery.test.ts @@ -25,6 +25,7 @@ import { } from '..'; import { mockLogger } from './mockLogger'; import { jest, describe, it, expect } from '@jest/globals'; +import { singleResult } from './ApolloServer.test'; async function runQuery( config: ApolloServerOptions, @@ -1192,4 +1193,52 @@ describe('parsing and validation cache', () => { expect(parsingDidStart.mock.calls.length).toBe(6); expect(validationDidStart.mock.calls.length).toBe(6); }); + + describe('validationMaxErrors option', () => { + it('should be 100 by default', async () => { + const server = new ApolloServer({ + schema, + }); + await server.start(); + + const vars = new Array(1000).fill('$a:a').join(','); + const query = `query aaa (${vars}) { a }`; + + const res = await server.executeOperation({ query }); + expect(res.http.status).toBe(400); + + const body = singleResult(res.body); + + // 100 by default plus one "Too many validation errors" error + // https://github.com/graphql/graphql-js/blob/main/src/validation/validate.ts#L46 + expect(body.errors).toHaveLength(101); + await server.stop(); + }); + + it('aborts the validation if max errors more than expected', async () => { + const server = new ApolloServer({ + schema, + validationOptions: { maxErrors: 1 }, + }); + await server.start(); + + const vars = new Array(1000).fill('$a:a').join(','); + const query = `query aaa (${vars}) { a }`; + + const res = await server.executeOperation({ query }); + expect(res.http.status).toBe(400); + + const body = singleResult(res.body); + + expect(body.errors).toHaveLength(2); + expect(body.errors?.[0]).toMatchObject({ + message: `There can be only one variable named "$a".`, + }); + expect(body.errors?.[1]).toMatchObject({ + message: `Too many validation errors, error limit reached. Validation aborted.`, + }); + + await server.stop(); + }); + }); }); diff --git a/packages/server/src/externalTypes/constructor.ts b/packages/server/src/externalTypes/constructor.ts index 6d6f354f655..794144d483c 100644 --- a/packages/server/src/externalTypes/constructor.ts +++ b/packages/server/src/externalTypes/constructor.ts @@ -20,6 +20,7 @@ import type { GatewayInterface } from '@apollo/server-gateway-interface'; import type { ApolloServerPlugin } from './plugins.js'; import type { BaseContext } from './index.js'; import type { GraphQLExperimentalIncrementalExecutionResults } from '../incrementalDeliveryPolyfill.js'; +import type { ValidateOptions } from '../ApolloServer.js'; export type DocumentStore = KeyValueCache; @@ -101,6 +102,7 @@ interface ApolloServerOptionsBase { nodeEnv?: string; documentStore?: DocumentStore | null; dangerouslyDisableValidation?: boolean; + validationOptions?: ValidateOptions; csrfPrevention?: CSRFPreventionOptions | boolean; // Used for parsing operations; unlike in AS3, this is not also used for diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index b9b4b5542dc..4ae15b6b1e1 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -247,6 +247,7 @@ export async function processGraphQLRequest( schemaDerivedData.schema, requestContext.document, [...specifiedRules, ...internals.validationRules], + internals.validationOptions, ); if (validationErrors.length === 0) {