Skip to content

Commit 17461b9

Browse files
JoviDeCroockbenjietwof
committed
Implement first version
Co-Authored-By: Benjie <[email protected]> Co-Authored-By: twof <[email protected]>
1 parent 31bf28f commit 17461b9

33 files changed

+984
-23
lines changed

Diff for: src/__tests__/starWarsIntrospection-test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('Star Wars Introspection Tests', () => {
4242
{ name: '__TypeKind' },
4343
{ name: '__Field' },
4444
{ name: '__InputValue' },
45+
{ name: '__TypeNullability' },
4546
{ name: '__EnumValue' },
4647
{ name: '__Directive' },
4748
{ name: '__DirectiveLocation' },

Diff for: src/execution/__tests__/executor-test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ describe('Execute: Handles basic execution tasks', () => {
263263
'rootValue',
264264
'operation',
265265
'variableValues',
266+
'errorPropagation',
266267
);
267268

268269
const operation = document.definitions[0];
@@ -275,6 +276,7 @@ describe('Execute: Handles basic execution tasks', () => {
275276
schema,
276277
rootValue,
277278
operation,
279+
errorPropagation: true
278280
});
279281

280282
const field = operation.selectionSet.selections[0];

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

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { GraphQLError } from '../../error/GraphQLError';
5+
6+
import type { ExecutableDefinitionNode, FieldNode } from '../../language/ast';
7+
import { parse } from '../../language/parser';
8+
9+
import {
10+
GraphQLNonNull,
11+
GraphQLObjectType,
12+
GraphQLSemanticNonNull,
13+
GraphQLSemanticNullable,
14+
} from '../../type/definition';
15+
import { GraphQLString } from '../../type/scalars';
16+
import { GraphQLSchema } from '../../type/schema';
17+
18+
import { execute } from '../execute';
19+
20+
describe('Execute: Handles Semantic Nullability', () => {
21+
const DeepDataType = new GraphQLObjectType({
22+
name: 'DeepDataType',
23+
fields: {
24+
f: { type: new GraphQLNonNull(GraphQLString) },
25+
},
26+
});
27+
28+
const DataType: GraphQLObjectType = new GraphQLObjectType({
29+
name: 'DataType',
30+
fields: () => ({
31+
a: { type: new GraphQLSemanticNullable(GraphQLString) },
32+
b: { type: new GraphQLSemanticNonNull(GraphQLString) },
33+
c: { type: new GraphQLNonNull(GraphQLString) },
34+
d: { type: new GraphQLSemanticNonNull(DeepDataType) },
35+
}),
36+
});
37+
38+
it('SemanticNonNull throws error on null without error', async () => {
39+
const data = {
40+
a: () => 'Apple',
41+
b: () => null,
42+
c: () => 'Cookie',
43+
};
44+
45+
const document = parse(`
46+
query {
47+
b
48+
}
49+
`);
50+
51+
const result = await execute({
52+
schema: new GraphQLSchema({ query: DataType }),
53+
document,
54+
rootValue: data,
55+
});
56+
57+
const executable = document.definitions?.values().next()
58+
.value as ExecutableDefinitionNode;
59+
const selectionSet = executable.selectionSet.selections
60+
.values()
61+
.next().value;
62+
63+
expect(result).to.deep.equal({
64+
data: {
65+
b: null,
66+
},
67+
errors: [
68+
new GraphQLError(
69+
'Cannot return null for semantic-non-nullable field DataType.b.',
70+
{
71+
nodes: selectionSet,
72+
path: ['b'],
73+
},
74+
),
75+
],
76+
});
77+
});
78+
79+
it('SemanticNonNull succeeds on null with error', async () => {
80+
const data = {
81+
a: () => 'Apple',
82+
b: () => {
83+
throw new Error('Something went wrong');
84+
},
85+
c: () => 'Cookie',
86+
};
87+
88+
const document = parse(`
89+
query {
90+
b
91+
}
92+
`);
93+
94+
const executable = document.definitions?.values().next()
95+
.value as ExecutableDefinitionNode;
96+
const selectionSet = executable.selectionSet.selections
97+
.values()
98+
.next().value;
99+
100+
const result = await execute({
101+
schema: new GraphQLSchema({ query: DataType }),
102+
document,
103+
rootValue: data,
104+
});
105+
106+
expect(result).to.deep.equal({
107+
data: {
108+
b: null,
109+
},
110+
errors: [
111+
new GraphQLError('Something went wrong', {
112+
nodes: selectionSet,
113+
path: ['b'],
114+
}),
115+
],
116+
});
117+
});
118+
119+
it('SemanticNonNull halts null propagation', async () => {
120+
const deepData = {
121+
f: () => null,
122+
};
123+
124+
const data = {
125+
a: () => 'Apple',
126+
b: () => null,
127+
c: () => 'Cookie',
128+
d: () => deepData,
129+
};
130+
131+
const document = parse(`
132+
query {
133+
d {
134+
f
135+
}
136+
}
137+
`);
138+
139+
const result = await execute({
140+
schema: new GraphQLSchema({ query: DataType }),
141+
document,
142+
rootValue: data,
143+
});
144+
145+
const executable = document.definitions?.values().next()
146+
.value as ExecutableDefinitionNode;
147+
const dSelectionSet = executable.selectionSet.selections.values().next()
148+
.value as FieldNode;
149+
const fSelectionSet = dSelectionSet.selectionSet?.selections
150+
.values()
151+
.next().value;
152+
153+
expect(result).to.deep.equal({
154+
data: {
155+
d: null,
156+
},
157+
errors: [
158+
new GraphQLError(
159+
'Cannot return null for non-nullable field DeepDataType.f.',
160+
{
161+
nodes: fSelectionSet,
162+
path: ['d', 'f'],
163+
},
164+
),
165+
],
166+
});
167+
});
168+
169+
it('SemanticNullable allows null values', async () => {
170+
const data = {
171+
a: () => null,
172+
b: () => null,
173+
c: () => 'Cookie',
174+
};
175+
176+
const document = parse(`
177+
query {
178+
a
179+
}
180+
`);
181+
182+
const result = await execute({
183+
schema: new GraphQLSchema({ query: DataType }),
184+
document,
185+
rootValue: data,
186+
});
187+
188+
expect(result).to.deep.equal({
189+
data: {
190+
a: null,
191+
},
192+
});
193+
});
194+
195+
it('SemanticNullable allows non-null values', async () => {
196+
const data = {
197+
a: () => 'Apple',
198+
b: () => null,
199+
c: () => 'Cookie',
200+
};
201+
202+
const document = parse(`
203+
query {
204+
a
205+
}
206+
`);
207+
208+
const result = await execute({
209+
schema: new GraphQLSchema({ query: DataType }),
210+
document,
211+
rootValue: data,
212+
});
213+
214+
expect(result).to.deep.equal({
215+
data: {
216+
a: 'Apple',
217+
},
218+
});
219+
});
220+
});

Diff for: src/execution/execute.ts

+44
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import {
4343
isListType,
4444
isNonNullType,
4545
isObjectType,
46+
isSemanticNonNullType,
47+
isSemanticNullableType,
4648
} from '../type/definition';
4749
import {
4850
SchemaMetaFieldDef,
@@ -115,6 +117,7 @@ export interface ExecutionContext {
115117
typeResolver: GraphQLTypeResolver<any, any>;
116118
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
117119
errors: Array<GraphQLError>;
120+
errorPropagation: boolean;
118121
}
119122

120123
/**
@@ -152,6 +155,13 @@ export interface ExecutionArgs {
152155
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
153156
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
154157
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
158+
/**
159+
* Set to `false` to disable error propagation. Experimental.
160+
* TODO: describe what this does
161+
*
162+
* @experimental
163+
*/
164+
errorPropagation?: boolean;
155165
}
156166

157167
/**
@@ -286,6 +296,7 @@ export function buildExecutionContext(
286296
fieldResolver,
287297
typeResolver,
288298
subscribeFieldResolver,
299+
errorPropagation
289300
} = args;
290301

291302
let operation: OperationDefinitionNode | undefined;
@@ -347,6 +358,7 @@ export function buildExecutionContext(
347358
typeResolver: typeResolver ?? defaultTypeResolver,
348359
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
349360
errors: [],
361+
errorPropagation: errorPropagation ?? true,
350362
};
351363
}
352364

@@ -585,6 +597,7 @@ export function buildResolveInfo(
585597
rootValue: exeContext.rootValue,
586598
operation: exeContext.operation,
587599
variableValues: exeContext.variableValues,
600+
errorPropagation: exeContext.errorPropagation,
588601
};
589602
}
590603

@@ -658,6 +671,37 @@ function completeValue(
658671
return completed;
659672
}
660673

674+
// If field type is SemanticNonNull, complete for inner type, and throw field error
675+
// if result is null and an error doesn't exist.
676+
if (isSemanticNonNullType(returnType)) {
677+
const completed = completeValue(
678+
exeContext,
679+
returnType.ofType,
680+
fieldNodes,
681+
info,
682+
path,
683+
result,
684+
);
685+
if (completed === null) {
686+
throw new Error(
687+
`Cannot return null for semantic-non-nullable field ${info.parentType.name}.${info.fieldName}.`,
688+
);
689+
}
690+
return completed;
691+
}
692+
693+
// If field type is SemanticNullable, complete for inner type
694+
if (isSemanticNullableType(returnType)) {
695+
return completeValue(
696+
exeContext,
697+
returnType.ofType,
698+
fieldNodes,
699+
info,
700+
path,
701+
result,
702+
);
703+
}
704+
661705
// If result value is null or undefined then return null.
662706
if (result == null) {
663707
return null;

Diff for: src/graphql.ts

+8
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ export interface GraphQLArgs {
6666
operationName?: Maybe<string>;
6767
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
6868
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
69+
/**
70+
* Set to `false` to disable error propagation. Experimental.
71+
*
72+
* @experimental
73+
*/
74+
errorPropagation?: boolean;
6975
}
7076

7177
export function graphql(args: GraphQLArgs): Promise<ExecutionResult> {
@@ -106,6 +112,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
106112
operationName,
107113
fieldResolver,
108114
typeResolver,
115+
errorPropagation,
109116
} = args;
110117

111118
// Validate Schema
@@ -138,5 +145,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
138145
operationName,
139146
fieldResolver,
140147
typeResolver,
148+
errorPropagation,
141149
});
142150
}

0 commit comments

Comments
 (0)