diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts index 20a33c2ffa..822a86bfc5 100644 --- a/src/execution/__tests__/semantic-nullability-test.ts +++ b/src/execution/__tests__/semantic-nullability-test.ts @@ -1,15 +1,14 @@ -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 { GraphQLNonNull, GraphQLObjectType, - GraphQLSemanticNonNull, GraphQLSemanticNullable, } from '../../type/definition'; import { GraphQLString } from '../../type/scalars'; @@ -29,139 +28,112 @@ 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 }, }), }); + 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 query = ` + 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; + 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({ query: DataType }), - document, - rootValue: data, - }); + const query = ` + query { + b + } + `; - expect(result).to.deep.equal({ + const result = await executeWithSemanticNullability(query, data); + + 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({ 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 }], + }, ], }); }); @@ -169,23 +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({ query: DataType }), - document, - rootValue: data, - }); + const result = await executeWithSemanticNullability(query, data); - expect(result).to.deep.equal({ + expectJSON(result).toDeepEqual({ data: { a: null, }, @@ -195,23 +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({ query: DataType }), - document, - rootValue: data, - }); + const result = await executeWithSemanticNullability(query, data); - expect(result).to.deep.equal({ + expectJSON(result).toDeepEqual({ data: { a: 'Apple', }, 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 2940df03fc..3378cee3f5 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, @@ -485,7 +484,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 5567fc70cf..e8488927a0 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -669,16 +669,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 a17edbb9bc..7e7cb70054 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'], @@ -528,7 +526,6 @@ export type TypeNode = | NamedTypeNode | ListTypeNode | NonNullTypeNode - | SemanticNonNullTypeNode | SemanticNullableTypeNode; export interface NamedTypeNode { @@ -549,12 +546,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 e947021baf..1d8693d111 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -50,7 +50,6 @@ import type { SchemaExtensionNode, SelectionNode, SelectionSetNode, - SemanticNonNullTypeNode, SemanticNullableTypeNode, StringValueNode, Token, @@ -754,7 +753,7 @@ export class Parser { * - NamedType * - ListType * - NonNullType - * - SemanticNonNullType + * - SemanticNullableType */ parseTypeReference(): TypeNode { const start = this._lexer.token; @@ -770,30 +769,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 08273f495f..174af5b65d 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 26340991ce..3249ae65e1 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 8ecd96212f..c22eaed6bd 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'; @@ -696,12 +696,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)) {