From 05f16f27b97cf77f240858e714f4f0e1c8011585 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 12 Feb 2025 03:54:43 +0100 Subject: [PATCH] Cleanup and code coverage --- .../__tests__/semantic-nullability-test.ts | 26 ----- src/type/__tests__/introspection-test.ts | 99 ++++++++++++++++--- src/type/directives.ts | 11 +++ src/type/introspection.ts | 47 +++------ .../__tests__/buildClientSchema-test.ts | 35 +++++++ src/utilities/__tests__/printSchema-test.ts | 16 +-- src/utilities/buildClientSchema.ts | 2 + src/utilities/getIntrospectionQuery.ts | 6 +- src/utilities/printSchema.ts | 2 + 9 files changed, 159 insertions(+), 85 deletions(-) diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts index 6d9098d016..613ab91d1c 100644 --- a/src/execution/__tests__/semantic-nullability-test.ts +++ b/src/execution/__tests__/semantic-nullability-test.ts @@ -165,32 +165,6 @@ 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 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', diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 9b0eaa11a4..8dfdb65e50 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -523,7 +523,7 @@ describe('Introspection', () => { ofType: null, }, }, - defaultValue: 'AUTO', + defaultValue: 'TRADITIONAL', }, ], type: { @@ -667,21 +667,11 @@ describe('Introspection', () => { 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, @@ -1804,4 +1794,91 @@ describe('Introspection', () => { }); expect(result).to.not.have.property('errors'); }); + + describe('semantic nullability', () => { + it('casts semantic-non-null types to nullable types in traditional mode', () => { + const schema = buildSchema(` + @SemanticNullability + type Query { + someField: String! + someField2: String + someField3: String? + } + `); + + const source = getIntrospectionQuery({ + nullability: 'TRADITIONAL', + }); + + const result = graphqlSync({ schema, source }); + // @ts-expect-error + const queryType = result.data?.__schema?.types.find((t) => t.name === 'Query'); + const defaults = { + args: [], + deprecationReason: null, + description: null, + isDeprecated: false, + } + expect(queryType?.fields).to.deep.equal([ + { + name: 'someField', + ...defaults, + type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null } }, + }, + { + name: 'someField2', + ...defaults, + type: { kind: 'SCALAR', name: 'String', ofType: null }, + }, + { + name: 'someField3', + ...defaults, + type: { kind: 'SCALAR', name: 'String', ofType: null }, + }, + ]); + }); + + it('returns semantic-non-null types in full mode', () => { + const schema = buildSchema(` + @SemanticNullability + type Query { + someField: String! + someField2: String + someField3: String? + } + `); + + const source = getIntrospectionQuery({ + nullability: 'FULL', + }); + + const result = graphqlSync({ schema, source }); + // @ts-expect-error + const queryType = result.data?.__schema?.types.find((t) => t.name === 'Query'); + const defaults = { + args: [], + deprecationReason: null, + description: null, + isDeprecated: false, + } + expect(queryType?.fields).to.deep.equal([ + { + name: 'someField', + ...defaults, + type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null } }, + }, + { + name: 'someField2', + ...defaults, + type: { kind: 'SEMANTIC_NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null } }, + }, + { + name: 'someField3', + ...defaults, + type: { kind: 'SCALAR', name: 'String', ofType: null }, + }, + ]); + }); + }); }); + diff --git a/src/type/directives.ts b/src/type/directives.ts index 6881f20532..276eb38aa7 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -165,6 +165,17 @@ export const GraphQLSkipDirective: GraphQLDirective = new GraphQLDirective({ }, }); +/** + * Used to indicate that the nullability of the document will be parsed as semantic-non-null types. + */ +export const GraphQLSemanticNullabilityDirective: GraphQLDirective = + new GraphQLDirective({ + name: 'SemanticNullability', + description: + 'Indicates that the nullability of the document will be parsed as semantic-non-null types.', + locations: [DirectiveLocation.SCHEMA], + }); + /** * Constant string used for default reason for a deprecation. */ diff --git a/src/type/introspection.ts b/src/type/introspection.ts index b77ea37380..950cf8958e 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -206,36 +206,23 @@ 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', + description: + 'This represents the type of nullability we want to return as part of the introspection.', 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!', + description: 'Allow for returning semantic-non-null types.', }, }, }); @@ -408,22 +395,11 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ args: { nullability: { type: new GraphQLNonNull(__TypeNullability), - defaultValue: TypeNullability.AUTO, + defaultValue: TypeNullability.TRADITIONAL, }, }, - 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); - }, + resolve: (field, { nullability }, _context) => + convertOutputTypeToNullabilityMode(field.type, nullability), }, isDeprecated: { type: new GraphQLNonNull(GraphQLBoolean), @@ -436,10 +412,9 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap, unknown>), }); -// TODO: move this elsewhere, rename, memoize function convertOutputTypeToNullabilityMode( type: GraphQLType, - mode: TypeNullability.TRADITIONAL | TypeNullability.SEMANTIC, + mode: TypeNullability, ): GraphQLType { if (mode === TypeNullability.TRADITIONAL) { if (isNonNullType(type)) { @@ -455,7 +430,12 @@ function convertOutputTypeToNullabilityMode( } return type; } - if (isNonNullType(type) || isSemanticNonNullType(type)) { + + if (isNonNullType(type)) { + return new GraphQLNonNull( + convertOutputTypeToNullabilityMode(type.ofType, mode), + ); + } else if (isSemanticNonNullType(type)) { return new GraphQLSemanticNonNull( convertOutputTypeToNullabilityMode(type.ofType, mode), ); @@ -464,6 +444,7 @@ function convertOutputTypeToNullabilityMode( convertOutputTypeToNullabilityMode(type.ofType, mode), ); } + return type; } diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index e8cf046921..5b240468f2 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -9,6 +9,7 @@ import { assertEnumType, GraphQLEnumType, GraphQLObjectType, + GraphQLSemanticNonNull, } from '../../type/definition'; import { GraphQLBoolean, @@ -983,4 +984,38 @@ describe('Type System: build schema from introspection', () => { ); }); }); + + describe('SemanticNullability', () => { + it('should build a clinet schema with semantic-non-null types', () => { + const sdl = ` + @SemanticNullability + type Query { + foo: String + bar: String? + } + `; + const schema = buildSchema(sdl, { assumeValid: true }); + const introspection = introspectionFromSchema(schema, { nullability: 'FULL' }); + + const clientSchema = buildClientSchema(introspection); + // expect(printSchema(clientSchema)).to.equal(sdl); + + const defaults = { + args: [], + astNode: undefined, + deprecationReason: null, + description: null, + extensions: {}, + resolve: undefined, + subscribe: undefined, + } + expect(clientSchema.getType('Query')).to.deep.include({ + name: 'Query', + _fields: { + foo: { ...defaults, name: 'foo', type: new GraphQLSemanticNonNull(GraphQLString) }, + bar: { ...defaults, name: 'bar', type: GraphQLString }, + }, + }); + }); + }); }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index b651bf16a8..e94bd2fb79 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -782,7 +782,7 @@ describe('Type System Printer', () => { name: String! description: String args(includeDeprecated: Boolean = false): [__InputValue!]! - type(nullability: __TypeNullability! = AUTO): __Type! + type(nullability: __TypeNullability! = TRADITIONAL): __Type! isDeprecated: Boolean! deprecationReason: String } @@ -803,20 +803,14 @@ describe('Type System Printer', () => { deprecationReason: String } - """TODO""" + """ + This represents the type of nullability we want to return as part of the introspection. + """ 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! - """ + """Allow for returning semantic-non-null types.""" FULL } diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 9b0809adf5..ca4aeddd5f 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -138,6 +138,8 @@ export function buildClientSchema( const nullableType = getType(nullableRef); return new GraphQLNonNull(assertNullableType(nullableType)); } + + console.log(typeRef.kind); if (typeRef.kind === TypeKind.SEMANTIC_NON_NULL) { const nullableRef = typeRef.ofType; if (!nullableRef) { diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index dda0e7f19a..e9a8a796df 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -42,13 +42,11 @@ export interface IntrospectionOptions { /** * 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'; + nullability?: 'TRADITIONAL' | 'FULL'; } /** @@ -63,7 +61,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { schemaDescription: false, inputValueDeprecation: false, oneOf: false, - nullability: null, + nullability: 'TRADITIONAL', ...options, }; diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index edac6262c5..7e5143cf59 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -36,6 +36,8 @@ import type { GraphQLSchema } from '../type/schema'; import { astFromValue } from './astFromValue'; +// TODO: we might need to add a flag to print the schema with semantic-non-null types +// or some kind of way to store the directive so we can print it out export function printSchema(schema: GraphQLSchema): string { return printFilteredSchema( schema,