From 46ac468f5cfeee8bd333a5493e02833e884c51d3 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:33:45 +0000 Subject: [PATCH 01/18] Implement onError proposal --- src/error/ErrorBehavior.ts | 9 +++++ src/error/index.ts | 1 + src/execution/__tests__/executor-test.ts | 2 ++ src/execution/execute.ts | 45 ++++++++++++++++++++++-- src/graphql.ts | 13 +++++++ src/index.ts | 1 + src/type/definition.ts | 3 ++ 7 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/error/ErrorBehavior.ts diff --git a/src/error/ErrorBehavior.ts b/src/error/ErrorBehavior.ts new file mode 100644 index 0000000000..665f241905 --- /dev/null +++ b/src/error/ErrorBehavior.ts @@ -0,0 +1,9 @@ +export type GraphQLErrorBehavior = 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; + +export function isErrorBehavior( + onError: unknown, +): onError is GraphQLErrorBehavior { + return ( + onError === 'PROPAGATE' || onError === 'NO_PROPAGATE' || onError === 'ABORT' + ); +} diff --git a/src/error/index.ts b/src/error/index.ts index 7e5d267f50..b9da3e897e 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -9,3 +9,4 @@ export type { export { syntaxError } from './syntaxError'; export { locatedError } from './locatedError'; +export type { GraphQLErrorBehavior } from './ErrorBehavior'; diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 0f0c5b2861..60c70b3ba5 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -264,6 +264,7 @@ describe('Execute: Handles basic execution tasks', () => { 'rootValue', 'operation', 'variableValues', + 'errorBehavior', ); const operation = document.definitions[0]; @@ -276,6 +277,7 @@ describe('Execute: Handles basic execution tasks', () => { schema, rootValue, operation, + errorBehavior: 'PROPAGATE', }); const field = operation.selectionSet.selections[0]; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 5cd64d40f9..fdf1616f09 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -13,6 +13,8 @@ import { promiseForObject } from '../jsutils/promiseForObject'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import { promiseReduce } from '../jsutils/promiseReduce'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import { isErrorBehavior } from '../error/ErrorBehavior'; import type { GraphQLFormattedError } from '../error/GraphQLError'; import { GraphQLError } from '../error/GraphQLError'; import { locatedError } from '../error/locatedError'; @@ -115,6 +117,7 @@ export interface ExecutionContext { typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; errors: Array; + errorBehavior: GraphQLErrorBehavior; } /** @@ -130,6 +133,7 @@ export interface ExecutionResult< > { errors?: ReadonlyArray; data?: TData | null; + onError?: GraphQLErrorBehavior; extensions?: TExtensions; } @@ -152,6 +156,15 @@ export interface ExecutionArgs { fieldResolver?: Maybe>; typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + /** + * Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to + * abort a request when any error occurs. + * + * Default: PROPAGATE + * + * @experimental + */ + onError?: GraphQLErrorBehavior; /** Additional execution options. */ options?: { /** Set the maximum number of errors allowed for coercing (defaults to 50). */ @@ -291,9 +304,18 @@ export function buildExecutionContext( fieldResolver, typeResolver, subscribeFieldResolver, + onError, options, } = args; + if (onError != null && !isErrorBehavior(onError)) { + return [ + new GraphQLError( + 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + ), + ]; + } + let operation: OperationDefinitionNode | undefined; const fragments: ObjMap = Object.create(null); for (const definition of document.definitions) { @@ -353,6 +375,7 @@ export function buildExecutionContext( typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], + errorBehavior: onError ?? 'PROPAGATE', }; } @@ -591,6 +614,7 @@ export function buildResolveInfo( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, + errorBehavior: exeContext.errorBehavior, }; } @@ -599,10 +623,25 @@ function handleFieldError( returnType: GraphQLOutputType, exeContext: ExecutionContext, ): null { - // If the field type is non-nullable, then it is resolved without any - // protection from errors, however it still properly locates the error. - if (isNonNullType(returnType)) { + if (exeContext.errorBehavior === 'PROPAGATE') { + // If the field type is non-nullable, then it is resolved without any + // protection from errors, however it still properly locates the error. + // Note: semantic non-null types are treated as nullable for the purposes + // of error handling. + if (isNonNullType(returnType)) { + throw error; + } + } else if (exeContext.errorBehavior === 'ABORT') { + // In this mode, any error aborts the request throw error; + } else if (exeContext.errorBehavior === 'NO_PROPAGATE') { + // In this mode, the client takes responsibility for error handling, so we + // treat the field as if it were nullable. + } else { + invariant( + false, + 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), + ); } // Otherwise, error protection is applied, logging the error and resolving diff --git a/src/graphql.ts b/src/graphql.ts index bc6fb9bb72..7edd260b83 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -3,6 +3,8 @@ import { isPromise } from './jsutils/isPromise'; import type { Maybe } from './jsutils/Maybe'; import type { PromiseOrValue } from './jsutils/PromiseOrValue'; +import type { GraphQLErrorBehavior } from './error/ErrorBehavior'; + import { parse } from './language/parser'; import type { Source } from './language/source'; @@ -66,6 +68,15 @@ export interface GraphQLArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + /** + * Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to + * abort a request when any error occurs. + * + * Default: PROPAGATE + * + * @experimental + */ + onError?: GraphQLErrorBehavior; } export function graphql(args: GraphQLArgs): Promise { @@ -106,6 +117,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + onError, } = args; // Validate Schema @@ -138,5 +150,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + onError, }); } diff --git a/src/index.ts b/src/index.ts index 73c713a203..4df70d7d74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -395,6 +395,7 @@ export { } from './error/index'; export type { + GraphQLErrorBehavior, GraphQLErrorOptions, GraphQLFormattedError, GraphQLErrorExtensions, diff --git a/src/type/definition.ts b/src/type/definition.ts index 7eaac560dc..61c57c4f38 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -14,6 +14,7 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import { suggestionList } from '../jsutils/suggestionList'; import { toObjMap } from '../jsutils/toObjMap'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; import { GraphQLError } from '../error/GraphQLError'; import type { @@ -988,6 +989,8 @@ export interface GraphQLResolveInfo { readonly rootValue: unknown; readonly operation: OperationDefinitionNode; readonly variableValues: { [variable: string]: unknown }; + /** @experimental */ + readonly errorBehavior: GraphQLErrorBehavior; } /** From 0d89dd136da6b0efacce3dc9b9cc50e2e69bafad Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:52:34 +0000 Subject: [PATCH 02/18] Add tests --- src/execution/__tests__/executor-test.ts | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 60c70b3ba5..01c1a2d0b8 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -288,6 +288,70 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('reflects onError:NO_PROPAGATE via errorBehavior', () => { + let resolvedInfo; + const testType = new GraphQLObjectType({ + name: 'Test', + fields: { + test: { + type: GraphQLString, + resolve(_val, _args, _ctx, info) { + resolvedInfo = info; + }, + }, + }, + }); + const schema = new GraphQLSchema({ query: testType }); + + const document = parse('query ($var: String) { result: test }'); + const rootValue = { root: 'val' }; + const variableValues = { var: 'abc' }; + + executeSync({ + schema, + document, + rootValue, + variableValues, + onError: 'NO_PROPAGATE', + }); + + expect(resolvedInfo).to.include({ + errorBehavior: 'NO_PROPAGATE', + }); + }); + + it('reflects onError:ABORT via errorBehavior', () => { + let resolvedInfo; + const testType = new GraphQLObjectType({ + name: 'Test', + fields: { + test: { + type: GraphQLString, + resolve(_val, _args, _ctx, info) { + resolvedInfo = info; + }, + }, + }, + }); + const schema = new GraphQLSchema({ query: testType }); + + const document = parse('query ($var: String) { result: test }'); + const rootValue = { root: 'val' }; + const variableValues = { var: 'abc' }; + + executeSync({ + schema, + document, + rootValue, + variableValues, + onError: 'ABORT', + }); + + expect(resolvedInfo).to.include({ + errorBehavior: 'ABORT', + }); + }); + it('populates path correctly with complex types', () => { let path; const someObject = new GraphQLObjectType({ @@ -742,6 +806,134 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('Full response path is included for non-nullable fields with onError:NO_PROPAGATE', () => { + const A: GraphQLObjectType = new GraphQLObjectType({ + name: 'A', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + nonNullA: { + type: new GraphQLNonNull(A), + resolve: () => ({}), + }, + throws: { + type: new GraphQLNonNull(GraphQLString), + resolve: () => { + throw new Error('Catch me if you can'); + }, + }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'query', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + }), + }), + }); + + const document = parse(` + query { + nullableA { + aliasedA: nullableA { + nonNullA { + anotherA: nonNullA { + throws + } + } + } + } + } + `); + + const result = executeSync({ schema, document, onError: 'NO_PROPAGATE' }); + expectJSON(result).toDeepEqual({ + data: { + nullableA: { + aliasedA: { + nonNullA: { + anotherA: { + throws: null, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Catch me if you can', + locations: [{ line: 7, column: 17 }], + path: ['nullableA', 'aliasedA', 'nonNullA', 'anotherA', 'throws'], + }, + ], + }); + }); + + it('Full response path is included for non-nullable fields with onError:ABORT', () => { + const A: GraphQLObjectType = new GraphQLObjectType({ + name: 'A', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + nonNullA: { + type: new GraphQLNonNull(A), + resolve: () => ({}), + }, + throws: { + type: new GraphQLNonNull(GraphQLString), + resolve: () => { + throw new Error('Catch me if you can'); + }, + }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'query', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + }), + }), + }); + + const document = parse(` + query { + nullableA { + aliasedA: nullableA { + nonNullA { + anotherA: nonNullA { + throws + } + } + } + } + } + `); + + const result = executeSync({ schema, document, onError: 'ABORT' }); + expectJSON(result).toDeepEqual({ + data: null, + errors: [ + { + message: 'Catch me if you can', + locations: [{ line: 7, column: 17 }], + path: ['nullableA', 'aliasedA', 'nonNullA', 'anotherA', 'throws'], + }, + ], + }); + }); + it('uses the inline operation if no operation name is provided', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ From 3381f98a000d9f84d4109ebc31ead5ca89a1c7f7 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:58:07 +0000 Subject: [PATCH 03/18] Test invalid onError is handled --- src/execution/__tests__/executor-test.ts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 01c1a2d0b8..bde56dbc38 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -934,6 +934,36 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('raises request error with invalid onError', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'query', + fields: () => ({ + a: { + type: GraphQLInt, + resolve: () => ({}), + }, + }), + }), + }); + + const document = parse('{ a }'); + const result = executeSync({ + schema, + document, + // @ts-expect-error + onError: 'DANCE', + }); + expectJSON(result).toDeepEqual({ + errors: [ + { + message: + 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + }, + ], + }); + }); + it('uses the inline operation if no operation name is provided', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ From 6bb45cf8f7efb1bee05a0cf512e695541a0f0d76 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:02:32 +0000 Subject: [PATCH 04/18] Ignore invariant from code coverage --- src/execution/execute.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index fdf1616f09..057747e667 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -638,6 +638,7 @@ function handleFieldError( // In this mode, the client takes responsibility for error handling, so we // treat the field as if it were nullable. } else { + /* c8 ignore next 4 */ invariant( false, 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), From cdb365e6f48f97b62a0eba8c9e2a967c76b592cd Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:03:44 +0000 Subject: [PATCH 05/18] Finickity --- src/execution/execute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 057747e667..60044a4420 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -638,7 +638,7 @@ function handleFieldError( // In this mode, the client takes responsibility for error handling, so we // treat the field as if it were nullable. } else { - /* c8 ignore next 4 */ + /* c8 ignore next 5 */ invariant( false, 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), From 0ff06c344ec445b9330fc6ac55046b65f9716ccb Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:06:20 +0000 Subject: [PATCH 06/18] Urghhhhhh --- src/execution/execute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 60044a4420..7026b207f2 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -637,8 +637,8 @@ function handleFieldError( } else if (exeContext.errorBehavior === 'NO_PROPAGATE') { // In this mode, the client takes responsibility for error handling, so we // treat the field as if it were nullable. + /* c8 ignore next 6 */ } else { - /* c8 ignore next 5 */ invariant( false, 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), From 34cb2cf8836b1904282028d72e9ef56e2ebc6ef0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:09:25 +0000 Subject: [PATCH 07/18] Remove unnecessary resolver causing coverage issue --- src/execution/__tests__/executor-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index bde56dbc38..f0ace0c136 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -941,7 +941,6 @@ describe('Execute: Handles basic execution tasks', () => { fields: () => ({ a: { type: GraphQLInt, - resolve: () => ({}), }, }), }), From a5474f455250524709b19cf1e48e2193a9d750f7 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 12:51:01 +0100 Subject: [PATCH 08/18] Reorder so NO_PROPAGATE is first --- src/error/ErrorBehavior.ts | 4 ++-- src/execution/__tests__/executor-test.ts | 2 +- src/execution/execute.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/error/ErrorBehavior.ts b/src/error/ErrorBehavior.ts index 665f241905..44495318ca 100644 --- a/src/error/ErrorBehavior.ts +++ b/src/error/ErrorBehavior.ts @@ -1,9 +1,9 @@ -export type GraphQLErrorBehavior = 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; +export type GraphQLErrorBehavior = 'NO_PROPAGATE' | 'PROPAGATE' | 'ABORT'; export function isErrorBehavior( onError: unknown, ): onError is GraphQLErrorBehavior { return ( - onError === 'PROPAGATE' || onError === 'NO_PROPAGATE' || onError === 'ABORT' + onError === 'NO_PROPAGATE' || onError === 'PROPAGATE' || onError === 'ABORT' ); } diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index f0ace0c136..86b4be0d9d 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -957,7 +957,7 @@ describe('Execute: Handles basic execution tasks', () => { errors: [ { message: - 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + 'Unsupported `onError` value; supported values are `NO_PROPAGATE`, `PROPAGATE` and `ABORT`.', }, ], }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 7026b207f2..89ee8ec461 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -311,7 +311,7 @@ export function buildExecutionContext( if (onError != null && !isErrorBehavior(onError)) { return [ new GraphQLError( - 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + 'Unsupported `onError` value; supported values are `NO_PROPAGATE`, `PROPAGATE` and `ABORT`.', ), ]; } From 252abf177386feaa17636c680e0f6e72d32e3bc4 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 13:00:42 +0100 Subject: [PATCH 09/18] Allow the schema to set a default error behavior --- src/execution/execute.ts | 2 +- src/type/schema.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 89ee8ec461..9ec4509f1a 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -375,7 +375,7 @@ export function buildExecutionContext( typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], - errorBehavior: onError ?? 'PROPAGATE', + errorBehavior: onError ?? schema.defaultErrorBehavior, }; } diff --git a/src/type/schema.ts b/src/type/schema.ts index 97c2782145..3681e66578 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -31,6 +31,8 @@ import { import type { GraphQLDirective } from './directives'; import { isDirective, specifiedDirectives } from './directives'; import { __Schema } from './introspection'; +import type { GraphQLErrorBehavior } from '../error'; +import { isErrorBehavior } from '../error/ErrorBehavior'; /** * Test if the given value is a GraphQL schema. @@ -129,6 +131,8 @@ export interface GraphQLSchemaExtensions { */ export class GraphQLSchema { description: Maybe; + /** @experimental */ + readonly defaultErrorBehavior: GraphQLErrorBehavior; extensions: Readonly; astNode: Maybe; extensionASTNodes: ReadonlyArray; @@ -163,8 +167,15 @@ export class GraphQLSchema { '"directives" must be Array if provided but got: ' + `${inspect(config.directives)}.`, ); + devAssert( + !config.defaultErrorBehavior || + isErrorBehavior(config.defaultErrorBehavior), + '"defaultErrorBehavior" must be one of "NO_PROPAGATE", "PROPAGATE" or "ABORT", but got: ' + + `${inspect(config.defaultErrorBehavior)}.`, + ); this.description = config.description; + this.defaultErrorBehavior = config.defaultErrorBehavior ?? 'PROPAGATE'; this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -386,6 +397,20 @@ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { subscription?: Maybe; types?: Maybe>; directives?: Maybe>; + /** + * Experimental. Defines the default GraphQL error behavior when the + * GraphQLArgs does not include an `onError` property. + * + * Set to NO_PROPAGATE if your schema only needs to support modern + * "error-handling" clients. + * + * It is not recommended to set this to ABORT. + * + * Default: PROPAGATE + * + * @experimental + */ + defaultErrorBehavior?: GraphQLErrorBehavior; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; From f945573b91228f070162c8d595aac21400d824d8 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 13:07:11 +0100 Subject: [PATCH 10/18] Started work on introspection --- src/type/__tests__/introspection-test.ts | 58 ++++++++++++++++++++++++ src/utilities/getIntrospectionQuery.ts | 11 +++++ 2 files changed, 69 insertions(+) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 8c5cacba0d..d7bd11cf95 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -26,6 +26,7 @@ describe('Introspection', () => { descriptions: false, specifiedByUrl: true, directiveIsRepeatable: true, + errorBehavior: true, }); const result = graphqlSync({ schema, source }); @@ -35,6 +36,7 @@ describe('Introspection', () => { queryType: { name: 'SomeObject', kind: 'OBJECT' }, mutationType: null, subscriptionType: null, + defaultErrorBehavior: 'PROPAGATE', types: [ { kind: 'OBJECT', @@ -1754,4 +1756,60 @@ describe('Introspection', () => { }); expect(result).to.not.have.property('errors'); }); + + it('reflects the default error behavior (default)', () => { + const schema = buildSchema(` + type SomeObject { + someField: String + } + + schema { + query: SomeObject + } + `); + + const source = getIntrospectionQuery({ + descriptions: false, + specifiedByUrl: true, + directiveIsRepeatable: true, + }); + + const result = graphqlSync({ schema, source }); + expect(result).to.deep.equal({ + data: { + __schema: { + defaultErrorBehavior: 'PROPAGATE', + }, + }, + }); + }); + + it('reflects the default error behavior (NO_PROPAGATE)', () => { + const schema = buildSchema(` + type SomeObject { + someField: String + } + + schema @behavior(onError: NO_PROPAGATE) { + query: SomeObject + } + `); + + const source = /* GraphQL */ ` + { + __schema { + defaultErrorBehavior + } + } + `; + + const result = graphqlSync({ schema, source }); + expect(result).to.deep.equal({ + data: { + __schema: { + defaultErrorBehavior: 'NO_PROPAGATE', + }, + }, + }); + }); }); diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 373b474ed5..0bf9692e10 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -38,6 +38,12 @@ export interface IntrospectionOptions { * Default: false */ oneOf?: boolean; + + /** + * Whether target GraphQL server supports changing error behaviors. + * Default: false + */ + errorBehavior?: boolean; } /** @@ -52,6 +58,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { schemaDescription: false, inputValueDeprecation: false, oneOf: false, + errorBehavior: false, ...options, }; @@ -65,6 +72,9 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { const schemaDescription = optionsWithDefault.schemaDescription ? descriptions : ''; + const defaultErrorBehavior = optionsWithDefault.errorBehavior + ? 'defaultErrorBehavior' + : ''; function inputDeprecation(str: string) { return optionsWithDefault.inputValueDeprecation ? str : ''; @@ -78,6 +88,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { queryType { name kind } mutationType { name kind } subscriptionType { name kind } + ${defaultErrorBehavior} types { ...FullType } From 775271bc668fb12838cbc237ce7c2e1f0ad92a56 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:15:19 +0100 Subject: [PATCH 11/18] Integrate into introspection --- src/__tests__/starWarsIntrospection-test.ts | 1 + src/index.ts | 1 + src/type/__tests__/introspection-test.ts | 73 +++++++++++++++++-- src/type/__tests__/schema-test.ts | 1 + src/type/directives.ts | 17 +++++ src/type/index.ts | 1 + src/type/introspection.ts | 30 ++++++++ .../__tests__/buildASTSchema-test.ts | 15 +++- .../__tests__/findBreakingChanges-test.ts | 2 + src/utilities/__tests__/printSchema-test.ts | 26 +++++++ src/utilities/extendSchema.ts | 22 ++++++ src/utilities/printSchema.ts | 15 +++- 12 files changed, 193 insertions(+), 11 deletions(-) diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index d637787c4a..8f453b01d4 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -37,6 +37,7 @@ describe('Star Wars Introspection Tests', () => { { name: 'Droid' }, { name: 'Query' }, { name: 'Boolean' }, + { name: '__ErrorBehavior' }, { name: '__Schema' }, { name: '__Type' }, { name: '__TypeKind' }, diff --git a/src/index.ts b/src/index.ts index 4df70d7d74..93f544ddc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,6 +79,7 @@ export { __InputValue, __EnumValue, __TypeKind, + __ErrorBehavior, // Meta-field definitions. SchemaMetaFieldDef, TypeMetaFieldDef, diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index d7bd11cf95..d6913c5440 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -80,6 +80,32 @@ describe('Introspection', () => { enumValues: null, possibleTypes: null, }, + { + enumValues: [ + { + deprecationReason: null, + isDeprecated: false, + name: 'NO_PROPAGATE', + }, + { + deprecationReason: null, + isDeprecated: false, + name: 'PROPAGATE', + }, + { + deprecationReason: null, + isDeprecated: false, + name: 'ABORT', + }, + ], + fields: null, + inputFields: null, + interfaces: null, + kind: 'ENUM', + name: '__ErrorBehavior', + possibleTypes: null, + specifiedByURL: null, + }, { kind: 'OBJECT', name: '__Schema', @@ -179,6 +205,21 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'defaultErrorBehavior', + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'ENUM', + name: '__ErrorBehavior', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, ], inputFields: null, interfaces: [], @@ -1008,6 +1049,26 @@ describe('Introspection', () => { locations: ['INPUT_OBJECT'], args: [], }, + { + args: [ + { + defaultValue: 'PROPAGATE', + name: 'onError', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'ENUM', + name: '__ErrorBehavior', + ofType: null, + }, + }, + }, + ], + isRepeatable: false, + locations: ['SCHEMA'], + name: 'behavior', + }, ], }, }, @@ -1768,11 +1829,13 @@ describe('Introspection', () => { } `); - const source = getIntrospectionQuery({ - descriptions: false, - specifiedByUrl: true, - directiveIsRepeatable: true, - }); + const source = /* GraphQL */ ` + { + __schema { + defaultErrorBehavior + } + } + `; const result = graphqlSync({ schema, source }); expect(result).to.deep.equal({ diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index 8a31b50ada..8878e545e2 100644 --- a/src/type/__tests__/schema-test.ts +++ b/src/type/__tests__/schema-test.ts @@ -296,6 +296,7 @@ describe('Type System: Schema', () => { 'ASub', 'Boolean', 'String', + '__ErrorBehavior', '__Schema', '__Type', '__TypeKind', diff --git a/src/type/directives.ts b/src/type/directives.ts index 6881f20532..a64be1f76e 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -18,6 +18,7 @@ import { defineArguments, GraphQLNonNull, } from './definition'; +import { __ErrorBehavior } from './introspection'; import { GraphQLBoolean, GraphQLString } from './scalars'; /** @@ -220,6 +221,21 @@ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({ args: {}, }); +/** + * Used to indicate the default error behavior. + */ +export const GraphQLBehaviorDirective: GraphQLDirective = new GraphQLDirective({ + name: 'behavior', + description: 'Indicates the default error behavior of the schema.', + locations: [DirectiveLocation.SCHEMA], + args: { + onError: { + type: new GraphQLNonNull(__ErrorBehavior), + defaultValue: 'PROPAGATE', + }, + }, +}); + /** * The full list of specified directives. */ @@ -230,6 +246,7 @@ export const specifiedDirectives: ReadonlyArray = GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, GraphQLOneOfDirective, + GraphQLBehaviorDirective, ]); export function isSpecifiedDirective(directive: GraphQLDirective): boolean { diff --git a/src/type/index.ts b/src/type/index.ts index cf276d1e02..6d9870a016 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -172,6 +172,7 @@ export { __InputValue, __EnumValue, __TypeKind, + __ErrorBehavior, // "Enum" of Type Kinds TypeKind, // Meta-field definitions. diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 2c66ca5098..c176e7175c 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -74,6 +74,12 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ ), resolve: (schema) => schema.getDirectives(), }, + defaultErrorBehavior: { + description: + 'The default error behavior that will be used for requests which do not specify `onError`.', + type: new GraphQLNonNull(__ErrorBehavior), + resolve: (schema) => schema.defaultErrorBehavior, + }, } as GraphQLFieldConfigMap), }); @@ -500,6 +506,29 @@ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ }, }); +export const __ErrorBehavior: GraphQLEnumType = new GraphQLEnumType({ + name: '__ErrorBehavior', + description: + 'An enum detailing the error behavior a GraphQL request should use.', + values: { + NO_PROPAGATE: { + value: 'NO_PROPAGATE', + description: + 'Indicates that an error should result in the response position becoming null, even if it is marked as non-null.', + }, + PROPAGATE: { + value: 'PROPAGATE', + description: + 'Indicates that an error that occurs in a non-null position should propagate to the nearest nullable response position.', + }, + ABORT: { + value: 'ABORT', + description: + 'Indicates execution should cease when the first error occurs, and that the response data should be null.', + }, + }, +}); + /** * Note that these are GraphQLField and not GraphQLFieldConfig, * so the format for args is different. @@ -558,6 +587,7 @@ export const introspectionTypes: ReadonlyArray = __InputValue, __EnumValue, __TypeKind, + __ErrorBehavior, ]); export function isIntrospectionType(type: GraphQLNamedType): boolean { diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 29280474ec..06cded5816 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -21,6 +21,7 @@ import { } from '../../type/definition'; import { assertDirective, + GraphQLBehaviorDirective, GraphQLDeprecatedDirective, GraphQLIncludeDirective, GraphQLOneOfDirective, @@ -223,7 +224,7 @@ describe('Schema Builder', () => { it('Maintains @include, @skip & @specifiedBy', () => { const schema = buildSchema('type Query'); - expect(schema.getDirectives()).to.have.lengthOf(5); + expect(schema.getDirectives()).to.have.lengthOf(6); expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective); expect(schema.getDirective('deprecated')).to.equal( @@ -232,6 +233,7 @@ describe('Schema Builder', () => { expect(schema.getDirective('specifiedBy')).to.equal( GraphQLSpecifiedByDirective, ); + expect(schema.getDirective('behavior')).to.equal(GraphQLBehaviorDirective); expect(schema.getDirective('oneOf')).to.equal(GraphQLOneOfDirective); }); @@ -241,10 +243,11 @@ describe('Schema Builder', () => { directive @include on FIELD directive @deprecated on FIELD_DEFINITION directive @specifiedBy on FIELD_DEFINITION + directive @behavior on SCHEMA directive @oneOf on OBJECT `); - expect(schema.getDirectives()).to.have.lengthOf(5); + expect(schema.getDirectives()).to.have.lengthOf(6); expect(schema.getDirective('skip')).to.not.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.not.equal( GraphQLIncludeDirective, @@ -255,19 +258,23 @@ describe('Schema Builder', () => { expect(schema.getDirective('specifiedBy')).to.not.equal( GraphQLSpecifiedByDirective, ); + expect(schema.getDirective('behavior')).to.not.equal( + GraphQLBehaviorDirective, + ); expect(schema.getDirective('oneOf')).to.not.equal(GraphQLOneOfDirective); }); - it('Adding directives maintains @include, @skip, @deprecated, @specifiedBy, and @oneOf', () => { + it('Adding directives maintains @include, @skip, @deprecated, @specifiedBy, @behavior and @oneOf', () => { const schema = buildSchema(` directive @foo(arg: Int) on FIELD `); - expect(schema.getDirectives()).to.have.lengthOf(6); + expect(schema.getDirectives()).to.have.lengthOf(7); expect(schema.getDirective('skip')).to.not.equal(undefined); expect(schema.getDirective('include')).to.not.equal(undefined); expect(schema.getDirective('deprecated')).to.not.equal(undefined); expect(schema.getDirective('specifiedBy')).to.not.equal(undefined); + expect(schema.getDirective('behavior')).to.not.equal(undefined); expect(schema.getDirective('oneOf')).to.not.equal(undefined); }); diff --git a/src/utilities/__tests__/findBreakingChanges-test.ts b/src/utilities/__tests__/findBreakingChanges-test.ts index ba526deb48..135b50b411 100644 --- a/src/utilities/__tests__/findBreakingChanges-test.ts +++ b/src/utilities/__tests__/findBreakingChanges-test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { + GraphQLBehaviorDirective, GraphQLDeprecatedDirective, GraphQLIncludeDirective, GraphQLOneOfDirective, @@ -803,6 +804,7 @@ describe('findBreakingChanges', () => { GraphQLSkipDirective, GraphQLIncludeDirective, GraphQLSpecifiedByDirective, + GraphQLBehaviorDirective, GraphQLOneOfDirective, ], }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 37af4a60f7..c6560cae2c 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -694,6 +694,27 @@ describe('Type System Printer', () => { """ directive @oneOf on INPUT_OBJECT + """Indicates the default error behavior of the schema.""" + directive @behavior(onError: __ErrorBehavior! = PROPAGATE) on SCHEMA + + """An enum detailing the error behavior a GraphQL request should use.""" + enum __ErrorBehavior { + """ + Indicates that an error should result in the response position becoming null, even if it is marked as non-null. + """ + NO_PROPAGATE + + """ + Indicates that an error that occurs in a non-null position should propagate to the nearest nullable response position. + """ + PROPAGATE + + """ + Indicates execution should cease when the first error occurs, and that the response data should be null. + """ + ABORT + } + """ A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. """ @@ -718,6 +739,11 @@ describe('Type System Printer', () => { """A list of all directives supported by this server.""" directives: [__Directive!]! + + """ + The default error behavior that will be used for requests which do not specify \`onError\`. + """ + defaultErrorBehavior: __ErrorBehavior! } """ diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index d53752d919..f5cc50001f 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -64,6 +64,7 @@ import { isUnionType, } from '../type/definition'; import { + GraphQLBehaviorDirective, GraphQLDeprecatedDirective, GraphQLDirective, GraphQLOneOfDirective, @@ -82,6 +83,7 @@ import { assertValidSDLExtension } from '../validation/validate'; import { getDirectiveValues } from '../execution/values'; import { valueFromAST } from './valueFromAST'; +import type { GraphQLErrorBehavior } from '../error'; interface Options extends GraphQLSchemaValidationOptions { /** @@ -165,6 +167,14 @@ export function extendSchemaImpl( } } + let defaultErrorBehavior: Maybe = schemaDef + ? getDefaultErrorBehavior(schemaDef) + : null; + for (const extensionNode of schemaExtensions) { + defaultErrorBehavior = + getDefaultErrorBehavior(extensionNode) ?? defaultErrorBehavior; + } + // If this document contains no new types, extensions, or directives then // return the same unmodified GraphQLSchema instance. if ( @@ -201,6 +211,7 @@ export function extendSchemaImpl( // Then produce and return a Schema config with these types. return { description: schemaDef?.description?.value, + defaultErrorBehavior, ...operationTypes, types: Object.values(typeMap), directives: [ @@ -691,3 +702,14 @@ function getSpecifiedByURL( function isOneOf(node: InputObjectTypeDefinitionNode): boolean { return Boolean(getDirectiveValues(GraphQLOneOfDirective, node)); } + +/** + * Given a schema node, returns the GraphQL error behavior from the `@behavior(onError:)` argument. + */ +function getDefaultErrorBehavior( + node: SchemaDefinitionNode | SchemaExtensionNode, +): Maybe { + const behavior = getDirectiveValues(GraphQLBehaviorDirective, node); + // @ts-expect-error validated by `getDirectiveValues` + return behavior?.onError; +} diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index edac6262c5..f248b9a367 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -70,7 +70,11 @@ function printFilteredSchema( } function printSchemaDefinition(schema: GraphQLSchema): Maybe { - if (schema.description == null && isSchemaOfCommonNames(schema)) { + if ( + schema.description == null && + schema.defaultErrorBehavior === 'PROPAGATE' && + isSchemaOfCommonNames(schema) + ) { return; } @@ -90,8 +94,15 @@ function printSchemaDefinition(schema: GraphQLSchema): Maybe { if (subscriptionType) { operationTypes.push(` subscription: ${subscriptionType.name}`); } + const directives = + schema.defaultErrorBehavior !== 'PROPAGATE' + ? `@behavior(onError: ${schema.defaultErrorBehavior}) ` + : ``; - return printDescription(schema) + `schema {\n${operationTypes.join('\n')}\n}`; + return ( + printDescription(schema) + + `schema ${directives}{\n${operationTypes.join('\n')}\n}` + ); } /** From a90f2a261921f398970c9e4eb0810aaa70c8216f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:27:24 +0100 Subject: [PATCH 12/18] Lint --- src/type/schema.ts | 4 ++-- src/utilities/extendSchema.ts | 3 ++- src/utilities/printSchema.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/type/schema.ts b/src/type/schema.ts index 3681e66578..8e431d6c52 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -6,6 +6,8 @@ import type { Maybe } from '../jsutils/Maybe'; import type { ObjMap } from '../jsutils/ObjMap'; import { toObjMap } from '../jsutils/toObjMap'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import { isErrorBehavior } from '../error/ErrorBehavior'; import type { GraphQLError } from '../error/GraphQLError'; import type { @@ -31,8 +33,6 @@ import { import type { GraphQLDirective } from './directives'; import { isDirective, specifiedDirectives } from './directives'; import { __Schema } from './introspection'; -import type { GraphQLErrorBehavior } from '../error'; -import { isErrorBehavior } from '../error/ErrorBehavior'; /** * Test if the given value is a GraphQL schema. diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index f5cc50001f..65a0414a5e 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -5,6 +5,8 @@ import { keyMap } from '../jsutils/keyMap'; import { mapValue } from '../jsutils/mapValue'; import type { Maybe } from '../jsutils/Maybe'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; + import type { DirectiveDefinitionNode, DocumentNode, @@ -83,7 +85,6 @@ import { assertValidSDLExtension } from '../validation/validate'; import { getDirectiveValues } from '../execution/values'; import { valueFromAST } from './valueFromAST'; -import type { GraphQLErrorBehavior } from '../error'; interface Options extends GraphQLSchemaValidationOptions { /** diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index f248b9a367..88e7e0fc19 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -97,7 +97,7 @@ function printSchemaDefinition(schema: GraphQLSchema): Maybe { const directives = schema.defaultErrorBehavior !== 'PROPAGATE' ? `@behavior(onError: ${schema.defaultErrorBehavior}) ` - : ``; + : ''; return ( printDescription(schema) + From 34d0991d5e206521a4b119fa8c951dc2b7b08ed9 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:32:59 +0100 Subject: [PATCH 13/18] Fix TypeScript issue --- src/type/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/type/schema.ts b/src/type/schema.ts index 8e431d6c52..632855f637 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -410,7 +410,7 @@ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { * * @experimental */ - defaultErrorBehavior?: GraphQLErrorBehavior; + defaultErrorBehavior?: Maybe; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; From 64a9162d8de1cd4ba961a27cab6710e4e44b09f7 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:36:46 +0100 Subject: [PATCH 14/18] Add missing test --- src/utilities/__tests__/printSchema-test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index c6560cae2c..ad6463bd29 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -276,6 +276,21 @@ describe('Type System Printer', () => { `); }); + it('Prints schema with NO_PROPAGATE error behavior', () => { + const schema = new GraphQLSchema({ + defaultErrorBehavior: 'NO_PROPAGATE', + query: new GraphQLObjectType({ name: 'Query', fields: {} }), + }); + + expectPrintedSchema(schema).to.equal(dedent` + schema @behavior(onError: NO_PROPAGATE) { + query: Query + } + + type Query + `); + }); + it('Omits schema of common names', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: {} }), From 0f0a31c85a88a4abd01416b717e1347a5fab2fca Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 1 May 2025 08:54:09 +0100 Subject: [PATCH 15/18] Reflect defaultErrorBehavior in buildClientSchema --- .../__tests__/buildClientSchema-test.ts | 21 +++++++++++++++++++ src/utilities/buildClientSchema.ts | 3 +++ src/utilities/getIntrospectionQuery.ts | 3 +++ 3 files changed, 27 insertions(+) diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index e8cf046921..bb2bfa7e22 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -18,6 +18,7 @@ import { GraphQLString, } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; +import { validateSchema } from '../../type/validate'; import { graphqlSync } from '../../graphql'; @@ -158,6 +159,26 @@ describe('Type System: build schema from introspection', () => { expect(clientSchema.getType('ID')).to.equal(undefined); }); + it('reflects defaultErrorBehavior', () => { + const schema = buildSchema(` + schema @behavior(onError: NO_PROPAGATE) { + query: Query + } + type Query { + foo: String + } + `); + const introspection = introspectionFromSchema(schema, { + errorBehavior: true, + }); + const clientSchema = buildClientSchema(introspection); + + expect(clientSchema.defaultErrorBehavior).to.equal('NO_PROPAGATE'); + + const errors = validateSchema(clientSchema); + expect(errors).to.have.length(0); + }); + it('builds a schema with a recursive type reference', () => { const sdl = dedent` schema { diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 83f6abada8..cc216af461 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -108,6 +108,8 @@ export function buildClientSchema( ? schemaIntrospection.directives.map(buildDirective) : []; + const defaultErrorBehavior = schemaIntrospection.defaultErrorBehavior; + // Then produce and return a Schema with these types. return new GraphQLSchema({ description: schemaIntrospection.description, @@ -116,6 +118,7 @@ export function buildClientSchema( subscription: subscriptionType, types: Object.values(typeMap), directives, + defaultErrorBehavior, assumeValid: options?.assumeValid, }); diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 0bf9692e10..38d8e2c1d2 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -1,5 +1,7 @@ import type { Maybe } from '../jsutils/Maybe'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; + import type { DirectiveLocation } from '../language/directiveLocation'; export interface IntrospectionOptions { @@ -206,6 +208,7 @@ export interface IntrospectionSchema { >; readonly types: ReadonlyArray; readonly directives: ReadonlyArray; + readonly defaultErrorBehavior?: Maybe; } export type IntrospectionType = From 726b4ba139c09209ee6a28e98043e375eb78568c Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 1 May 2025 09:34:23 +0100 Subject: [PATCH 16/18] Elevate ErrorBehavior to be a specified enum --- src/__tests__/starWarsIntrospection-test.ts | 2 +- src/error/ErrorBehavior.ts | 6 ++-- src/error/index.ts | 2 +- src/execution/execute.ts | 7 ++--- src/graphql.ts | 4 +-- src/index.ts | 7 +++-- src/type/__tests__/introspection-test.ts | 6 ++-- src/type/__tests__/predicate-test.ts | 11 +++++++ src/type/__tests__/schema-test.ts | 2 +- src/type/definition.ts | 4 +-- src/type/directives.ts | 4 +-- src/type/enums.ts | 33 +++++++++++++++++++++ src/type/index.ts | 8 ++++- src/type/introspection.ts | 28 ++--------------- src/type/schema.ts | 6 ++-- src/utilities/__tests__/printSchema-test.ts | 6 ++-- src/utilities/buildClientSchema.ts | 7 ++++- src/utilities/extendSchema.ts | 15 ++++++---- src/utilities/findBreakingChanges.ts | 3 ++ src/utilities/getIntrospectionQuery.ts | 4 +-- src/utilities/lexicographicSortSchema.ts | 8 ++++- src/utilities/printSchema.ts | 17 +++++++++-- src/validation/rules/KnownTypeNamesRule.ts | 9 ++++-- 23 files changed, 131 insertions(+), 68 deletions(-) create mode 100644 src/type/enums.ts diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index 8f453b01d4..662361b038 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -37,7 +37,7 @@ describe('Star Wars Introspection Tests', () => { { name: 'Droid' }, { name: 'Query' }, { name: 'Boolean' }, - { name: '__ErrorBehavior' }, + { name: 'ErrorBehavior' }, { name: '__Schema' }, { name: '__Type' }, { name: '__TypeKind' }, diff --git a/src/error/ErrorBehavior.ts b/src/error/ErrorBehavior.ts index 44495318ca..973dccf7d1 100644 --- a/src/error/ErrorBehavior.ts +++ b/src/error/ErrorBehavior.ts @@ -1,8 +1,6 @@ -export type GraphQLErrorBehavior = 'NO_PROPAGATE' | 'PROPAGATE' | 'ABORT'; +export type ErrorBehavior = 'NO_PROPAGATE' | 'PROPAGATE' | 'ABORT'; -export function isErrorBehavior( - onError: unknown, -): onError is GraphQLErrorBehavior { +export function isErrorBehavior(onError: unknown): onError is ErrorBehavior { return ( onError === 'NO_PROPAGATE' || onError === 'PROPAGATE' || onError === 'ABORT' ); diff --git a/src/error/index.ts b/src/error/index.ts index b9da3e897e..f0d55c34ed 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -9,4 +9,4 @@ export type { export { syntaxError } from './syntaxError'; export { locatedError } from './locatedError'; -export type { GraphQLErrorBehavior } from './ErrorBehavior'; +export type { ErrorBehavior } from './ErrorBehavior'; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 9ec4509f1a..477510f33a 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -13,7 +13,7 @@ import { promiseForObject } from '../jsutils/promiseForObject'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import { promiseReduce } from '../jsutils/promiseReduce'; -import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import type { ErrorBehavior } from '../error/ErrorBehavior'; import { isErrorBehavior } from '../error/ErrorBehavior'; import type { GraphQLFormattedError } from '../error/GraphQLError'; import { GraphQLError } from '../error/GraphQLError'; @@ -117,7 +117,7 @@ export interface ExecutionContext { typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; errors: Array; - errorBehavior: GraphQLErrorBehavior; + errorBehavior: ErrorBehavior; } /** @@ -133,7 +133,6 @@ export interface ExecutionResult< > { errors?: ReadonlyArray; data?: TData | null; - onError?: GraphQLErrorBehavior; extensions?: TExtensions; } @@ -164,7 +163,7 @@ export interface ExecutionArgs { * * @experimental */ - onError?: GraphQLErrorBehavior; + onError?: ErrorBehavior; /** Additional execution options. */ options?: { /** Set the maximum number of errors allowed for coercing (defaults to 50). */ diff --git a/src/graphql.ts b/src/graphql.ts index 7edd260b83..cbd09a9b44 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -3,7 +3,7 @@ import { isPromise } from './jsutils/isPromise'; import type { Maybe } from './jsutils/Maybe'; import type { PromiseOrValue } from './jsutils/PromiseOrValue'; -import type { GraphQLErrorBehavior } from './error/ErrorBehavior'; +import type { ErrorBehavior } from './error/ErrorBehavior'; import { parse } from './language/parser'; import type { Source } from './language/source'; @@ -76,7 +76,7 @@ export interface GraphQLArgs { * * @experimental */ - onError?: GraphQLErrorBehavior; + onError?: ErrorBehavior; } export function graphql(args: GraphQLArgs): Promise { diff --git a/src/index.ts b/src/index.ts index 93f544ddc9..66d3f5267c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,6 +55,9 @@ export { GraphQLString, GraphQLBoolean, GraphQLID, + // Standard GraphQL Enums + specifiedEnumTypes, + GraphQLErrorBehavior, // Int boundaries constants GRAPHQL_MAX_INT, GRAPHQL_MIN_INT, @@ -79,7 +82,6 @@ export { __InputValue, __EnumValue, __TypeKind, - __ErrorBehavior, // Meta-field definitions. SchemaMetaFieldDef, TypeMetaFieldDef, @@ -107,6 +109,7 @@ export { isRequiredArgument, isRequiredInputField, isSpecifiedScalarType, + isSpecifiedEnumType, isIntrospectionType, isSpecifiedDirective, // Assertions @@ -396,7 +399,7 @@ export { } from './error/index'; export type { - GraphQLErrorBehavior, + ErrorBehavior, GraphQLErrorOptions, GraphQLFormattedError, GraphQLErrorExtensions, diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index d6913c5440..67f0c2cf2b 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -102,7 +102,7 @@ describe('Introspection', () => { inputFields: null, interfaces: null, kind: 'ENUM', - name: '__ErrorBehavior', + name: 'ErrorBehavior', possibleTypes: null, specifiedByURL: null, }, @@ -213,7 +213,7 @@ describe('Introspection', () => { name: null, ofType: { kind: 'ENUM', - name: '__ErrorBehavior', + name: 'ErrorBehavior', ofType: null, }, }, @@ -1059,7 +1059,7 @@ describe('Introspection', () => { name: null, ofType: { kind: 'ENUM', - name: '__ErrorBehavior', + name: 'ErrorBehavior', ofType: null, }, }, diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index 81e721e7df..9f77449869 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -65,6 +65,7 @@ import { isDirective, isSpecifiedDirective, } from '../directives'; +import { GraphQLErrorBehavior, isSpecifiedEnumType } from '../enums'; import { GraphQLBoolean, GraphQLFloat, @@ -166,6 +167,16 @@ describe('Type predicates', () => { }); }); + describe('isSpecifiedEnumType', () => { + it('returns true for specified enums', () => { + expect(isSpecifiedEnumType(GraphQLErrorBehavior)).to.equal(true); + }); + + it('returns false for custom scalar', () => { + expect(isSpecifiedEnumType(EnumType)).to.equal(false); + }); + }); + describe('isObjectType', () => { it('returns true for object type', () => { expect(isObjectType(ObjectType)).to.equal(true); diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index 8878e545e2..4e4f34b5ba 100644 --- a/src/type/__tests__/schema-test.ts +++ b/src/type/__tests__/schema-test.ts @@ -296,7 +296,7 @@ describe('Type System: Schema', () => { 'ASub', 'Boolean', 'String', - '__ErrorBehavior', + 'ErrorBehavior', '__Schema', '__Type', '__TypeKind', diff --git a/src/type/definition.ts b/src/type/definition.ts index 61c57c4f38..ddd3b5e18a 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -14,7 +14,7 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import { suggestionList } from '../jsutils/suggestionList'; import { toObjMap } from '../jsutils/toObjMap'; -import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import type { ErrorBehavior } from '../error/ErrorBehavior'; import { GraphQLError } from '../error/GraphQLError'; import type { @@ -990,7 +990,7 @@ export interface GraphQLResolveInfo { readonly operation: OperationDefinitionNode; readonly variableValues: { [variable: string]: unknown }; /** @experimental */ - readonly errorBehavior: GraphQLErrorBehavior; + readonly errorBehavior: ErrorBehavior; } /** diff --git a/src/type/directives.ts b/src/type/directives.ts index a64be1f76e..c19e7d36d7 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -18,7 +18,7 @@ import { defineArguments, GraphQLNonNull, } from './definition'; -import { __ErrorBehavior } from './introspection'; +import { GraphQLErrorBehavior } from './enums'; import { GraphQLBoolean, GraphQLString } from './scalars'; /** @@ -230,7 +230,7 @@ export const GraphQLBehaviorDirective: GraphQLDirective = new GraphQLDirective({ locations: [DirectiveLocation.SCHEMA], args: { onError: { - type: new GraphQLNonNull(__ErrorBehavior), + type: new GraphQLNonNull(GraphQLErrorBehavior), defaultValue: 'PROPAGATE', }, }, diff --git a/src/type/enums.ts b/src/type/enums.ts new file mode 100644 index 0000000000..c047830474 --- /dev/null +++ b/src/type/enums.ts @@ -0,0 +1,33 @@ +import type { GraphQLNamedType } from './definition'; +import { GraphQLEnumType } from './definition'; + +export const GraphQLErrorBehavior: GraphQLEnumType = new GraphQLEnumType({ + name: 'ErrorBehavior', + description: + 'An enum detailing the error behavior a GraphQL request should use.', + values: { + NO_PROPAGATE: { + value: 'NO_PROPAGATE', + description: + 'Indicates that an error should result in the response position becoming null, even if it is marked as non-null.', + }, + PROPAGATE: { + value: 'PROPAGATE', + description: + 'Indicates that an error that occurs in a non-null position should propagate to the nearest nullable response position.', + }, + ABORT: { + value: 'ABORT', + description: + 'Indicates execution should cease when the first error occurs, and that the response data should be null.', + }, + }, +}); + +export const specifiedEnumTypes: ReadonlyArray = Object.freeze( + [GraphQLErrorBehavior], +); + +export function isSpecifiedEnumType(type: GraphQLNamedType): boolean { + return specifiedEnumTypes.some(({ name }) => type.name === name); +} diff --git a/src/type/index.ts b/src/type/index.ts index 6d9870a016..585cf1d6fe 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -158,6 +158,13 @@ export { GRAPHQL_MAX_INT, GRAPHQL_MIN_INT, } from './scalars'; +export { + // Predicate + isSpecifiedEnumType, + // Standard GraphQL Enums + specifiedEnumTypes, + GraphQLErrorBehavior, +} from './enums'; export { // Predicate @@ -172,7 +179,6 @@ export { __InputValue, __EnumValue, __TypeKind, - __ErrorBehavior, // "Enum" of Type Kinds TypeKind, // Meta-field definitions. diff --git a/src/type/introspection.ts b/src/type/introspection.ts index c176e7175c..e2ce198d9d 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -30,6 +30,7 @@ import { isUnionType, } from './definition'; import type { GraphQLDirective } from './directives'; +import { GraphQLErrorBehavior } from './enums'; import { GraphQLBoolean, GraphQLString } from './scalars'; import type { GraphQLSchema } from './schema'; @@ -77,7 +78,7 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ defaultErrorBehavior: { description: 'The default error behavior that will be used for requests which do not specify `onError`.', - type: new GraphQLNonNull(__ErrorBehavior), + type: new GraphQLNonNull(GraphQLErrorBehavior), resolve: (schema) => schema.defaultErrorBehavior, }, } as GraphQLFieldConfigMap), @@ -506,29 +507,6 @@ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ }, }); -export const __ErrorBehavior: GraphQLEnumType = new GraphQLEnumType({ - name: '__ErrorBehavior', - description: - 'An enum detailing the error behavior a GraphQL request should use.', - values: { - NO_PROPAGATE: { - value: 'NO_PROPAGATE', - description: - 'Indicates that an error should result in the response position becoming null, even if it is marked as non-null.', - }, - PROPAGATE: { - value: 'PROPAGATE', - description: - 'Indicates that an error that occurs in a non-null position should propagate to the nearest nullable response position.', - }, - ABORT: { - value: 'ABORT', - description: - 'Indicates execution should cease when the first error occurs, and that the response data should be null.', - }, - }, -}); - /** * Note that these are GraphQLField and not GraphQLFieldConfig, * so the format for args is different. @@ -587,7 +565,7 @@ export const introspectionTypes: ReadonlyArray = __InputValue, __EnumValue, __TypeKind, - __ErrorBehavior, + // ErrorBehavior, ]); export function isIntrospectionType(type: GraphQLNamedType): boolean { diff --git a/src/type/schema.ts b/src/type/schema.ts index 632855f637..c4d590ca0a 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -6,7 +6,7 @@ import type { Maybe } from '../jsutils/Maybe'; import type { ObjMap } from '../jsutils/ObjMap'; import { toObjMap } from '../jsutils/toObjMap'; -import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import type { ErrorBehavior } from '../error/ErrorBehavior'; import { isErrorBehavior } from '../error/ErrorBehavior'; import type { GraphQLError } from '../error/GraphQLError'; @@ -132,7 +132,7 @@ export interface GraphQLSchemaExtensions { export class GraphQLSchema { description: Maybe; /** @experimental */ - readonly defaultErrorBehavior: GraphQLErrorBehavior; + readonly defaultErrorBehavior: ErrorBehavior; extensions: Readonly; astNode: Maybe; extensionASTNodes: ReadonlyArray; @@ -410,7 +410,7 @@ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { * * @experimental */ - defaultErrorBehavior?: Maybe; + defaultErrorBehavior?: Maybe; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index ad6463bd29..6f1a04823d 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -710,10 +710,10 @@ describe('Type System Printer', () => { directive @oneOf on INPUT_OBJECT """Indicates the default error behavior of the schema.""" - directive @behavior(onError: __ErrorBehavior! = PROPAGATE) on SCHEMA + directive @behavior(onError: ErrorBehavior! = PROPAGATE) on SCHEMA """An enum detailing the error behavior a GraphQL request should use.""" - enum __ErrorBehavior { + enum ErrorBehavior { """ Indicates that an error should result in the response position becoming null, even if it is marked as non-null. """ @@ -758,7 +758,7 @@ describe('Type System Printer', () => { """ The default error behavior that will be used for requests which do not specify \`onError\`. """ - defaultErrorBehavior: __ErrorBehavior! + defaultErrorBehavior: ErrorBehavior! } """ diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index cc216af461..463235b0a5 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -27,6 +27,7 @@ import { isOutputType, } from '../type/definition'; import { GraphQLDirective } from '../type/directives'; +import { specifiedEnumTypes } from '../type/enums'; import { introspectionTypes, TypeKind } from '../type/introspection'; import { specifiedScalarTypes } from '../type/scalars'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; @@ -83,7 +84,11 @@ export function buildClientSchema( ); // Include standard types only if they are used. - for (const stdType of [...specifiedScalarTypes, ...introspectionTypes]) { + for (const stdType of [ + ...specifiedScalarTypes, + ...specifiedEnumTypes, + ...introspectionTypes, + ]) { if (typeMap[stdType.name]) { typeMap[stdType.name] = stdType; } diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 65a0414a5e..463d70b63d 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -5,7 +5,7 @@ import { keyMap } from '../jsutils/keyMap'; import { mapValue } from '../jsutils/mapValue'; import type { Maybe } from '../jsutils/Maybe'; -import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import type { ErrorBehavior } from '../error/ErrorBehavior'; import type { DirectiveDefinitionNode, @@ -72,6 +72,7 @@ import { GraphQLOneOfDirective, GraphQLSpecifiedByDirective, } from '../type/directives'; +import { isSpecifiedEnumType, specifiedEnumTypes } from '../type/enums'; import { introspectionTypes, isIntrospectionType } from '../type/introspection'; import { isSpecifiedScalarType, specifiedScalarTypes } from '../type/scalars'; import type { @@ -168,7 +169,7 @@ export function extendSchemaImpl( } } - let defaultErrorBehavior: Maybe = schemaDef + let defaultErrorBehavior: Maybe = schemaDef ? getDefaultErrorBehavior(schemaDef) : null; for (const extensionNode of schemaExtensions) { @@ -257,7 +258,11 @@ export function extendSchemaImpl( } function extendNamedType(type: GraphQLNamedType): GraphQLNamedType { - if (isIntrospectionType(type) || isSpecifiedScalarType(type)) { + if ( + isIntrospectionType(type) || + isSpecifiedScalarType(type) || + isSpecifiedEnumType(type) + ) { // Builtin types are not extended. return type; } @@ -667,7 +672,7 @@ export function extendSchemaImpl( } const stdTypeMap = keyMap( - [...specifiedScalarTypes, ...introspectionTypes], + [...specifiedScalarTypes, ...specifiedEnumTypes, ...introspectionTypes], (type) => type.name, ); @@ -709,7 +714,7 @@ function isOneOf(node: InputObjectTypeDefinitionNode): boolean { */ function getDefaultErrorBehavior( node: SchemaDefinitionNode | SchemaExtensionNode, -): Maybe { +): Maybe { const behavior = getDirectiveValues(GraphQLBehaviorDirective, node); // @ts-expect-error validated by `getDirectiveValues` return behavior?.onError; diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index 2489af9d62..db8627e3df 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -28,6 +28,7 @@ import { isScalarType, isUnionType, } from '../type/definition'; +import { isSpecifiedEnumType } from '../type/enums'; import { isSpecifiedScalarType } from '../type/scalars'; import type { GraphQLSchema } from '../type/schema'; @@ -185,6 +186,8 @@ function findTypeChanges( type: BreakingChangeType.TYPE_REMOVED, description: isSpecifiedScalarType(oldType) ? `Standard scalar ${oldType.name} was removed because it is not referenced anymore.` + : isSpecifiedEnumType(oldType) + ? `Standard enum ${oldType.name} was removed because it is not referenced anymore.` : `${oldType.name} was removed.`, }); } diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 38d8e2c1d2..28250f8f4d 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -1,6 +1,6 @@ import type { Maybe } from '../jsutils/Maybe'; -import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import type { ErrorBehavior } from '../error/ErrorBehavior'; import type { DirectiveLocation } from '../language/directiveLocation'; @@ -208,7 +208,7 @@ export interface IntrospectionSchema { >; readonly types: ReadonlyArray; readonly directives: ReadonlyArray; - readonly defaultErrorBehavior?: Maybe; + readonly defaultErrorBehavior?: Maybe; } export type IntrospectionType = diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index 26b6908c9f..269c151ba1 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -30,6 +30,7 @@ import { isUnionType, } from '../type/definition'; import { GraphQLDirective } from '../type/directives'; +import { isSpecifiedEnumType } from '../type/enums'; import { isIntrospectionType } from '../type/introspection'; import { GraphQLSchema } from '../type/schema'; @@ -115,7 +116,12 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { } function sortNamedType(type: GraphQLNamedType): GraphQLNamedType { - if (isScalarType(type) || isIntrospectionType(type)) { + if ( + isScalarType(type) || + isIntrospectionType(type) || + isIntrospectionType(type) || + isSpecifiedEnumType(type) + ) { return type; } if (isObjectType(type)) { diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 88e7e0fc19..3c81636ba9 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -30,6 +30,7 @@ import { DEFAULT_DEPRECATION_REASON, isSpecifiedDirective, } from '../type/directives'; +import { isSpecifiedEnumType } from '../type/enums'; import { isIntrospectionType } from '../type/introspection'; import { isSpecifiedScalarType } from '../type/scalars'; import type { GraphQLSchema } from '../type/schema'; @@ -45,11 +46,23 @@ export function printSchema(schema: GraphQLSchema): string { } export function printIntrospectionSchema(schema: GraphQLSchema): string { - return printFilteredSchema(schema, isSpecifiedDirective, isIntrospectionType); + return printFilteredSchema( + schema, + isSpecifiedDirective, + isIntrospectionSchemaType, + ); } function isDefinedType(type: GraphQLNamedType): boolean { - return !isSpecifiedScalarType(type) && !isIntrospectionType(type); + return ( + !isSpecifiedScalarType(type) && + !isSpecifiedEnumType(type) && + !isIntrospectionType(type) + ); +} + +function isIntrospectionSchemaType(type: GraphQLNamedType): boolean { + return isIntrospectionType(type) || isSpecifiedEnumType(type); } function printFilteredSchema( diff --git a/src/validation/rules/KnownTypeNamesRule.ts b/src/validation/rules/KnownTypeNamesRule.ts index fadc080c35..4219c3c9e3 100644 --- a/src/validation/rules/KnownTypeNamesRule.ts +++ b/src/validation/rules/KnownTypeNamesRule.ts @@ -11,6 +11,7 @@ import { } from '../../language/predicates'; import type { ASTVisitor } from '../../language/visitor'; +import { specifiedEnumTypes } from '../../type/enums'; import { introspectionTypes } from '../../type/introspection'; import { specifiedScalarTypes } from '../../type/scalars'; @@ -70,9 +71,11 @@ export function KnownTypeNamesRule( }; } -const standardTypeNames = [...specifiedScalarTypes, ...introspectionTypes].map( - (type) => type.name, -); +const standardTypeNames = [ + ...specifiedScalarTypes, + ...specifiedEnumTypes, + ...introspectionTypes, +].map((type) => type.name); function isSDLNode(value: ASTNode | ReadonlyArray): boolean { return ( From 2a8180f8b94aa85f0f08d1f35f2b3b2f5688a00b Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 1 May 2025 09:56:29 +0100 Subject: [PATCH 17/18] Coverage --- src/utilities/findBreakingChanges.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index db8627e3df..a22bdbbabe 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -186,7 +186,8 @@ function findTypeChanges( type: BreakingChangeType.TYPE_REMOVED, description: isSpecifiedScalarType(oldType) ? `Standard scalar ${oldType.name} was removed because it is not referenced anymore.` - : isSpecifiedEnumType(oldType) + : /* c8 ignore next 2 */ + isSpecifiedEnumType(oldType) ? `Standard enum ${oldType.name} was removed because it is not referenced anymore.` : `${oldType.name} was removed.`, }); From 3ae999fbf77aa19b7f6e4d85f277f6997f1f28f0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 1 May 2025 09:57:21 +0100 Subject: [PATCH 18/18] Alternative coverage fix --- src/utilities/findBreakingChanges.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index a22bdbbabe..3916036b74 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -184,10 +184,10 @@ function findTypeChanges( for (const oldType of typesDiff.removed) { schemaChanges.push({ type: BreakingChangeType.TYPE_REMOVED, + /* c8 ignore next 5 */ description: isSpecifiedScalarType(oldType) ? `Standard scalar ${oldType.name} was removed because it is not referenced anymore.` - : /* c8 ignore next 2 */ - isSpecifiedEnumType(oldType) + : isSpecifiedEnumType(oldType) ? `Standard enum ${oldType.name} was removed because it is not referenced anymore.` : `${oldType.name} was removed.`, });