Skip to content

Commit a6424ab

Browse files
committed
[Validation] Allow safe divergence
Related GraphQL commit https://github.com/graphql/graphql-js/tree/d71e063fdd1d4c376b4948147e54438b6f1e13de
1 parent 062a6af commit a6424ab

File tree

2 files changed

+113
-23
lines changed

2 files changed

+113
-23
lines changed

graphql/core/validation/rules/overlapping_fields_can_be_merged.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,27 @@ def find_conflicts(self, field_map):
3636

3737
return conflicts
3838

39-
def find_conflict(self, response_name, pair1, pair2):
40-
ast1, def1 = pair1
41-
ast2, def2 = pair2
39+
def find_conflict(self, response_name, field1, field2):
40+
parent_type1, ast1, def1 = field1
41+
parent_type2, ast2, def2 = field2
4242

43-
if ast1 is ast2 or self.compared_set.has(ast1, ast2):
43+
# Not a pair
44+
if ast1 is ast2:
45+
return
46+
47+
# If the statically known parent types could not possibly apply at the same
48+
# time, then it is safe to permit them to diverge as they will not present
49+
# any ambiguity by differing.
50+
# It is known that two parent types could never overlap if they are
51+
# different Object types. Interface or Union types might overlap - if not
52+
# in the current state of the schema, then perhaps in some future version,
53+
# thus may not safely diverge.
54+
if parent_type1 != parent_type2 and \
55+
isinstance(parent_type1, GraphQLObjectType) and \
56+
isinstance(parent_type2, GraphQLObjectType):
57+
return
58+
59+
if self.compared_set.has(ast1, ast2):
4460
return
4561

4662
self.compared_set.add(ast1, ast2)
@@ -192,7 +208,7 @@ def collect_field_asts_and_defs(self, parent_type, selection_set, visited_fragme
192208
field_def = parent_type.get_fields().get(field_name)
193209

194210
response_name = selection.alias.value if selection.alias else field_name
195-
ast_and_defs[response_name].append((selection, field_def))
211+
ast_and_defs[response_name].append((parent_type, selection, field_def))
196212

197213
elif isinstance(selection, ast.InlineFragment):
198214
type_condition = selection.type_condition

tests/core_validation/test_overlapping_fields_can_be_merged.py

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from graphql.core.language.location import SourceLocation as L
2-
from graphql.core.type.definition import GraphQLObjectType, GraphQLArgument, GraphQLNonNull, GraphQLUnionType, \
2+
from graphql.core.type.definition import GraphQLObjectType, GraphQLArgument, GraphQLNonNull, GraphQLInterfaceType, \
33
GraphQLList, GraphQLField
44
from graphql.core.type.scalars import GraphQLString, GraphQLInt, GraphQLID
55
from graphql.core.type.schema import GraphQLSchema
@@ -79,6 +79,19 @@ def test_same_aliases_with_different_field_targets():
7979
], sort_list=False)
8080

8181

82+
def test_same_aliases_allowed_on_nonoverlapping_fields():
83+
expect_passes_rule(OverlappingFieldsCanBeMerged, '''
84+
fragment sameAliasesWithDifferentFieldTargets on Pet {
85+
... on Dog {
86+
name
87+
}
88+
... on Cat {
89+
name: nickname
90+
}
91+
}
92+
''')
93+
94+
8295
def test_alias_masking_direct_field_access():
8396
expect_fails_rule(OverlappingFieldsCanBeMerged, '''
8497
fragment aliasMaskingDirectFieldAccess on Dog {
@@ -90,6 +103,28 @@ def test_alias_masking_direct_field_access():
90103
], sort_list=False)
91104

92105

106+
def test_diferent_args_second_adds_an_argument():
107+
expect_fails_rule(OverlappingFieldsCanBeMerged, '''
108+
fragment conflictingArgs on Dog {
109+
doesKnowCommand
110+
doesKnowCommand(dogCommand: HEEL)
111+
}
112+
''', [
113+
fields_conflict('doesKnowCommand', 'they have differing arguments', L(3, 9), L(4, 9))
114+
], sort_list=False)
115+
116+
117+
def test_diferent_args_second_missing_an_argument():
118+
expect_fails_rule(OverlappingFieldsCanBeMerged, '''
119+
fragment conflictingArgs on Dog {
120+
doesKnowCommand(dogCommand: SIT)
121+
doesKnowCommand
122+
}
123+
''', [
124+
fields_conflict('doesKnowCommand', 'they have differing arguments', L(3, 9), L(4, 9))
125+
], sort_list=False)
126+
127+
93128
def test_conflicting_args():
94129
expect_fails_rule(OverlappingFieldsCanBeMerged, '''
95130
fragment conflictingArgs on Dog {
@@ -101,6 +136,18 @@ def test_conflicting_args():
101136
], sort_list=False)
102137

103138

139+
def test_allows_different_args_where_no_conflict_is_possible():
140+
expect_passes_rule(OverlappingFieldsCanBeMerged, '''
141+
fragment conflictingArgs on Pet {
142+
... on Dog {
143+
name(surname: true)
144+
}
145+
... on Cat {
146+
name
147+
}
148+
}
149+
''')
150+
104151
def test_conflicting_directives():
105152
expect_fails_rule(OverlappingFieldsCanBeMerged, '''
106153
fragment conflictingDirectiveArgs on Dog {
@@ -276,25 +323,37 @@ def test_reports_deep_conflict_to_nearest_common_ancestor():
276323
], sort_list=False)
277324

278325

326+
SomeBox = GraphQLInterfaceType('SomeBox', {
327+
'unrelatedField': GraphQLField(GraphQLString)
328+
}, resolve_type=lambda *_: StringBox)
329+
279330
StringBox = GraphQLObjectType('StringBox', {
280-
'scalar': GraphQLField(GraphQLString)
281-
})
331+
'scalar': GraphQLField(GraphQLString),
332+
'unrelatedField': GraphQLField(GraphQLString)
333+
}, interfaces=[SomeBox])
282334

283335
IntBox = GraphQLObjectType('IntBox', {
284-
'scalar': GraphQLField(GraphQLInt)
285-
})
336+
'scalar': GraphQLField(GraphQLInt),
337+
'unrelatedField': GraphQLField(GraphQLString)
338+
}, interfaces=[SomeBox])
286339

287-
NonNullStringBox1 = GraphQLObjectType('NonNullStringBox1', {
340+
NonNullStringBox1 = GraphQLInterfaceType('NonNullStringBox1', {
288341
'scalar': GraphQLField(GraphQLNonNull(GraphQLString))
289-
})
342+
}, resolve_type=lambda *_: StringBox)
343+
344+
NonNullStringBox1Impl = GraphQLObjectType('NonNullStringBox1Impl', {
345+
'scalar': GraphQLField(GraphQLNonNull(GraphQLString)),
346+
'unrelatedField': GraphQLField(GraphQLString)
347+
}, interfaces=[ SomeBox, NonNullStringBox1 ])
290348

291-
NonNullStringBox2 = GraphQLObjectType('NonNullStringBox2', {
349+
NonNullStringBox2 = GraphQLInterfaceType('NonNullStringBox2', {
292350
'scalar': GraphQLField(GraphQLNonNull(GraphQLString))
293-
})
351+
}, resolve_type=lambda *_: StringBox)
294352

295-
BoxUnion = GraphQLUnionType('BoxUnion', [
296-
StringBox, IntBox, NonNullStringBox1, NonNullStringBox2
297-
], resolve_type=lambda *_: StringBox)
353+
NonNullStringBox2Impl = GraphQLObjectType('NonNullStringBox2Impl', {
354+
'scalar': GraphQLField(GraphQLNonNull(GraphQLString)),
355+
'unrelatedField': GraphQLField(GraphQLString)
356+
}, interfaces=[ SomeBox, NonNullStringBox2 ])
298357

299358
Connection = GraphQLObjectType('Connection', {
300359
'edges': GraphQLField(GraphQLList(GraphQLObjectType('Edge', {
@@ -306,33 +365,48 @@ def test_reports_deep_conflict_to_nearest_common_ancestor():
306365
})
307366

308367
schema = GraphQLSchema(GraphQLObjectType('QueryRoot', {
309-
'boxUnion': GraphQLField(BoxUnion),
368+
'someBox': GraphQLField(SomeBox),
310369
'connection': GraphQLField(Connection)
311370
}))
312371

313372

314-
def test_conflicting_scalar_return_types():
373+
def test_conflicting_return_types_which_potentially_overlap():
315374
expect_fails_rule_with_schema(schema, OverlappingFieldsCanBeMerged, '''
316375
{
317-
boxUnion {
376+
someBox {
318377
...on IntBox {
319378
scalar
320379
}
321-
...on StringBox {
380+
...on NonNullStringBox1 {
322381
scalar
323382
}
324383
}
325384
}
326385
327386
''', [
328-
fields_conflict('scalar', 'they return differing types Int and String', L(5, 17), L(8, 17))
387+
fields_conflict('scalar', 'they return differing types Int and String!', L(5, 17), L(8, 17))
329388
], sort_list=False)
330389

331390

391+
def test_allows_differing_return_types_which_cannot_overlap():
392+
expect_passes_rule_with_schema(schema, OverlappingFieldsCanBeMerged, '''
393+
{
394+
someBox {
395+
...on IntBox {
396+
scalar
397+
}
398+
...on StringBox {
399+
scalar
400+
}
401+
}
402+
}
403+
''')
404+
405+
332406
def test_same_wrapped_scalar_return_types():
333407
expect_passes_rule_with_schema(schema, OverlappingFieldsCanBeMerged, '''
334408
{
335-
boxUnion {
409+
someBox {
336410
...on NonNullStringBox1 {
337411
scalar
338412
}

0 commit comments

Comments
 (0)