Skip to content

Commit d7278da

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

18 files changed

+669
-99
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

+105-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,108 @@ 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+
// @ts-expect-error
1817+
(t) => t.name === 'Query',
1818+
);
1819+
const defaults = {
1820+
args: [],
1821+
deprecationReason: null,
1822+
description: null,
1823+
isDeprecated: false,
1824+
};
1825+
expect(queryType?.fields).to.deep.equal([
1826+
{
1827+
name: 'someField',
1828+
...defaults,
1829+
type: {
1830+
kind: 'NON_NULL',
1831+
name: null,
1832+
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
1833+
},
1834+
},
1835+
{
1836+
name: 'someField2',
1837+
...defaults,
1838+
type: { kind: 'SCALAR', name: 'String', ofType: null },
1839+
},
1840+
{
1841+
name: 'someField3',
1842+
...defaults,
1843+
type: { kind: 'SCALAR', name: 'String', ofType: null },
1844+
},
1845+
]);
1846+
});
1847+
1848+
it('returns semantic-non-null types in full mode', () => {
1849+
const schema = buildSchema(`
1850+
@SemanticNullability
1851+
type Query {
1852+
someField: String!
1853+
someField2: String
1854+
someField3: String?
1855+
}
1856+
`);
1857+
1858+
const source = getIntrospectionQuery({
1859+
nullability: 'FULL',
1860+
});
1861+
1862+
const result = graphqlSync({ schema, source });
1863+
// @ts-expect-error
1864+
const queryType = result.data?.__schema?.types.find(
1865+
// @ts-expect-error
1866+
(t) => t.name === 'Query',
1867+
);
1868+
const defaults = {
1869+
args: [],
1870+
deprecationReason: null,
1871+
description: null,
1872+
isDeprecated: false,
1873+
};
1874+
expect(queryType?.fields).to.deep.equal([
1875+
{
1876+
name: 'someField',
1877+
...defaults,
1878+
type: {
1879+
kind: 'NON_NULL',
1880+
name: null,
1881+
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
1882+
},
1883+
},
1884+
{
1885+
name: 'someField2',
1886+
...defaults,
1887+
type: {
1888+
kind: 'SEMANTIC_NON_NULL',
1889+
name: null,
1890+
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
1891+
},
1892+
},
1893+
{
1894+
name: 'someField3',
1895+
...defaults,
1896+
type: { kind: 'SCALAR', name: 'String', ofType: null },
1897+
},
1898+
]);
1899+
});
1900+
});
18071901
});

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__/TypeInfo-test.ts

+62
Original file line numberDiff line numberDiff line change
@@ -457,4 +457,66 @@ describe('visitWithTypeInfo', () => {
457457
['leave', 'SelectionSet', null, 'Human', 'Human'],
458458
]);
459459
});
460+
461+
it('supports traversals of semantic non-null types', () => {
462+
const schema = buildSchema(`
463+
@SemanticNullability
464+
type Query {
465+
id: String!
466+
name: String
467+
something: String?
468+
}
469+
`);
470+
471+
const typeInfo = new TypeInfo(schema);
472+
473+
const visited: Array<any> = [];
474+
const ast = parse('{ id name something }');
475+
476+
visit(
477+
ast,
478+
visitWithTypeInfo(typeInfo, {
479+
enter(node) {
480+
const type = typeInfo.getType();
481+
visited.push([
482+
'enter',
483+
node.kind,
484+
node.kind === 'Name' ? node.value : null,
485+
String(type),
486+
]);
487+
},
488+
leave(node) {
489+
const type = typeInfo.getType();
490+
visited.push([
491+
'leave',
492+
node.kind,
493+
node.kind === 'Name' ? node.value : null,
494+
// TODO: inspect currently returns "String" for a nullable type
495+
String(type),
496+
]);
497+
},
498+
}),
499+
);
500+
501+
expect(visited).to.deep.equal([
502+
['enter', 'Document', null, 'undefined'],
503+
['enter', 'OperationDefinition', null, 'Query'],
504+
['enter', 'SelectionSet', null, 'Query'],
505+
['enter', 'Field', null, 'String!'],
506+
['enter', 'Name', 'id', 'String!'],
507+
['leave', 'Name', 'id', 'String!'],
508+
['leave', 'Field', null, 'String!'],
509+
['enter', 'Field', null, 'String'],
510+
['enter', 'Name', 'name', 'String'],
511+
['leave', 'Name', 'name', 'String'],
512+
['leave', 'Field', null, 'String'],
513+
['enter', 'Field', null, 'String'],
514+
['enter', 'Name', 'something', 'String'],
515+
['leave', 'Name', 'something', 'String'],
516+
['leave', 'Field', null, 'String'],
517+
['leave', 'SelectionSet', null, 'Query'],
518+
['leave', 'OperationDefinition', null, 'Query'],
519+
['leave', 'Document', null, 'undefined'],
520+
]);
521+
});
460522
});

0 commit comments

Comments
 (0)