Skip to content

Commit 88ab1e7

Browse files
committed
[validation] Allow safe divergence
As pointed out in #53, our validation is more conservative than it needs to be in some cases. Particularly, two fields which could not overlap are still treated as a potential conflict. This change loosens this rule, allowing fields which can never both apply in a frame of execution to diverge.
1 parent 228215a commit 88ab1e7

File tree

2 files changed

+113
-21
lines changed

2 files changed

+113
-21
lines changed

src/validation/__tests__/OverlappingFieldsCanBeMerged.js

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import {
2222
GraphQLSchema,
2323
GraphQLObjectType,
24-
GraphQLUnionType,
24+
GraphQLInterfaceType,
2525
GraphQLList,
2626
GraphQLNonNull,
2727
GraphQLInt,
@@ -101,6 +101,21 @@ describe('Validate: Overlapping fields can be merged', () => {
101101
]);
102102
});
103103

104+
it('Same aliases allowed on non-overlapping fields', () => {
105+
// This is valid since no object can be both a "Dog" and a "Cat", thus
106+
// these fields can never overlap.
107+
expectPassesRule(OverlappingFieldsCanBeMerged, `
108+
fragment sameAliasesWithDifferentFieldTargets on Pet {
109+
... on Dog {
110+
name
111+
}
112+
... on Cat {
113+
name: nickname
114+
}
115+
}
116+
`);
117+
});
118+
104119
it('Alias masking direct field access', () => {
105120
expectFailsRule(OverlappingFieldsCanBeMerged, `
106121
fragment aliasMaskingDirectFieldAccess on Dog {
@@ -131,6 +146,21 @@ describe('Validate: Overlapping fields can be merged', () => {
131146
]);
132147
});
133148

149+
it('allows different args where no conflict is possible', () => {
150+
// This is valid since no object can be both a "Dog" and a "Cat", thus
151+
// these fields can never overlap.
152+
expectPassesRule(OverlappingFieldsCanBeMerged, `
153+
fragment conflictingArgs on Pet {
154+
... on Dog {
155+
name(surname: true)
156+
}
157+
... on Cat {
158+
name
159+
}
160+
}
161+
`);
162+
});
163+
134164
it('conflicting directives', () => {
135165
expectFailsRule(OverlappingFieldsCanBeMerged, `
136166
fragment conflictingDirectiveArgs on Dog {
@@ -353,39 +383,67 @@ describe('Validate: Overlapping fields can be merged', () => {
353383

354384
describe('return types must be unambiguous', () => {
355385

386+
var SomeBox = new GraphQLInterfaceType({
387+
name: 'SomeBox',
388+
resolveType: () => StringBox,
389+
fields: {
390+
unrelatedField: { type: GraphQLString }
391+
}
392+
});
393+
394+
/* eslint-disable no-unused-vars */
356395
var StringBox = new GraphQLObjectType({
357396
name: 'StringBox',
397+
interfaces: [ SomeBox ],
358398
fields: {
359-
scalar: { type: GraphQLString }
399+
scalar: { type: GraphQLString },
400+
unrelatedField: { type: GraphQLString },
360401
}
361402
});
362403

363404
var IntBox = new GraphQLObjectType({
364405
name: 'IntBox',
406+
interfaces: [ SomeBox ],
365407
fields: {
366-
scalar: { type: GraphQLInt }
408+
scalar: { type: GraphQLInt },
409+
unrelatedField: { type: GraphQLString },
367410
}
368411
});
369412

370-
var NonNullStringBox1 = new GraphQLObjectType({
413+
var NonNullStringBox1 = new GraphQLInterfaceType({
371414
name: 'NonNullStringBox1',
415+
resolveType: () => StringBox,
372416
fields: {
373417
scalar: { type: new GraphQLNonNull(GraphQLString) }
374418
}
375419
});
376420

377-
var NonNullStringBox2 = new GraphQLObjectType({
421+
var NonNullStringBox1Impl = new GraphQLObjectType({
422+
name: 'NonNullStringBox1Impl',
423+
interfaces: [ SomeBox, NonNullStringBox1 ],
424+
fields: {
425+
scalar: { type: new GraphQLNonNull(GraphQLString) },
426+
unrelatedField: { type: GraphQLString },
427+
}
428+
});
429+
430+
var NonNullStringBox2 = new GraphQLInterfaceType({
378431
name: 'NonNullStringBox2',
432+
resolveType: () => StringBox,
379433
fields: {
380434
scalar: { type: new GraphQLNonNull(GraphQLString) }
381435
}
382436
});
383437

384-
var BoxUnion = new GraphQLUnionType({
385-
name: 'BoxUnion',
386-
resolveType: () => StringBox,
387-
types: [ StringBox, IntBox, NonNullStringBox1, NonNullStringBox2 ]
438+
var NonNullStringBox2Impl = new GraphQLObjectType({
439+
name: 'NonNullStringBox2Impl',
440+
interfaces: [ SomeBox, NonNullStringBox2 ],
441+
fields: {
442+
scalar: { type: new GraphQLNonNull(GraphQLString) },
443+
unrelatedField: { type: GraphQLString },
444+
}
388445
});
446+
/* eslint-enable no-unused-vars */
389447

390448
var Connection = new GraphQLObjectType({
391449
name: 'Connection',
@@ -413,37 +471,57 @@ describe('Validate: Overlapping fields can be merged', () => {
413471
query: new GraphQLObjectType({
414472
name: 'QueryRoot',
415473
fields: () => ({
416-
boxUnion: { type: BoxUnion },
474+
someBox: { type: SomeBox },
417475
connection: { type: Connection }
418476
})
419477
})
420478
});
421479

422-
it('conflicting scalar return types', () => {
480+
it('conflicting return types which potentially overlap', () => {
481+
// This is invalid since an object could potentially be both the Object
482+
// type IntBox and the interface type NonNullStringBox1. While that
483+
// condition does not exist in the current schema, the schema could
484+
// expand in the future to allow this. Thus it is invalid.
423485
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
424486
{
425-
boxUnion {
487+
someBox {
426488
...on IntBox {
427489
scalar
428490
}
429-
...on StringBox {
491+
...on NonNullStringBox1 {
430492
scalar
431493
}
432494
}
433495
}
434496
`, [
435497
{ message: fieldsConflictMessage(
436498
'scalar',
437-
'they return differing types Int and String'
499+
'they return differing types Int and String!'
438500
),
439501
locations: [ { line: 5, column: 15 }, { line: 8, column: 15 } ] }
440502
]);
441503
});
442504

505+
it('allows differing return types which cannot overlap', () => {
506+
// This is valid since an object cannot be both an IntBox and a StringBox.
507+
expectPassesRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
508+
{
509+
someBox {
510+
...on IntBox {
511+
scalar
512+
}
513+
...on StringBox {
514+
scalar
515+
}
516+
}
517+
}
518+
`);
519+
});
520+
443521
it('same wrapped scalar return types', () => {
444522
expectPassesRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
445523
{
446-
boxUnion {
524+
someBox {
447525
...on NonNullStringBox1 {
448526
scalar
449527
}
@@ -505,7 +583,7 @@ describe('Validate: Overlapping fields can be merged', () => {
505583
it('ignores unknown types', () => {
506584
expectPassesRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
507585
{
508-
boxUnion {
586+
someBox {
509587
...on UnknownType {
510588
scalar
511589
}

src/validation/rules/OverlappingFieldsCanBeMerged.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import type {
2828
GraphQLType,
2929
GraphQLNamedType,
30+
GraphQLCompositeType,
3031
GraphQLFieldDefinition
3132
} from '../../type/definition';
3233
import { typeFromAST } from '../../utilities/typeFromAST';
@@ -75,16 +76,29 @@ export function OverlappingFieldsCanBeMerged(context: ValidationContext): any {
7576

7677
function findConflict(
7778
responseName: string,
78-
pair1: [Field, GraphQLFieldDefinition],
79-
pair2: [Field, GraphQLFieldDefinition]
79+
pair1: [ GraphQLCompositeType, Field, GraphQLFieldDefinition ],
80+
pair2: [ GraphQLCompositeType, Field, GraphQLFieldDefinition ]
8081
): ?Conflict {
81-
var [ ast1, def1 ] = pair1;
82-
var [ ast2, def2 ] = pair2;
82+
var [ parentType1, ast1, def1 ] = pair1;
83+
var [ parentType2, ast2, def2 ] = pair2;
8384
if (ast1 === ast2 || comparedSet.has(ast1, ast2)) {
8485
return;
8586
}
8687
comparedSet.add(ast1, ast2);
8788

89+
// If the statically known parent types could not possibly apply at the same
90+
// time, then it is safe to permit them to diverge as they will not present
91+
// any ambiguity by differing.
92+
// It is known that two parent types could never overlap if they are
93+
// different Object types. Interface or Union types might overlap - if not
94+
// in the current state of the schema, then perhaps in some future version,
95+
// thus may not safely diverge.
96+
if (parentType1 !== parentType2 &&
97+
parentType1 instanceof GraphQLObjectType &&
98+
parentType2 instanceof GraphQLObjectType) {
99+
return;
100+
}
101+
88102
var name1 = ast1.name.value;
89103
var name2 = ast2.name.value;
90104
if (name1 !== name2) {
@@ -267,7 +281,7 @@ function collectFieldASTsAndDefs(
267281
if (!_astAndDefs[responseName]) {
268282
_astAndDefs[responseName] = [];
269283
}
270-
_astAndDefs[responseName].push([ selection, fieldDef ]);
284+
_astAndDefs[responseName].push([ parentType, selection, fieldDef ]);
271285
break;
272286
case INLINE_FRAGMENT:
273287
var typeCondition = selection.typeCondition;

0 commit comments

Comments
 (0)