Skip to content

Commit 650f403

Browse files
committed
apply old changes on top of up to date main branch
1 parent 2df59f1 commit 650f403

File tree

17 files changed

+594
-25
lines changed

17 files changed

+594
-25
lines changed

src/__testUtils__/kitchenSinkQuery.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
1010
...frag @onFragmentSpread
1111
}
1212
}
13+
field3!
14+
requiredField4: field4!
15+
field5?
16+
optionalField6: field6?
1317
}
1418
... @skip(unless: $foo) {
1519
id

src/execution/__tests__/executor-test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,4 +1220,155 @@ describe('Execute: Handles basic execution tasks', () => {
12201220
expect(result).to.deep.equal({ data: { foo: { bar: 'bar' } } });
12211221
expect(possibleTypes).to.deep.equal([fooObject]);
12221222
});
1223+
1224+
it('bubbles null up to nearest nullable parent', () => {
1225+
const schema = new GraphQLSchema({
1226+
query: new GraphQLObjectType({
1227+
name: 'Query',
1228+
fields: {
1229+
food: {
1230+
type: new GraphQLObjectType({
1231+
name: 'Food',
1232+
fields: {
1233+
name: { type: GraphQLString },
1234+
calories: { type: GraphQLInt },
1235+
},
1236+
}),
1237+
resolve() {
1238+
return {
1239+
name: null,
1240+
calories: 10,
1241+
};
1242+
},
1243+
},
1244+
},
1245+
}),
1246+
});
1247+
1248+
const plainDocument = parse(`
1249+
query {
1250+
food {
1251+
name
1252+
calories
1253+
}
1254+
}
1255+
`);
1256+
const plainResult = executeSync({ schema, document: plainDocument });
1257+
1258+
expect(plainResult).to.deep.equal({
1259+
data: { food: { name: null, calories: 10 } },
1260+
});
1261+
1262+
const singleNonNullOnNullValueDocument = parse(`
1263+
query {
1264+
food {
1265+
name!
1266+
calories
1267+
}
1268+
}
1269+
`);
1270+
const singleNonNullOnNullValueResult = executeSync({
1271+
schema,
1272+
document: singleNonNullOnNullValueDocument,
1273+
});
1274+
1275+
expect(singleNonNullOnNullValueResult).to.deep.equal({
1276+
data: { food: null },
1277+
errors: [
1278+
{
1279+
locations: [{ column: 11, line: 4 }],
1280+
message: 'Cannot return null for non-nullable field Food.name.',
1281+
path: ['food', 'name'],
1282+
},
1283+
],
1284+
});
1285+
1286+
const bothNonNullOnNullValueDocument = parse(`
1287+
query {
1288+
food {
1289+
name!
1290+
calories!
1291+
}
1292+
}
1293+
`);
1294+
const bothNonNullOnNullValueResult = executeSync({
1295+
schema,
1296+
document: bothNonNullOnNullValueDocument,
1297+
});
1298+
1299+
expect(bothNonNullOnNullValueResult).to.deep.equal({
1300+
data: { food: null },
1301+
errors: [
1302+
{
1303+
locations: [{ column: 11, line: 4 }],
1304+
message: 'Cannot return null for non-nullable field Food.name.',
1305+
path: ['food', 'name'],
1306+
},
1307+
],
1308+
});
1309+
1310+
const singleNonNullOnNonNullValueDocument = parse(`
1311+
query {
1312+
food {
1313+
calories!
1314+
}
1315+
}
1316+
`);
1317+
const singleNonNullOnNonNullValueResult = executeSync({
1318+
schema,
1319+
document: singleNonNullOnNonNullValueDocument,
1320+
});
1321+
1322+
expect(singleNonNullOnNonNullValueResult).to.deep.equal({
1323+
data: { food: { calories: 10 } },
1324+
});
1325+
1326+
const nonNullAliasOnNullValueDocument = parse(`
1327+
query {
1328+
food {
1329+
theNameOfTheFood: name!
1330+
}
1331+
}
1332+
`);
1333+
const nonNullAliasOnNullValueResult = executeSync({
1334+
schema,
1335+
document: nonNullAliasOnNullValueDocument,
1336+
});
1337+
1338+
expect(nonNullAliasOnNullValueResult).to.deep.equal({
1339+
data: { food: null },
1340+
errors: [
1341+
{
1342+
locations: [{ column: 11, line: 4 }],
1343+
message: 'Cannot return null for non-nullable field Food.name.',
1344+
path: ['food', 'theNameOfTheFood'],
1345+
},
1346+
],
1347+
});
1348+
1349+
const nonNullInFragmentDocument = parse(`
1350+
query {
1351+
food {
1352+
... on Food {
1353+
name!
1354+
}
1355+
}
1356+
}
1357+
`);
1358+
const nonNullInFragmentResult = executeSync({
1359+
schema,
1360+
document: nonNullInFragmentDocument,
1361+
});
1362+
1363+
expect(nonNullInFragmentResult).to.deep.equal({
1364+
data: { food: null },
1365+
errors: [
1366+
{
1367+
locations: [{ column: 13, line: 5 }],
1368+
message: 'Cannot return null for non-nullable field Food.name.',
1369+
path: ['food', 'name'],
1370+
},
1371+
],
1372+
});
1373+
});
12231374
});

src/execution/execute.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ import {
5353

5454
import { getOperationRootType } from '../utilities/getOperationRootType';
5555

56+
import { modifiedOutputType } from '../utilities/applyRequiredStatus';
57+
5658
import { getVariableValues, getArgumentValues } from './values';
5759
import { collectFields } from './collectFields';
5860

@@ -456,7 +458,7 @@ function executeField(
456458
return;
457459
}
458460

459-
const returnType = fieldDef.type;
461+
const returnType = modifiedOutputType(fieldDef.type, fieldNodes[0].required);
460462
const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver;
461463

462464
const info = buildResolveInfo(

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export {
223223
isTypeDefinitionNode,
224224
isTypeSystemExtensionNode,
225225
isTypeExtensionNode,
226+
RequiredStatus,
226227
} from './language/index';
227228

228229
export type {
@@ -297,6 +298,7 @@ export type {
297298
UnionTypeExtensionNode,
298299
EnumTypeExtensionNode,
299300
InputObjectTypeExtensionNode,
301+
RequiredStatus,
300302
} from './language/index';
301303

302304
/** Execute GraphQL queries. */
@@ -438,6 +440,7 @@ export {
438440
DangerousChangeType,
439441
findBreakingChanges,
440442
findDangerousChanges,
443+
modifiedOutputType,
441444
} from './utilities/index';
442445

443446
export type {

src/language/__tests__/lexer-test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,16 @@ describe('Lexer', () => {
167167
it('errors respect whitespace', () => {
168168
let caughtError;
169169
try {
170-
lexOne(['', '', ' ?', ''].join('\n'));
170+
lexOne(['', '', ' ~', ''].join('\n'));
171171
} catch (error) {
172172
caughtError = error;
173173
}
174174
expect(String(caughtError)).to.equal(dedent`
175-
Syntax Error: Unexpected character: "?".
175+
Syntax Error: Unexpected character: "~".
176176
177177
GraphQL request:3:5
178178
2 |
179-
3 | ?
179+
3 | ~
180180
| ^
181181
4 |
182182
`);
@@ -185,18 +185,18 @@ describe('Lexer', () => {
185185
it('updates line numbers in error for file context', () => {
186186
let caughtError;
187187
try {
188-
const str = ['', '', ' ?', ''].join('\n');
188+
const str = ['', '', ' ~', ''].join('\n');
189189
const source = new Source(str, 'foo.js', { line: 11, column: 12 });
190190
new Lexer(source).advance();
191191
} catch (error) {
192192
caughtError = error;
193193
}
194194
expect(String(caughtError)).to.equal(dedent`
195-
Syntax Error: Unexpected character: "?".
195+
Syntax Error: Unexpected character: "~".
196196
197197
foo.js:13:6
198198
12 |
199-
13 | ?
199+
13 | ~
200200
| ^
201201
14 |
202202
`);
@@ -205,16 +205,16 @@ describe('Lexer', () => {
205205
it('updates column numbers in error for file context', () => {
206206
let caughtError;
207207
try {
208-
const source = new Source('?', 'foo.js', { line: 1, column: 5 });
208+
const source = new Source('~', 'foo.js', { line: 1, column: 5 });
209209
new Lexer(source).advance();
210210
} catch (error) {
211211
caughtError = error;
212212
}
213213
expect(String(caughtError)).to.equal(dedent`
214-
Syntax Error: Unexpected character: "?".
214+
Syntax Error: Unexpected character: "~".
215215
216216
foo.js:1:5
217-
1 | ?
217+
1 | ~
218218
| ^
219219
`);
220220
});
@@ -1004,8 +1004,8 @@ describe('Lexer', () => {
10041004
locations: [{ line: 1, column: 1 }],
10051005
});
10061006

1007-
expectSyntaxError('?').to.deep.equal({
1008-
message: 'Syntax Error: Unexpected character: "?".',
1007+
expectSyntaxError('~').to.deep.equal({
1008+
message: 'Syntax Error: Unexpected character: "~".',
10091009
locations: [{ line: 1, column: 1 }],
10101010
});
10111011

src/language/__tests__/parser-test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,66 @@ describe('Parser', () => {
225225
).to.not.throw();
226226
});
227227

228+
it('parses required field', () => {
229+
expect(() =>
230+
parse(`
231+
query {
232+
requiredField!
233+
}
234+
`),
235+
).to.not.throw();
236+
});
237+
238+
it('parses optional field', () => {
239+
expect(() =>
240+
parse(`
241+
query {
242+
optionalField?
243+
}
244+
`),
245+
).to.not.throw();
246+
});
247+
248+
it('parses required with alias', () => {
249+
expect(() =>
250+
parse(`
251+
query {
252+
requiredField: field!
253+
}
254+
`),
255+
).to.not.throw();
256+
});
257+
258+
it('does not parse aliased field with bang on left of colon', () => {
259+
expect(() =>
260+
parse(`
261+
query {
262+
requiredField!: field
263+
}
264+
`),
265+
).to.throw();
266+
});
267+
268+
it('does not parse aliased field with bang on left and right of colon', () => {
269+
expect(() =>
270+
parse(`
271+
query {
272+
requiredField!: field!
273+
}
274+
`),
275+
).to.throw();
276+
});
277+
278+
it('parses required within fragment', () => {
279+
expect(() =>
280+
parse(`
281+
fragment MyFragment on Query {
282+
field!
283+
}
284+
`),
285+
).to.not.throw();
286+
});
287+
228288
it('creates ast', () => {
229289
const result = parse(dedent`
230290
{
@@ -246,6 +306,7 @@ describe('Parser', () => {
246306
name: undefined,
247307
variableDefinitions: [],
248308
directives: [],
309+
required: 'unset',
249310
selectionSet: {
250311
kind: Kind.SELECTION_SET,
251312
loc: { start: 0, end: 40 },
@@ -276,6 +337,7 @@ describe('Parser', () => {
276337
},
277338
],
278339
directives: [],
340+
required: 'unset',
279341
selectionSet: {
280342
kind: Kind.SELECTION_SET,
281343
loc: { start: 16, end: 38 },
@@ -336,6 +398,7 @@ describe('Parser', () => {
336398
name: undefined,
337399
variableDefinitions: [],
338400
directives: [],
401+
required: 'unset',
339402
selectionSet: {
340403
kind: Kind.SELECTION_SET,
341404
loc: { start: 6, end: 29 },
@@ -351,6 +414,7 @@ describe('Parser', () => {
351414
},
352415
arguments: [],
353416
directives: [],
417+
required: 'unset',
354418
selectionSet: {
355419
kind: Kind.SELECTION_SET,
356420
loc: { start: 15, end: 27 },

src/language/__tests__/printer-test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ describe('Printer: Query document', () => {
1212
const ast = {
1313
kind: 'Field',
1414
name: { kind: 'Name', value: 'foo' },
15+
required: 'unset',
1516
} as const;
1617
expect(print(ast)).to.equal('foo');
1718
});
@@ -164,6 +165,10 @@ describe('Printer: Query document', () => {
164165
...frag @onFragmentSpread
165166
}
166167
}
168+
field3!
169+
requiredField4: field4!
170+
field5?
171+
optionalField6: field6?
167172
}
168173
... @skip(unless: $foo) {
169174
id

0 commit comments

Comments
 (0)