Skip to content

Commit 702da5d

Browse files
committed
Cleanup and code coverage
1 parent 7121006 commit 702da5d

10 files changed

+266
-97
lines changed

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',

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
});

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
*/

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

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('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
});

0 commit comments

Comments
 (0)