From 7ca49b2e708bf8d6193b2de7c3dbcdc9774d5fff Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 18:16:40 -0700 Subject: [PATCH 01/30] New GraphQLSemanticNonNull type --- src/type/definition.ts | 111 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 7 deletions(-) diff --git a/src/type/definition.ts b/src/type/definition.ts index 7eaac560dc..2afc002e2f 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -66,6 +66,15 @@ export type GraphQLType = | GraphQLEnumType | GraphQLInputObjectType | GraphQLList + > + | GraphQLSemanticNonNull< + | GraphQLScalarType + | GraphQLObjectType + | GraphQLInterfaceType + | GraphQLUnionType + | GraphQLEnumType + | GraphQLInputObjectType + | GraphQLList >; export function isType(type: unknown): type is GraphQLType { @@ -77,7 +86,8 @@ export function isType(type: unknown): type is GraphQLType { isEnumType(type) || isInputObjectType(type) || isListType(type) || - isNonNullType(type) + isNonNullType(type) || + isSemanticNonNullType(type) ); } @@ -203,6 +213,32 @@ export function assertNonNullType(type: unknown): GraphQLNonNull { return type; } +export function isSemanticNonNullType( + type: GraphQLInputType, +): type is GraphQLSemanticNonNull; +export function isSemanticNonNullType( + type: GraphQLOutputType, +): type is GraphQLSemanticNonNull; +export function isSemanticNonNullType( + type: unknown, +): type is GraphQLSemanticNonNull; +export function isSemanticNonNullType( + type: unknown, +): type is GraphQLSemanticNonNull { + return instanceOf(type, GraphQLSemanticNonNull); +} + +export function assertSemanticNonNullType( + type: unknown, +): GraphQLSemanticNonNull { + if (!isSemanticNonNullType(type)) { + throw new Error( + `Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`, + ); + } + return type; +} + /** * These types may be used as input types for arguments and directives. */ @@ -217,6 +253,7 @@ export type GraphQLInputType = | GraphQLInputObjectType | GraphQLList >; +// Note: GraphQLSemanticNonNull is currently not allowed for input types export function isInputType(type: unknown): type is GraphQLInputType { return ( @@ -251,6 +288,14 @@ export type GraphQLOutputType = | GraphQLUnionType | GraphQLEnumType | GraphQLList + > + | GraphQLSemanticNonNull< + | GraphQLScalarType + | GraphQLObjectType + | GraphQLInterfaceType + | GraphQLUnionType + | GraphQLEnumType + | GraphQLList >; export function isOutputType(type: unknown): type is GraphQLOutputType { @@ -414,16 +459,66 @@ export class GraphQLNonNull { } } +/** + * Semantic-Non-Null Type Wrapper + * + * A semantic-non-null is a wrapping type which points to another type. + * Semantic-non-null types enforce that their values are never null unless + * caused by an error being raised. It is useful for fields which you can make + * a guarantee on non-nullability in a no-error case, for example when you know + * that a related entity must exist (but acknowledge that retrieving it may + * produce an error). + * + * Example: + * + * ```ts + * const RowType = new GraphQLObjectType({ + * name: 'Row', + * fields: () => ({ + * email: { type: new GraphQLSemanticNonNull(GraphQLString) }, + * }) + * }) + * ``` + * Note: the enforcement of non-nullability occurs within the executor. + * + * @experimental + */ +export class GraphQLSemanticNonNull { + readonly ofType: T; + + constructor(ofType: T) { + devAssert( + isNullableType(ofType), + `Expected ${inspect(ofType)} to be a GraphQL nullable type.`, + ); + + this.ofType = ofType; + } + + get [Symbol.toStringTag]() { + return 'GraphQLSemanticNonNull'; + } + + toString(): string { + return String(this.ofType) + '*'; + } + + toJSON(): string { + return this.toString(); + } +} + /** * These types wrap and modify other types */ export type GraphQLWrappingType = | GraphQLList - | GraphQLNonNull; + | GraphQLNonNull + | GraphQLSemanticNonNull; export function isWrappingType(type: unknown): type is GraphQLWrappingType { - return isListType(type) || isNonNullType(type); + return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type); } export function assertWrappingType(type: unknown): GraphQLWrappingType { @@ -446,7 +541,7 @@ export type GraphQLNullableType = | GraphQLList; export function isNullableType(type: unknown): type is GraphQLNullableType { - return isType(type) && !isNonNullType(type); + return isType(type) && !isNonNullType(type) && !isSemanticNonNullType(type); } export function assertNullableType(type: unknown): GraphQLNullableType { @@ -458,7 +553,7 @@ export function assertNullableType(type: unknown): GraphQLNullableType { export function getNullableType(type: undefined | null): void; export function getNullableType( - type: T | GraphQLNonNull, + type: T | GraphQLNonNull | GraphQLSemanticNonNull, ): T; export function getNullableType( type: Maybe, @@ -467,12 +562,14 @@ export function getNullableType( type: Maybe, ): GraphQLNullableType | undefined { if (type) { - return isNonNullType(type) ? type.ofType : type; + return isNonNullType(type) || isSemanticNonNullType(type) + ? type.ofType + : type; } } /** - * These named types do not include modifiers like List or NonNull. + * These named types do not include modifiers like List, NonNull, or SemanticNonNull */ export type GraphQLNamedType = GraphQLNamedInputType | GraphQLNamedOutputType; From 16a2114f3e203cef115aaf944d11ea37605fd6db Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:28:48 -0700 Subject: [PATCH 02/30] Handle isNonNullType --- src/execution/execute.ts | 23 +++++++++- src/index.ts | 5 +++ src/language/ast.ts | 6 +++ src/language/index.ts | 1 + src/language/kinds.ts | 1 + src/type/__tests__/predicate-test.ts | 42 +++++++++++++++++++ src/type/definition.ts | 6 ++- src/type/index.ts | 3 ++ src/type/introspection.ts | 7 +++- src/utilities/astFromValue.ts | 1 + src/utilities/extendSchema.ts | 5 +++ src/utilities/findBreakingChanges.ts | 20 ++++++++- src/utilities/getIntrospectionQuery.ts | 7 ++++ src/utilities/index.ts | 1 + src/utilities/lexicographicSortSchema.ts | 4 ++ src/utilities/typeComparators.ts | 17 +++++++- .../rules/OverlappingFieldsCanBeMergedRule.ts | 11 ++++- .../rules/ValuesOfCorrectTypeRule.ts | 1 + .../rules/VariablesInAllowedPositionRule.ts | 1 + 19 files changed, 155 insertions(+), 7 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 55c22ea9de..8d1af1e866 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -42,6 +42,7 @@ import { isLeafType, isListType, isNonNullType, + isSemanticNonNullType, isObjectType, } from '../type/definition'; import { @@ -115,6 +116,7 @@ export interface ExecutionContext { typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; errors: Array; + errorPropagation: boolean; } /** @@ -595,7 +597,7 @@ function handleFieldError( ): 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.errorPropagation && isNonNullType(returnType)) { throw error; } @@ -658,6 +660,25 @@ function completeValue( return completed; } + // If field type is SemanticNonNull, complete for inner type, and throw field error + // if result is null. + if (isSemanticNonNullType(returnType)) { + const completed = completeValue( + exeContext, + returnType.ofType, + fieldNodes, + info, + path, + result, + ); + if (completed === null) { + throw new Error( + `Cannot return null for semantic-non-nullable field ${info.parentType.name}.${info.fieldName}.`, + ); + } + return completed; + } + // If result value is null or undefined then return null. if (result == null) { return null; diff --git a/src/index.ts b/src/index.ts index 877939d879..e3896fbf05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,7 @@ export { GraphQLInputObjectType, GraphQLList, GraphQLNonNull, + GraphQLSemanticNonNull, // Standard GraphQL Scalars specifiedScalarTypes, GraphQLInt, @@ -95,6 +96,7 @@ export { isInputObjectType, isListType, isNonNullType, + isSemanticNonNullType, isInputType, isOutputType, isLeafType, @@ -120,6 +122,7 @@ export { assertInputObjectType, assertListType, assertNonNullType, + assertSemanticNonNullType, assertInputType, assertOutputType, assertLeafType, @@ -287,6 +290,7 @@ export type { NamedTypeNode, ListTypeNode, NonNullTypeNode, + SemanticNonNullTypeNode, TypeSystemDefinitionNode, SchemaDefinitionNode, OperationTypeDefinitionNode, @@ -480,6 +484,7 @@ export type { IntrospectionNamedTypeRef, IntrospectionListTypeRef, IntrospectionNonNullTypeRef, + IntrospectionSemanticNonNullTypeRef, IntrospectionField, IntrospectionInputValue, IntrospectionEnumValue, diff --git a/src/language/ast.ts b/src/language/ast.ts index 29029342a1..af5704ceae 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -540,6 +540,12 @@ export interface NonNullTypeNode { readonly type: NamedTypeNode | ListTypeNode; } +export interface SemanticNonNullTypeNode { + readonly kind: Kind.SEMANTIC_NON_NULL_TYPE; + readonly loc?: Location; + readonly type: NamedTypeNode | ListTypeNode; +} + /** Type System Definition */ export type TypeSystemDefinitionNode = diff --git a/src/language/index.ts b/src/language/index.ts index ec4d195e1a..a760fd21b3 100644 --- a/src/language/index.ts +++ b/src/language/index.ts @@ -67,6 +67,7 @@ export type { NamedTypeNode, ListTypeNode, NonNullTypeNode, + SemanticNonNullTypeNode, TypeSystemDefinitionNode, SchemaDefinitionNode, OperationTypeDefinitionNode, diff --git a/src/language/kinds.ts b/src/language/kinds.ts index cd05f66a3b..e91373746c 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -37,6 +37,7 @@ enum Kind { NAMED_TYPE = 'NamedType', LIST_TYPE = 'ListType', NON_NULL_TYPE = 'NonNullType', + SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType', /** Type System Definitions */ SCHEMA_DEFINITION = 'SchemaDefinition', diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index 81e721e7df..750897012e 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -19,6 +19,7 @@ import { assertListType, assertNamedType, assertNonNullType, + assertSemanticNonNullType, assertNullableType, assertObjectType, assertOutputType, @@ -33,6 +34,7 @@ import { GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLSemanticNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, @@ -46,6 +48,7 @@ import { isListType, isNamedType, isNonNullType, + isSemanticNonNullType, isNullableType, isObjectType, isOutputType, @@ -298,6 +301,45 @@ describe('Type predicates', () => { expect(() => assertNonNullType(new GraphQLList(new GraphQLNonNull(ObjectType))), ).to.throw(); + expect(isNonNullType(new GraphQLSemanticNonNull(ObjectType))).to.equal( + false, + ); + expect(() => + assertNonNullType(new GraphQLSemanticNonNull(ObjectType)), + ).to.throw(); + }); + }); + + describe('isSemanticNonNullType', () => { + it('returns true for a semantic-non-null wrapped type', () => { + expect( + isSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)), + ).to.equal(true); + expect(() => + assertSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)), + ).to.not.throw(); + }); + + it('returns false for an unwrapped type', () => { + expect(isSemanticNonNullType(ObjectType)).to.equal(false); + expect(() => assertSemanticNonNullType(ObjectType)).to.throw(); + }); + + it('returns false for a not non-null wrapped type', () => { + expect( + isSemanticNonNullType( + new GraphQLList(new GraphQLSemanticNonNull(ObjectType)), + ), + ).to.equal(false); + expect(() => + assertSemanticNonNullType( + new GraphQLList(new GraphQLSemanticNonNull(ObjectType)), + ), + ).to.throw(); + expect(isNonNullType(new GraphQLNonNull(ObjectType))).to.equal(false); + expect(() => + assertNonNullType(new GraphQLNonNull(ObjectType)), + ).to.throw(); }); }); diff --git a/src/type/definition.ts b/src/type/definition.ts index 2afc002e2f..f2c9892400 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -260,7 +260,9 @@ export function isInputType(type: unknown): type is GraphQLInputType { isScalarType(type) || isEnumType(type) || isInputObjectType(type) || - (isWrappingType(type) && isInputType(type.ofType)) + (!isSemanticNonNullType(type) && + isWrappingType(type) && + isInputType(type.ofType)) ); } @@ -1167,6 +1169,7 @@ export interface GraphQLArgument { } export function isRequiredArgument(arg: GraphQLArgument): boolean { + // Note: input types cannot be SemanticNonNull return isNonNullType(arg.type) && arg.defaultValue === undefined; } @@ -1858,6 +1861,7 @@ export interface GraphQLInputField { } export function isRequiredInputField(field: GraphQLInputField): boolean { + // Note: input types cannot be SemanticNonNull return isNonNullType(field.type) && field.defaultValue === undefined; } diff --git a/src/type/index.ts b/src/type/index.ts index cf276d1e02..0729b840e7 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -23,6 +23,7 @@ export { isInputObjectType, isListType, isNonNullType, + isSemanticNonNullType, isInputType, isOutputType, isLeafType, @@ -43,6 +44,7 @@ export { assertInputObjectType, assertListType, assertNonNullType, + assertSemanticNonNullType, assertInputType, assertOutputType, assertLeafType, @@ -64,6 +66,7 @@ export { // Type Wrappers GraphQLList, GraphQLNonNull, + GraphQLSemanticNonNull, } from './definition'; export type { diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 2c66ca5098..12f7261c1d 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -6,13 +6,14 @@ import { print } from '../language/printer'; import { astFromValue } from '../utilities/astFromValue'; -import type { +import { GraphQLEnumValue, GraphQLField, GraphQLFieldConfigMap, GraphQLInputField, GraphQLNamedType, GraphQLType, + isSemanticNonNullType, } from './definition'; import { GraphQLEnumType, @@ -237,6 +238,9 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ if (isNonNullType(type)) { return TypeKind.NON_NULL; } + if (isSemanticNonNullType(type)) { + return TypeKind.SEMANTIC_NON_NULL; + } /* c8 ignore next 3 */ // Not reachable, all possible types have been considered) invariant(false, `Unexpected type: "${inspect(type)}".`); @@ -452,6 +456,7 @@ enum TypeKind { INPUT_OBJECT = 'INPUT_OBJECT', LIST = 'LIST', NON_NULL = 'NON_NULL', + SEMANTIC_NON_NULL = 'SEMANTIC_NON_NULL', } export { TypeKind }; diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index 1a880449c8..c605025035 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -42,6 +42,7 @@ export function astFromValue( value: unknown, type: GraphQLInputType, ): Maybe { + // Note: input types cannot be SemanticNonNull if (isNonNullType(type)) { const astValue = astFromValue(value, type.ofType); if (astValue?.kind === Kind.NULL) { diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index d53752d919..e81917d312 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -59,6 +59,7 @@ import { isInterfaceType, isListType, isNonNullType, + isSemanticNonNullType, isObjectType, isScalarType, isUnionType, @@ -225,6 +226,10 @@ export function extendSchemaImpl( // @ts-expect-error return new GraphQLNonNull(replaceType(type.ofType)); } + if (isSemanticNonNullType(type)) { + // @ts-expect-error + return new GraphQLSemanticNonNull(replaceType(type.ofType)); + } // @ts-expect-error FIXME return replaceNamedType(type); } diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index 2489af9d62..c7ab88504f 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -22,6 +22,7 @@ import { isListType, isNamedType, isNonNullType, + isSemanticNonNullType, isObjectType, isRequiredArgument, isRequiredInputField, @@ -458,7 +459,10 @@ function isChangeSafeForObjectOrInterfaceField( )) || // moving from nullable to non-null of the same underlying type is safe (isNonNullType(newType) && - isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) + isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) || + // moving from nullable to semantic-non-null of the same underlying type is safe + (isSemanticNonNullType(newType) && + isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) || ); } @@ -470,11 +474,25 @@ function isChangeSafeForObjectOrInterfaceField( ); } + if (isSemanticNonNullType(oldType)) { + return ( + // if they're both semantic-non-null, make sure the underlying types are compatible + (isSemanticNonNullType(newType) && + isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)) || + // moving from semantic-non-null to non-null of the same underlying type is safe + isNonNullType(newType) && + isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType) + ); + } + return ( // if they're both named types, see if their names are equivalent (isNamedType(newType) && oldType.name === newType.name) || // moving from nullable to non-null of the same underlying type is safe (isNonNullType(newType) && + isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) || + // moving from nullable to semantic-non-null of the same underlying type is safe + (isSemanticNonNullType(newType) && isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) ); } diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index c502f0f7b4..e51551e807 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -285,6 +285,13 @@ export interface IntrospectionNonNullTypeRef< readonly ofType: T; } +export interface IntrospectionSemanticNonNullTypeRef< + T extends IntrospectionTypeRef = IntrospectionTypeRef, +> { + readonly kind: 'SEMANTIC_NON_NULL'; + readonly ofType: T; +} + export type IntrospectionTypeRef = | IntrospectionNamedTypeRef | IntrospectionListTypeRef diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 452b975233..fa69583012 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -20,6 +20,7 @@ export type { IntrospectionNamedTypeRef, IntrospectionListTypeRef, IntrospectionNonNullTypeRef, + IntrospectionSemanticNonNullTypeRef, IntrospectionField, IntrospectionInputValue, IntrospectionEnumValue, diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index 26b6908c9f..728cf23380 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -25,6 +25,7 @@ import { isInterfaceType, isListType, isNonNullType, + isSemanticNonNullType, isObjectType, isScalarType, isUnionType, @@ -62,6 +63,9 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { } else if (isNonNullType(type)) { // @ts-expect-error return new GraphQLNonNull(replaceType(type.ofType)); + } else if (isSemanticNonNullType(type)) { + // @ts-expect-error + return new GraphQLSemanticNonNull(replaceType(type.ofType)); } // @ts-expect-error FIXME: TS Conversion return replaceNamedType(type); diff --git a/src/utilities/typeComparators.ts b/src/utilities/typeComparators.ts index 287be40bfe..79116e482b 100644 --- a/src/utilities/typeComparators.ts +++ b/src/utilities/typeComparators.ts @@ -4,6 +4,7 @@ import { isInterfaceType, isListType, isNonNullType, + isSemanticNonNullType, isObjectType, } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; @@ -22,6 +23,11 @@ export function isEqualType(typeA: GraphQLType, typeB: GraphQLType): boolean { return isEqualType(typeA.ofType, typeB.ofType); } + // If either type is semantic-non-null, the other must also be semantic-non-null. + if (isSemanticNonNullType(typeA) && isSemanticNonNullType(typeB)) { + return isEqualType(typeA.ofType, typeB.ofType); + } + // If either type is a list, the other must also be a list. if (isListType(typeA) && isListType(typeB)) { return isEqualType(typeA.ofType, typeB.ofType); @@ -52,8 +58,15 @@ export function isTypeSubTypeOf( } return false; } - if (isNonNullType(maybeSubType)) { - // If superType is nullable, maybeSubType may be non-null or nullable. + // If superType is semantic-non-null, maybeSubType must be semantic-non-null or non-null. + if (isSemanticNonNullType(superType)) { + if (isNonNullType(maybeSubType) || isSemanticNonNullType(maybeSubType)) { + return isTypeSubTypeOf(schema, maybeSubType.ofType, superType.ofType); + } + return false; + } + if (isNonNullType(maybeSubType) || isSemanticNonNullType(maybeSubType)) { + // If superType is nullable, maybeSubType may be non-null, semantic-non-null, or nullable. return isTypeSubTypeOf(schema, maybeSubType.ofType, superType); } diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index 4305064a6f..cba8daf3c5 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -15,10 +15,11 @@ import { Kind } from '../../language/kinds'; import { print } from '../../language/printer'; import type { ASTVisitor } from '../../language/visitor'; -import type { +import { GraphQLField, GraphQLNamedType, GraphQLOutputType, + isSemanticNonNullType, } from '../../type/definition'; import { getNamedType, @@ -695,6 +696,14 @@ function doTypesConflict( if (isNonNullType(type2)) { return true; } + if (isSemanticNonNullType(type1)) { + return isSemanticNonNullType(type2) + ? doTypesConflict(type1.ofType, type2.ofType) + : true; + } + if (isSemanticNonNullType(type2)) { + return true; + } if (isLeafType(type1) || isLeafType(type2)) { return type1 !== type2; } diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index 3f284d7103..716135effd 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.ts +++ b/src/validation/rules/ValuesOfCorrectTypeRule.ts @@ -118,6 +118,7 @@ export function ValuesOfCorrectTypeRule( ), ); } + // Note: SemanticNonNull cannot happen on input. }, EnumValue: (node) => isValidValueNode(context, node), IntValue: (node) => isValidValueNode(context, node), diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts index a0b7e991a6..2871b49bba 100644 --- a/src/validation/rules/VariablesInAllowedPositionRule.ts +++ b/src/validation/rules/VariablesInAllowedPositionRule.ts @@ -88,6 +88,7 @@ function allowedVariableUsage( locationType: GraphQLType, locationDefaultValue: Maybe, ): boolean { + // Note: SemanticNonNull cannot occur on input. if (isNonNullType(locationType) && !isNonNullType(varType)) { const hasNonNullVariableDefaultValue = varDefaultValue != null && varDefaultValue.kind !== Kind.NULL; From 2b13389e27c4ecb6afbc453bc8fdac1f6aad06c9 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:32:11 -0700 Subject: [PATCH 03/30] More fixes --- src/type/__tests__/predicate-test.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index 750897012e..39cf159dd4 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -336,9 +336,11 @@ describe('Type predicates', () => { new GraphQLList(new GraphQLSemanticNonNull(ObjectType)), ), ).to.throw(); - expect(isNonNullType(new GraphQLNonNull(ObjectType))).to.equal(false); + expect(isSemanticNonNullType(new GraphQLNonNull(ObjectType))).to.equal( + false, + ); expect(() => - assertNonNullType(new GraphQLNonNull(ObjectType)), + assertSemanticNonNullType(new GraphQLNonNull(ObjectType)), ).to.throw(); }); }); @@ -539,6 +541,14 @@ describe('Type predicates', () => { expect(() => assertNullableType(new GraphQLList(new GraphQLNonNull(ObjectType))), ).to.not.throw(); + expect( + isNullableType(new GraphQLList(new GraphQLSemanticNonNull(ObjectType))), + ).to.equal(true); + expect(() => + assertNullableType( + new GraphQLList(new GraphQLSemanticNonNull(ObjectType)), + ), + ).to.not.throw(); }); it('returns false for non-null types', () => { @@ -546,6 +556,12 @@ describe('Type predicates', () => { expect(() => assertNullableType(new GraphQLNonNull(ObjectType)), ).to.throw(); + expect(isNullableType(new GraphQLSemanticNonNull(ObjectType))).to.equal( + false, + ); + expect(() => + assertNullableType(new GraphQLSemanticNonNull(ObjectType)), + ).to.throw(); }); }); From 04a8e91580ca4818ea8b092d54004785ef06f5d6 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:35:06 -0700 Subject: [PATCH 04/30] More fixes --- src/type/__tests__/predicate-test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index 39cf159dd4..e3395727bc 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -520,6 +520,12 @@ describe('Type predicates', () => { expect(() => assertWrappingType(new GraphQLNonNull(ObjectType)), ).to.not.throw(); + expect(isWrappingType(new GraphQLSemanticNonNull(ObjectType))).to.equal( + true, + ); + expect(() => + assertWrappingType(new GraphQLSemanticNonNull(ObjectType)), + ).to.not.throw(); }); it('returns false for unwrapped types', () => { From 076a735662059b1894f52e5640d438afaf516d24 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:45:32 -0700 Subject: [PATCH 05/30] Yet more updates --- src/language/__tests__/parser-test.ts | 17 +++++++++++++++++ src/language/ast.ts | 7 ++++++- src/language/parser.ts | 8 ++++++++ src/language/predicates.ts | 3 ++- src/language/tokenKind.ts | 1 + src/type/introspection.ts | 5 +++++ src/utilities/__tests__/printSchema-test.ts | 3 +++ src/utilities/buildClientSchema.ts | 9 +++++++++ src/utilities/extendSchema.ts | 4 ++++ src/utilities/getIntrospectionQuery.ts | 3 +++ src/utilities/typeFromAST.ts | 10 +++++++++- 11 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 87e7b92c34..596a6d60c7 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -631,6 +631,23 @@ describe('Parser', () => { }); }); + it('parses semantic-non-null types', () => { + const result = parseType('MyType*'); + expectJSON(result).toDeepEqual({ + kind: Kind.SEMANTIC_NON_NULL_TYPE, + loc: { start: 0, end: 7 }, + type: { + kind: Kind.NAMED_TYPE, + loc: { start: 0, end: 6 }, + name: { + kind: Kind.NAME, + loc: { start: 0, end: 6 }, + value: 'MyType', + }, + }, + }); + }); + it('parses nested types', () => { const result = parseType('[MyType!]'); expectJSON(result).toDeepEqual({ diff --git a/src/language/ast.ts b/src/language/ast.ts index af5704ceae..9ad68ea0c3 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -161,6 +161,7 @@ export type ASTNode = | NamedTypeNode | ListTypeNode | NonNullTypeNode + | SemanticNonNullTypeNode | SchemaDefinitionNode | OperationTypeDefinitionNode | ScalarTypeDefinitionNode @@ -520,7 +521,11 @@ export interface ConstDirectiveNode { /** Type Reference */ -export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode; +export type TypeNode = + | NamedTypeNode + | ListTypeNode + | NonNullTypeNode + | SemanticNonNullTypeNode; export interface NamedTypeNode { readonly kind: Kind.NAMED_TYPE; diff --git a/src/language/parser.ts b/src/language/parser.ts index eb54a0376b..afe8ddbba8 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -37,6 +37,7 @@ import type { NamedTypeNode, NameNode, NonNullTypeNode, + SemanticNonNullTypeNode, NullValueNode, ObjectFieldNode, ObjectTypeDefinitionNode, @@ -740,6 +741,7 @@ export class Parser { * - NamedType * - ListType * - NonNullType + * - SemanticNonNullType */ parseTypeReference(): TypeNode { const start = this._lexer.token; @@ -761,6 +763,12 @@ export class Parser { type, }); } + if (this.expectOptionalToken(TokenKind.ASTERISK)) { + return this.node(start, { + kind: Kind.SEMANTIC_NON_NULL_TYPE, + type, + }); + } return type; } diff --git a/src/language/predicates.ts b/src/language/predicates.ts index a390f4ee55..3ddf52b94c 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -67,7 +67,8 @@ export function isTypeNode(node: ASTNode): node is TypeNode { return ( node.kind === Kind.NAMED_TYPE || node.kind === Kind.LIST_TYPE || - node.kind === Kind.NON_NULL_TYPE + node.kind === Kind.NON_NULL_TYPE || + node.kind === Kind.SEMANTIC_NON_NULL_TYPE ); } diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts index 0c260df99e..fd53d6ebf3 100644 --- a/src/language/tokenKind.ts +++ b/src/language/tokenKind.ts @@ -6,6 +6,7 @@ enum TokenKind { SOF = '', EOF = '', BANG = '!', + ASTERISK = '*', DOLLAR = '$', AMP = '&', PAREN_L = '(', diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 12f7261c1d..9affc21777 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -502,6 +502,11 @@ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ description: 'Indicates this type is a non-null. `ofType` is a valid field.', }, + SEMANTIC_NON_NULL: { + value: TypeKind.SEMANTIC_NON_NULL, + description: + 'Indicates this type is a semantic-non-null. `ofType` is a valid field.', + }, }, }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 37af4a60f7..ef6376e183 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -770,6 +770,9 @@ describe('Type System Printer', () => { """Indicates this type is a non-null. \`ofType\` is a valid field.""" NON_NULL + + """Indicates this type is a semantic-non-null. \`ofType\` is a valid field.""" + SEMANTIC_NON_NULL } """ diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 83f6abada8..ceb0487e18 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -20,6 +20,7 @@ import { GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLSemanticNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, @@ -137,6 +138,14 @@ export function buildClientSchema( const nullableType = getType(nullableRef); return new GraphQLNonNull(assertNullableType(nullableType)); } + if (typeRef.kind === TypeKind.SEMANTIC_NON_NULL) { + const nullableRef = typeRef.ofType; + if (!nullableRef) { + throw new Error('Decorated type deeper than introspection query.'); + } + const nullableType = getType(nullableRef); + return new GraphQLSemanticNonNull(assertNullableType(nullableType)); + } return getNamedType(typeRef); } diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index e81917d312..c133ee851e 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -51,6 +51,7 @@ import { GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLSemanticNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, @@ -437,6 +438,9 @@ export function extendSchemaImpl( if (node.kind === Kind.NON_NULL_TYPE) { return new GraphQLNonNull(getWrappedType(node.type)); } + if (node.kind === Kind.SEMANTIC_NON_NULL_TYPE) { + return new GraphQLSemanticNonNull(getWrappedType(node.type)); + } return getNamedType(node); } diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index e51551e807..587b1588bd 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -297,6 +297,9 @@ export type IntrospectionTypeRef = | IntrospectionListTypeRef | IntrospectionNonNullTypeRef< IntrospectionNamedTypeRef | IntrospectionListTypeRef + > + | IntrospectionSemanticNonNullTypeRef< + IntrospectionNamedTypeRef | IntrospectionListTypeRef >; export type IntrospectionOutputTypeRef = diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts index 7510df1046..c5d5f537a2 100644 --- a/src/utilities/typeFromAST.ts +++ b/src/utilities/typeFromAST.ts @@ -7,7 +7,11 @@ import type { import { Kind } from '../language/kinds'; import type { GraphQLNamedType, GraphQLType } from '../type/definition'; -import { GraphQLList, GraphQLNonNull } from '../type/definition'; +import { + GraphQLList, + GraphQLNonNull, + GraphQLSemanticNonNull, +} from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; /** @@ -46,6 +50,10 @@ export function typeFromAST( const innerType = typeFromAST(schema, typeNode.type); return innerType && new GraphQLNonNull(innerType); } + case Kind.SEMANTIC_NON_NULL_TYPE: { + const innerType = typeFromAST(schema, typeNode.type); + return innerType && new GraphQLSemanticNonNull(innerType); + } case Kind.NAMED_TYPE: return schema.getType(typeNode.name.value); } From c2196a05711635d9a8d700849e3412c296503678 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:11:02 -0700 Subject: [PATCH 06/30] Recognize in introspection, enable disabling null bubbling --- src/execution/execute.ts | 8 ++++ src/type/definition.ts | 2 + src/type/introspection.ts | 89 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 8d1af1e866..20c1fa2016 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -154,6 +154,11 @@ export interface ExecutionArgs { fieldResolver?: Maybe>; typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + /** + * @default {true} + * @experimental + */ + errorPropagation?: boolean; } /** @@ -288,6 +293,7 @@ export function buildExecutionContext( fieldResolver, typeResolver, subscribeFieldResolver, + errorPropagation, } = args; let operation: OperationDefinitionNode | undefined; @@ -349,6 +355,7 @@ export function buildExecutionContext( typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], + errorPropagation: errorPropagation ?? true, }; } @@ -587,6 +594,7 @@ export function buildResolveInfo( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, + errorPropagation: exeContext.errorPropagation, }; } diff --git a/src/type/definition.ts b/src/type/definition.ts index f2c9892400..554ec0fe41 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1087,6 +1087,8 @@ export interface GraphQLResolveInfo { readonly rootValue: unknown; readonly operation: OperationDefinitionNode; readonly variableValues: { [variable: string]: unknown }; + /** @experimental */ + readonly errorPropagation: boolean; } /** diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 9affc21777..669cd60968 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -19,6 +19,7 @@ import { GraphQLEnumType, GraphQLList, GraphQLNonNull, + GraphQLSemanticNonNull, GraphQLObjectType, isAbstractType, isEnumType, @@ -205,6 +206,40 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({ }, }); +// TODO: rename enum and options +enum TypeNullability { + AUTO = 'AUTO', + TRADITIONAL = 'TRADITIONAL', + SEMANTIC = 'SEMANTIC', + FULL = 'FULL', +} + +// TODO: rename +export const __TypeNullability: GraphQLEnumType = new GraphQLEnumType({ + name: '__TypeNullability', + description: 'TODO', + values: { + AUTO: { + value: TypeNullability.AUTO, + description: + 'Determines nullability mode based on errorPropagation mode.', + }, + TRADITIONAL: { + value: TypeNullability.TRADITIONAL, + description: 'Turn semantic-non-null types into nullable types.', + }, + SEMANTIC: { + value: TypeNullability.SEMANTIC, + description: 'Turn non-null types into semantic-non-null types.', + }, + FULL: { + value: TypeNullability.FULL, + description: + 'Render the true nullability in the schema; be prepared for new types of nullability in future!', + }, + }, +}); + export const __Type: GraphQLObjectType = new GraphQLObjectType({ name: '__Type', description: @@ -370,7 +405,25 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ }, type: { type: new GraphQLNonNull(__Type), - resolve: (field) => field.type, + args: { + nullability: { + type: __TypeNullability, + defaultValue: 'AUTO', + }, + }, + resolve: (field, { nullability }, _context, info) => { + if (nullability === TypeNullability.FULL) { + return field.type; + } else { + const mode = + nullability === TypeNullability.AUTO + ? info.errorPropagation + ? TypeNullability.TRADITIONAL + : TypeNullability.SEMANTIC + : nullability; + return convertOutputTypeToNullabilityMode(field.type, mode); + } + }, }, isDeprecated: { type: new GraphQLNonNull(GraphQLBoolean), @@ -383,6 +436,40 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap, unknown>), }); +// TODO: move this elsewhere, rename, memoize +function convertOutputTypeToNullabilityMode( + type: GraphQLType, + mode: TypeNullability.TRADITIONAL | TypeNullability.SEMANTIC, +): GraphQLType { + if (mode === TypeNullability.TRADITIONAL) { + if (isNonNullType(type)) { + return new GraphQLNonNull( + convertOutputTypeToNullabilityMode(type.ofType, mode), + ); + } else if (isSemanticNonNullType(type)) { + return convertOutputTypeToNullabilityMode(type.ofType, mode); + } else if (isListType(type)) { + return new GraphQLList( + convertOutputTypeToNullabilityMode(type.ofType, mode), + ); + } else { + return type; + } + } else { + if (isNonNullType(type) || isSemanticNonNullType(type)) { + return new GraphQLSemanticNonNull( + convertOutputTypeToNullabilityMode(type.ofType, mode), + ); + } else if (isListType(type)) { + return new GraphQLList( + convertOutputTypeToNullabilityMode(type.ofType, mode), + ); + } else { + return type; + } + } +} + export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ name: '__InputValue', description: From f5880469f769b07dd74619497c9073a33187a23e Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:15:04 -0700 Subject: [PATCH 07/30] Lint fixes --- src/execution/execute.ts | 5 ++-- src/language/parser.ts | 2 +- src/type/__tests__/predicate-test.ts | 6 ++--- src/type/introspection.ts | 25 +++++++++---------- src/utilities/buildClientSchema.ts | 2 +- src/utilities/extendSchema.ts | 4 +-- src/utilities/findBreakingChanges.ts | 15 ++++++----- src/utilities/lexicographicSortSchema.ts | 3 ++- src/utilities/typeComparators.ts | 2 +- .../rules/OverlappingFieldsCanBeMergedRule.ts | 7 +++--- 10 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 20c1fa2016..b7cb7f4a02 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -42,8 +42,8 @@ import { isLeafType, isListType, isNonNullType, - isSemanticNonNullType, isObjectType, + isSemanticNonNullType, } from '../type/definition'; import { SchemaMetaFieldDef, @@ -155,7 +155,8 @@ export interface ExecutionArgs { typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; /** - * @default {true} + * Set to `false` to disable error propagation. Experimental. + * * @experimental */ errorPropagation?: boolean; diff --git a/src/language/parser.ts b/src/language/parser.ts index afe8ddbba8..129849d5e7 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -37,7 +37,6 @@ import type { NamedTypeNode, NameNode, NonNullTypeNode, - SemanticNonNullTypeNode, NullValueNode, ObjectFieldNode, ObjectTypeDefinitionNode, @@ -51,6 +50,7 @@ import type { SchemaExtensionNode, SelectionNode, SelectionSetNode, + SemanticNonNullTypeNode, StringValueNode, Token, TypeNode, diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index e3395727bc..1c576e8eaa 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -19,11 +19,11 @@ import { assertListType, assertNamedType, assertNonNullType, - assertSemanticNonNullType, assertNullableType, assertObjectType, assertOutputType, assertScalarType, + assertSemanticNonNullType, assertType, assertUnionType, assertWrappingType, @@ -34,9 +34,9 @@ import { GraphQLInterfaceType, GraphQLList, GraphQLNonNull, - GraphQLSemanticNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLSemanticNonNull, GraphQLUnionType, isAbstractType, isCompositeType, @@ -48,13 +48,13 @@ import { isListType, isNamedType, isNonNullType, - isSemanticNonNullType, isNullableType, isObjectType, isOutputType, isRequiredArgument, isRequiredInputField, isScalarType, + isSemanticNonNullType, isType, isUnionType, isWrappingType, diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 669cd60968..66bad79b80 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -6,21 +6,19 @@ import { print } from '../language/printer'; import { astFromValue } from '../utilities/astFromValue'; -import { +import type { GraphQLEnumValue, GraphQLField, GraphQLFieldConfigMap, GraphQLInputField, GraphQLNamedType, - GraphQLType, - isSemanticNonNullType, -} from './definition'; + GraphQLType} from './definition'; import { GraphQLEnumType, GraphQLList, GraphQLNonNull, - GraphQLSemanticNonNull, GraphQLObjectType, + GraphQLSemanticNonNull, isAbstractType, isEnumType, isInputObjectType, @@ -29,6 +27,7 @@ import { isNonNullType, isObjectType, isScalarType, + isSemanticNonNullType, isUnionType, } from './definition'; import type { GraphQLDirective } from './directives'; @@ -414,7 +413,7 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ resolve: (field, { nullability }, _context, info) => { if (nullability === TypeNullability.FULL) { return field.type; - } else { + } const mode = nullability === TypeNullability.AUTO ? info.errorPropagation @@ -422,7 +421,7 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ : TypeNullability.SEMANTIC : nullability; return convertOutputTypeToNullabilityMode(field.type, mode); - } + }, }, isDeprecated: { @@ -452,10 +451,10 @@ function convertOutputTypeToNullabilityMode( return new GraphQLList( convertOutputTypeToNullabilityMode(type.ofType, mode), ); - } else { + } return type; - } - } else { + + } if (isNonNullType(type) || isSemanticNonNullType(type)) { return new GraphQLSemanticNonNull( convertOutputTypeToNullabilityMode(type.ofType, mode), @@ -464,10 +463,10 @@ function convertOutputTypeToNullabilityMode( return new GraphQLList( convertOutputTypeToNullabilityMode(type.ofType, mode), ); - } else { + } return type; - } - } + + } export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index ceb0487e18..9b0809adf5 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -20,9 +20,9 @@ import { GraphQLInterfaceType, GraphQLList, GraphQLNonNull, - GraphQLSemanticNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLSemanticNonNull, GraphQLUnionType, isInputType, isOutputType, diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index c133ee851e..876aae277f 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -51,18 +51,18 @@ import { GraphQLInterfaceType, GraphQLList, GraphQLNonNull, - GraphQLSemanticNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLSemanticNonNull, GraphQLUnionType, isEnumType, isInputObjectType, isInterfaceType, isListType, isNonNullType, - isSemanticNonNullType, isObjectType, isScalarType, + isSemanticNonNullType, isUnionType, } from '../type/definition'; import { diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index c7ab88504f..5ed0313ae3 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -22,11 +22,11 @@ import { isListType, isNamedType, isNonNullType, - isSemanticNonNullType, isObjectType, isRequiredArgument, isRequiredInputField, isScalarType, + isSemanticNonNullType, isUnionType, } from '../type/definition'; import { isSpecifiedScalarType } from '../type/scalars'; @@ -462,7 +462,7 @@ function isChangeSafeForObjectOrInterfaceField( isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) || // moving from nullable to semantic-non-null of the same underlying type is safe (isSemanticNonNullType(newType) && - isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) || + isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) ); } @@ -477,11 +477,14 @@ function isChangeSafeForObjectOrInterfaceField( if (isSemanticNonNullType(oldType)) { return ( // if they're both semantic-non-null, make sure the underlying types are compatible - (isSemanticNonNullType(newType) && - isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)) || + (isSemanticNonNullType(newType) && + isChangeSafeForObjectOrInterfaceField( + oldType.ofType, + newType.ofType, + )) || // moving from semantic-non-null to non-null of the same underlying type is safe - isNonNullType(newType) && - isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType) + (isNonNullType(newType) && + isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)) ); } diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index 728cf23380..5beb646859 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -19,15 +19,16 @@ import { GraphQLList, GraphQLNonNull, GraphQLObjectType, + GraphQLSemanticNonNull, GraphQLUnionType, isEnumType, isInputObjectType, isInterfaceType, isListType, isNonNullType, - isSemanticNonNullType, isObjectType, isScalarType, + isSemanticNonNullType, isUnionType, } from '../type/definition'; import { GraphQLDirective } from '../type/directives'; diff --git a/src/utilities/typeComparators.ts b/src/utilities/typeComparators.ts index 79116e482b..338ca24528 100644 --- a/src/utilities/typeComparators.ts +++ b/src/utilities/typeComparators.ts @@ -4,8 +4,8 @@ import { isInterfaceType, isListType, isNonNullType, - isSemanticNonNullType, isObjectType, + isSemanticNonNullType, } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index cba8daf3c5..da56d827f1 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -15,12 +15,10 @@ import { Kind } from '../../language/kinds'; import { print } from '../../language/printer'; import type { ASTVisitor } from '../../language/visitor'; -import { +import type { GraphQLField, GraphQLNamedType, - GraphQLOutputType, - isSemanticNonNullType, -} from '../../type/definition'; + GraphQLOutputType} from '../../type/definition'; import { getNamedType, isInterfaceType, @@ -28,6 +26,7 @@ import { isListType, isNonNullType, isObjectType, + isSemanticNonNullType, } from '../../type/definition'; import { sortValueNode } from '../../utilities/sortValueNode'; From fa3f1778d8bbf9632bad7c95b2db12004070ea07 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:17:36 -0700 Subject: [PATCH 08/30] More missing pieces --- src/language/__tests__/predicates-test.ts | 1 + src/language/ast.ts | 1 + src/language/printer.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts index 13477f8de9..32ef7d1fe1 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -92,6 +92,7 @@ describe('AST node predicates', () => { 'NamedType', 'ListType', 'NonNullType', + 'SemanticNonNullType', ]); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index 9ad68ea0c3..f3fc60c3cb 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -236,6 +236,7 @@ export const QueryDocumentKeys: { NamedType: ['name'], ListType: ['type'], NonNullType: ['type'], + SemanticNonNullType: ['type'], SchemaDefinition: ['description', 'directives', 'operationTypes'], OperationTypeDefinition: ['type'], diff --git a/src/language/printer.ts b/src/language/printer.ts index e95c118d8b..f08ba54f56 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -131,6 +131,7 @@ const printDocASTReducer: ASTReducer = { NamedType: { leave: ({ name }) => name }, ListType: { leave: ({ type }) => '[' + type + ']' }, NonNullType: { leave: ({ type }) => type + '!' }, + SemanticNonNullType: { leave: ({ type }) => type + '*' }, // Type System Definitions From b5e81bdd5f96dcd705bb86cede9aaea4b865af61 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:26:12 -0700 Subject: [PATCH 09/30] More fixes --- src/__tests__/starWarsIntrospection-test.ts | 1 + src/index.ts | 1 + src/type/__tests__/introspection-test.ts | 47 +++++++++++++++++- src/type/__tests__/schema-test.ts | 1 + src/type/index.ts | 1 + src/type/introspection.ts | 54 ++++++++++----------- src/utilities/__tests__/printSchema-test.ts | 9 +++- 7 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index d637787c4a..f72b04e855 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -45,6 +45,7 @@ describe('Star Wars Introspection Tests', () => { { name: '__EnumValue' }, { name: '__Directive' }, { name: '__DirectiveLocation' }, + { name: '__TypeNullability' }, ], }, }); diff --git a/src/index.ts b/src/index.ts index e3896fbf05..2940df03fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,6 +75,7 @@ export { __Schema, __Directive, __DirectiveLocation, + __TypeNullability, __Type, __Field, __InputValue, diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 29994c76ed..1c3896cc23 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -506,7 +506,21 @@ describe('Introspection', () => { }, { name: 'type', - args: [], + args: [ + { + name: 'nullability', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'ENUM', + name: '__TypeNullability', + ofType: null, + }, + }, + defaultValue: 'AUTO', + }, + ], type: { kind: 'NON_NULL', name: null, @@ -917,6 +931,37 @@ describe('Introspection', () => { ], possibleTypes: null, }, + { + kind: 'ENUM', + name: '__TypeNullability', + specifiedByURL: null, + fields: null, + inputFields: null, + interfaces: null, + enumValues: [ + { + name: 'AUTO', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'TRADITIONAL', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'SEMANTIC', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'FULL', + isDeprecated: false, + deprecationReason: null, + }, + ], + possibleTypes: null, + }, ], directives: [ { diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index 8a31b50ada..357135bace 100644 --- a/src/type/__tests__/schema-test.ts +++ b/src/type/__tests__/schema-test.ts @@ -304,6 +304,7 @@ describe('Type System: Schema', () => { '__EnumValue', '__Directive', '__DirectiveLocation', + '__TypeNullability', ]); // Also check that this order is stable diff --git a/src/type/index.ts b/src/type/index.ts index 0729b840e7..e6cf627bd5 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -170,6 +170,7 @@ export { __Schema, __Directive, __DirectiveLocation, + __TypeNullability, __Type, __Field, __InputValue, diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 66bad79b80..186a20f8d3 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -12,7 +12,8 @@ import type { GraphQLFieldConfigMap, GraphQLInputField, GraphQLNamedType, - GraphQLType} from './definition'; + GraphQLType, +} from './definition'; import { GraphQLEnumType, GraphQLList, @@ -406,22 +407,21 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ type: new GraphQLNonNull(__Type), args: { nullability: { - type: __TypeNullability, - defaultValue: 'AUTO', + type: new GraphQLNonNull(__TypeNullability), + defaultValue: TypeNullability.AUTO, }, }, resolve: (field, { nullability }, _context, info) => { if (nullability === TypeNullability.FULL) { return field.type; - } - const mode = - nullability === TypeNullability.AUTO - ? info.errorPropagation - ? TypeNullability.TRADITIONAL - : TypeNullability.SEMANTIC - : nullability; - return convertOutputTypeToNullabilityMode(field.type, mode); - + } + const mode = + nullability === TypeNullability.AUTO + ? info.errorPropagation + ? TypeNullability.TRADITIONAL + : TypeNullability.SEMANTIC + : nullability; + return convertOutputTypeToNullabilityMode(field.type, mode); }, }, isDeprecated: { @@ -451,22 +451,19 @@ function convertOutputTypeToNullabilityMode( return new GraphQLList( convertOutputTypeToNullabilityMode(type.ofType, mode), ); - } - return type; - - } - if (isNonNullType(type) || isSemanticNonNullType(type)) { - return new GraphQLSemanticNonNull( - convertOutputTypeToNullabilityMode(type.ofType, mode), - ); - } else if (isListType(type)) { - return new GraphQLList( - convertOutputTypeToNullabilityMode(type.ofType, mode), - ); - } - return type; - - + } + return type; + } + if (isNonNullType(type) || isSemanticNonNullType(type)) { + return new GraphQLSemanticNonNull( + convertOutputTypeToNullabilityMode(type.ofType, mode), + ); + } else if (isListType(type)) { + return new GraphQLList( + convertOutputTypeToNullabilityMode(type.ofType, mode), + ); + } + return type; } export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ @@ -649,6 +646,7 @@ export const introspectionTypes: ReadonlyArray = __Schema, __Directive, __DirectiveLocation, + __TypeNullability, __Type, __Field, __InputValue, diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index ef6376e183..33c3d2e3b1 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -782,7 +782,7 @@ describe('Type System Printer', () => { name: String! description: String args(includeDeprecated: Boolean = false): [__InputValue!]! - type: __Type! + type(nullability: __TypeNullability!): __Type! isDeprecated: Boolean! deprecationReason: String } @@ -887,6 +887,13 @@ describe('Type System Printer', () => { """Location adjacent to an input object field definition.""" INPUT_FIELD_DEFINITION } + + enum __TypeNullability { + AUTO + TRADITIONAL + SEMANTIC + FULL + } `); }); }); From 1f6a0197419a29d92de25f6dd97c2c56d8f223b2 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:28:38 -0700 Subject: [PATCH 10/30] Fix schema --- src/utilities/__tests__/printSchema-test.ts | 26 ++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 33c3d2e3b1..b651bf16a8 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -782,7 +782,7 @@ describe('Type System Printer', () => { name: String! description: String args(includeDeprecated: Boolean = false): [__InputValue!]! - type(nullability: __TypeNullability!): __Type! + type(nullability: __TypeNullability! = AUTO): __Type! isDeprecated: Boolean! deprecationReason: String } @@ -803,6 +803,23 @@ describe('Type System Printer', () => { deprecationReason: String } + """TODO""" + enum __TypeNullability { + """Determines nullability mode based on errorPropagation mode.""" + AUTO + + """Turn semantic-non-null types into nullable types.""" + TRADITIONAL + + """Turn non-null types into semantic-non-null types.""" + SEMANTIC + + """ + Render the true nullability in the schema; be prepared for new types of nullability in future! + """ + FULL + } + """ One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. """ @@ -887,13 +904,6 @@ describe('Type System Printer', () => { """Location adjacent to an input object field definition.""" INPUT_FIELD_DEFINITION } - - enum __TypeNullability { - AUTO - TRADITIONAL - SEMANTIC - FULL - } `); }); }); From 491f49b6cad487a60e79af70b8a112d4f2f0c7d1 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:31:03 -0700 Subject: [PATCH 11/30] Fix another test --- src/type/__tests__/schema-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index 357135bace..dc2c7c75c8 100644 --- a/src/type/__tests__/schema-test.ts +++ b/src/type/__tests__/schema-test.ts @@ -301,10 +301,10 @@ describe('Type System: Schema', () => { '__TypeKind', '__Field', '__InputValue', + '__TypeNullability', '__EnumValue', '__Directive', '__DirectiveLocation', - '__TypeNullability', ]); // Also check that this order is stable From 3a91590cc869bd0fde133e721a3369c9ea2559a8 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:34:59 -0700 Subject: [PATCH 12/30] More minor test fixes --- src/__tests__/starWarsIntrospection-test.ts | 2 +- src/execution/__tests__/executor-test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index f72b04e855..0dc95f0a7e 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -42,10 +42,10 @@ describe('Star Wars Introspection Tests', () => { { name: '__TypeKind' }, { name: '__Field' }, { name: '__InputValue' }, + { name: '__TypeNullability' }, { name: '__EnumValue' }, { name: '__Directive' }, { name: '__DirectiveLocation' }, - { name: '__TypeNullability' }, ], }, }); diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index c758d3e426..a7bc1c8265 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -263,6 +263,7 @@ describe('Execute: Handles basic execution tasks', () => { 'rootValue', 'operation', 'variableValues', + 'errorPropagation', ); const operation = document.definitions[0]; @@ -275,6 +276,7 @@ describe('Execute: Handles basic execution tasks', () => { schema, rootValue, operation, + errorPropagation: true, }); const field = operation.selectionSet.selections[0]; From 56db880c072fe658dfad046e35f058c0974b6775 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:39:39 -0700 Subject: [PATCH 13/30] Fix introspection test --- src/type/__tests__/introspection-test.ts | 67 +++++++++++++----------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 1c3896cc23..08273f495f 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -437,6 +437,11 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'SEMANTIC_NON_NULL', + isDeprecated: false, + deprecationReason: null, + }, ], possibleTypes: null, }, @@ -654,6 +659,37 @@ describe('Introspection', () => { enumValues: null, possibleTypes: null, }, + { + kind: 'ENUM', + name: '__TypeNullability', + specifiedByURL: null, + fields: null, + inputFields: null, + interfaces: null, + enumValues: [ + { + name: 'AUTO', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'TRADITIONAL', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'SEMANTIC', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'FULL', + isDeprecated: false, + deprecationReason: null, + }, + ], + possibleTypes: null, + }, { kind: 'OBJECT', name: '__EnumValue', @@ -931,37 +967,6 @@ describe('Introspection', () => { ], possibleTypes: null, }, - { - kind: 'ENUM', - name: '__TypeNullability', - specifiedByURL: null, - fields: null, - inputFields: null, - interfaces: null, - enumValues: [ - { - name: 'AUTO', - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'TRADITIONAL', - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'SEMANTIC', - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'FULL', - isDeprecated: false, - deprecationReason: null, - }, - ], - possibleTypes: null, - }, ], directives: [ { From 593ce448b1f595bc83cf3d9854bab1cdb9090828 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:41:55 -0700 Subject: [PATCH 14/30] Add support for * to lexer --- src/language/lexer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/language/lexer.ts b/src/language/lexer.ts index 818f81b286..8fccd4e709 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -91,6 +91,7 @@ export class Lexer { export function isPunctuatorTokenKind(kind: TokenKind): boolean { return ( kind === TokenKind.BANG || + kind === TokenKind.ASTERISK || kind === TokenKind.DOLLAR || kind === TokenKind.AMP || kind === TokenKind.PAREN_L || @@ -246,7 +247,7 @@ function readNextToken(lexer: Lexer, start: number): Token { // - FloatValue // - StringValue // - // Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | } + // Punctuator :: one of ! $ & ( ) * ... : = @ [ ] { | } case 0x0021: // ! return createToken(lexer, TokenKind.BANG, position, position + 1); case 0x0024: // $ @@ -257,6 +258,8 @@ function readNextToken(lexer: Lexer, start: number): Token { return createToken(lexer, TokenKind.PAREN_L, position, position + 1); case 0x0029: // ) return createToken(lexer, TokenKind.PAREN_R, position, position + 1); + case 0x002a: // * + return createToken(lexer, TokenKind.ASTERISK, position, position + 1); case 0x002e: // . if ( body.charCodeAt(position + 1) === 0x002e && From 131190604e240b1bad55cab440d1afbded94bdd4 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:49:32 -0700 Subject: [PATCH 15/30] Allow specifying errorPropagation at top level --- src/graphql.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/graphql.ts b/src/graphql.ts index bc6fb9bb72..d3f05f991e 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -66,6 +66,12 @@ export interface GraphQLArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + /** + * Set to `false` to disable error propagation. Experimental. + * + * @experimental + */ + errorPropagation?: boolean; } export function graphql(args: GraphQLArgs): Promise { @@ -106,6 +112,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + errorPropagation, } = args; // Validate Schema @@ -138,5 +145,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + errorPropagation, }); } From 9d706d2d6701ac32856e7d08aa004049e72fa8a5 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:58:03 -0700 Subject: [PATCH 16/30] Factor into getIntrospectionQuery --- src/utilities/getIntrospectionQuery.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 587b1588bd..8c94ba8f79 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -38,6 +38,17 @@ export interface IntrospectionOptions { * Default: false */ oneOf?: boolean; + + /** + * Choose the type of nullability you would like to see. + * + * - AUTO: SEMANTIC if errorPropagation is set to false, otherwise TRADITIONAL + * - TRADITIONAL: all GraphQLSemanticNonNull will be unwrapped + * - SEMANTIC: all GraphQLNonNull will be converted to GraphQLSemanticNonNull + * - FULL: the true nullability will be returned + * + */ + nullability?: 'AUTO' | 'TRADITIONAL' | 'SEMANTIC' | 'FULL'; } /** @@ -52,6 +63,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { schemaDescription: false, inputValueDeprecation: false, oneOf: false, + nullability: null, ...options, }; @@ -70,6 +82,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { return optionsWithDefault.inputValueDeprecation ? str : ''; } const oneOf = optionsWithDefault.oneOf ? 'isOneOf' : ''; + const nullability = optionsWithDefault.nullability; return ` query IntrospectionQuery { @@ -105,7 +118,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { args${inputDeprecation('(includeDeprecated: true)')} { ...InputValue } - type { + type${nullability ? `(nullability: ${nullability})` : ``} { ...TypeRef } isDeprecated From e9f9628a35f2dddc510bd030d7cb75618445c50e Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:59:15 -0700 Subject: [PATCH 17/30] Lint --- src/utilities/getIntrospectionQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 8c94ba8f79..26340991ce 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -118,7 +118,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { args${inputDeprecation('(includeDeprecated: true)')} { ...InputValue } - type${nullability ? `(nullability: ${nullability})` : ``} { + type${nullability ? `(nullability: ${nullability})` : ''} { ...TypeRef } isDeprecated From eb9b6c8a77193fc5ed3685d1e95d32b99c9df9d9 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 21:02:07 -0700 Subject: [PATCH 18/30] Prettier --- src/validation/rules/OverlappingFieldsCanBeMergedRule.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index da56d827f1..8ecd96212f 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -18,7 +18,8 @@ import type { ASTVisitor } from '../../language/visitor'; import type { GraphQLField, GraphQLNamedType, - GraphQLOutputType} from '../../type/definition'; + GraphQLOutputType, +} from '../../type/definition'; import { getNamedType, isInterfaceType, From 8fcacc85db72f06bfd9a5f1a72db1abf014df378 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 17:20:07 +0000 Subject: [PATCH 19/30] Switch to errorBehavior, replace contextual introspection to simple includeSemanticNonNull --- src/__tests__/starWarsIntrospection-test.ts | 1 - src/execution/__tests__/executor-test.ts | 4 +- src/execution/execute.ts | 36 ++++++-- src/graphql.ts | 11 ++- src/index.ts | 1 - src/type/__tests__/introspection-test.ts | 39 +-------- src/type/__tests__/schema-test.ts | 1 - src/type/definition.ts | 2 +- src/type/index.ts | 1 - src/type/introspection.ts | 93 +++++---------------- src/utilities/__tests__/printSchema-test.ts | 19 +---- src/utilities/getIntrospectionQuery.ts | 21 +++-- 12 files changed, 73 insertions(+), 156 deletions(-) diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index 0dc95f0a7e..d637787c4a 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -42,7 +42,6 @@ describe('Star Wars Introspection Tests', () => { { name: '__TypeKind' }, { name: '__Field' }, { name: '__InputValue' }, - { name: '__TypeNullability' }, { name: '__EnumValue' }, { name: '__Directive' }, { name: '__DirectiveLocation' }, diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index a7bc1c8265..80be5a9ff0 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -263,7 +263,7 @@ describe('Execute: Handles basic execution tasks', () => { 'rootValue', 'operation', 'variableValues', - 'errorPropagation', + 'errorBehavior', ); const operation = document.definitions[0]; @@ -276,7 +276,7 @@ describe('Execute: Handles basic execution tasks', () => { schema, rootValue, operation, - errorPropagation: true, + errorBehavior: 'PROPAGATE', }); const field = operation.selectionSet.selections[0]; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index b7cb7f4a02..f789f585f3 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -116,7 +116,7 @@ export interface ExecutionContext { typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; errors: Array; - errorPropagation: boolean; + errorBehavior: 'PROPAGATE' | 'NULL' | 'ABORT'; } /** @@ -155,11 +155,14 @@ export interface ExecutionArgs { typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; /** - * Set to `false` to disable error propagation. Experimental. + * Experimental. Set to NULL to prevent error propagation. Set to ABORT to + * abort a request when any error occurs. + * + * Default: PROPAGATE * * @experimental */ - errorPropagation?: boolean; + errorBehavior?: 'PROPAGATE' | 'NULL' | 'ABORT'; } /** @@ -294,7 +297,7 @@ export function buildExecutionContext( fieldResolver, typeResolver, subscribeFieldResolver, - errorPropagation, + errorBehavior, } = args; let operation: OperationDefinitionNode | undefined; @@ -356,7 +359,7 @@ export function buildExecutionContext( typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], - errorPropagation: errorPropagation ?? true, + errorBehavior: errorBehavior ?? 'PROPAGATE', }; } @@ -595,7 +598,7 @@ export function buildResolveInfo( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - errorPropagation: exeContext.errorPropagation, + errorBehavior: exeContext.errorBehavior, }; } @@ -604,10 +607,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 (exeContext.errorPropagation && 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 === 'NULL') { + // 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 d3f05f991e..5214ab8ac6 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -67,11 +67,14 @@ export interface GraphQLArgs { fieldResolver?: Maybe>; typeResolver?: Maybe>; /** - * Set to `false` to disable error propagation. Experimental. + * Experimental. Set to NULL to prevent error propagation. Set to ABORT to + * abort a request when any error occurs. + * + * Default: PROPAGATE * * @experimental */ - errorPropagation?: boolean; + errorBehavior?: 'PROPAGATE' | 'NULL' | 'ABORT'; } export function graphql(args: GraphQLArgs): Promise { @@ -112,7 +115,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, - errorPropagation, + errorBehavior, } = args; // Validate Schema @@ -145,6 +148,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, - errorPropagation, + errorBehavior, }); } diff --git a/src/index.ts b/src/index.ts index dacea5d145..d305cfb434 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,7 +75,6 @@ export { __Schema, __Directive, __DirectiveLocation, - __TypeNullability, __Type, __Field, __InputValue, diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index f57baf8894..77d024c9cf 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -513,17 +513,17 @@ describe('Introspection', () => { name: 'type', args: [ { - name: 'nullability', + name: 'includeSemanticNonNull', type: { kind: 'NON_NULL', name: null, ofType: { - kind: 'ENUM', - name: '__TypeNullability', + kind: 'SCALAR', + name: 'Boolean', ofType: null, }, }, - defaultValue: 'AUTO', + defaultValue: 'false', }, ], type: { @@ -659,37 +659,6 @@ describe('Introspection', () => { enumValues: null, possibleTypes: null, }, - { - kind: 'ENUM', - name: '__TypeNullability', - specifiedByURL: null, - fields: null, - inputFields: null, - interfaces: null, - enumValues: [ - { - name: 'AUTO', - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'TRADITIONAL', - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'SEMANTIC', - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'FULL', - isDeprecated: false, - deprecationReason: null, - }, - ], - possibleTypes: null, - }, { kind: 'OBJECT', name: '__EnumValue', diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index dc2c7c75c8..8a31b50ada 100644 --- a/src/type/__tests__/schema-test.ts +++ b/src/type/__tests__/schema-test.ts @@ -301,7 +301,6 @@ describe('Type System: Schema', () => { '__TypeKind', '__Field', '__InputValue', - '__TypeNullability', '__EnumValue', '__Directive', '__DirectiveLocation', diff --git a/src/type/definition.ts b/src/type/definition.ts index 554ec0fe41..2854f9362b 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1088,7 +1088,7 @@ export interface GraphQLResolveInfo { readonly operation: OperationDefinitionNode; readonly variableValues: { [variable: string]: unknown }; /** @experimental */ - readonly errorPropagation: boolean; + readonly errorBehavior: 'PROPAGATE' | 'NULL' | 'ABORT'; } /** diff --git a/src/type/index.ts b/src/type/index.ts index e6cf627bd5..0729b840e7 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -170,7 +170,6 @@ export { __Schema, __Directive, __DirectiveLocation, - __TypeNullability, __Type, __Field, __InputValue, diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 186a20f8d3..7036973c1d 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -12,6 +12,7 @@ import type { GraphQLFieldConfigMap, GraphQLInputField, GraphQLNamedType, + GraphQLOutputType, GraphQLType, } from './definition'; import { @@ -19,7 +20,6 @@ import { GraphQLList, GraphQLNonNull, GraphQLObjectType, - GraphQLSemanticNonNull, isAbstractType, isEnumType, isInputObjectType, @@ -206,40 +206,6 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({ }, }); -// TODO: rename enum and options -enum TypeNullability { - AUTO = 'AUTO', - TRADITIONAL = 'TRADITIONAL', - SEMANTIC = 'SEMANTIC', - FULL = 'FULL', -} - -// TODO: rename -export const __TypeNullability: GraphQLEnumType = new GraphQLEnumType({ - name: '__TypeNullability', - description: 'TODO', - values: { - AUTO: { - value: TypeNullability.AUTO, - description: - 'Determines nullability mode based on errorPropagation mode.', - }, - TRADITIONAL: { - value: TypeNullability.TRADITIONAL, - description: 'Turn semantic-non-null types into nullable types.', - }, - SEMANTIC: { - value: TypeNullability.SEMANTIC, - description: 'Turn non-null types into semantic-non-null types.', - }, - FULL: { - value: TypeNullability.FULL, - description: - 'Render the true nullability in the schema; be prepared for new types of nullability in future!', - }, - }, -}); - export const __Type: GraphQLObjectType = new GraphQLObjectType({ name: '__Type', description: @@ -406,22 +372,17 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ type: { type: new GraphQLNonNull(__Type), args: { - nullability: { - type: new GraphQLNonNull(__TypeNullability), - defaultValue: TypeNullability.AUTO, + includeSemanticNonNull: { + type: new GraphQLNonNull(GraphQLBoolean), + defaultValue: false, }, }, - resolve: (field, { nullability }, _context, info) => { - if (nullability === TypeNullability.FULL) { + resolve: (field, { includeSemanticNonNull }, _context) => { + if (includeSemanticNonNull) { return field.type; } - const mode = - nullability === TypeNullability.AUTO - ? info.errorPropagation - ? TypeNullability.TRADITIONAL - : TypeNullability.SEMANTIC - : nullability; - return convertOutputTypeToNullabilityMode(field.type, mode); + // TODO: memoize + return stripSemanticNonNullTypes(field.type); }, }, isDeprecated: { @@ -436,32 +397,21 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ }); // TODO: move this elsewhere, rename, memoize -function convertOutputTypeToNullabilityMode( - type: GraphQLType, - mode: TypeNullability.TRADITIONAL | TypeNullability.SEMANTIC, -): GraphQLType { - if (mode === TypeNullability.TRADITIONAL) { - if (isNonNullType(type)) { - return new GraphQLNonNull( - convertOutputTypeToNullabilityMode(type.ofType, mode), - ); - } else if (isSemanticNonNullType(type)) { - return convertOutputTypeToNullabilityMode(type.ofType, mode); - } else if (isListType(type)) { - return new GraphQLList( - convertOutputTypeToNullabilityMode(type.ofType, mode), - ); +function stripSemanticNonNullTypes(type: GraphQLOutputType): GraphQLOutputType { + if (isNonNullType(type)) { + const convertedInner = stripSemanticNonNullTypes(type.ofType); + if (convertedInner === type.ofType) { + return type; // No change needed } - return type; - } - if (isNonNullType(type) || isSemanticNonNullType(type)) { - return new GraphQLSemanticNonNull( - convertOutputTypeToNullabilityMode(type.ofType, mode), - ); + return new GraphQLNonNull(convertedInner); } else if (isListType(type)) { - return new GraphQLList( - convertOutputTypeToNullabilityMode(type.ofType, mode), - ); + const convertedInner = stripSemanticNonNullTypes(type.ofType); + if (convertedInner === type.ofType) { + return type; // No change needed + } + return new GraphQLList(convertedInner); + } else if (isSemanticNonNullType(type)) { + return stripSemanticNonNullTypes(type.ofType); } return type; } @@ -646,7 +596,6 @@ export const introspectionTypes: ReadonlyArray = __Schema, __Directive, __DirectiveLocation, - __TypeNullability, __Type, __Field, __InputValue, diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index b651bf16a8..4596a73cf4 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -782,7 +782,7 @@ describe('Type System Printer', () => { name: String! description: String args(includeDeprecated: Boolean = false): [__InputValue!]! - type(nullability: __TypeNullability! = AUTO): __Type! + type(includeSemanticNonNull: Boolean! = false): __Type! isDeprecated: Boolean! deprecationReason: String } @@ -803,23 +803,6 @@ describe('Type System Printer', () => { deprecationReason: String } - """TODO""" - enum __TypeNullability { - """Determines nullability mode based on errorPropagation mode.""" - AUTO - - """Turn semantic-non-null types into nullable types.""" - TRADITIONAL - - """Turn non-null types into semantic-non-null types.""" - SEMANTIC - - """ - Render the true nullability in the schema; be prepared for new types of nullability in future! - """ - FULL - } - """ One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. """ diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index dda0e7f19a..fa39a3ee58 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -40,15 +40,10 @@ export interface IntrospectionOptions { oneOf?: boolean; /** - * Choose the type of nullability you would like to see. - * - * - AUTO: SEMANTIC if errorPropagation is set to false, otherwise TRADITIONAL - * - TRADITIONAL: all GraphQLSemanticNonNull will be unwrapped - * - SEMANTIC: all GraphQLNonNull will be converted to GraphQLSemanticNonNull - * - FULL: the true nullability will be returned - * + * Whether semantic non-null type wrappers should be included in the result. + * Default: false */ - nullability?: 'AUTO' | 'TRADITIONAL' | 'SEMANTIC' | 'FULL'; + includeSemanticNonNull?: boolean; } /** @@ -63,7 +58,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { schemaDescription: false, inputValueDeprecation: false, oneOf: false, - nullability: null, + includeSemanticNonNull: false, ...options, }; @@ -82,7 +77,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { return optionsWithDefault.inputValueDeprecation ? str : ''; } const oneOf = optionsWithDefault.oneOf ? 'isOneOf' : ''; - const nullability = optionsWithDefault.nullability; + const includeSemanticNonNull = optionsWithDefault.includeSemanticNonNull; return ` query IntrospectionQuery { @@ -118,7 +113,11 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { args${inputDeprecation('(includeDeprecated: true)')} { ...InputValue } - type${nullability ? `(nullability: ${nullability})` : ''} { + type${ + includeSemanticNonNull + ? `(includeSemanticNonNull: ${includeSemanticNonNull})` + : '' + } { ...TypeRef } isDeprecated From 88c5d93aa1e7dcae2430ac3b22ba78b14de6bdf8 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 17:24:04 +0000 Subject: [PATCH 20/30] Simplify --- src/execution/execute.ts | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index f789f585f3..d179dc8582 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -668,9 +668,9 @@ function completeValue( throw result; } - // If field type is NonNull, complete for inner type, and throw field error + // If field type is non-nullable, complete for inner type, and throw field error // if result is null. - if (isNonNullType(returnType)) { + if (isNonNullType(returnType) || isSemanticNonNullType(returnType)) { const completed = completeValue( exeContext, returnType.ofType, @@ -681,26 +681,9 @@ function completeValue( ); if (completed === null) { throw new Error( - `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, - ); - } - return completed; - } - - // If field type is SemanticNonNull, complete for inner type, and throw field error - // if result is null. - if (isSemanticNonNullType(returnType)) { - const completed = completeValue( - exeContext, - returnType.ofType, - fieldNodes, - info, - path, - result, - ); - if (completed === null) { - throw new Error( - `Cannot return null for semantic-non-nullable field ${info.parentType.name}.${info.fieldName}.`, + `Cannot return null for ${ + isSemanticNonNullType(returnType) ? 'semantically ' : '' + }non-nullable field ${info.parentType.name}.${info.fieldName}.`, ); } return completed; From 62d1b7520cc00ba20a84cf176e2105a47f96b220 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 17:48:02 +0000 Subject: [PATCH 21/30] Stricter types: semantic non null may only wrap output types --- src/type/definition.ts | 29 ++++++++++++----------------- src/utilities/buildClientSchema.ts | 9 +++++++-- src/utilities/extendSchema.ts | 7 ++++++- src/utilities/typeFromAST.ts | 4 ++++ 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/type/definition.ts b/src/type/definition.ts index 2854f9362b..84f451ca16 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -73,8 +73,7 @@ export type GraphQLType = | GraphQLInterfaceType | GraphQLUnionType | GraphQLEnumType - | GraphQLInputObjectType - | GraphQLList + | GraphQLList >; export function isType(type: unknown): type is GraphQLType { @@ -213,24 +212,15 @@ export function assertNonNullType(type: unknown): GraphQLNonNull { return type; } -export function isSemanticNonNullType( - type: GraphQLInputType, -): type is GraphQLSemanticNonNull; -export function isSemanticNonNullType( - type: GraphQLOutputType, -): type is GraphQLSemanticNonNull; -export function isSemanticNonNullType( - type: unknown, -): type is GraphQLSemanticNonNull; export function isSemanticNonNullType( type: unknown, -): type is GraphQLSemanticNonNull { +): type is GraphQLSemanticNonNull { return instanceOf(type, GraphQLSemanticNonNull); } export function assertSemanticNonNullType( type: unknown, -): GraphQLSemanticNonNull { +): GraphQLSemanticNonNull { if (!isSemanticNonNullType(type)) { throw new Error( `Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`, @@ -485,7 +475,9 @@ export class GraphQLNonNull { * * @experimental */ -export class GraphQLSemanticNonNull { +export class GraphQLSemanticNonNull< + T extends GraphQLNullableType & GraphQLOutputType, +> { readonly ofType: T; constructor(ofType: T) { @@ -516,8 +508,8 @@ export class GraphQLSemanticNonNull { export type GraphQLWrappingType = | GraphQLList - | GraphQLNonNull - | GraphQLSemanticNonNull; + | GraphQLNonNull + | GraphQLSemanticNonNull; export function isWrappingType(type: unknown): type is GraphQLWrappingType { return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type); @@ -555,7 +547,10 @@ export function assertNullableType(type: unknown): GraphQLNullableType { export function getNullableType(type: undefined | null): void; export function getNullableType( - type: T | GraphQLNonNull | GraphQLSemanticNonNull, + type: + | T + | GraphQLNonNull + | GraphQLSemanticNonNull, ): T; export function getNullableType( type: Maybe, diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 9b0809adf5..35dd0cac22 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -143,8 +143,13 @@ export function buildClientSchema( if (!nullableRef) { throw new Error('Decorated type deeper than introspection query.'); } - const nullableType = getType(nullableRef); - return new GraphQLSemanticNonNull(assertNullableType(nullableType)); + const nullableType = assertNullableType(getType(nullableRef)); + if (!isOutputType(nullableType)) { + throw new Error( + 'A semantic non-null wrapper must wrap an output type.', + ); + } + return new GraphQLSemanticNonNull(nullableType); } return getNamedType(typeRef); } diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 876aae277f..f33feaa038 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -61,6 +61,7 @@ import { isListType, isNonNullType, isObjectType, + isOutputType, isScalarType, isSemanticNonNullType, isUnionType, @@ -439,7 +440,11 @@ export function extendSchemaImpl( return new GraphQLNonNull(getWrappedType(node.type)); } if (node.kind === Kind.SEMANTIC_NON_NULL_TYPE) { - return new GraphQLSemanticNonNull(getWrappedType(node.type)); + const wrapped = getWrappedType(node.type); + if (!isOutputType(wrapped)) { + throw new Error('A semantic non-null type cannot wrap an input type.'); + } + return new GraphQLSemanticNonNull(wrapped); } return getNamedType(node); } diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts index c5d5f537a2..67b7bbc5a2 100644 --- a/src/utilities/typeFromAST.ts +++ b/src/utilities/typeFromAST.ts @@ -11,6 +11,7 @@ import { GraphQLList, GraphQLNonNull, GraphQLSemanticNonNull, + isOutputType, } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; @@ -52,6 +53,9 @@ export function typeFromAST( } case Kind.SEMANTIC_NON_NULL_TYPE: { const innerType = typeFromAST(schema, typeNode.type); + if (!isOutputType(innerType)) { + throw new Error('A semantic non-null type must wrap an output type.'); + } return innerType && new GraphQLSemanticNonNull(innerType); } case Kind.NAMED_TYPE: From 96e8b53832d0c67f513fef72b631acea332627e6 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 17:58:57 +0000 Subject: [PATCH 22/30] Use GraphQLNullableOutputType instead of intersection --- src/index.ts | 1 + src/type/definition.ts | 21 +++++++++++++++------ src/type/index.ts | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index d305cfb434..6d4a78b58c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -151,6 +151,7 @@ export type { GraphQLAbstractType, GraphQLWrappingType, GraphQLNullableType, + GraphQLNullableOutputType, GraphQLNamedType, GraphQLNamedInputType, GraphQLNamedOutputType, diff --git a/src/type/definition.ts b/src/type/definition.ts index 84f451ca16..3ff269b911 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -214,13 +214,13 @@ export function assertNonNullType(type: unknown): GraphQLNonNull { export function isSemanticNonNullType( type: unknown, -): type is GraphQLSemanticNonNull { +): type is GraphQLSemanticNonNull { return instanceOf(type, GraphQLSemanticNonNull); } export function assertSemanticNonNullType( type: unknown, -): GraphQLSemanticNonNull { +): GraphQLSemanticNonNull { if (!isSemanticNonNullType(type)) { throw new Error( `Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`, @@ -475,9 +475,7 @@ export class GraphQLNonNull { * * @experimental */ -export class GraphQLSemanticNonNull< - T extends GraphQLNullableType & GraphQLOutputType, -> { +export class GraphQLSemanticNonNull { readonly ofType: T; constructor(ofType: T) { @@ -509,7 +507,7 @@ export class GraphQLSemanticNonNull< export type GraphQLWrappingType = | GraphQLList | GraphQLNonNull - | GraphQLSemanticNonNull; + | GraphQLSemanticNonNull; export function isWrappingType(type: unknown): type is GraphQLWrappingType { return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type); @@ -534,6 +532,17 @@ export type GraphQLNullableType = | GraphQLInputObjectType | GraphQLList; +/** + * These types can all accept null as a value, and are suitable for output. + */ +export type GraphQLNullableOutputType = + | GraphQLScalarType + | GraphQLObjectType + | GraphQLInterfaceType + | GraphQLUnionType + | GraphQLEnumType + | GraphQLList; + export function isNullableType(type: unknown): type is GraphQLNullableType { return isType(type) && !isNonNullType(type) && !isSemanticNonNullType(type); } diff --git a/src/type/index.ts b/src/type/index.ts index 0729b840e7..92199ca0d6 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -78,6 +78,7 @@ export type { GraphQLAbstractType, GraphQLWrappingType, GraphQLNullableType, + GraphQLNullableOutputType, GraphQLNamedType, GraphQLNamedInputType, GraphQLNamedOutputType, From a2169ac2d8ada32bd84fb044bc2a0b01046a94b3 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 18:10:19 +0000 Subject: [PATCH 23/30] Simpler type --- src/type/definition.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/type/definition.ts b/src/type/definition.ts index 3ff269b911..d108f549f5 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -555,11 +555,12 @@ export function assertNullableType(type: unknown): GraphQLNullableType { } export function getNullableType(type: undefined | null): void; +export function getNullableType( + type: T | GraphQLNonNull | GraphQLSemanticNonNull, +): T; export function getNullableType( - type: - | T - | GraphQLNonNull - | GraphQLSemanticNonNull, + // eslint-disable-next-line @typescript-eslint/unified-signatures + type: T | GraphQLNonNull, ): T; export function getNullableType( type: Maybe, From 1ce6880f0440df32d9a3e153331f3db41655c6d4 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 18:12:50 +0000 Subject: [PATCH 24/30] Only allow GraphQLSemanticNonNull of output type --- src/type/definition.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/type/definition.ts b/src/type/definition.ts index d108f549f5..c295ddbd87 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -483,6 +483,10 @@ export class GraphQLSemanticNonNull { isNullableType(ofType), `Expected ${inspect(ofType)} to be a GraphQL nullable type.`, ); + devAssert( + isOutputType(ofType), + `Expected ${inspect(ofType)} to be a GraphQL output type.`, + ); this.ofType = ofType; } From 97256f0c4740112ba93cc962d51fd797329f3570 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 18:14:47 +0000 Subject: [PATCH 25/30] Tidy --- src/type/introspection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 7036973c1d..ae3968dcb3 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -377,7 +377,7 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ defaultValue: false, }, }, - resolve: (field, { includeSemanticNonNull }, _context) => { + resolve: (field, { includeSemanticNonNull }) => { if (includeSemanticNonNull) { return field.type; } From f464644ac5bebf6efbe4070600a5041d1d420f46 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 26 Mar 2025 18:18:46 +0000 Subject: [PATCH 26/30] Memoize --- src/jsutils/memoize1.ts | 22 ++++++++++++++++++++++ src/type/introspection.ts | 8 +++++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/jsutils/memoize1.ts diff --git a/src/jsutils/memoize1.ts b/src/jsutils/memoize1.ts new file mode 100644 index 0000000000..d3ea100b68 --- /dev/null +++ b/src/jsutils/memoize1.ts @@ -0,0 +1,22 @@ +/** + * Memoizes the provided single-argument function. + */ +export function memoize1( + fn: (a1: A1) => R, +): (a1: A1) => R { + let cache0: WeakMap; + + return function memoized(a1) { + if (cache0 === undefined) { + cache0 = new WeakMap(); + } + + let fnResult = cache0.get(a1); + if (fnResult === undefined) { + fnResult = fn(a1); + cache0.set(a1, fnResult); + } + + return fnResult; + }; +} diff --git a/src/type/introspection.ts b/src/type/introspection.ts index ae3968dcb3..ced8e56e0c 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -1,5 +1,6 @@ import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; +import { memoize1 } from '../jsutils/memoize1'; import { DirectiveLocation } from '../language/directiveLocation'; import { print } from '../language/printer'; @@ -381,7 +382,6 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ if (includeSemanticNonNull) { return field.type; } - // TODO: memoize return stripSemanticNonNullTypes(field.type); }, }, @@ -396,8 +396,10 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap, unknown>), }); -// TODO: move this elsewhere, rename, memoize -function stripSemanticNonNullTypes(type: GraphQLOutputType): GraphQLOutputType { +const stripSemanticNonNullTypes = memoize1(_stripSemanticNonNullTypes); +function _stripSemanticNonNullTypes( + type: GraphQLOutputType, +): GraphQLOutputType { if (isNonNullType(type)) { const convertedInner = stripSemanticNonNullTypes(type.ofType); if (convertedInner === type.ofType) { From 21136764185523c84609b266c77ef9601daf0937 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 15:50:53 +0000 Subject: [PATCH 27/30] Rename errorBehavior to onError and NULL to NO_PROPAGATE --- src/execution/execute.ts | 25 +++++++++++++++++++------ src/graphql.ts | 8 ++++---- src/type/definition.ts | 2 +- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index d179dc8582..9d02db4436 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -116,7 +116,7 @@ export interface ExecutionContext { typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; errors: Array; - errorBehavior: 'PROPAGATE' | 'NULL' | 'ABORT'; + errorBehavior: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; } /** @@ -155,14 +155,14 @@ export interface ExecutionArgs { typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; /** - * Experimental. Set to NULL to prevent error propagation. Set to ABORT to + * Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to * abort a request when any error occurs. * * Default: PROPAGATE * * @experimental */ - errorBehavior?: 'PROPAGATE' | 'NULL' | 'ABORT'; + onError?: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; } /** @@ -297,9 +297,22 @@ export function buildExecutionContext( fieldResolver, typeResolver, subscribeFieldResolver, - errorBehavior, + onError, } = args; + if ( + onError != null && + onError !== 'PROPAGATE' && + onError !== 'NO_PROPAGATE' && + onError !== 'ABORT' + ) { + 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) { @@ -359,7 +372,7 @@ export function buildExecutionContext( typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], - errorBehavior: errorBehavior ?? 'PROPAGATE', + errorBehavior: onError ?? 'PROPAGATE', }; } @@ -618,7 +631,7 @@ function handleFieldError( } else if (exeContext.errorBehavior === 'ABORT') { // In this mode, any error aborts the request throw error; - } else if (exeContext.errorBehavior === 'NULL') { + } 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 { diff --git a/src/graphql.ts b/src/graphql.ts index 5214ab8ac6..8b28ff60ae 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -67,14 +67,14 @@ export interface GraphQLArgs { fieldResolver?: Maybe>; typeResolver?: Maybe>; /** - * Experimental. Set to NULL to prevent error propagation. Set to ABORT to + * Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to * abort a request when any error occurs. * * Default: PROPAGATE * * @experimental */ - errorBehavior?: 'PROPAGATE' | 'NULL' | 'ABORT'; + onError?: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; } export function graphql(args: GraphQLArgs): Promise { @@ -115,7 +115,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, - errorBehavior, + onError, } = args; // Validate Schema @@ -148,6 +148,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, - errorBehavior, + onError, }); } diff --git a/src/type/definition.ts b/src/type/definition.ts index c295ddbd87..d101d4003a 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1097,7 +1097,7 @@ export interface GraphQLResolveInfo { readonly operation: OperationDefinitionNode; readonly variableValues: { [variable: string]: unknown }; /** @experimental */ - readonly errorBehavior: 'PROPAGATE' | 'NULL' | 'ABORT'; + readonly errorBehavior: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; } /** From 95da88d58565a99c1de63c25febff9f20b427d97 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:12:13 +0000 Subject: [PATCH 28/30] Centralise the definition of GraphQLErrorBehavior --- src/error/ErrorBehavior.ts | 9 +++++++++ src/error/index.ts | 1 + src/execution/execute.ts | 13 +++++-------- src/graphql.ts | 3 ++- src/index.ts | 1 + src/type/definition.ts | 3 ++- 6 files changed, 20 insertions(+), 10 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/execute.ts b/src/execution/execute.ts index 9d02db4436..900cd36e06 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -58,6 +58,7 @@ import { collectSubfields as _collectSubfields, } from './collectFields'; import { getArgumentValues, getVariableValues } from './values'; +import { GraphQLErrorBehavior, isErrorBehavior } from '../error/ErrorBehavior'; /** * A memoized collection of relevant subfields with regard to the return @@ -116,7 +117,7 @@ export interface ExecutionContext { typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; errors: Array; - errorBehavior: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; + errorBehavior: GraphQLErrorBehavior; } /** @@ -132,6 +133,7 @@ export interface ExecutionResult< > { errors?: ReadonlyArray; data?: TData | null; + onError?: GraphQLErrorBehavior; extensions?: TExtensions; } @@ -162,7 +164,7 @@ export interface ExecutionArgs { * * @experimental */ - onError?: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; + onError?: GraphQLErrorBehavior; } /** @@ -300,12 +302,7 @@ export function buildExecutionContext( onError, } = args; - if ( - onError != null && - onError !== 'PROPAGATE' && - onError !== 'NO_PROPAGATE' && - onError !== 'ABORT' - ) { + if (onError != null && !isErrorBehavior(onError)) { return [ new GraphQLError( 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', diff --git a/src/graphql.ts b/src/graphql.ts index 8b28ff60ae..f7f4f8e3ed 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -17,6 +17,7 @@ import { validate } from './validation/validate'; import type { ExecutionResult } from './execution/execute'; import { execute } from './execution/execute'; +import type { GraphQLErrorBehavior } from './error/ErrorBehavior'; /** * This is the primary entry point function for fulfilling GraphQL operations @@ -74,7 +75,7 @@ export interface GraphQLArgs { * * @experimental */ - onError?: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; + onError?: GraphQLErrorBehavior; } export function graphql(args: GraphQLArgs): Promise { diff --git a/src/index.ts b/src/index.ts index 6d4a78b58c..fc00f68ae7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -400,6 +400,7 @@ export { } from './error/index'; export type { + GraphQLErrorBehavior, GraphQLErrorOptions, GraphQLFormattedError, GraphQLErrorExtensions, diff --git a/src/type/definition.ts b/src/type/definition.ts index d101d4003a..3a814944dc 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 { @@ -1097,7 +1098,7 @@ export interface GraphQLResolveInfo { readonly operation: OperationDefinitionNode; readonly variableValues: { [variable: string]: unknown }; /** @experimental */ - readonly errorBehavior: 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; + readonly errorBehavior: GraphQLErrorBehavior; } /** From a1d2dbe15b85f4fde603ea74e5de7c36b86c25f4 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:27:50 +0000 Subject: [PATCH 29/30] Lint --- src/execution/execute.ts | 3 ++- src/graphql.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 900cd36e06..54114d4d05 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'; @@ -58,7 +60,6 @@ import { collectSubfields as _collectSubfields, } from './collectFields'; import { getArgumentValues, getVariableValues } from './values'; -import { GraphQLErrorBehavior, isErrorBehavior } from '../error/ErrorBehavior'; /** * A memoized collection of relevant subfields with regard to the return diff --git a/src/graphql.ts b/src/graphql.ts index f7f4f8e3ed..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'; @@ -17,7 +19,6 @@ import { validate } from './validation/validate'; import type { ExecutionResult } from './execution/execute'; import { execute } from './execution/execute'; -import type { GraphQLErrorBehavior } from './error/ErrorBehavior'; /** * This is the primary entry point function for fulfilling GraphQL operations From 70dc6f8ca6e37db6b26e6f5999cd97b715ef7144 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:28:18 +0000 Subject: [PATCH 30/30] Prettier --- 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 54114d4d05..50f16c9080 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 { GraphQLErrorBehavior } from '../error/ErrorBehavior'; import { isErrorBehavior } from '../error/ErrorBehavior'; import type { GraphQLFormattedError } from '../error/GraphQLError'; import { GraphQLError } from '../error/GraphQLError';