Skip to content

Commit beb54c4

Browse files
committed
Cleanup and code coverage
1 parent 7121006 commit beb54c4

9 files changed

+230
-95
lines changed

Diff for: src/execution/__tests__/semantic-nullability-test.ts

-26
Original file line numberDiff line numberDiff line change
@@ -165,32 +165,6 @@ describe('Execute: Handles Semantic Nullability', () => {
165165
});
166166
});
167167

168-
it('SemanticNullable allows null values', async () => {
169-
const data = {
170-
a: () => null,
171-
b: () => null,
172-
c: () => 'Cookie',
173-
};
174-
175-
const document = parse(`
176-
query {
177-
a
178-
}
179-
`);
180-
181-
const result = await execute({
182-
schema: new GraphQLSchema({ query: DataType }),
183-
document,
184-
rootValue: data,
185-
});
186-
187-
expect(result).to.deep.equal({
188-
data: {
189-
a: null,
190-
},
191-
});
192-
});
193-
194168
it('SemanticNullable allows non-null values', async () => {
195169
const data = {
196170
a: () => 'Apple',

Diff for: src/type/__tests__/introspection-test.ts

+103-11
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ describe('Introspection', () => {
523523
ofType: null,
524524
},
525525
},
526-
defaultValue: 'AUTO',
526+
defaultValue: 'TRADITIONAL',
527527
},
528528
],
529529
type: {
@@ -667,21 +667,11 @@ describe('Introspection', () => {
667667
inputFields: null,
668668
interfaces: null,
669669
enumValues: [
670-
{
671-
name: 'AUTO',
672-
isDeprecated: false,
673-
deprecationReason: null,
674-
},
675670
{
676671
name: 'TRADITIONAL',
677672
isDeprecated: false,
678673
deprecationReason: null,
679674
},
680-
{
681-
name: 'SEMANTIC',
682-
isDeprecated: false,
683-
deprecationReason: null,
684-
},
685675
{
686676
name: 'FULL',
687677
isDeprecated: false,
@@ -1804,4 +1794,106 @@ describe('Introspection', () => {
18041794
});
18051795
expect(result).to.not.have.property('errors');
18061796
});
1797+
1798+
describe('semantic nullability', () => {
1799+
it('casts semantic-non-null types to nullable types in traditional mode', () => {
1800+
const schema = buildSchema(`
1801+
@SemanticNullability
1802+
type Query {
1803+
someField: String!
1804+
someField2: String
1805+
someField3: String?
1806+
}
1807+
`);
1808+
1809+
const source = getIntrospectionQuery({
1810+
nullability: 'TRADITIONAL',
1811+
});
1812+
1813+
const result = graphqlSync({ schema, source });
1814+
// @ts-expect-error
1815+
const queryType = result.data?.__schema?.types.find(
1816+
(t) => t.name === 'Query',
1817+
);
1818+
const defaults = {
1819+
args: [],
1820+
deprecationReason: null,
1821+
description: null,
1822+
isDeprecated: false,
1823+
};
1824+
expect(queryType?.fields).to.deep.equal([
1825+
{
1826+
name: 'someField',
1827+
...defaults,
1828+
type: {
1829+
kind: 'NON_NULL',
1830+
name: null,
1831+
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
1832+
},
1833+
},
1834+
{
1835+
name: 'someField2',
1836+
...defaults,
1837+
type: { kind: 'SCALAR', name: 'String', ofType: null },
1838+
},
1839+
{
1840+
name: 'someField3',
1841+
...defaults,
1842+
type: { kind: 'SCALAR', name: 'String', ofType: null },
1843+
},
1844+
]);
1845+
});
1846+
1847+
it('returns semantic-non-null types in full mode', () => {
1848+
const schema = buildSchema(`
1849+
@SemanticNullability
1850+
type Query {
1851+
someField: String!
1852+
someField2: String
1853+
someField3: String?
1854+
}
1855+
`);
1856+
1857+
const source = getIntrospectionQuery({
1858+
nullability: 'FULL',
1859+
});
1860+
1861+
const result = graphqlSync({ schema, source });
1862+
// @ts-expect-error
1863+
const queryType = result.data?.__schema?.types.find(
1864+
(t) => t.name === 'Query',
1865+
);
1866+
const defaults = {
1867+
args: [],
1868+
deprecationReason: null,
1869+
description: null,
1870+
isDeprecated: false,
1871+
};
1872+
expect(queryType?.fields).to.deep.equal([
1873+
{
1874+
name: 'someField',
1875+
...defaults,
1876+
type: {
1877+
kind: 'NON_NULL',
1878+
name: null,
1879+
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
1880+
},
1881+
},
1882+
{
1883+
name: 'someField2',
1884+
...defaults,
1885+
type: {
1886+
kind: 'SEMANTIC_NON_NULL',
1887+
name: null,
1888+
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
1889+
},
1890+
},
1891+
{
1892+
name: 'someField3',
1893+
...defaults,
1894+
type: { kind: 'SCALAR', name: 'String', ofType: null },
1895+
},
1896+
]);
1897+
});
1898+
});
18071899
});

Diff for: src/type/directives.ts

+11
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ export const GraphQLSkipDirective: GraphQLDirective = new GraphQLDirective({
165165
},
166166
});
167167

168+
/**
169+
* Used to indicate that the nullability of the document will be parsed as semantic-non-null types.
170+
*/
171+
export const GraphQLSemanticNullabilityDirective: GraphQLDirective =
172+
new GraphQLDirective({
173+
name: 'SemanticNullability',
174+
description:
175+
'Indicates that the nullability of the document will be parsed as semantic-non-null types.',
176+
locations: [DirectiveLocation.SCHEMA],
177+
});
178+
168179
/**
169180
* Constant string used for default reason for a deprecation.
170181
*/

Diff for: src/type/introspection.ts

+14-33
Original file line numberDiff line numberDiff line change
@@ -206,36 +206,23 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({
206206
},
207207
});
208208

209-
// TODO: rename enum and options
210209
enum TypeNullability {
211-
AUTO = 'AUTO',
212210
TRADITIONAL = 'TRADITIONAL',
213-
SEMANTIC = 'SEMANTIC',
214211
FULL = 'FULL',
215212
}
216213

217-
// TODO: rename
218214
export const __TypeNullability: GraphQLEnumType = new GraphQLEnumType({
219215
name: '__TypeNullability',
220-
description: 'TODO',
216+
description:
217+
'This represents the type of nullability we want to return as part of the introspection.',
221218
values: {
222-
AUTO: {
223-
value: TypeNullability.AUTO,
224-
description:
225-
'Determines nullability mode based on errorPropagation mode.',
226-
},
227219
TRADITIONAL: {
228220
value: TypeNullability.TRADITIONAL,
229221
description: 'Turn semantic-non-null types into nullable types.',
230222
},
231-
SEMANTIC: {
232-
value: TypeNullability.SEMANTIC,
233-
description: 'Turn non-null types into semantic-non-null types.',
234-
},
235223
FULL: {
236224
value: TypeNullability.FULL,
237-
description:
238-
'Render the true nullability in the schema; be prepared for new types of nullability in future!',
225+
description: 'Allow for returning semantic-non-null types.',
239226
},
240227
},
241228
});
@@ -408,22 +395,11 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
408395
args: {
409396
nullability: {
410397
type: new GraphQLNonNull(__TypeNullability),
411-
defaultValue: TypeNullability.AUTO,
398+
defaultValue: TypeNullability.TRADITIONAL,
412399
},
413400
},
414-
resolve: (field, { nullability }, _context, info) => {
415-
if (nullability === TypeNullability.FULL) {
416-
return field.type;
417-
}
418-
419-
const mode =
420-
nullability === TypeNullability.AUTO
421-
? info.errorPropagation
422-
? TypeNullability.TRADITIONAL
423-
: TypeNullability.SEMANTIC
424-
: nullability;
425-
return convertOutputTypeToNullabilityMode(field.type, mode);
426-
},
401+
resolve: (field, { nullability }, _context) =>
402+
convertOutputTypeToNullabilityMode(field.type, nullability),
427403
},
428404
isDeprecated: {
429405
type: new GraphQLNonNull(GraphQLBoolean),
@@ -436,10 +412,9 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
436412
} as GraphQLFieldConfigMap<GraphQLField<unknown, unknown>, unknown>),
437413
});
438414

439-
// TODO: move this elsewhere, rename, memoize
440415
function convertOutputTypeToNullabilityMode(
441416
type: GraphQLType,
442-
mode: TypeNullability.TRADITIONAL | TypeNullability.SEMANTIC,
417+
mode: TypeNullability,
443418
): GraphQLType {
444419
if (mode === TypeNullability.TRADITIONAL) {
445420
if (isNonNullType(type)) {
@@ -455,7 +430,12 @@ function convertOutputTypeToNullabilityMode(
455430
}
456431
return type;
457432
}
458-
if (isNonNullType(type) || isSemanticNonNullType(type)) {
433+
434+
if (isNonNullType(type)) {
435+
return new GraphQLNonNull(
436+
convertOutputTypeToNullabilityMode(type.ofType, mode),
437+
);
438+
} else if (isSemanticNonNullType(type)) {
459439
return new GraphQLSemanticNonNull(
460440
convertOutputTypeToNullabilityMode(type.ofType, mode),
461441
);
@@ -464,6 +444,7 @@ function convertOutputTypeToNullabilityMode(
464444
convertOutputTypeToNullabilityMode(type.ofType, mode),
465445
);
466446
}
447+
467448
return type;
468449
}
469450

Diff for: src/utilities/__tests__/buildClientSchema-test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
assertEnumType,
1010
GraphQLEnumType,
1111
GraphQLObjectType,
12+
GraphQLSemanticNonNull,
1213
} from '../../type/definition';
1314
import {
1415
GraphQLBoolean,
@@ -983,4 +984,63 @@ describe('Type System: build schema from introspection', () => {
983984
);
984985
});
985986
});
987+
988+
describe('SemanticNullability', () => {
989+
it('should build a client schema with semantic-non-null types', () => {
990+
const sdl = dedent`
991+
@SemanticNullability
992+
993+
type Query {
994+
foo: String
995+
bar: String?
996+
}
997+
`;
998+
const schema = buildSchema(sdl, { assumeValid: true });
999+
const introspection = introspectionFromSchema(schema, {
1000+
nullability: 'FULL',
1001+
});
1002+
1003+
const clientSchema = buildClientSchema(introspection);
1004+
expect(printSchema(clientSchema)).to.equal(sdl);
1005+
1006+
const defaults = {
1007+
args: [],
1008+
astNode: undefined,
1009+
deprecationReason: null,
1010+
description: null,
1011+
extensions: {},
1012+
resolve: undefined,
1013+
subscribe: undefined,
1014+
};
1015+
expect(clientSchema.getType('Query')).to.deep.include({
1016+
name: 'Query',
1017+
_fields: {
1018+
foo: {
1019+
...defaults,
1020+
name: 'foo',
1021+
type: new GraphQLSemanticNonNull(GraphQLString),
1022+
},
1023+
bar: { ...defaults, name: 'bar', type: GraphQLString },
1024+
},
1025+
});
1026+
});
1027+
1028+
it.only('should throw when semantic-non-null types are too deep', () => {
1029+
const sdl = dedent`
1030+
@SemanticNullability
1031+
1032+
type Query {
1033+
bar: [[[[[[String?]]]]]]?
1034+
}
1035+
`;
1036+
const schema = buildSchema(sdl, { assumeValid: true });
1037+
const introspection = introspectionFromSchema(schema, {
1038+
nullability: 'FULL',
1039+
});
1040+
1041+
expect(() => buildClientSchema(introspection)).to.throw(
1042+
'Decorated type deeper than introspection query.',
1043+
);
1044+
});
1045+
});
9861046
});

Diff for: src/utilities/__tests__/printSchema-test.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ describe('Type System Printer', () => {
782782
name: String!
783783
description: String
784784
args(includeDeprecated: Boolean = false): [__InputValue!]!
785-
type(nullability: __TypeNullability! = AUTO): __Type!
785+
type(nullability: __TypeNullability! = TRADITIONAL): __Type!
786786
isDeprecated: Boolean!
787787
deprecationReason: String
788788
}
@@ -803,20 +803,14 @@ describe('Type System Printer', () => {
803803
deprecationReason: String
804804
}
805805
806-
"""TODO"""
806+
"""
807+
This represents the type of nullability we want to return as part of the introspection.
808+
"""
807809
enum __TypeNullability {
808-
"""Determines nullability mode based on errorPropagation mode."""
809-
AUTO
810-
811810
"""Turn semantic-non-null types into nullable types."""
812811
TRADITIONAL
813812
814-
"""Turn non-null types into semantic-non-null types."""
815-
SEMANTIC
816-
817-
"""
818-
Render the true nullability in the schema; be prepared for new types of nullability in future!
819-
"""
813+
"""Allow for returning semantic-non-null types."""
820814
FULL
821815
}
822816

0 commit comments

Comments
 (0)