From fb9bd1f90f119d3f4001e09f39685423d2581e7d Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 18:16:40 -0700 Subject: [PATCH 01/36] 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 0c64761e35fdcf4b0ace305575ab082036fd621f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:28:48 -0700 Subject: [PATCH 02/36] 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 73c713a203..d305cfb434 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, @@ -481,6 +485,7 @@ export type { IntrospectionNamedTypeRef, IntrospectionListTypeRef, IntrospectionNonNullTypeRef, + IntrospectionSemanticNonNullTypeRef, IntrospectionField, IntrospectionInputValue, IntrospectionEnumValue, diff --git a/src/language/ast.ts b/src/language/ast.ts index 6137eb6c1a..38d5b68efc 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -541,6 +541,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 373b474ed5..06572568c3 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 8397a35b80..a01ae0dd6b 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, @@ -723,6 +724,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 934c634a2e74d0cb95a5c8e8a90539f7dc23bda9 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:32:11 -0700 Subject: [PATCH 03/36] 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 48c7b531d4d792dbf40de1c60b3650364455465c Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:35:06 -0700 Subject: [PATCH 04/36] 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 b758b6708456fdada385023280523142812b1ce0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 19:45:32 -0700 Subject: [PATCH 05/36] 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 caa922a27d..59d39cd872 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -636,6 +636,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 38d5b68efc..dd5ef0c51f 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 @@ -521,7 +522,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 03e4166210..2872d7ce72 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -37,6 +37,7 @@ import type { NamedTypeNode, NameNode, NonNullTypeNode, + SemanticNonNullTypeNode, NullValueNode, ObjectFieldNode, ObjectTypeDefinitionNode, @@ -749,6 +750,7 @@ export class Parser { * - NamedType * - ListType * - NonNullType + * - SemanticNonNullType */ parseTypeReference(): TypeNode { const start = this._lexer.token; @@ -770,6 +772,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 06572568c3..fdba15eae0 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 e309ed51178c1091f30c5caf0ab389e5703534c4 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:11:02 -0700 Subject: [PATCH 06/36] 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 f599c4e1e3acedc0461f62a2e8a4f08e41dc1d6a Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:15:04 -0700 Subject: [PATCH 07/36] 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 2872d7ce72..57477430a3 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 a01ae0dd6b..96aab04c3c 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 b7e2c7fc8529f45774d1ecdde1fd093edfb4febd Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:17:36 -0700 Subject: [PATCH 08/36] 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 dd5ef0c51f..1fc99414ef 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 033e917af97940d5a15cba839864c3ee6ff97016 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:26:12 -0700 Subject: [PATCH 09/36] 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 d305cfb434..dacea5d145 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 8c5cacba0d..1942ca0403 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 2787de038095322da5d06f1bc62d1b6fdf37e7a0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:28:38 -0700 Subject: [PATCH 10/36] 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 e029b6d07ec0a2952b713c617fb8cd3327f0d88c Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:31:03 -0700 Subject: [PATCH 11/36] 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 f69db210757af89233d9e1828e6dc301ad01bf7d Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:34:59 -0700 Subject: [PATCH 12/36] 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 e73c4ba96f7d93bdbdb5a022def45eb012c6f1f0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:39:39 -0700 Subject: [PATCH 13/36] 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 1942ca0403..f57baf8894 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 8cc7fabd5e50a9eb0a69e75f97e68b5c8d3823b1 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:41:55 -0700 Subject: [PATCH 14/36] 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 6b0611d74239722e767c76ab7fd0737aab6d3c8b Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:49:32 -0700 Subject: [PATCH 15/36] 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 dd60b9ea20122d3e94ba7dc1a0119ce2795d8ad7 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:58:03 -0700 Subject: [PATCH 16/36] 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 fdba15eae0..a13353f4c8 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 3e01bb2bc717be3767182ca779381a1e5253bde8 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 20:59:15 -0700 Subject: [PATCH 17/36] 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 a13353f4c8..dda0e7f19a 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 0df7bdcdacc482d896d67875ebc982f9cd19359e Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 13 Sep 2024 21:02:07 -0700 Subject: [PATCH 18/36] 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 96aab04c3c..182215fd3f 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 36b3cf280e1646baf64da6c3e73f5fa6fb6d54dd Mon Sep 17 00:00:00 2001 From: twof Date: Tue, 29 Oct 2024 23:05:31 -0700 Subject: [PATCH 19/36] parser tests passing --- src/language/__tests__/parser-test.ts | 77 +++++++++++++++++++++------ src/language/lexer.ts | 8 +-- src/language/parser.ts | 46 +++++++++++----- src/language/tokenKind.ts | 2 +- 4 files changed, 99 insertions(+), 34 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 59d39cd872..43dd097d34 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -11,6 +11,7 @@ import { Kind } from '../kinds'; import { parse, parseConstValue, parseType, parseValue } from '../parser'; import { Source } from '../source'; import { TokenKind } from '../tokenKind'; +import { Console } from 'console'; function expectSyntaxError(text: string) { return expectToThrowJSON(() => parse(text)); @@ -636,11 +637,46 @@ describe('Parser', () => { }); }); + it('parses nested types', () => { + const result = parseType('[MyType!]'); + expectJSON(result).toDeepEqual({ + kind: Kind.LIST_TYPE, + loc: { start: 0, end: 9 }, + type: { + kind: Kind.NON_NULL_TYPE, + loc: { start: 1, end: 8 }, + type: { + kind: Kind.NAMED_TYPE, + loc: { start: 1, end: 7 }, + name: { + kind: Kind.NAME, + loc: { start: 1, end: 7 }, + value: 'MyType', + }, + }, + }, + }); + }); + }); + + describe('parseDocumentDirective', () => { + it('doesnt throw on document-level directive', () => { + parse(dedent` + @SemanticNullability + + type Query { + hello: String + world: String? + foo: String! + } + `); + }); + it('parses semantic-non-null types', () => { - const result = parseType('MyType*'); + const result = parseType('MyType', { useSemanticNullability: true }); expectJSON(result).toDeepEqual({ kind: Kind.SEMANTIC_NON_NULL_TYPE, - loc: { start: 0, end: 7 }, + loc: { start: 0, end: 6 }, type: { kind: Kind.NAMED_TYPE, loc: { start: 0, end: 6 }, @@ -653,22 +689,31 @@ describe('Parser', () => { }); }); - it('parses nested types', () => { - const result = parseType('[MyType!]'); + it('parses nullable types', () => { + const result = parseType('MyType?', { useSemanticNullability: true }); expectJSON(result).toDeepEqual({ - kind: Kind.LIST_TYPE, - loc: { start: 0, end: 9 }, + kind: Kind.NAMED_TYPE, + loc: { start: 0, end: 6 }, + name: { + kind: Kind.NAME, + loc: { start: 0, end: 6 }, + value: 'MyType', + }, + }); + }); + + it('parses non-nullable types', () => { + const result = parseType('MyType!', { useSemanticNullability: true }); + expectJSON(result).toDeepEqual({ + kind: Kind.NON_NULL_TYPE, + loc: { start: 0, end: 7 }, type: { - kind: Kind.NON_NULL_TYPE, - loc: { start: 1, end: 8 }, - type: { - kind: Kind.NAMED_TYPE, - loc: { start: 1, end: 7 }, - name: { - kind: Kind.NAME, - loc: { start: 1, end: 7 }, - value: 'MyType', - }, + kind: Kind.NAMED_TYPE, + loc: { start: 0, end: 6 }, + name: { + kind: Kind.NAME, + loc: { start: 0, end: 6 }, + value: 'MyType', }, }, }); diff --git a/src/language/lexer.ts b/src/language/lexer.ts index 8fccd4e709..bdaf8b9b25 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -91,7 +91,7 @@ export class Lexer { export function isPunctuatorTokenKind(kind: TokenKind): boolean { return ( kind === TokenKind.BANG || - kind === TokenKind.ASTERISK || + kind === TokenKind.QUESTION_MARK || kind === TokenKind.DOLLAR || kind === TokenKind.AMP || kind === TokenKind.PAREN_L || @@ -247,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: // $ @@ -258,8 +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 0x003f: // ? + return createToken(lexer, TokenKind.QUESTION_MARK, position, position + 1); case 0x002e: // . if ( body.charCodeAt(position + 1) === 0x002e && diff --git a/src/language/parser.ts b/src/language/parser.ts index 57477430a3..8a27b0a73c 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -104,6 +104,8 @@ export interface ParseOptions { * ``` */ allowLegacyFragmentVariables?: boolean; + + useSemanticNullability?: boolean; } /** @@ -259,6 +261,14 @@ export class Parser { * - InputObjectTypeDefinition */ parseDefinition(): DefinitionNode { + // TODO: I don't know what isConst represents. Every other callsite has it false + let directives = this.parseDirectives(false); + for (let directive of directives) { + if (directive.name.value == "SemanticNullability") { + this._options.useSemanticNullability = true; + } + } + if (this.peek(TokenKind.BRACE_L)) { return this.parseOperationDefinition(); } @@ -766,20 +776,30 @@ export class Parser { type = this.parseNamedType(); } - if (this.expectOptionalToken(TokenKind.BANG)) { - return this.node(start, { - kind: Kind.NON_NULL_TYPE, - type, - }); - } - if (this.expectOptionalToken(TokenKind.ASTERISK)) { - return this.node(start, { - kind: Kind.SEMANTIC_NON_NULL_TYPE, - type, - }); - } + if (this._options.useSemanticNullability) { + if (this.expectOptionalToken(TokenKind.BANG)) { + return this.node(start, { + kind: Kind.NON_NULL_TYPE, + type, + }); + } else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) { + return type; + } else { + return this.node(start, { + kind: Kind.SEMANTIC_NON_NULL_TYPE, + type, + }); + } + } else { + if (this.expectOptionalToken(TokenKind.BANG)) { + return this.node(start, { + kind: Kind.NON_NULL_TYPE, + type, + }); + } - return type; + return type; + } } /** diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts index fd53d6ebf3..0b651d36b0 100644 --- a/src/language/tokenKind.ts +++ b/src/language/tokenKind.ts @@ -6,7 +6,7 @@ enum TokenKind { SOF = '', EOF = '', BANG = '!', - ASTERISK = '*', + QUESTION_MARK = '?', DOLLAR = '$', AMP = '&', PAREN_L = '(', From 69739ebb839cec86e4cdf442e3664d9b17dd0000 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 09:17:05 -0800 Subject: [PATCH 20/36] Add semantic optional type --- src/language/ast.ts | 11 ++++++++++- src/language/kinds.ts | 1 + src/language/parser.ts | 6 +++++- src/language/printer.ts | 16 +++++++++++++--- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/language/ast.ts b/src/language/ast.ts index 1fc99414ef..67177a766c 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -162,6 +162,7 @@ export type ASTNode = | ListTypeNode | NonNullTypeNode | SemanticNonNullTypeNode + | SemanticOptionalTypeNode | SchemaDefinitionNode | OperationTypeDefinitionNode | ScalarTypeDefinitionNode @@ -237,6 +238,7 @@ export const QueryDocumentKeys: { ListType: ['type'], NonNullType: ['type'], SemanticNonNullType: ['type'], + SemanticOptionalType: ['type'], SchemaDefinition: ['description', 'directives', 'operationTypes'], OperationTypeDefinition: ['type'], @@ -527,7 +529,8 @@ export type TypeNode = | NamedTypeNode | ListTypeNode | NonNullTypeNode - | SemanticNonNullTypeNode; + | SemanticNonNullTypeNode + | SemanticOptionalTypeNode; export interface NamedTypeNode { readonly kind: Kind.NAMED_TYPE; @@ -553,6 +556,12 @@ export interface SemanticNonNullTypeNode { readonly type: NamedTypeNode | ListTypeNode; } +export interface SemanticOptionalTypeNode { + readonly kind: Kind.SEMANTIC_OPTIONAL_TYPE; + readonly loc?: Location; + readonly type: NamedTypeNode | ListTypeNode; +} + /** Type System Definition */ export type TypeSystemDefinitionNode = diff --git a/src/language/kinds.ts b/src/language/kinds.ts index e91373746c..3b63882795 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -38,6 +38,7 @@ enum Kind { LIST_TYPE = 'ListType', NON_NULL_TYPE = 'NonNullType', SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType', + SEMANTIC_OPTIONAL_TYPE = 'SemanticOptionalType', /** Type System Definitions */ SCHEMA_DEFINITION = 'SchemaDefinition', diff --git a/src/language/parser.ts b/src/language/parser.ts index 8a27b0a73c..61ebe354de 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -51,6 +51,7 @@ import type { SelectionNode, SelectionSetNode, SemanticNonNullTypeNode, + SemanticOptionalTypeNode, StringValueNode, Token, TypeNode, @@ -783,7 +784,10 @@ export class Parser { type, }); } else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) { - return type; + return this.node(start, { + kind: Kind.SEMANTIC_OPTIONAL_TYPE, + type, + }); } else { return this.node(start, { kind: Kind.SEMANTIC_NON_NULL_TYPE, diff --git a/src/language/printer.ts b/src/language/printer.ts index f08ba54f56..6c6e6b2fe1 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -6,11 +6,18 @@ import { printString } from './printString'; import type { ASTReducer } from './visitor'; import { visit } from './visitor'; +/** + * Configuration options to control parser behavior + */ +export interface PrintOptions { + useSemanticNullability?: boolean; +} + /** * Converts an AST into a string, using one set of reasonable * formatting rules. */ -export function print(ast: ASTNode): string { +export function print(ast: ASTNode, options: PrintOptions = {}): string { return visit(ast, printDocASTReducer); } @@ -128,10 +135,13 @@ const printDocASTReducer: ASTReducer = { // Type - NamedType: { leave: ({ name }) => name }, + NamedType: { leave: ({ name }) => + name +}, ListType: { leave: ({ type }) => '[' + type + ']' }, NonNullType: { leave: ({ type }) => type + '!' }, - SemanticNonNullType: { leave: ({ type }) => type + '*' }, + SemanticNonNullType: { leave: ({ type }) => type }, + SemanticOptionalType: { leave: ({ type }) => type + '?' }, // Type System Definitions From 4dcf01c2b9dca45dfc7a16ca71af14efb5750743 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 09:38:25 -0800 Subject: [PATCH 21/36] printer and parser tests passing --- src/language/__tests__/parser-test.ts | 22 +++++++++++-------- src/language/__tests__/schema-printer-test.ts | 19 +++++++++++++++- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 43dd097d34..989232c35d 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -691,15 +691,19 @@ describe('Parser', () => { it('parses nullable types', () => { const result = parseType('MyType?', { useSemanticNullability: true }); - expectJSON(result).toDeepEqual({ - kind: Kind.NAMED_TYPE, - loc: { start: 0, end: 6 }, - name: { - kind: Kind.NAME, - loc: { start: 0, end: 6 }, - value: 'MyType', - }, - }); + expectJSON(result).toDeepEqual( + { kind: Kind.SEMANTIC_OPTIONAL_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 non-nullable types', () => { diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts index 41cf6c5419..7151c5dcef 100644 --- a/src/language/__tests__/schema-printer-test.ts +++ b/src/language/__tests__/schema-printer-test.ts @@ -5,7 +5,7 @@ import { dedent } from '../../__testUtils__/dedent'; import { kitchenSinkSDL } from '../../__testUtils__/kitchenSinkSDL'; import { Kind } from '../kinds'; -import { parse } from '../parser'; +import { parse, parseType } from '../parser'; import { print } from '../printer'; describe('Printer: SDL document', () => { @@ -180,4 +180,21 @@ describe('Printer: SDL document', () => { } `); }); + + it('prints NamedType', () => { + expect(print(parseType('MyType', { useSemanticNullability: false }))).to.equal(dedent`MyType`); + }); + + it('prints SemanticOptionalType', () => { + expect(print(parseType('MyType?', { useSemanticNullability: true }))).to.equal(dedent`MyType?`); + }); + + it('prints SemanticNonNullType', () => { + expect(print(parseType('MyType', { useSemanticNullability: true }))).to.equal(dedent`MyType`); + }); + + it('prints NonNullType', () => { + expect(print(parseType('MyType!', { useSemanticNullability: true }))).to.equal(dedent`MyType!`); + expect(print(parseType('MyType!', { useSemanticNullability: false }))).to.equal(dedent`MyType!`); + }); }); From 509d0c6dbbb19eba9b55f138b5110baa4e982e1f Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 21:57:10 -0800 Subject: [PATCH 22/36] some new semanticNullability execution tests --- package.json | 2 +- src/execution/__tests__/executor-test.ts | 153 +++++++++++++++++++++++ src/execution/execute.ts | 23 +++- src/type/definition.ts | 93 +++++++++++++- 4 files changed, 265 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 670a0698f3..5bc5b0b52a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "npm run lint && npm run check && npm run testonly && npm run prettier:check && npm run check:spelling && npm run check:integrations", "lint": "eslint --cache --max-warnings 0 .", "check": "tsc --pretty", - "testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts", + "testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts -g 'SemanticNonNull halts null propagation'", "testonly:cover": "c8 npm run testonly", "prettier": "prettier --write --list-different .", "prettier:check": "prettier --check .", diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index a7bc1c8265..eff7b43bf4 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -16,12 +16,16 @@ import { GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLSemanticNonNull, + GraphQLSemanticOptional, GraphQLUnionType, } from '../../type/definition'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; import { execute, executeSync } from '../execute'; +import { GraphQLError } from '../../error'; +import { ExecutableDefinitionNode, FieldNode, SelectionSetNode } from '../../language'; describe('Execute: Handles basic execution tasks', () => { it('throws if no document is provided', () => { @@ -1323,3 +1327,152 @@ describe('Execute: Handles basic execution tasks', () => { expect(possibleTypes).to.deep.equal([fooObject]); }); }); + +describe('Execute: Handles Semantic Nullability', () => { + const DeepDataType = new GraphQLObjectType({ + name: 'DeepDataType', + fields: { + f: { type: new GraphQLNonNull(GraphQLString) } + }, + }); + + const DataType: GraphQLObjectType = new GraphQLObjectType({ + name: 'DataType', + fields: () => ({ + a: { type: new GraphQLSemanticOptional(GraphQLString) }, + b: { type: new GraphQLSemanticNonNull(GraphQLString) }, + c: { type: new GraphQLNonNull(GraphQLString) }, + d: { type: new GraphQLSemanticNonNull(DeepDataType) } + }), + }); + + it('SemanticNonNull throws error on null without error', async () => { + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie' + }; + + const document = parse(` + query { + b + } + `); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + let selectionSet = executable.selectionSet.selections.values().next().value; + + expect(result).to.deep.equal({ + data: { + b: null + }, + errors: [ + new GraphQLError( + 'Cannot return null for semantic-non-nullable field DataType.b.', + { + nodes: selectionSet, + path: ['b'] + } + ) + ] + }); + }); + + it('SemanticNonNull succeeds on null with error', async () => { + const data = { + a: () => 'Apple', + b: () => { throw new Error( + `Something went wrong`, + ); }, + c: () => 'Cookie' + }; + + const document = parse(` + query { + b + } + `); + + let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + let selectionSet = executable.selectionSet.selections.values().next().value; + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + b: null + }, + errors: [ + new GraphQLError( + 'Something went wrong', + { + nodes: selectionSet, + path: ['b'] + } + ) + ] + }); + }); + + it('SemanticNonNull halts null propagation', async () => { + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie', + d: () => { + f: () => null + } + }; + + const document = parse(` + query { + d { + f + } + } + `); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + let dSelectionSet = executable.selectionSet.selections.values().next().value as FieldNode; + let fSelectionSet = dSelectionSet.selectionSet?.selections.values().next().value; + + expect(result).to.deep.equal({ + data: { + d: null + }, + errors: [ + new GraphQLError( + 'Cannot return null for non-nullable field DeepDataType.f.', + { + nodes: fSelectionSet, + path: ['d', 'f'] + } + ) + ] + }); + }); + + it('SemanticOptional allows null values', async () => { + + }); + + it('SemanticOptional allows non-null values', async () => { + + }); +}); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index b7cb7f4a02..eab3ff210d 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -44,6 +44,7 @@ import { isNonNullType, isObjectType, isSemanticNonNullType, + isSemanticOptionalType, } from '../type/definition'; import { SchemaMetaFieldDef, @@ -650,9 +651,12 @@ function completeValue( throw result; } + console.log("anything", path); + // If field type is NonNull, complete for inner type, and throw field error // if result is null. if (isNonNullType(returnType)) { + console.log("is non null"); const completed = completeValue( exeContext, returnType.ofType, @@ -670,8 +674,9 @@ function completeValue( } // If field type is SemanticNonNull, complete for inner type, and throw field error - // if result is null. + // if result is null and an error doesn't exist. if (isSemanticNonNullType(returnType)) { + console.log("Is semantic non null") const completed = completeValue( exeContext, returnType.ofType, @@ -688,8 +693,21 @@ function completeValue( return completed; } + // If field type is SemanticOptional, complete for inner type + if (isSemanticOptionalType(returnType)) { + return completeValue( + exeContext, + returnType.ofType, + fieldNodes, + info, + path, + result, + ); + } + // If result value is null or undefined then return null. if (result == null) { + console.log("is null") return null; } @@ -708,12 +726,14 @@ function completeValue( // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning null if serialization is not possible. if (isLeafType(returnType)) { + console.log("is leaf") return completeLeafValue(returnType, result); } // If field type is an abstract type, Interface or Union, determine the // runtime Object type and complete for that type. if (isAbstractType(returnType)) { + console.log("is abstract") return completeAbstractValue( exeContext, returnType, @@ -726,6 +746,7 @@ function completeValue( // If field type is Object, execute and complete all sub-selections. if (isObjectType(returnType)) { + console.log("is object") return completeObjectValue( exeContext, returnType, diff --git a/src/type/definition.ts b/src/type/definition.ts index 554ec0fe41..dcad08a501 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -75,7 +75,16 @@ export type GraphQLType = | GraphQLEnumType | GraphQLInputObjectType | GraphQLList - >; + > + | GraphQLSemanticOptional< + | GraphQLScalarType + | GraphQLObjectType + | GraphQLInterfaceType + | GraphQLUnionType + | GraphQLEnumType + | GraphQLInputObjectType + | GraphQLList + >; export function isType(type: unknown): type is GraphQLType { return ( @@ -239,6 +248,32 @@ export function assertSemanticNonNullType( return type; } +export function isSemanticOptionalType( + type: GraphQLInputType, +): type is GraphQLSemanticOptional; +export function isSemanticOptionalType( + type: GraphQLOutputType, +): type is GraphQLSemanticOptional; +export function isSemanticOptionalType( + type: unknown, +): type is GraphQLSemanticOptional; +export function isSemanticOptionalType( + type: unknown, +): type is GraphQLSemanticOptional { + return instanceOf(type, GraphQLSemanticOptional); +} + +export function assertSemanticOptionalType( + type: unknown, +): GraphQLSemanticOptional { + if (!isSemanticOptionalType(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. */ @@ -502,7 +537,56 @@ export class GraphQLSemanticNonNull { } toString(): string { - return String(this.ofType) + '*'; + return String(this.ofType); + } + + toJSON(): string { + return this.toString(); + } +} + +/** + * 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 GraphQLSemanticOptional { + 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 'GraphQLSemanticOptional'; + } + + toString(): string { + return String(this.ofType) + '?'; } toJSON(): string { @@ -517,10 +601,11 @@ export class GraphQLSemanticNonNull { export type GraphQLWrappingType = | GraphQLList | GraphQLNonNull - | GraphQLSemanticNonNull; + | GraphQLSemanticNonNull + | GraphQLSemanticOptional; export function isWrappingType(type: unknown): type is GraphQLWrappingType { - return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type); + return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type) || isSemanticOptionalType(type); } export function assertWrappingType(type: unknown): GraphQLWrappingType { From 71e40570bad4fc1393fc2fef6e96eeba431be9af Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:05:35 -0800 Subject: [PATCH 23/36] SemanticNonNull halts null propagation --- src/execution/__tests__/executor-test.ts | 9 ++++++--- src/execution/execute.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index eff7b43bf4..955a5417a9 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -1425,14 +1425,17 @@ describe('Execute: Handles Semantic Nullability', () => { }); it('SemanticNonNull halts null propagation', async () => { + const deepData = { + f: () => null + }; + const data = { a: () => 'Apple', b: () => null, c: () => 'Cookie', - d: () => { - f: () => null - } + d: () => deepData }; + const document = parse(` query { diff --git a/src/execution/execute.ts b/src/execution/execute.ts index eab3ff210d..d1c72b3ada 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -651,7 +651,7 @@ function completeValue( throw result; } - console.log("anything", path); + console.log("anything", path, result); // If field type is NonNull, complete for inner type, and throw field error // if result is null. From 5cdbf819dfa4ef059cfeab4d591296284076dc16 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:08:47 -0800 Subject: [PATCH 24/36] SemanticOptional cleared --- package.json | 2 +- src/execution/__tests__/executor-test.ts | 44 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5bc5b0b52a..25b51e2c47 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "npm run lint && npm run check && npm run testonly && npm run prettier:check && npm run check:spelling && npm run check:integrations", "lint": "eslint --cache --max-warnings 0 .", "check": "tsc --pretty", - "testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts -g 'SemanticNonNull halts null propagation'", + "testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts -g 'SemanticOptional allows null values'", "testonly:cover": "c8 npm run testonly", "prettier": "prettier --write --list-different .", "prettier:check": "prettier --check .", diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 955a5417a9..0be5c672b4 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -1472,10 +1472,54 @@ describe('Execute: Handles Semantic Nullability', () => { }); it('SemanticOptional allows null values', async () => { + const data = { + a: () => null, + b: () => null, + c: () => 'Cookie' + }; + + const document = parse(` + query { + a + } + `); + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + a: null + } + }); }); it('SemanticOptional allows non-null values', async () => { + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie' + }; + + const document = parse(` + query { + a + } + `); + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + a: 'Apple' + } + }); }); }); From 3099d632136ab323c111b54b26704702688aaaa1 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:09:46 -0800 Subject: [PATCH 25/36] logging cleanup --- src/execution/execute.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index d1c72b3ada..220621f98a 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -651,12 +651,9 @@ function completeValue( throw result; } - console.log("anything", path, result); - // If field type is NonNull, complete for inner type, and throw field error // if result is null. if (isNonNullType(returnType)) { - console.log("is non null"); const completed = completeValue( exeContext, returnType.ofType, @@ -676,7 +673,6 @@ function completeValue( // If field type is SemanticNonNull, complete for inner type, and throw field error // if result is null and an error doesn't exist. if (isSemanticNonNullType(returnType)) { - console.log("Is semantic non null") const completed = completeValue( exeContext, returnType.ofType, @@ -707,7 +703,6 @@ function completeValue( // If result value is null or undefined then return null. if (result == null) { - console.log("is null") return null; } @@ -726,14 +721,12 @@ function completeValue( // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning null if serialization is not possible. if (isLeafType(returnType)) { - console.log("is leaf") return completeLeafValue(returnType, result); } // If field type is an abstract type, Interface or Union, determine the // runtime Object type and complete for that type. if (isAbstractType(returnType)) { - console.log("is abstract") return completeAbstractValue( exeContext, returnType, @@ -746,7 +739,6 @@ function completeValue( // If field type is Object, execute and complete all sub-selections. if (isObjectType(returnType)) { - console.log("is object") return completeObjectValue( exeContext, returnType, From 9e4bf7d623c2bacc08b1d50a9039b489fb523827 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:14:37 -0800 Subject: [PATCH 26/36] rename to SemanticNullable --- package.json | 2 +- src/execution/__tests__/executor-test.ts | 8 ++--- src/execution/execute.ts | 6 ++-- src/language/__tests__/parser-test.ts | 2 +- src/language/__tests__/schema-printer-test.ts | 2 +- src/language/ast.ts | 10 +++--- src/language/kinds.ts | 2 +- src/language/parser.ts | 6 ++-- src/language/printer.ts | 2 +- src/type/definition.ts | 36 +++++++++---------- 10 files changed, 38 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 25b51e2c47..670a0698f3 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "npm run lint && npm run check && npm run testonly && npm run prettier:check && npm run check:spelling && npm run check:integrations", "lint": "eslint --cache --max-warnings 0 .", "check": "tsc --pretty", - "testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts -g 'SemanticOptional allows null values'", + "testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts", "testonly:cover": "c8 npm run testonly", "prettier": "prettier --write --list-different .", "prettier:check": "prettier --check .", diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 0be5c672b4..4c0600285f 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -17,7 +17,7 @@ import { GraphQLObjectType, GraphQLScalarType, GraphQLSemanticNonNull, - GraphQLSemanticOptional, + GraphQLSemanticNullable, GraphQLUnionType, } from '../../type/definition'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; @@ -1339,7 +1339,7 @@ describe('Execute: Handles Semantic Nullability', () => { const DataType: GraphQLObjectType = new GraphQLObjectType({ name: 'DataType', fields: () => ({ - a: { type: new GraphQLSemanticOptional(GraphQLString) }, + a: { type: new GraphQLSemanticNullable(GraphQLString) }, b: { type: new GraphQLSemanticNonNull(GraphQLString) }, c: { type: new GraphQLNonNull(GraphQLString) }, d: { type: new GraphQLSemanticNonNull(DeepDataType) } @@ -1471,7 +1471,7 @@ describe('Execute: Handles Semantic Nullability', () => { }); }); - it('SemanticOptional allows null values', async () => { + it('SemanticNullable allows null values', async () => { const data = { a: () => null, b: () => null, @@ -1497,7 +1497,7 @@ describe('Execute: Handles Semantic Nullability', () => { }); }); - it('SemanticOptional allows non-null values', async () => { + it('SemanticNullable allows non-null values', async () => { const data = { a: () => 'Apple', b: () => null, diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 220621f98a..b50395d2e3 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -44,7 +44,7 @@ import { isNonNullType, isObjectType, isSemanticNonNullType, - isSemanticOptionalType, + isSemanticNullableType, } from '../type/definition'; import { SchemaMetaFieldDef, @@ -689,8 +689,8 @@ function completeValue( return completed; } - // If field type is SemanticOptional, complete for inner type - if (isSemanticOptionalType(returnType)) { + // If field type is SemanticNullable, complete for inner type + if (isSemanticNullableType(returnType)) { return completeValue( exeContext, returnType.ofType, diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 989232c35d..96aa8e6338 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -692,7 +692,7 @@ describe('Parser', () => { it('parses nullable types', () => { const result = parseType('MyType?', { useSemanticNullability: true }); expectJSON(result).toDeepEqual( - { kind: Kind.SEMANTIC_OPTIONAL_TYPE, + { kind: Kind.SEMANTIC_NULLABLE_TYPE, loc: { start: 0, end: 7 }, type: { kind: Kind.NAMED_TYPE, diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts index 7151c5dcef..f98d37e28f 100644 --- a/src/language/__tests__/schema-printer-test.ts +++ b/src/language/__tests__/schema-printer-test.ts @@ -185,7 +185,7 @@ describe('Printer: SDL document', () => { expect(print(parseType('MyType', { useSemanticNullability: false }))).to.equal(dedent`MyType`); }); - it('prints SemanticOptionalType', () => { + it('prints SemanticNullableType', () => { expect(print(parseType('MyType?', { useSemanticNullability: true }))).to.equal(dedent`MyType?`); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index 67177a766c..aa62451c0c 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -162,7 +162,7 @@ export type ASTNode = | ListTypeNode | NonNullTypeNode | SemanticNonNullTypeNode - | SemanticOptionalTypeNode + | SemanticNullableTypeNode | SchemaDefinitionNode | OperationTypeDefinitionNode | ScalarTypeDefinitionNode @@ -238,7 +238,7 @@ export const QueryDocumentKeys: { ListType: ['type'], NonNullType: ['type'], SemanticNonNullType: ['type'], - SemanticOptionalType: ['type'], + SemanticNullableType: ['type'], SchemaDefinition: ['description', 'directives', 'operationTypes'], OperationTypeDefinition: ['type'], @@ -530,7 +530,7 @@ export type TypeNode = | ListTypeNode | NonNullTypeNode | SemanticNonNullTypeNode - | SemanticOptionalTypeNode; + | SemanticNullableTypeNode; export interface NamedTypeNode { readonly kind: Kind.NAMED_TYPE; @@ -556,8 +556,8 @@ export interface SemanticNonNullTypeNode { readonly type: NamedTypeNode | ListTypeNode; } -export interface SemanticOptionalTypeNode { - readonly kind: Kind.SEMANTIC_OPTIONAL_TYPE; +export interface SemanticNullableTypeNode { + readonly kind: Kind.SEMANTIC_NULLABLE_TYPE; readonly loc?: Location; readonly type: NamedTypeNode | ListTypeNode; } diff --git a/src/language/kinds.ts b/src/language/kinds.ts index 3b63882795..7111a94834 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -38,7 +38,7 @@ enum Kind { LIST_TYPE = 'ListType', NON_NULL_TYPE = 'NonNullType', SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType', - SEMANTIC_OPTIONAL_TYPE = 'SemanticOptionalType', + SEMANTIC_NULLABLE_TYPE = 'SemanticNullableType', /** Type System Definitions */ SCHEMA_DEFINITION = 'SchemaDefinition', diff --git a/src/language/parser.ts b/src/language/parser.ts index 61ebe354de..d68148febe 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -51,7 +51,7 @@ import type { SelectionNode, SelectionSetNode, SemanticNonNullTypeNode, - SemanticOptionalTypeNode, + SemanticNullableTypeNode, StringValueNode, Token, TypeNode, @@ -784,8 +784,8 @@ export class Parser { type, }); } else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) { - return this.node(start, { - kind: Kind.SEMANTIC_OPTIONAL_TYPE, + return this.node(start, { + kind: Kind.SEMANTIC_NULLABLE_TYPE, type, }); } else { diff --git a/src/language/printer.ts b/src/language/printer.ts index 6c6e6b2fe1..a380b5c778 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -141,7 +141,7 @@ const printDocASTReducer: ASTReducer = { ListType: { leave: ({ type }) => '[' + type + ']' }, NonNullType: { leave: ({ type }) => type + '!' }, SemanticNonNullType: { leave: ({ type }) => type }, - SemanticOptionalType: { leave: ({ type }) => type + '?' }, + SemanticNullableType: { leave: ({ type }) => type + '?' }, // Type System Definitions diff --git a/src/type/definition.ts b/src/type/definition.ts index dcad08a501..3376e2a74d 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -76,7 +76,7 @@ export type GraphQLType = | GraphQLInputObjectType | GraphQLList > - | GraphQLSemanticOptional< + | GraphQLSemanticNullable< | GraphQLScalarType | GraphQLObjectType | GraphQLInterfaceType @@ -248,25 +248,25 @@ export function assertSemanticNonNullType( return type; } -export function isSemanticOptionalType( +export function isSemanticNullableType( type: GraphQLInputType, -): type is GraphQLSemanticOptional; -export function isSemanticOptionalType( +): type is GraphQLSemanticNullable; +export function isSemanticNullableType( type: GraphQLOutputType, -): type is GraphQLSemanticOptional; -export function isSemanticOptionalType( +): type is GraphQLSemanticNullable; +export function isSemanticNullableType( type: unknown, -): type is GraphQLSemanticOptional; -export function isSemanticOptionalType( +): type is GraphQLSemanticNullable; +export function isSemanticNullableType( type: unknown, -): type is GraphQLSemanticOptional { - return instanceOf(type, GraphQLSemanticOptional); +): type is GraphQLSemanticNullable { + return instanceOf(type, GraphQLSemanticNullable); } -export function assertSemanticOptionalType( +export function assertSemanticNullableType( type: unknown, -): GraphQLSemanticOptional { - if (!isSemanticOptionalType(type)) { +): GraphQLSemanticNullable { + if (!isSemanticNullableType(type)) { throw new Error( `Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`, ); @@ -546,7 +546,7 @@ export class GraphQLSemanticNonNull { } /** - * Semantic-Non-Null Type Wrapper + * Semantic-Nullable 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 @@ -569,7 +569,7 @@ export class GraphQLSemanticNonNull { * * @experimental */ -export class GraphQLSemanticOptional { +export class GraphQLSemanticNullable { readonly ofType: T; constructor(ofType: T) { @@ -582,7 +582,7 @@ export class GraphQLSemanticOptional { } get [Symbol.toStringTag]() { - return 'GraphQLSemanticOptional'; + return 'GraphQLSemanticNullable'; } toString(): string { @@ -602,10 +602,10 @@ export type GraphQLWrappingType = | GraphQLList | GraphQLNonNull | GraphQLSemanticNonNull - | GraphQLSemanticOptional; + | GraphQLSemanticNullable; export function isWrappingType(type: unknown): type is GraphQLWrappingType { - return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type) || isSemanticOptionalType(type); + return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type) || isSemanticNullableType(type); } export function assertWrappingType(type: unknown): GraphQLWrappingType { From 7002eb1978b440e28ca66380710d803d435eccbb Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:20:09 -0800 Subject: [PATCH 27/36] better SemanticNullable docs --- src/type/definition.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/type/definition.ts b/src/type/definition.ts index 3376e2a74d..a7d7d2cd0d 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -548,12 +548,8 @@ export class GraphQLSemanticNonNull { /** * Semantic-Nullable 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). + * A semantic-nullable is a wrapping type which points to another type. + * Semantic-nullable types allow their values to be null. * * Example: * @@ -561,11 +557,12 @@ export class GraphQLSemanticNonNull { * const RowType = new GraphQLObjectType({ * name: 'Row', * fields: () => ({ - * email: { type: new GraphQLSemanticNonNull(GraphQLString) }, + * email: { type: new GraphQLSemanticNullable(GraphQLString) }, * }) * }) * ``` - * Note: the enforcement of non-nullability occurs within the executor. + * Note: This is equivalent to the unadorned named type that is + * used by GraphQL when it is not operating in SemanticNullability mode. * * @experimental */ From 6a63f91ba9a443dc97bea60132e76a6a4156bb83 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:29:42 -0800 Subject: [PATCH 28/36] move semantic nullability tests to their own file --- src/execution/__tests__/executor-test.ts | 199 ----------------- .../__tests__/semantic-nullability-test.ts | 206 ++++++++++++++++++ 2 files changed, 206 insertions(+), 199 deletions(-) create mode 100644 src/execution/__tests__/semantic-nullability-test.ts diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 4c0600285f..04bab46d24 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -16,16 +16,12 @@ import { GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, - GraphQLSemanticNonNull, - GraphQLSemanticNullable, GraphQLUnionType, } from '../../type/definition'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; import { execute, executeSync } from '../execute'; -import { GraphQLError } from '../../error'; -import { ExecutableDefinitionNode, FieldNode, SelectionSetNode } from '../../language'; describe('Execute: Handles basic execution tasks', () => { it('throws if no document is provided', () => { @@ -1328,198 +1324,3 @@ describe('Execute: Handles basic execution tasks', () => { }); }); -describe('Execute: Handles Semantic Nullability', () => { - const DeepDataType = new GraphQLObjectType({ - name: 'DeepDataType', - fields: { - f: { type: new GraphQLNonNull(GraphQLString) } - }, - }); - - const DataType: GraphQLObjectType = new GraphQLObjectType({ - name: 'DataType', - fields: () => ({ - a: { type: new GraphQLSemanticNullable(GraphQLString) }, - b: { type: new GraphQLSemanticNonNull(GraphQLString) }, - c: { type: new GraphQLNonNull(GraphQLString) }, - d: { type: new GraphQLSemanticNonNull(DeepDataType) } - }), - }); - - it('SemanticNonNull throws error on null without error', async () => { - const data = { - a: () => 'Apple', - b: () => null, - c: () => 'Cookie' - }; - - const document = parse(` - query { - b - } - `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - let selectionSet = executable.selectionSet.selections.values().next().value; - - expect(result).to.deep.equal({ - data: { - b: null - }, - errors: [ - new GraphQLError( - 'Cannot return null for semantic-non-nullable field DataType.b.', - { - nodes: selectionSet, - path: ['b'] - } - ) - ] - }); - }); - - it('SemanticNonNull succeeds on null with error', async () => { - const data = { - a: () => 'Apple', - b: () => { throw new Error( - `Something went wrong`, - ); }, - c: () => 'Cookie' - }; - - const document = parse(` - query { - b - } - `); - - let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - let selectionSet = executable.selectionSet.selections.values().next().value; - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - expect(result).to.deep.equal({ - data: { - b: null - }, - errors: [ - new GraphQLError( - 'Something went wrong', - { - nodes: selectionSet, - path: ['b'] - } - ) - ] - }); - }); - - it('SemanticNonNull halts null propagation', async () => { - const deepData = { - f: () => null - }; - - const data = { - a: () => 'Apple', - b: () => null, - c: () => 'Cookie', - d: () => deepData - }; - - - const document = parse(` - query { - d { - f - } - } - `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - let dSelectionSet = executable.selectionSet.selections.values().next().value as FieldNode; - let fSelectionSet = dSelectionSet.selectionSet?.selections.values().next().value; - - expect(result).to.deep.equal({ - data: { - d: null - }, - errors: [ - new GraphQLError( - 'Cannot return null for non-nullable field DeepDataType.f.', - { - nodes: fSelectionSet, - path: ['d', 'f'] - } - ) - ] - }); - }); - - it('SemanticNullable allows null values', async () => { - const data = { - a: () => null, - b: () => null, - c: () => 'Cookie' - }; - - const document = parse(` - query { - a - } - `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - expect(result).to.deep.equal({ - data: { - a: null - } - }); - }); - - it('SemanticNullable allows non-null values', async () => { - const data = { - a: () => 'Apple', - b: () => null, - c: () => 'Cookie' - }; - - const document = parse(` - query { - a - } - `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - expect(result).to.deep.equal({ - data: { - a: 'Apple' - } - }); - }); -}); diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts new file mode 100644 index 0000000000..f3ae81dc67 --- /dev/null +++ b/src/execution/__tests__/semantic-nullability-test.ts @@ -0,0 +1,206 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { parse } from '../../language/parser'; +import { GraphQLNonNull, GraphQLObjectType, GraphQLSemanticNonNull, GraphQLSemanticNullable } from '../../type/definition'; +import { execute } from '../execute'; +import { GraphQLSchema } from '../../type/schema'; +import { GraphQLString } from '../../type/scalars'; +import { ExecutableDefinitionNode, FieldNode } from '../../language'; +import { GraphQLError } from '../../error/GraphQLError'; + +describe('Execute: Handles Semantic Nullability', () => { + const DeepDataType = new GraphQLObjectType({ + name: 'DeepDataType', + fields: { + f: { type: new GraphQLNonNull(GraphQLString) } + }, + }); + + const DataType: GraphQLObjectType = new GraphQLObjectType({ + name: 'DataType', + fields: () => ({ + a: { type: new GraphQLSemanticNullable(GraphQLString) }, + b: { type: new GraphQLSemanticNonNull(GraphQLString) }, + c: { type: new GraphQLNonNull(GraphQLString) }, + d: { type: new GraphQLSemanticNonNull(DeepDataType) } + }), + }); + + it('SemanticNonNull throws error on null without error', async () => { + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie' + }; + + const document = parse(` + query { + b + } + `); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + let selectionSet = executable.selectionSet.selections.values().next().value; + + expect(result).to.deep.equal({ + data: { + b: null + }, + errors: [ + new GraphQLError( + 'Cannot return null for semantic-non-nullable field DataType.b.', + { + nodes: selectionSet, + path: ['b'] + } + ) + ] + }); + }); + + it('SemanticNonNull succeeds on null with error', async () => { + const data = { + a: () => 'Apple', + b: () => { throw new Error( + `Something went wrong`, + ); }, + c: () => 'Cookie' + }; + + const document = parse(` + query { + b + } + `); + + let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + let selectionSet = executable.selectionSet.selections.values().next().value; + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + b: null + }, + errors: [ + new GraphQLError( + 'Something went wrong', + { + nodes: selectionSet, + path: ['b'] + } + ) + ] + }); + }); + + it('SemanticNonNull halts null propagation', async () => { + const deepData = { + f: () => null + }; + + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie', + d: () => deepData + }; + + + const document = parse(` + query { + d { + f + } + } + `); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + let dSelectionSet = executable.selectionSet.selections.values().next().value as FieldNode; + let fSelectionSet = dSelectionSet.selectionSet?.selections.values().next().value; + + expect(result).to.deep.equal({ + data: { + d: null + }, + errors: [ + new GraphQLError( + 'Cannot return null for non-nullable field DeepDataType.f.', + { + nodes: fSelectionSet, + path: ['d', 'f'] + } + ) + ] + }); + }); + + it('SemanticNullable allows null values', async () => { + const data = { + a: () => null, + b: () => null, + c: () => 'Cookie' + }; + + const document = parse(` + query { + a + } + `); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + a: null + } + }); + }); + + it('SemanticNullable allows non-null values', async () => { + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie' + }; + + const document = parse(` + query { + a + } + `); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + a: 'Apple' + } + }); + }); + }); + \ No newline at end of file From 3d7cd0602d0208ecadb7a2f113875317cfa2bae3 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:39:06 -0800 Subject: [PATCH 29/36] fix git status --- .../__tests__/semantic-nullability-test.ts | 28 ++++++++------- src/language/__tests__/parser-test.ts | 1 - src/language/parser.ts | 34 +++++++++---------- src/language/printer.ts | 2 +- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts index f3ae81dc67..73c7db65ad 100644 --- a/src/execution/__tests__/semantic-nullability-test.ts +++ b/src/execution/__tests__/semantic-nullability-test.ts @@ -1,12 +1,16 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; + +import { GraphQLError } from '../../error/GraphQLError'; + +import type { ExecutableDefinitionNode, FieldNode } from '../../language/ast'; import { parse } from '../../language/parser'; + import { GraphQLNonNull, GraphQLObjectType, GraphQLSemanticNonNull, GraphQLSemanticNullable } from '../../type/definition'; -import { execute } from '../execute'; -import { GraphQLSchema } from '../../type/schema'; import { GraphQLString } from '../../type/scalars'; -import { ExecutableDefinitionNode, FieldNode } from '../../language'; -import { GraphQLError } from '../../error/GraphQLError'; +import { GraphQLSchema } from '../../type/schema'; + +import { execute } from '../execute'; describe('Execute: Handles Semantic Nullability', () => { const DeepDataType = new GraphQLObjectType({ @@ -45,8 +49,8 @@ describe('Execute: Handles Semantic Nullability', () => { rootValue: data, }); - let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - let selectionSet = executable.selectionSet.selections.values().next().value; + const executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + const selectionSet = executable.selectionSet.selections.values().next().value; expect(result).to.deep.equal({ data: { @@ -68,7 +72,7 @@ describe('Execute: Handles Semantic Nullability', () => { const data = { a: () => 'Apple', b: () => { throw new Error( - `Something went wrong`, + 'Something went wrong', ); }, c: () => 'Cookie' }; @@ -79,8 +83,8 @@ describe('Execute: Handles Semantic Nullability', () => { } `); - let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - let selectionSet = executable.selectionSet.selections.values().next().value; + const executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + const selectionSet = executable.selectionSet.selections.values().next().value; const result = await execute({ schema: new GraphQLSchema({ query: DataType }), @@ -131,9 +135,9 @@ describe('Execute: Handles Semantic Nullability', () => { rootValue: data, }); - let executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - let dSelectionSet = executable.selectionSet.selections.values().next().value as FieldNode; - let fSelectionSet = dSelectionSet.selectionSet?.selections.values().next().value; + const executable = document.definitions?.values().next().value as ExecutableDefinitionNode; + const dSelectionSet = executable.selectionSet.selections.values().next().value as FieldNode; + const fSelectionSet = dSelectionSet.selectionSet?.selections.values().next().value; expect(result).to.deep.equal({ data: { diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 96aa8e6338..7b3782e60e 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -11,7 +11,6 @@ import { Kind } from '../kinds'; import { parse, parseConstValue, parseType, parseValue } from '../parser'; import { Source } from '../source'; import { TokenKind } from '../tokenKind'; -import { Console } from 'console'; function expectSyntaxError(text: string) { return expectToThrowJSON(() => parse(text)); diff --git a/src/language/parser.ts b/src/language/parser.ts index d68148febe..20992c40f7 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -263,9 +263,9 @@ export class Parser { */ parseDefinition(): DefinitionNode { // TODO: I don't know what isConst represents. Every other callsite has it false - let directives = this.parseDirectives(false); - for (let directive of directives) { - if (directive.name.value == "SemanticNullability") { + const directives = this.parseDirectives(false); + for (const directive of directives) { + if (directive.name.value === 'SemanticNullability') { this._options.useSemanticNullability = true; } } @@ -788,22 +788,22 @@ export class Parser { kind: Kind.SEMANTIC_NULLABLE_TYPE, type, }); - } else { - return this.node(start, { - kind: Kind.SEMANTIC_NON_NULL_TYPE, - type, - }); } - } else { - if (this.expectOptionalToken(TokenKind.BANG)) { - return this.node(start, { - kind: Kind.NON_NULL_TYPE, - type, - }); - } - - return type; + + return this.node(start, { + kind: Kind.SEMANTIC_NON_NULL_TYPE, + type, + }); + } + + if (this.expectOptionalToken(TokenKind.BANG)) { + return this.node(start, { + kind: Kind.NON_NULL_TYPE, + type, + }); } + + return type; } /** diff --git a/src/language/printer.ts b/src/language/printer.ts index a380b5c778..a72f5b8505 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -17,7 +17,7 @@ export interface PrintOptions { * Converts an AST into a string, using one set of reasonable * formatting rules. */ -export function print(ast: ASTNode, options: PrintOptions = {}): string { +export function print(ast: ASTNode): string { return visit(ast, printDocASTReducer); } From 9247ff2dc187954c05a59b11e740244cde6a97bd Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:41:28 -0800 Subject: [PATCH 30/36] run prettier --- src/execution/__tests__/executor-test.ts | 1 - .../__tests__/semantic-nullability-test.ts | 346 +++++++++--------- src/language/__tests__/parser-test.ts | 24 +- src/language/__tests__/schema-printer-test.ts | 22 +- src/language/lexer.ts | 7 +- src/language/parser.ts | 6 +- src/language/printer.ts | 4 +- src/type/definition.ts | 11 +- 8 files changed, 224 insertions(+), 197 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 04bab46d24..a7bc1c8265 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -1323,4 +1323,3 @@ describe('Execute: Handles basic execution tasks', () => { expect(possibleTypes).to.deep.equal([fooObject]); }); }); - diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts index 73c7db65ad..20a33c2ffa 100644 --- a/src/execution/__tests__/semantic-nullability-test.ts +++ b/src/execution/__tests__/semantic-nullability-test.ts @@ -6,205 +6,215 @@ import { GraphQLError } from '../../error/GraphQLError'; import type { ExecutableDefinitionNode, FieldNode } from '../../language/ast'; import { parse } from '../../language/parser'; -import { GraphQLNonNull, GraphQLObjectType, GraphQLSemanticNonNull, GraphQLSemanticNullable } from '../../type/definition'; +import { + GraphQLNonNull, + GraphQLObjectType, + GraphQLSemanticNonNull, + GraphQLSemanticNullable, +} from '../../type/definition'; import { GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; import { execute } from '../execute'; describe('Execute: Handles Semantic Nullability', () => { - const DeepDataType = new GraphQLObjectType({ - name: 'DeepDataType', - fields: { - f: { type: new GraphQLNonNull(GraphQLString) } - }, - }); - - const DataType: GraphQLObjectType = new GraphQLObjectType({ - name: 'DataType', - fields: () => ({ - a: { type: new GraphQLSemanticNullable(GraphQLString) }, - b: { type: new GraphQLSemanticNonNull(GraphQLString) }, - c: { type: new GraphQLNonNull(GraphQLString) }, - d: { type: new GraphQLSemanticNonNull(DeepDataType) } - }), - }); - - it('SemanticNonNull throws error on null without error', async () => { - const data = { - a: () => 'Apple', - b: () => null, - c: () => 'Cookie' - }; - - const document = parse(` + const DeepDataType = new GraphQLObjectType({ + name: 'DeepDataType', + fields: { + f: { type: new GraphQLNonNull(GraphQLString) }, + }, + }); + + const DataType: GraphQLObjectType = new GraphQLObjectType({ + name: 'DataType', + fields: () => ({ + a: { type: new GraphQLSemanticNullable(GraphQLString) }, + b: { type: new GraphQLSemanticNonNull(GraphQLString) }, + c: { type: new GraphQLNonNull(GraphQLString) }, + d: { type: new GraphQLSemanticNonNull(DeepDataType) }, + }), + }); + + it('SemanticNonNull throws error on null without error', async () => { + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie', + }; + + const document = parse(` query { b } `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - const executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - const selectionSet = executable.selectionSet.selections.values().next().value; - - expect(result).to.deep.equal({ - data: { - b: null - }, - errors: [ - new GraphQLError( - 'Cannot return null for semantic-non-nullable field DataType.b.', - { - nodes: selectionSet, - path: ['b'] - } - ) - ] - }); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + const executable = document.definitions?.values().next() + .value as ExecutableDefinitionNode; + const selectionSet = executable.selectionSet.selections + .values() + .next().value; + + expect(result).to.deep.equal({ + data: { + b: null, + }, + errors: [ + new GraphQLError( + 'Cannot return null for semantic-non-nullable field DataType.b.', + { + nodes: selectionSet, + path: ['b'], + }, + ), + ], }); - - it('SemanticNonNull succeeds on null with error', async () => { - const data = { - a: () => 'Apple', - b: () => { throw new Error( - 'Something went wrong', - ); }, - c: () => 'Cookie' - }; - - const document = parse(` + }); + + it('SemanticNonNull succeeds on null with error', async () => { + const data = { + a: () => 'Apple', + b: () => { + throw new Error('Something went wrong'); + }, + c: () => 'Cookie', + }; + + const document = parse(` query { b } `); - - const executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - const selectionSet = executable.selectionSet.selections.values().next().value; - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - expect(result).to.deep.equal({ - data: { - b: null - }, - errors: [ - new GraphQLError( - 'Something went wrong', - { - nodes: selectionSet, - path: ['b'] - } - ) - ] - }); + + const executable = document.definitions?.values().next() + .value as ExecutableDefinitionNode; + const selectionSet = executable.selectionSet.selections + .values() + .next().value; + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + b: null, + }, + errors: [ + new GraphQLError('Something went wrong', { + nodes: selectionSet, + path: ['b'], + }), + ], }); - - it('SemanticNonNull halts null propagation', async () => { - const deepData = { - f: () => null - }; - - const data = { - a: () => 'Apple', - b: () => null, - c: () => 'Cookie', - d: () => deepData - }; - - - const document = parse(` + }); + + it('SemanticNonNull halts null propagation', async () => { + const deepData = { + f: () => null, + }; + + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie', + d: () => deepData, + }; + + const document = parse(` query { d { f } } `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - const executable = document.definitions?.values().next().value as ExecutableDefinitionNode; - const dSelectionSet = executable.selectionSet.selections.values().next().value as FieldNode; - const fSelectionSet = dSelectionSet.selectionSet?.selections.values().next().value; - - expect(result).to.deep.equal({ - data: { - d: null - }, - errors: [ - new GraphQLError( - 'Cannot return null for non-nullable field DeepDataType.f.', - { - nodes: fSelectionSet, - path: ['d', 'f'] - } - ) - ] - }); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + const executable = document.definitions?.values().next() + .value as ExecutableDefinitionNode; + const dSelectionSet = executable.selectionSet.selections.values().next() + .value as FieldNode; + const fSelectionSet = dSelectionSet.selectionSet?.selections + .values() + .next().value; + + expect(result).to.deep.equal({ + data: { + d: null, + }, + errors: [ + new GraphQLError( + 'Cannot return null for non-nullable field DeepDataType.f.', + { + nodes: fSelectionSet, + path: ['d', 'f'], + }, + ), + ], }); - - it('SemanticNullable allows null values', async () => { - const data = { - a: () => null, - b: () => null, - c: () => 'Cookie' - }; - - const document = parse(` + }); + + it('SemanticNullable allows null values', async () => { + const data = { + a: () => null, + b: () => null, + c: () => 'Cookie', + }; + + const document = parse(` query { a } `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - expect(result).to.deep.equal({ - data: { - a: null - } - }); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, }); - - it('SemanticNullable allows non-null values', async () => { - const data = { - a: () => 'Apple', - b: () => null, - c: () => 'Cookie' - }; - - const document = parse(` + + expect(result).to.deep.equal({ + data: { + a: null, + }, + }); + }); + + it('SemanticNullable allows non-null values', async () => { + const data = { + a: () => 'Apple', + b: () => null, + c: () => 'Cookie', + }; + + const document = parse(` query { a } `); - - const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), - document, - rootValue: data, - }); - - expect(result).to.deep.equal({ - data: { - a: 'Apple' - } - }); + + const result = await execute({ + schema: new GraphQLSchema({ query: DataType }), + document, + rootValue: data, + }); + + expect(result).to.deep.equal({ + data: { + a: 'Apple', + }, }); }); - \ No newline at end of file +}); diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 7b3782e60e..c01768737c 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -690,19 +690,19 @@ describe('Parser', () => { it('parses nullable types', () => { const result = parseType('MyType?', { useSemanticNullability: true }); - expectJSON(result).toDeepEqual( - { kind: Kind.SEMANTIC_NULLABLE_TYPE, - loc: { start: 0, end: 7 }, - type: { - kind: Kind.NAMED_TYPE, + expectJSON(result).toDeepEqual({ + kind: Kind.SEMANTIC_NULLABLE_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 }, - name: { - kind: Kind.NAME, - loc: { start: 0, end: 6 }, - value: 'MyType', - }, - } - }); + value: 'MyType', + }, + }, + }); }); it('parses non-nullable types', () => { diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts index f98d37e28f..ea4be63a3b 100644 --- a/src/language/__tests__/schema-printer-test.ts +++ b/src/language/__tests__/schema-printer-test.ts @@ -180,21 +180,31 @@ describe('Printer: SDL document', () => { } `); }); - + it('prints NamedType', () => { - expect(print(parseType('MyType', { useSemanticNullability: false }))).to.equal(dedent`MyType`); + expect( + print(parseType('MyType', { useSemanticNullability: false })), + ).to.equal(dedent`MyType`); }); it('prints SemanticNullableType', () => { - expect(print(parseType('MyType?', { useSemanticNullability: true }))).to.equal(dedent`MyType?`); + expect( + print(parseType('MyType?', { useSemanticNullability: true })), + ).to.equal(dedent`MyType?`); }); it('prints SemanticNonNullType', () => { - expect(print(parseType('MyType', { useSemanticNullability: true }))).to.equal(dedent`MyType`); + expect( + print(parseType('MyType', { useSemanticNullability: true })), + ).to.equal(dedent`MyType`); }); it('prints NonNullType', () => { - expect(print(parseType('MyType!', { useSemanticNullability: true }))).to.equal(dedent`MyType!`); - expect(print(parseType('MyType!', { useSemanticNullability: false }))).to.equal(dedent`MyType!`); + expect( + print(parseType('MyType!', { useSemanticNullability: true })), + ).to.equal(dedent`MyType!`); + expect( + print(parseType('MyType!', { useSemanticNullability: false })), + ).to.equal(dedent`MyType!`); }); }); diff --git a/src/language/lexer.ts b/src/language/lexer.ts index bdaf8b9b25..1696fa5c83 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -259,7 +259,12 @@ function readNextToken(lexer: Lexer, start: number): Token { case 0x0029: // ) return createToken(lexer, TokenKind.PAREN_R, position, position + 1); case 0x003f: // ? - return createToken(lexer, TokenKind.QUESTION_MARK, position, position + 1); + return createToken( + lexer, + TokenKind.QUESTION_MARK, + position, + position + 1, + ); case 0x002e: // . if ( body.charCodeAt(position + 1) === 0x002e && diff --git a/src/language/parser.ts b/src/language/parser.ts index 20992c40f7..670b197e7d 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -789,13 +789,13 @@ export class Parser { type, }); } - + return this.node(start, { kind: Kind.SEMANTIC_NON_NULL_TYPE, type, }); - } - + } + if (this.expectOptionalToken(TokenKind.BANG)) { return this.node(start, { kind: Kind.NON_NULL_TYPE, diff --git a/src/language/printer.ts b/src/language/printer.ts index a72f5b8505..17b805e624 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -135,9 +135,7 @@ const printDocASTReducer: ASTReducer = { // Type - NamedType: { leave: ({ name }) => - name -}, + NamedType: { leave: ({ name }) => name }, ListType: { leave: ({ type }) => '[' + type + ']' }, NonNullType: { leave: ({ type }) => type + '!' }, SemanticNonNullType: { leave: ({ type }) => type }, diff --git a/src/type/definition.ts b/src/type/definition.ts index a7d7d2cd0d..9d2fc95089 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -84,7 +84,7 @@ export type GraphQLType = | GraphQLEnumType | GraphQLInputObjectType | GraphQLList - >; + >; export function isType(type: unknown): type is GraphQLType { return ( @@ -561,7 +561,7 @@ export class GraphQLSemanticNonNull { * }) * }) * ``` - * Note: This is equivalent to the unadorned named type that is + * Note: This is equivalent to the unadorned named type that is * used by GraphQL when it is not operating in SemanticNullability mode. * * @experimental @@ -602,7 +602,12 @@ export type GraphQLWrappingType = | GraphQLSemanticNullable; export function isWrappingType(type: unknown): type is GraphQLWrappingType { - return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type) || isSemanticNullableType(type); + return ( + isListType(type) || + isNonNullType(type) || + isSemanticNonNullType(type) || + isSemanticNullableType(type) + ); } export function assertWrappingType(type: unknown): GraphQLWrappingType { From e6c223917be3321cfc04e813503d152e45348626 Mon Sep 17 00:00:00 2001 From: twof Date: Thu, 7 Nov 2024 22:59:55 -0800 Subject: [PATCH 31/36] Add comment to parser about document directive --- src/language/parser.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/language/parser.ts b/src/language/parser.ts index 670b197e7d..6e1778ed81 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -262,8 +262,10 @@ export class Parser { * - InputObjectTypeDefinition */ parseDefinition(): DefinitionNode { - // TODO: I don't know what isConst represents. Every other callsite has it false const directives = this.parseDirectives(false); + // If a document-level SemanticNullability directive exists as + // the first element in a document, then all parsing will + // happen in SemanticNullability mode. for (const directive of directives) { if (directive.name.value === 'SemanticNullability') { this._options.useSemanticNullability = true; From f45b47448580e122e72f36b21976ce23a05d3915 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 10 Nov 2024 14:16:34 +0200 Subject: [PATCH 32/36] use semantic nullable wrapper only --- .../__tests__/semantic-nullability-test.ts | 30 ++-- src/execution/execute.ts | 129 +++++++----------- src/index.ts | 9 +- src/language/__tests__/parser-test.ts | 12 +- src/language/__tests__/predicates-test.ts | 2 +- src/language/ast.ts | 9 -- src/language/index.ts | 2 +- src/language/kinds.ts | 1 - src/language/parser.ts | 30 ++-- src/language/predicates.ts | 2 +- src/language/printer.ts | 8 -- src/type/__tests__/introspection-test.ts | 2 +- src/type/__tests__/predicate-test.ts | 52 ++++--- src/type/definition.ts | 110 ++------------- src/type/index.ts | 6 +- src/type/introspection.ts | 76 ++++++----- src/type/schema.ts | 5 + src/utilities/__tests__/printSchema-test.ts | 4 +- src/utilities/astFromValue.ts | 2 +- src/utilities/buildASTSchema.ts | 11 ++ src/utilities/buildClientSchema.ts | 12 +- src/utilities/extendSchema.ts | 14 +- src/utilities/findBreakingChanges.ts | 21 ++- src/utilities/getIntrospectionQuery.ts | 6 +- src/utilities/index.ts | 2 +- src/utilities/lexicographicSortSchema.ts | 8 +- src/utilities/printSchema.ts | 49 +++++-- src/utilities/typeComparators.ts | 27 ++-- src/utilities/typeFromAST.ts | 6 +- .../rules/OverlappingFieldsCanBeMergedRule.ts | 8 +- 30 files changed, 286 insertions(+), 369 deletions(-) diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts index 20a33c2ffa..97846fdd38 100644 --- a/src/execution/__tests__/semantic-nullability-test.ts +++ b/src/execution/__tests__/semantic-nullability-test.ts @@ -9,7 +9,6 @@ import { parse } from '../../language/parser'; import { GraphQLNonNull, GraphQLObjectType, - GraphQLSemanticNonNull, GraphQLSemanticNullable, } from '../../type/definition'; import { GraphQLString } from '../../type/scalars'; @@ -29,9 +28,9 @@ describe('Execute: Handles Semantic Nullability', () => { name: 'DataType', fields: () => ({ a: { type: new GraphQLSemanticNullable(GraphQLString) }, - b: { type: new GraphQLSemanticNonNull(GraphQLString) }, + b: { type: GraphQLString }, c: { type: new GraphQLNonNull(GraphQLString) }, - d: { type: new GraphQLSemanticNonNull(DeepDataType) }, + d: { type: DeepDataType }, }), }); @@ -49,7 +48,10 @@ describe('Execute: Handles Semantic Nullability', () => { `); const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), + schema: new GraphQLSchema({ + useSemanticNullability: true, + query: DataType, + }), document, rootValue: data, }); @@ -98,7 +100,10 @@ describe('Execute: Handles Semantic Nullability', () => { .next().value; const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), + schema: new GraphQLSchema({ + useSemanticNullability: true, + query: DataType, + }), document, rootValue: data, }); @@ -137,7 +142,10 @@ describe('Execute: Handles Semantic Nullability', () => { `); const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), + schema: new GraphQLSchema({ + useSemanticNullability: true, + query: DataType, + }), document, rootValue: data, }); @@ -180,7 +188,10 @@ describe('Execute: Handles Semantic Nullability', () => { `); const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), + schema: new GraphQLSchema({ + useSemanticNullability: true, + query: DataType, + }), document, rootValue: data, }); @@ -206,7 +217,10 @@ describe('Execute: Handles Semantic Nullability', () => { `); const result = await execute({ - schema: new GraphQLSchema({ query: DataType }), + schema: new GraphQLSchema({ + useSemanticNullability: true, + query: DataType, + }), document, rootValue: data, }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index b50395d2e3..7b753db08b 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -43,7 +43,6 @@ import { isListType, isNonNullType, isObjectType, - isSemanticNonNullType, isSemanticNullableType, } from '../type/definition'; import { @@ -651,109 +650,83 @@ function completeValue( throw result; } - // If field type is NonNull, complete for inner type, and throw field error - // if result is null. + let nonNull; + let semanticNull; + let nullableType; if (isNonNullType(returnType)) { - const completed = completeValue( - exeContext, - returnType.ofType, - fieldNodes, - info, - path, - result, - ); - if (completed === null) { - throw new Error( - `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, - ); - } - return completed; + nonNull = true; + nullableType = returnType.ofType; + } else if (isSemanticNullableType(returnType)) { + semanticNull = true; + nullableType = returnType.ofType; + } else { + nullableType = returnType; } - // If field type is SemanticNonNull, complete for inner type, and throw field error - // if result is null and an error doesn't exist. - if (isSemanticNonNullType(returnType)) { - const completed = completeValue( + let completed; + if (result == null) { + // If result value is null or undefined then return null. + completed = null; + } else if (isListType(nullableType)) { + // If field type is List, complete each item in the list with the inner type + completed = completeListValue( exeContext, - returnType.ofType, + nullableType, 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 field type is SemanticNullable, complete for inner type - if (isSemanticNullableType(returnType)) { - return completeValue( + } else if (isLeafType(nullableType)) { + // If field type is a leaf type, Scalar or Enum, serialize to a valid value, + // returning null if serialization is not possible. + completed = completeLeafValue(nullableType, result); + } else if (isAbstractType(nullableType)) { + // If field type is an abstract type, Interface or Union, determine the + // runtime Object type and complete for that type. + completed = completeAbstractValue( exeContext, - returnType.ofType, + nullableType, fieldNodes, info, path, result, ); - } - - // If result value is null or undefined then return null. - if (result == null) { - return null; - } - - // If field type is List, complete each item in the list with the inner type - if (isListType(returnType)) { - return completeListValue( + } else if (isObjectType(nullableType)) { + // If field type is Object, execute and complete all sub-selections. + completed = completeObjectValue( exeContext, - returnType, + nullableType, fieldNodes, info, path, result, ); + } else { + /* c8 ignore next 6 */ + // Not reachable, all possible output types have been considered. + invariant( + false, + 'Cannot complete value of unexpected output type: ' + + inspect(nullableType), + ); } - // If field type is a leaf type, Scalar or Enum, serialize to a valid value, - // returning null if serialization is not possible. - if (isLeafType(returnType)) { - return completeLeafValue(returnType, result); - } + if (completed === null) { + if (nonNull) { + throw new Error( + `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, + ); + } - // If field type is an abstract type, Interface or Union, determine the - // runtime Object type and complete for that type. - if (isAbstractType(returnType)) { - return completeAbstractValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); + if (!semanticNull && exeContext.schema.usingSemanticNullability) { + throw new Error( + `Cannot return null for semantic-non-nullable field ${info.parentType.name}.${info.fieldName}.`, + ); + } } - // If field type is Object, execute and complete all sub-selections. - if (isObjectType(returnType)) { - return completeObjectValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); - } - /* c8 ignore next 6 */ - // Not reachable, all possible output types have been considered. - invariant( - false, - 'Cannot complete value of unexpected output type: ' + inspect(returnType), - ); + return completed; } /** diff --git a/src/index.ts b/src/index.ts index dacea5d145..e2eb5ee073 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,6 @@ export { GraphQLInputObjectType, GraphQLList, GraphQLNonNull, - GraphQLSemanticNonNull, // Standard GraphQL Scalars specifiedScalarTypes, GraphQLInt, @@ -97,7 +96,7 @@ export { isInputObjectType, isListType, isNonNullType, - isSemanticNonNullType, + isSemanticNullableType, isInputType, isOutputType, isLeafType, @@ -123,7 +122,7 @@ export { assertInputObjectType, assertListType, assertNonNullType, - assertSemanticNonNullType, + assertSemanticNullableType, assertInputType, assertOutputType, assertLeafType, @@ -291,7 +290,7 @@ export type { NamedTypeNode, ListTypeNode, NonNullTypeNode, - SemanticNonNullTypeNode, + SemanticNullableTypeNode, TypeSystemDefinitionNode, SchemaDefinitionNode, OperationTypeDefinitionNode, @@ -486,7 +485,7 @@ export type { IntrospectionNamedTypeRef, IntrospectionListTypeRef, IntrospectionNonNullTypeRef, - IntrospectionSemanticNonNullTypeRef, + IntrospectionSemanticNullableTypeRef, IntrospectionField, IntrospectionInputValue, IntrospectionEnumValue, diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index c01768737c..cbd171570e 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -674,16 +674,12 @@ describe('Parser', () => { it('parses semantic-non-null types', () => { const result = parseType('MyType', { useSemanticNullability: true }); expectJSON(result).toDeepEqual({ - kind: Kind.SEMANTIC_NON_NULL_TYPE, + kind: Kind.NAMED_TYPE, loc: { start: 0, end: 6 }, - type: { - kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, loc: { start: 0, end: 6 }, - name: { - kind: Kind.NAME, - loc: { start: 0, end: 6 }, - value: 'MyType', - }, + value: 'MyType', }, }); }); diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts index 32ef7d1fe1..14f7f14707 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -92,7 +92,7 @@ describe('AST node predicates', () => { 'NamedType', 'ListType', 'NonNullType', - 'SemanticNonNullType', + 'SemanticNullableType', ]); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index aa62451c0c..81154e88d7 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -161,7 +161,6 @@ export type ASTNode = | NamedTypeNode | ListTypeNode | NonNullTypeNode - | SemanticNonNullTypeNode | SemanticNullableTypeNode | SchemaDefinitionNode | OperationTypeDefinitionNode @@ -237,7 +236,6 @@ export const QueryDocumentKeys: { NamedType: ['name'], ListType: ['type'], NonNullType: ['type'], - SemanticNonNullType: ['type'], SemanticNullableType: ['type'], SchemaDefinition: ['description', 'directives', 'operationTypes'], @@ -529,7 +527,6 @@ export type TypeNode = | NamedTypeNode | ListTypeNode | NonNullTypeNode - | SemanticNonNullTypeNode | SemanticNullableTypeNode; export interface NamedTypeNode { @@ -550,12 +547,6 @@ export interface NonNullTypeNode { readonly type: NamedTypeNode | ListTypeNode; } -export interface SemanticNonNullTypeNode { - readonly kind: Kind.SEMANTIC_NON_NULL_TYPE; - readonly loc?: Location; - readonly type: NamedTypeNode | ListTypeNode; -} - export interface SemanticNullableTypeNode { readonly kind: Kind.SEMANTIC_NULLABLE_TYPE; readonly loc?: Location; diff --git a/src/language/index.ts b/src/language/index.ts index a760fd21b3..1e4e4d947f 100644 --- a/src/language/index.ts +++ b/src/language/index.ts @@ -67,7 +67,7 @@ export type { NamedTypeNode, ListTypeNode, NonNullTypeNode, - SemanticNonNullTypeNode, + SemanticNullableTypeNode, TypeSystemDefinitionNode, SchemaDefinitionNode, OperationTypeDefinitionNode, diff --git a/src/language/kinds.ts b/src/language/kinds.ts index 7111a94834..a9ff334472 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -37,7 +37,6 @@ enum Kind { NAMED_TYPE = 'NamedType', LIST_TYPE = 'ListType', NON_NULL_TYPE = 'NonNullType', - SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType', SEMANTIC_NULLABLE_TYPE = 'SemanticNullableType', /** Type System Definitions */ diff --git a/src/language/parser.ts b/src/language/parser.ts index 6e1778ed81..22303077ea 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -50,7 +50,6 @@ import type { SchemaExtensionNode, SelectionNode, SelectionSetNode, - SemanticNonNullTypeNode, SemanticNullableTypeNode, StringValueNode, Token, @@ -763,7 +762,7 @@ export class Parser { * - NamedType * - ListType * - NonNullType - * - SemanticNonNullType + * - SemanticNullableType */ parseTypeReference(): TypeNode { const start = this._lexer.token; @@ -779,30 +778,19 @@ export class Parser { type = this.parseNamedType(); } - if (this._options.useSemanticNullability) { - if (this.expectOptionalToken(TokenKind.BANG)) { - return this.node(start, { - kind: Kind.NON_NULL_TYPE, - type, - }); - } else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) { - return this.node(start, { - kind: Kind.SEMANTIC_NULLABLE_TYPE, - type, - }); - } - - return this.node(start, { - kind: Kind.SEMANTIC_NON_NULL_TYPE, - type, - }); - } - if (this.expectOptionalToken(TokenKind.BANG)) { return this.node(start, { kind: Kind.NON_NULL_TYPE, type, }); + } else if ( + this._options.useSemanticNullability && + this.expectOptionalToken(TokenKind.QUESTION_MARK) + ) { + return this.node(start, { + kind: Kind.SEMANTIC_NULLABLE_TYPE, + type, + }); } return type; diff --git a/src/language/predicates.ts b/src/language/predicates.ts index 3ddf52b94c..5d1454147a 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -68,7 +68,7 @@ export function isTypeNode(node: ASTNode): node is TypeNode { node.kind === Kind.NAMED_TYPE || node.kind === Kind.LIST_TYPE || node.kind === Kind.NON_NULL_TYPE || - node.kind === Kind.SEMANTIC_NON_NULL_TYPE + node.kind === Kind.SEMANTIC_NULLABLE_TYPE ); } diff --git a/src/language/printer.ts b/src/language/printer.ts index 17b805e624..bec2110721 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -6,13 +6,6 @@ import { printString } from './printString'; import type { ASTReducer } from './visitor'; import { visit } from './visitor'; -/** - * Configuration options to control parser behavior - */ -export interface PrintOptions { - useSemanticNullability?: boolean; -} - /** * Converts an AST into a string, using one set of reasonable * formatting rules. @@ -138,7 +131,6 @@ const printDocASTReducer: ASTReducer = { NamedType: { leave: ({ name }) => name }, ListType: { leave: ({ type }) => '[' + type + ']' }, NonNullType: { leave: ({ type }) => type + '!' }, - SemanticNonNullType: { leave: ({ type }) => type }, SemanticNullableType: { leave: ({ type }) => type + '?' }, // Type System Definitions diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index f57baf8894..19e51732aa 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -438,7 +438,7 @@ describe('Introspection', () => { deprecationReason: null, }, { - name: 'SEMANTIC_NON_NULL', + name: 'SEMANTIC_NULLABLE', isDeprecated: false, deprecationReason: null, }, diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index 1c576e8eaa..47017b560a 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -23,7 +23,7 @@ import { assertObjectType, assertOutputType, assertScalarType, - assertSemanticNonNullType, + assertSemanticNullableType, assertType, assertUnionType, assertWrappingType, @@ -36,7 +36,7 @@ import { GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, - GraphQLSemanticNonNull, + GraphQLSemanticNullable, GraphQLUnionType, isAbstractType, isCompositeType, @@ -54,7 +54,7 @@ import { isRequiredArgument, isRequiredInputField, isScalarType, - isSemanticNonNullType, + isSemanticNullableType, isType, isUnionType, isWrappingType, @@ -301,46 +301,42 @@ 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(); + expect(isNonNullType(ObjectType)).to.equal(false); + expect(() => assertNonNullType(ObjectType)).to.throw(); }); }); - describe('isSemanticNonNullType', () => { + describe('isSemanticNullableType', () => { it('returns true for a semantic-non-null wrapped type', () => { expect( - isSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)), + isSemanticNullableType(new GraphQLSemanticNullable(ObjectType)), ).to.equal(true); expect(() => - assertSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)), + assertSemanticNullableType(new GraphQLSemanticNullable(ObjectType)), ).to.not.throw(); }); it('returns false for an unwrapped type', () => { - expect(isSemanticNonNullType(ObjectType)).to.equal(false); - expect(() => assertSemanticNonNullType(ObjectType)).to.throw(); + expect(isSemanticNullableType(ObjectType)).to.equal(false); + expect(() => assertSemanticNullableType(ObjectType)).to.throw(); }); - it('returns false for a not non-null wrapped type', () => { + it('returns false for a not semantic-non-null wrapped type', () => { expect( - isSemanticNonNullType( - new GraphQLList(new GraphQLSemanticNonNull(ObjectType)), + isSemanticNullableType( + new GraphQLList(new GraphQLSemanticNullable(ObjectType)), ), ).to.equal(false); expect(() => - assertSemanticNonNullType( - new GraphQLList(new GraphQLSemanticNonNull(ObjectType)), + assertSemanticNullableType( + new GraphQLList(new GraphQLSemanticNullable(ObjectType)), ), ).to.throw(); - expect(isSemanticNonNullType(new GraphQLNonNull(ObjectType))).to.equal( + expect(isSemanticNullableType(new GraphQLNonNull(ObjectType))).to.equal( false, ); expect(() => - assertSemanticNonNullType(new GraphQLNonNull(ObjectType)), + assertSemanticNullableType(new GraphQLNonNull(ObjectType)), ).to.throw(); }); }); @@ -520,11 +516,11 @@ describe('Type predicates', () => { expect(() => assertWrappingType(new GraphQLNonNull(ObjectType)), ).to.not.throw(); - expect(isWrappingType(new GraphQLSemanticNonNull(ObjectType))).to.equal( + expect(isWrappingType(new GraphQLSemanticNullable(ObjectType))).to.equal( true, ); expect(() => - assertWrappingType(new GraphQLSemanticNonNull(ObjectType)), + assertWrappingType(new GraphQLSemanticNullable(ObjectType)), ).to.not.throw(); }); @@ -548,11 +544,13 @@ describe('Type predicates', () => { assertNullableType(new GraphQLList(new GraphQLNonNull(ObjectType))), ).to.not.throw(); expect( - isNullableType(new GraphQLList(new GraphQLSemanticNonNull(ObjectType))), + isNullableType( + new GraphQLList(new GraphQLSemanticNullable(ObjectType)), + ), ).to.equal(true); expect(() => assertNullableType( - new GraphQLList(new GraphQLSemanticNonNull(ObjectType)), + new GraphQLList(new GraphQLSemanticNullable(ObjectType)), ), ).to.not.throw(); }); @@ -562,11 +560,11 @@ describe('Type predicates', () => { expect(() => assertNullableType(new GraphQLNonNull(ObjectType)), ).to.throw(); - expect(isNullableType(new GraphQLSemanticNonNull(ObjectType))).to.equal( + expect(isNullableType(new GraphQLSemanticNullable(ObjectType))).to.equal( false, ); expect(() => - assertNullableType(new GraphQLSemanticNonNull(ObjectType)), + assertNullableType(new GraphQLSemanticNullable(ObjectType)), ).to.throw(); }); }); diff --git a/src/type/definition.ts b/src/type/definition.ts index 9d2fc95089..5d53a039c9 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -67,15 +67,6 @@ export type GraphQLType = | GraphQLInputObjectType | GraphQLList > - | GraphQLSemanticNonNull< - | GraphQLScalarType - | GraphQLObjectType - | GraphQLInterfaceType - | GraphQLUnionType - | GraphQLEnumType - | GraphQLInputObjectType - | GraphQLList - > | GraphQLSemanticNullable< | GraphQLScalarType | GraphQLObjectType @@ -96,7 +87,7 @@ export function isType(type: unknown): type is GraphQLType { isInputObjectType(type) || isListType(type) || isNonNullType(type) || - isSemanticNonNullType(type) + isSemanticNullableType(type) ); } @@ -222,32 +213,6 @@ 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; -} - export function isSemanticNullableType( type: GraphQLInputType, ): type is GraphQLSemanticNullable; @@ -288,14 +253,14 @@ export type GraphQLInputType = | GraphQLInputObjectType | GraphQLList >; -// Note: GraphQLSemanticNonNull is currently not allowed for input types +// Note: GraphQLSemanticNullableType is currently not allowed for input types export function isInputType(type: unknown): type is GraphQLInputType { return ( isScalarType(type) || isEnumType(type) || isInputObjectType(type) || - (!isSemanticNonNullType(type) && + (!isSemanticNullableType(type) && isWrappingType(type) && isInputType(type.ofType)) ); @@ -326,7 +291,7 @@ export type GraphQLOutputType = | GraphQLEnumType | GraphQLList > - | GraphQLSemanticNonNull< + | GraphQLSemanticNullable< | GraphQLScalarType | GraphQLObjectType | GraphQLInterfaceType @@ -496,55 +461,6 @@ 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(); - } -} - /** * Semantic-Nullable Type Wrapper * @@ -598,15 +514,11 @@ export class GraphQLSemanticNullable { export type GraphQLWrappingType = | GraphQLList | GraphQLNonNull - | GraphQLSemanticNonNull | GraphQLSemanticNullable; export function isWrappingType(type: unknown): type is GraphQLWrappingType { return ( - isListType(type) || - isNonNullType(type) || - isSemanticNonNullType(type) || - isSemanticNullableType(type) + isListType(type) || isNonNullType(type) || isSemanticNullableType(type) ); } @@ -630,7 +542,7 @@ export type GraphQLNullableType = | GraphQLList; export function isNullableType(type: unknown): type is GraphQLNullableType { - return isType(type) && !isNonNullType(type) && !isSemanticNonNullType(type); + return isType(type) && !isNonNullType(type) && !isSemanticNullableType(type); } export function assertNullableType(type: unknown): GraphQLNullableType { @@ -642,7 +554,7 @@ export function assertNullableType(type: unknown): GraphQLNullableType { export function getNullableType(type: undefined | null): void; export function getNullableType( - type: T | GraphQLNonNull | GraphQLSemanticNonNull, + type: T | GraphQLNonNull | GraphQLSemanticNullable, ): T; export function getNullableType( type: Maybe, @@ -651,14 +563,14 @@ export function getNullableType( type: Maybe, ): GraphQLNullableType | undefined { if (type) { - return isNonNullType(type) || isSemanticNonNullType(type) + return isNonNullType(type) || isSemanticNullableType(type) ? type.ofType : type; } } /** - * These named types do not include modifiers like List, NonNull, or SemanticNonNull + * These named types do not include modifiers like List, NonNull, or SemanticNullable */ export type GraphQLNamedType = GraphQLNamedInputType | GraphQLNamedOutputType; @@ -1258,7 +1170,7 @@ export interface GraphQLArgument { } export function isRequiredArgument(arg: GraphQLArgument): boolean { - // Note: input types cannot be SemanticNonNull + // Note: input types cannot be SemanticNullable return isNonNullType(arg.type) && arg.defaultValue === undefined; } @@ -1950,7 +1862,7 @@ export interface GraphQLInputField { } export function isRequiredInputField(field: GraphQLInputField): boolean { - // Note: input types cannot be SemanticNonNull + // Note: input types cannot be SemanticNullable return isNonNullType(field.type) && field.defaultValue === undefined; } diff --git a/src/type/index.ts b/src/type/index.ts index e6cf627bd5..ccffbaf93d 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -23,7 +23,7 @@ export { isInputObjectType, isListType, isNonNullType, - isSemanticNonNullType, + isSemanticNullableType, isInputType, isOutputType, isLeafType, @@ -44,7 +44,7 @@ export { assertInputObjectType, assertListType, assertNonNullType, - assertSemanticNonNullType, + assertSemanticNullableType, assertInputType, assertOutputType, assertLeafType, @@ -66,7 +66,7 @@ export { // Type Wrappers GraphQLList, GraphQLNonNull, - GraphQLSemanticNonNull, + GraphQLSemanticNullable, } from './definition'; export type { diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 186a20f8d3..1e90eb1abf 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -19,7 +19,7 @@ import { GraphQLList, GraphQLNonNull, GraphQLObjectType, - GraphQLSemanticNonNull, + GraphQLSemanticNullable, isAbstractType, isEnumType, isInputObjectType, @@ -28,7 +28,7 @@ import { isNonNullType, isObjectType, isScalarType, - isSemanticNonNullType, + isSemanticNullableType, isUnionType, } from './definition'; import type { GraphQLDirective } from './directives'; @@ -42,7 +42,7 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ fields: () => ({ description: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (schema) => schema.description, }, types: { @@ -60,13 +60,13 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ mutationType: { description: 'If this server supports mutation, the type that mutation operations will be rooted at.', - type: __Type, + type: new GraphQLSemanticNullable(__Type), resolve: (schema) => schema.getMutationType(), }, subscriptionType: { description: 'If this server support subscription, the type that subscription operations will be rooted at.', - type: __Type, + type: new GraphQLSemanticNullable(__Type), resolve: (schema) => schema.getSubscriptionType(), }, directives: { @@ -90,7 +90,7 @@ export const __Directive: GraphQLObjectType = new GraphQLObjectType({ resolve: (directive) => directive.name, }, description: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (directive) => directive.description, }, isRepeatable: { @@ -273,8 +273,8 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ if (isNonNullType(type)) { return TypeKind.NON_NULL; } - if (isSemanticNonNullType(type)) { - return TypeKind.SEMANTIC_NON_NULL; + if (isSemanticNullableType(type)) { + return TypeKind.SEMANTIC_NULLABLE; } /* c8 ignore next 3 */ // Not reachable, all possible types have been considered) @@ -282,23 +282,25 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ }, }, name: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (type) => ('name' in type ? type.name : undefined), }, description: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (type) => // FIXME: add test case /* c8 ignore next */ 'description' in type ? type.description : undefined, }, specifiedByURL: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (obj) => 'specifiedByURL' in obj ? obj.specifiedByURL : undefined, }, fields: { - type: new GraphQLList(new GraphQLNonNull(__Field)), + type: new GraphQLSemanticNullable( + new GraphQLList(new GraphQLNonNull(__Field)), + ), args: { includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, }, @@ -312,7 +314,9 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ }, }, interfaces: { - type: new GraphQLList(new GraphQLNonNull(__Type)), + type: new GraphQLSemanticNullable( + new GraphQLList(new GraphQLNonNull(__Type)), + ), resolve(type) { if (isObjectType(type) || isInterfaceType(type)) { return type.getInterfaces(); @@ -320,7 +324,9 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ }, }, possibleTypes: { - type: new GraphQLList(new GraphQLNonNull(__Type)), + type: new GraphQLSemanticNullable( + new GraphQLList(new GraphQLNonNull(__Type)), + ), resolve(type, _args, _context, { schema }) { if (isAbstractType(type)) { return schema.getPossibleTypes(type); @@ -328,7 +334,9 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ }, }, enumValues: { - type: new GraphQLList(new GraphQLNonNull(__EnumValue)), + type: new GraphQLSemanticNullable( + new GraphQLList(new GraphQLNonNull(__EnumValue)), + ), args: { includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, }, @@ -342,7 +350,9 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ }, }, inputFields: { - type: new GraphQLList(new GraphQLNonNull(__InputValue)), + type: new GraphQLSemanticNullable( + new GraphQLList(new GraphQLNonNull(__InputValue)), + ), args: { includeDeprecated: { type: GraphQLBoolean, @@ -359,11 +369,11 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ }, }, ofType: { - type: __Type, + type: new GraphQLSemanticNullable(__Type), resolve: (type) => ('ofType' in type ? type.ofType : undefined), }, isOneOf: { - type: GraphQLBoolean, + type: new GraphQLSemanticNullable(GraphQLBoolean), resolve: (type) => { if (isInputObjectType(type)) { return type.isOneOf; @@ -384,7 +394,7 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ resolve: (field) => field.name, }, description: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (field) => field.description, }, args: { @@ -429,7 +439,7 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ resolve: (field) => field.deprecationReason != null, }, deprecationReason: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (field) => field.deprecationReason, }, } as GraphQLFieldConfigMap, unknown>), @@ -445,7 +455,7 @@ function convertOutputTypeToNullabilityMode( return new GraphQLNonNull( convertOutputTypeToNullabilityMode(type.ofType, mode), ); - } else if (isSemanticNonNullType(type)) { + } else if (isSemanticNullableType(type)) { return convertOutputTypeToNullabilityMode(type.ofType, mode); } else if (isListType(type)) { return new GraphQLList( @@ -454,10 +464,8 @@ function convertOutputTypeToNullabilityMode( } return type; } - if (isNonNullType(type) || isSemanticNonNullType(type)) { - return new GraphQLSemanticNonNull( - convertOutputTypeToNullabilityMode(type.ofType, mode), - ); + if (isNonNullType(type) || !isSemanticNullableType(type)) { + return convertOutputTypeToNullabilityMode(type, mode); } else if (isListType(type)) { return new GraphQLList( convertOutputTypeToNullabilityMode(type.ofType, mode), @@ -477,7 +485,7 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ resolve: (inputValue) => inputValue.name, }, description: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (inputValue) => inputValue.description, }, type: { @@ -485,7 +493,7 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ resolve: (inputValue) => inputValue.type, }, defaultValue: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), description: 'A GraphQL-formatted string representing the default value for this input value.', resolve(inputValue) { @@ -499,7 +507,7 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ resolve: (field) => field.deprecationReason != null, }, deprecationReason: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (obj) => obj.deprecationReason, }, } as GraphQLFieldConfigMap), @@ -516,7 +524,7 @@ export const __EnumValue: GraphQLObjectType = new GraphQLObjectType({ resolve: (enumValue) => enumValue.name, }, description: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (enumValue) => enumValue.description, }, isDeprecated: { @@ -524,7 +532,7 @@ export const __EnumValue: GraphQLObjectType = new GraphQLObjectType({ resolve: (enumValue) => enumValue.deprecationReason != null, }, deprecationReason: { - type: GraphQLString, + type: new GraphQLSemanticNullable(GraphQLString), resolve: (enumValue) => enumValue.deprecationReason, }, } as GraphQLFieldConfigMap), @@ -539,7 +547,7 @@ enum TypeKind { INPUT_OBJECT = 'INPUT_OBJECT', LIST = 'LIST', NON_NULL = 'NON_NULL', - SEMANTIC_NON_NULL = 'SEMANTIC_NON_NULL', + SEMANTIC_NULLABLE = 'SEMANTIC_NULLABLE', } export { TypeKind }; @@ -585,10 +593,10 @@ 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, + SEMANTIC_NULLABLE: { + value: TypeKind.SEMANTIC_NULLABLE, description: - 'Indicates this type is a semantic-non-null. `ofType` is a valid field.', + 'Indicates this type is a semantic-nullable. `ofType` is a valid field.', }, }, }); diff --git a/src/type/schema.ts b/src/type/schema.ts index 97c2782145..16c6a04abb 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -128,6 +128,7 @@ export interface GraphQLSchemaExtensions { * ``` */ export class GraphQLSchema { + usingSemanticNullability: Maybe; description: Maybe; extensions: Readonly; astNode: Maybe; @@ -164,6 +165,7 @@ export class GraphQLSchema { `${inspect(config.directives)}.`, ); + this.usingSemanticNullability = config.useSemanticNullability; this.description = config.description; this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; @@ -352,6 +354,7 @@ export class GraphQLSchema { toConfig(): GraphQLSchemaNormalizedConfig { return { + useSemanticNullability: this.usingSemanticNullability, description: this.description, query: this.getQueryType(), mutation: this.getMutationType(), @@ -380,6 +383,7 @@ export interface GraphQLSchemaValidationOptions { } export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { + useSemanticNullability?: Maybe; description?: Maybe; query?: Maybe; mutation?: Maybe; @@ -395,6 +399,7 @@ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { * @internal */ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { + useSemanticNullability?: Maybe; description: Maybe; types: ReadonlyArray; directives: ReadonlyArray; diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index b651bf16a8..a865889376 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -771,8 +771,8 @@ 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 + """Indicates this type is a semantic-nullable. \`ofType\` is a valid field.""" + SEMANTIC_NULLABLE } """ diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index c605025035..3a655c321b 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -42,7 +42,7 @@ export function astFromValue( value: unknown, type: GraphQLInputType, ): Maybe { - // Note: input types cannot be SemanticNonNull + // Note: input types cannot be SemanticNullable if (isNonNullType(type)) { const astValue = astFromValue(value, type.ofType); if (astValue?.kind === Kind.NULL) { diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index eeff08e6ed..b4e7134d26 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -46,7 +46,17 @@ export function buildASTSchema( assertValidSDL(documentAST); } + let useSemanticNullability; + for (const definition of documentAST.definitions) { + if (definition.kind === Kind.DIRECTIVE_DEFINITION) { + if (definition.name.value === 'SemanticNullability') { + useSemanticNullability = true; + } + } + } + const emptySchemaConfig = { + useSemanticNullability, description: undefined, types: [], directives: [], @@ -102,6 +112,7 @@ export function buildSchema( const document = parse(source, { noLocation: options?.noLocation, allowLegacyFragmentVariables: options?.allowLegacyFragmentVariables, + useSemanticNullability: options?.useSemanticNullability, }); return buildASTSchema(document, { diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 9b0809adf5..1329a7a4d9 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -22,7 +22,7 @@ import { GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, - GraphQLSemanticNonNull, + GraphQLSemanticNullable, GraphQLUnionType, isInputType, isOutputType, @@ -138,13 +138,13 @@ 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) { + if (typeRef.kind === TypeKind.SEMANTIC_NULLABLE) { + const nonSemanticNullableRef = typeRef.ofType; + if (!nonSemanticNullableRef) { throw new Error('Decorated type deeper than introspection query.'); } - const nullableType = getType(nullableRef); - return new GraphQLSemanticNonNull(assertNullableType(nullableType)); + const nullableType = getType(nonSemanticNullableRef); + return new GraphQLSemanticNullable(assertNullableType(nullableType)); } return getNamedType(typeRef); } diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 876aae277f..7895cdc378 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -53,7 +53,7 @@ import { GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, - GraphQLSemanticNonNull, + GraphQLSemanticNullable, GraphQLUnionType, isEnumType, isInputObjectType, @@ -62,7 +62,7 @@ import { isNonNullType, isObjectType, isScalarType, - isSemanticNonNullType, + isSemanticNullableType, isUnionType, } from '../type/definition'; import { @@ -92,6 +92,8 @@ interface Options extends GraphQLSchemaValidationOptions { * Default: false */ assumeValidSDL?: boolean; + + useSemanticNullability?: boolean; } /** @@ -227,9 +229,9 @@ export function extendSchemaImpl( // @ts-expect-error return new GraphQLNonNull(replaceType(type.ofType)); } - if (isSemanticNonNullType(type)) { + if (isSemanticNullableType(type)) { // @ts-expect-error - return new GraphQLSemanticNonNull(replaceType(type.ofType)); + return new GraphQLSemanticNullable(replaceType(type.ofType)); } // @ts-expect-error FIXME return replaceNamedType(type); @@ -438,8 +440,8 @@ 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)); + if (node.kind === Kind.SEMANTIC_NULLABLE_TYPE) { + return new GraphQLSemanticNullable(getWrappedType(node.type)); } return getNamedType(node); } diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index 5ed0313ae3..5c6b84dd7d 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -26,7 +26,7 @@ import { isRequiredArgument, isRequiredInputField, isScalarType, - isSemanticNonNullType, + isSemanticNullableType, isUnionType, } from '../type/definition'; import { isSpecifiedScalarType } from '../type/scalars'; @@ -457,11 +457,8 @@ function isChangeSafeForObjectOrInterfaceField( oldType.ofType, newType.ofType, )) || - // moving from nullable to non-null of the same underlying type is safe + // moving from semantic-non-null 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)) ); } @@ -474,15 +471,16 @@ function isChangeSafeForObjectOrInterfaceField( ); } - if (isSemanticNonNullType(oldType)) { + if (isSemanticNullableType(oldType)) { return ( - // if they're both semantic-non-null, make sure the underlying types are compatible - (isSemanticNonNullType(newType) && + // if they're both semantic-nullable, make sure the underlying types are compatible + isSemanticNullableType(newType) || + (isNonNullType(newType) && isChangeSafeForObjectOrInterfaceField( oldType.ofType, newType.ofType, )) || - // moving from semantic-non-null to non-null of the same underlying type is safe + // moving from semantic-nullable to semantic-non-null of the same underlying type is safe (isNonNullType(newType) && isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)) ); @@ -491,11 +489,8 @@ function isChangeSafeForObjectOrInterfaceField( 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 + // moving from semantic-non-null 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 dda0e7f19a..7318d1909c 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -298,10 +298,10 @@ export interface IntrospectionNonNullTypeRef< readonly ofType: T; } -export interface IntrospectionSemanticNonNullTypeRef< +export interface IntrospectionSemanticNullableTypeRef< T extends IntrospectionTypeRef = IntrospectionTypeRef, > { - readonly kind: 'SEMANTIC_NON_NULL'; + readonly kind: 'SEMANTIC_NULLABLE'; readonly ofType: T; } @@ -311,7 +311,7 @@ export type IntrospectionTypeRef = | IntrospectionNonNullTypeRef< IntrospectionNamedTypeRef | IntrospectionListTypeRef > - | IntrospectionSemanticNonNullTypeRef< + | IntrospectionSemanticNullableTypeRef< IntrospectionNamedTypeRef | IntrospectionListTypeRef >; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index fa69583012..f612dd6daa 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -20,7 +20,7 @@ export type { IntrospectionNamedTypeRef, IntrospectionListTypeRef, IntrospectionNonNullTypeRef, - IntrospectionSemanticNonNullTypeRef, + IntrospectionSemanticNullableTypeRef, IntrospectionField, IntrospectionInputValue, IntrospectionEnumValue, diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index 5beb646859..dfb70e663b 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -19,7 +19,7 @@ import { GraphQLList, GraphQLNonNull, GraphQLObjectType, - GraphQLSemanticNonNull, + GraphQLSemanticNullable, GraphQLUnionType, isEnumType, isInputObjectType, @@ -28,7 +28,7 @@ import { isNonNullType, isObjectType, isScalarType, - isSemanticNonNullType, + isSemanticNullableType, isUnionType, } from '../type/definition'; import { GraphQLDirective } from '../type/directives'; @@ -64,9 +64,9 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { } else if (isNonNullType(type)) { // @ts-expect-error return new GraphQLNonNull(replaceType(type.ofType)); - } else if (isSemanticNonNullType(type)) { + } else if (isSemanticNullableType(type)) { // @ts-expect-error - return new GraphQLSemanticNonNull(replaceType(type.ofType)); + return new GraphQLSemanticNullable(replaceType(type.ofType)); } // @ts-expect-error FIXME: TS Conversion return replaceNamedType(type); diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index edac6262c5..e57afe0a06 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -14,6 +14,7 @@ import type { GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, + GraphQLOutputType, GraphQLScalarType, GraphQLUnionType, } from '../type/definition'; @@ -23,6 +24,7 @@ import { isInterfaceType, isObjectType, isScalarType, + isSemanticNullableType, isUnionType, } from '../type/definition'; import type { GraphQLDirective } from '../type/directives'; @@ -60,10 +62,12 @@ function printFilteredSchema( const directives = schema.getDirectives().filter(directiveFilter); const types = Object.values(schema.getTypeMap()).filter(typeFilter); + const usingSemanticNullability = schema.usingSemanticNullability; + return [ printSchemaDefinition(schema), ...directives.map((directive) => printDirective(directive)), - ...types.map((type) => printType(type)), + ...types.map((type) => printType(type, usingSemanticNullability)), ] .filter(Boolean) .join('\n\n'); @@ -128,15 +132,18 @@ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { return true; } -export function printType(type: GraphQLNamedType): string { +export function printType( + type: GraphQLNamedType, + usingSemanticNullability: Maybe, +): string { if (isScalarType(type)) { return printScalar(type); } if (isObjectType(type)) { - return printObject(type); + return printObject(type, usingSemanticNullability); } if (isInterfaceType(type)) { - return printInterface(type); + return printInterface(type, usingSemanticNullability); } if (isUnionType(type)) { return printUnion(type); @@ -167,21 +174,27 @@ function printImplementedInterfaces( : ''; } -function printObject(type: GraphQLObjectType): string { +function printObject( + type: GraphQLObjectType, + usingSemanticNullability: Maybe, +): string { return ( printDescription(type) + `type ${type.name}` + printImplementedInterfaces(type) + - printFields(type) + printFields(type, usingSemanticNullability) ); } -function printInterface(type: GraphQLInterfaceType): string { +function printInterface( + type: GraphQLInterfaceType, + usingSemanticNullability: Maybe, +): string { return ( printDescription(type) + `interface ${type.name}` + printImplementedInterfaces(type) + - printFields(type) + printFields(type, usingSemanticNullability) ); } @@ -217,7 +230,10 @@ function printInputObject(type: GraphQLInputObjectType): string { ); } -function printFields(type: GraphQLObjectType | GraphQLInterfaceType): string { +function printFields( + type: GraphQLObjectType | GraphQLInterfaceType, + usingSemanticNullability: Maybe, +): string { const fields = Object.values(type.getFields()).map( (f, i) => printDescription(f, ' ', !i) + @@ -225,12 +241,25 @@ function printFields(type: GraphQLObjectType | GraphQLInterfaceType): string { f.name + printArgs(f.args, ' ') + ': ' + - String(f.type) + + printReturnType(f.type, usingSemanticNullability) + printDeprecated(f.deprecationReason), ); return printBlock(fields); } +function printReturnType( + type: GraphQLOutputType, + usingSemanticNullability: Maybe, +): string { + if (usingSemanticNullability) { + return String(type); + } + if (isSemanticNullableType(type)) { + return String(type.ofType); + } + return String(type); +} + function printBlock(items: ReadonlyArray): string { return items.length !== 0 ? ' {\n' + items.join('\n') + '\n}' : ''; } diff --git a/src/utilities/typeComparators.ts b/src/utilities/typeComparators.ts index 338ca24528..b276975afe 100644 --- a/src/utilities/typeComparators.ts +++ b/src/utilities/typeComparators.ts @@ -5,7 +5,7 @@ import { isListType, isNonNullType, isObjectType, - isSemanticNonNullType, + isSemanticNullableType, } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; @@ -24,7 +24,7 @@ export function isEqualType(typeA: GraphQLType, typeB: GraphQLType): boolean { } // If either type is semantic-non-null, the other must also be semantic-non-null. - if (isSemanticNonNullType(typeA) && isSemanticNonNullType(typeB)) { + if (isSemanticNullableType(typeA) && isSemanticNullableType(typeB)) { return isEqualType(typeA.ofType, typeB.ofType); } @@ -58,16 +58,13 @@ export function isTypeSubTypeOf( } return false; } - // 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); + + // If superType is semantic-nullable, maybeSubType may be non-null, semantic-non-null, or nullable. + if (isSemanticNullableType(superType)) { + if (isSemanticNullableType(maybeSubType) || isNonNullType(maybeSubType)) { + return isTypeSubTypeOf(schema, maybeSubType.ofType, superType); } - 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); + return isTypeSubTypeOf(schema, maybeSubType, superType); } // If superType type is a list, maybeSubType type must also be a list. @@ -82,6 +79,14 @@ export function isTypeSubTypeOf( return false; } + // If superType is semantic-non-null, maybeSubType must be semantic-non-null or non-null. + if (isNonNullType(maybeSubType)) { + return isTypeSubTypeOf(schema, maybeSubType.ofType, superType); + } + if (isSemanticNullableType(maybeSubType)) { + return false; + } + // If superType type is an abstract type, check if it is super type of maybeSubType. // Otherwise, the child type is not a valid subtype of the parent type. return ( diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts index c5d5f537a2..3c020671ff 100644 --- a/src/utilities/typeFromAST.ts +++ b/src/utilities/typeFromAST.ts @@ -10,7 +10,7 @@ import type { GraphQLNamedType, GraphQLType } from '../type/definition'; import { GraphQLList, GraphQLNonNull, - GraphQLSemanticNonNull, + GraphQLSemanticNullable, } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; @@ -50,9 +50,9 @@ export function typeFromAST( const innerType = typeFromAST(schema, typeNode.type); return innerType && new GraphQLNonNull(innerType); } - case Kind.SEMANTIC_NON_NULL_TYPE: { + case Kind.SEMANTIC_NULLABLE_TYPE: { const innerType = typeFromAST(schema, typeNode.type); - return innerType && new GraphQLSemanticNonNull(innerType); + return innerType && new GraphQLSemanticNullable(innerType); } case Kind.NAMED_TYPE: return schema.getType(typeNode.name.value); diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index 182215fd3f..cf7b8d6f37 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -27,7 +27,7 @@ import { isListType, isNonNullType, isObjectType, - isSemanticNonNullType, + isSemanticNullableType, } from '../../type/definition'; import { sortValueNode } from '../../utilities/sortValueNode'; @@ -724,12 +724,12 @@ function doTypesConflict( if (isNonNullType(type2)) { return true; } - if (isSemanticNonNullType(type1)) { - return isSemanticNonNullType(type2) + if (isSemanticNullableType(type1)) { + return isSemanticNullableType(type2) ? doTypesConflict(type1.ofType, type2.ofType) : true; } - if (isSemanticNonNullType(type2)) { + if (isSemanticNullableType(type2)) { return true; } if (isLeafType(type1) || isLeafType(type2)) { From e6afbfd3470f3a46f23d35ce7e60d339aa341898 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 10 Nov 2024 14:19:00 +0200 Subject: [PATCH 33/36] use expectJSON and other usual testing shorthands --- .../__tests__/semantic-nullability-test.ts | 202 +++++++----------- 1 file changed, 74 insertions(+), 128 deletions(-) diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts index 97846fdd38..822a86bfc5 100644 --- a/src/execution/__tests__/semantic-nullability-test.ts +++ b/src/execution/__tests__/semantic-nullability-test.ts @@ -1,9 +1,9 @@ -import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { GraphQLError } from '../../error/GraphQLError'; +import { expectJSON } from '../../__testUtils__/expectJSON'; + +import type { ObjMap } from '../../jsutils/ObjMap'; -import type { ExecutableDefinitionNode, FieldNode } from '../../language/ast'; import { parse } from '../../language/parser'; import { @@ -34,142 +34,106 @@ describe('Execute: Handles Semantic Nullability', () => { }), }); + const schema = new GraphQLSchema({ + useSemanticNullability: true, + query: DataType, + }); + + function executeWithSemanticNullability( + query: string, + rootValue: ObjMap, + ) { + return execute({ + schema, + document: parse(query), + rootValue, + }); + } + it('SemanticNonNull throws error on null without error', async () => { const data = { - a: () => 'Apple', b: () => null, - c: () => 'Cookie', }; - const document = parse(` - query { - b - } - `); - - const result = await execute({ - schema: new GraphQLSchema({ - useSemanticNullability: true, - query: DataType, - }), - document, - rootValue: data, - }); + const query = ` + query { + b + } + `; - const executable = document.definitions?.values().next() - .value as ExecutableDefinitionNode; - const selectionSet = executable.selectionSet.selections - .values() - .next().value; + const result = await executeWithSemanticNullability(query, data); - expect(result).to.deep.equal({ + expectJSON(result).toDeepEqual({ data: { b: null, }, errors: [ - new GraphQLError( - 'Cannot return null for semantic-non-nullable field DataType.b.', - { - nodes: selectionSet, - path: ['b'], - }, - ), + { + message: + 'Cannot return null for semantic-non-nullable field DataType.b.', + path: ['b'], + locations: [{ line: 3, column: 9 }], + }, ], }); }); it('SemanticNonNull succeeds on null with error', async () => { const data = { - a: () => 'Apple', b: () => { throw new Error('Something went wrong'); }, - c: () => 'Cookie', }; - const document = parse(` - query { - b - } - `); - - const executable = document.definitions?.values().next() - .value as ExecutableDefinitionNode; - const selectionSet = executable.selectionSet.selections - .values() - .next().value; - - const result = await execute({ - schema: new GraphQLSchema({ - useSemanticNullability: true, - query: DataType, - }), - document, - rootValue: data, - }); + const query = ` + query { + b + } + `; + + const result = await executeWithSemanticNullability(query, data); - expect(result).to.deep.equal({ + expectJSON(result).toDeepEqual({ data: { b: null, }, errors: [ - new GraphQLError('Something went wrong', { - nodes: selectionSet, + { + message: 'Something went wrong', path: ['b'], - }), + locations: [{ line: 3, column: 9 }], + }, ], }); }); it('SemanticNonNull halts null propagation', async () => { - const deepData = { - f: () => null, - }; - const data = { - a: () => 'Apple', - b: () => null, - c: () => 'Cookie', - d: () => deepData, + d: () => ({ + f: () => null, + }), }; - const document = parse(` - query { - d { - f - } + const query = ` + query { + d { + f } - `); - - const result = await execute({ - schema: new GraphQLSchema({ - useSemanticNullability: true, - query: DataType, - }), - document, - rootValue: data, - }); + } + `; - const executable = document.definitions?.values().next() - .value as ExecutableDefinitionNode; - const dSelectionSet = executable.selectionSet.selections.values().next() - .value as FieldNode; - const fSelectionSet = dSelectionSet.selectionSet?.selections - .values() - .next().value; + const result = await executeWithSemanticNullability(query, data); - expect(result).to.deep.equal({ + expectJSON(result).toDeepEqual({ data: { d: null, }, errors: [ - new GraphQLError( - 'Cannot return null for non-nullable field DeepDataType.f.', - { - nodes: fSelectionSet, - path: ['d', 'f'], - }, - ), + { + message: 'Cannot return null for non-nullable field DeepDataType.f.', + path: ['d', 'f'], + locations: [{ line: 4, column: 11 }], + }, ], }); }); @@ -177,26 +141,17 @@ describe('Execute: Handles Semantic Nullability', () => { it('SemanticNullable allows null values', async () => { const data = { a: () => null, - b: () => null, - c: () => 'Cookie', }; - const document = parse(` - query { - a - } - `); + const query = ` + query { + a + } + `; - const result = await execute({ - schema: new GraphQLSchema({ - useSemanticNullability: true, - query: DataType, - }), - document, - rootValue: data, - }); + const result = await executeWithSemanticNullability(query, data); - expect(result).to.deep.equal({ + expectJSON(result).toDeepEqual({ data: { a: null, }, @@ -206,26 +161,17 @@ describe('Execute: Handles Semantic Nullability', () => { it('SemanticNullable allows non-null values', async () => { const data = { a: () => 'Apple', - b: () => null, - c: () => 'Cookie', }; - const document = parse(` - query { - a - } - `); + const query = ` + query { + a + } + `; - const result = await execute({ - schema: new GraphQLSchema({ - useSemanticNullability: true, - query: DataType, - }), - document, - rootValue: data, - }); + const result = await executeWithSemanticNullability(query, data); - expect(result).to.deep.equal({ + expectJSON(result).toDeepEqual({ data: { a: 'Apple', }, From 7c737fef1c42f3232c5afb5ea4b0a6f3723ef98e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 Feb 2025 15:33:55 +0200 Subject: [PATCH 34/36] fix sp --- src/language/__tests__/parser-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index cbd171570e..4cb4a1465b 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -659,7 +659,7 @@ describe('Parser', () => { }); describe('parseDocumentDirective', () => { - it('doesnt throw on document-level directive', () => { + it('does not throw on document-level directive', () => { parse(dedent` @SemanticNullability From 4ad3587cddc79897a233f89f793e109f30bbc5f1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 Feb 2025 15:36:53 +0200 Subject: [PATCH 35/36] fix types --- src/utilities/printSchema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index e57afe0a06..b95172c84d 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -134,7 +134,7 @@ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { export function printType( type: GraphQLNamedType, - usingSemanticNullability: Maybe, + usingSemanticNullability?: Maybe, ): string { if (isScalarType(type)) { return printScalar(type); From a0ca9d9b24e676067c111921b45dccdab1bb3114 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 2 Feb 2025 21:19:45 +0200 Subject: [PATCH 36/36] add actually printing the semantic nullability directive and ability to override --- src/utilities/printSchema.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index b95172c84d..d13012db60 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -38,16 +38,32 @@ import type { GraphQLSchema } from '../type/schema'; import { astFromValue } from './astFromValue'; -export function printSchema(schema: GraphQLSchema): string { +interface PrintOptions { + usingSemanticNullability?: boolean; +} + +export function printSchema( + schema: GraphQLSchema, + options: PrintOptions = {}, +): string { return printFilteredSchema( schema, (n) => !isSpecifiedDirective(n), isDefinedType, + options, ); } -export function printIntrospectionSchema(schema: GraphQLSchema): string { - return printFilteredSchema(schema, isSpecifiedDirective, isIntrospectionType); +export function printIntrospectionSchema( + schema: GraphQLSchema, + options: PrintOptions = {}, +): string { + return printFilteredSchema( + schema, + isSpecifiedDirective, + isIntrospectionType, + options, + ); } function isDefinedType(type: GraphQLNamedType): boolean { @@ -58,13 +74,16 @@ function printFilteredSchema( schema: GraphQLSchema, directiveFilter: (type: GraphQLDirective) => boolean, typeFilter: (type: GraphQLNamedType) => boolean, + options: PrintOptions, ): string { const directives = schema.getDirectives().filter(directiveFilter); const types = Object.values(schema.getTypeMap()).filter(typeFilter); - const usingSemanticNullability = schema.usingSemanticNullability; + const usingSemanticNullability = + options.usingSemanticNullability ?? schema.usingSemanticNullability; return [ + usingSemanticNullability ? '@SemanticNullability' : undefined, printSchemaDefinition(schema), ...directives.map((directive) => printDirective(directive)), ...types.map((type) => printType(type, usingSemanticNullability)),