Skip to content

Commit c034de9

Browse files
committed
RFC: Return type overlap validation
Implements graphql/graphql-spec#162 This alters the "Overlapping Fields Can Be Merged" validation rule to better express return type validation issues. The existing rule has presented some false-positives in some schema where interfaces with co-variant types are commonly used. The "same return type" check doesn't remove any ambiguity. Instead what that "same return type" check is attempting to prevent is spreading two fragments (or inline fragments) which have fields with return types where ambiguity would be introduced in the response. In order to curb false-positives, we later changed this rule such that if two fields were known to never apply simultaneously, then we would skip the remainder of the rule. ``` { ... on Person { foo: fullName } ... on Pet { foo: petName } } ``` However this can introduce false-negatives! ``` { ... on Person { foo: birthday { bar: year } } ... on Business { foo: location { bar: street } } } ``` In the above example, `data.foo.bar` could be of type `Int` or type `String`, it's ambiguous! This differing return type breaks some client model implementations (Fragment models) in addition to just being confusing.
1 parent ffe76c5 commit c034de9

File tree

2 files changed

+320
-62
lines changed

2 files changed

+320
-62
lines changed

src/validation/__tests__/OverlappingFieldsCanBeMerged-test.js

Lines changed: 189 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -368,27 +368,36 @@ describe('Validate: Overlapping fields can be merged', () => {
368368
const SomeBox = new GraphQLInterfaceType({
369369
name: 'SomeBox',
370370
resolveType: () => StringBox,
371-
fields: {
371+
fields: () => ({
372+
deepBox: { type: SomeBox },
372373
unrelatedField: { type: GraphQLString }
373-
}
374+
})
374375
});
375376

376377
const StringBox = new GraphQLObjectType({
377378
name: 'StringBox',
378379
interfaces: [ SomeBox ],
379-
fields: {
380+
fields: () => ({
380381
scalar: { type: GraphQLString },
382+
deepBox: { type: StringBox },
381383
unrelatedField: { type: GraphQLString },
382-
}
384+
listStringBox: { type: new GraphQLList(StringBox) },
385+
stringBox: { type: StringBox },
386+
intBox: { type: IntBox },
387+
})
383388
});
384389

385390
const IntBox = new GraphQLObjectType({
386391
name: 'IntBox',
387392
interfaces: [ SomeBox ],
388-
fields: {
393+
fields: () => ({
389394
scalar: { type: GraphQLInt },
395+
deepBox: { type: IntBox },
390396
unrelatedField: { type: GraphQLString },
391-
}
397+
listStringBox: { type: new GraphQLList(StringBox) },
398+
stringBox: { type: StringBox },
399+
intBox: { type: IntBox },
400+
})
392401
});
393402

394403
const NonNullStringBox1 = new GraphQLInterfaceType({
@@ -405,6 +414,7 @@ describe('Validate: Overlapping fields can be merged', () => {
405414
fields: {
406415
scalar: { type: new GraphQLNonNull(GraphQLString) },
407416
unrelatedField: { type: GraphQLString },
417+
deepBox: { type: SomeBox },
408418
}
409419
});
410420

@@ -422,6 +432,7 @@ describe('Validate: Overlapping fields can be merged', () => {
422432
fields: {
423433
scalar: { type: new GraphQLNonNull(GraphQLString) },
424434
unrelatedField: { type: GraphQLString },
435+
deepBox: { type: SomeBox },
425436
}
426437
});
427438

@@ -455,7 +466,7 @@ describe('Validate: Overlapping fields can be merged', () => {
455466
connection: { type: Connection }
456467
})
457468
}),
458-
types: [ IntBox, NonNullStringBox1Impl, NonNullStringBox2Impl ]
469+
types: [ IntBox, StringBox, NonNullStringBox1Impl, NonNullStringBox2Impl ]
459470
});
460471

461472
it('conflicting return types which potentially overlap', () => {
@@ -477,21 +488,187 @@ describe('Validate: Overlapping fields can be merged', () => {
477488
`, [
478489
{ message: fieldsConflictMessage(
479490
'scalar',
480-
'they return differing types Int and String!'
491+
'they return conflicting types Int and String!'
481492
),
482493
locations: [ { line: 5, column: 15 }, { line: 8, column: 15 } ] }
483494
]);
484495
});
485496

486-
it('allows differing return types which cannot overlap', () => {
487-
// This is valid since an object cannot be both an IntBox and a StringBox.
497+
it('compatible return shapes on different return types', () => {
498+
// In this case `deepBox` returns `SomeBox` in the first usage, and
499+
// `StringBox` in the second usage. These return types are not the same!
500+
// however this is valid because the return *shapes* are compatible.
488501
expectPassesRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
502+
{
503+
someBox {
504+
... on SomeBox {
505+
deepBox {
506+
unrelatedField
507+
}
508+
}
509+
... on StringBox {
510+
deepBox {
511+
unrelatedField
512+
}
513+
}
514+
}
515+
}
516+
`);
517+
});
518+
519+
it('disallows differing return types despite no overlap', () => {
520+
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
489521
{
490522
someBox {
491-
...on IntBox {
523+
... on IntBox {
524+
scalar
525+
}
526+
... on StringBox {
527+
scalar
528+
}
529+
}
530+
}
531+
`, [
532+
{ message: fieldsConflictMessage(
533+
'scalar',
534+
'they return conflicting types Int and String'
535+
),
536+
locations: [ { line: 5, column: 15 }, { line: 8, column: 15 } ] }
537+
]);
538+
});
539+
540+
it('disallows differing return type nullability despite no overlap', () => {
541+
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
542+
{
543+
someBox {
544+
... on NonNullStringBox1 {
492545
scalar
493546
}
494-
...on StringBox {
547+
... on StringBox {
548+
scalar
549+
}
550+
}
551+
}
552+
`, [
553+
{ message: fieldsConflictMessage(
554+
'scalar',
555+
'they return conflicting types String! and String'
556+
),
557+
locations: [ { line: 5, column: 15 }, { line: 8, column: 15 } ] }
558+
]);
559+
});
560+
561+
it('disallows differing return type list despite no overlap', () => {
562+
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
563+
{
564+
someBox {
565+
... on IntBox {
566+
box: listStringBox {
567+
scalar
568+
}
569+
}
570+
... on StringBox {
571+
box: stringBox {
572+
scalar
573+
}
574+
}
575+
}
576+
}
577+
`, [
578+
{ message: fieldsConflictMessage(
579+
'box',
580+
'they return conflicting types [StringBox] and StringBox'
581+
),
582+
locations: [ { line: 5, column: 15 }, { line: 10, column: 15 } ] }
583+
]);
584+
585+
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
586+
{
587+
someBox {
588+
... on IntBox {
589+
box: stringBox {
590+
scalar
591+
}
592+
}
593+
... on StringBox {
594+
box: listStringBox {
595+
scalar
596+
}
597+
}
598+
}
599+
}
600+
`, [
601+
{ message: fieldsConflictMessage(
602+
'box',
603+
'they return conflicting types StringBox and [StringBox]'
604+
),
605+
locations: [ { line: 5, column: 15 }, { line: 10, column: 15 } ] }
606+
]);
607+
});
608+
609+
it('disallows differing subfields', () => {
610+
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
611+
{
612+
someBox {
613+
... on IntBox {
614+
box: stringBox {
615+
val: scalar
616+
val: unrelatedField
617+
}
618+
}
619+
... on StringBox {
620+
box: stringBox {
621+
val: scalar
622+
}
623+
}
624+
}
625+
}
626+
`, [
627+
{ message: fieldsConflictMessage(
628+
'val',
629+
'scalar and unrelatedField are different fields'
630+
),
631+
locations: [ { line: 6, column: 17 }, { line: 7, column: 17 } ] }
632+
]);
633+
});
634+
635+
it('disallows differing deep return types despite no overlap', () => {
636+
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
637+
{
638+
someBox {
639+
... on IntBox {
640+
box: stringBox {
641+
scalar
642+
}
643+
}
644+
... on StringBox {
645+
box: intBox {
646+
scalar
647+
}
648+
}
649+
}
650+
}
651+
`, [
652+
{ message: fieldsConflictMessage(
653+
'box',
654+
[ [ 'scalar', 'they return conflicting types String and Int' ] ]
655+
),
656+
locations: [
657+
{ line: 5, column: 15 },
658+
{ line: 6, column: 17 },
659+
{ line: 10, column: 15 },
660+
{ line: 11, column: 17 } ] }
661+
]);
662+
});
663+
664+
it('allows non-conflicting overlaping types', () => {
665+
expectPassesRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
666+
{
667+
someBox {
668+
... on IntBox {
669+
scalar: unrelatedField
670+
}
671+
... on StringBox {
495672
scalar
496673
}
497674
}

0 commit comments

Comments
 (0)