Skip to content

Commit 6d1e48b

Browse files
author
nambrosini
committed
✨ Add validation rule to limit field arguments
1 parent f35e460 commit 6d1e48b

File tree

6 files changed

+137
-22
lines changed

6 files changed

+137
-22
lines changed

schemadiff/changes/field.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from graphql import is_non_null_type
1+
from graphql import is_non_null_type, GraphQLField
22

33
from schemadiff.changes import Change, Criticality, is_safe_type_change
44

@@ -71,13 +71,14 @@ def path(self):
7171

7272

7373
class FieldArgumentAdded(Change):
74-
def __init__(self, parent, field_name, argument_name, arg_type):
74+
def __init__(self, parent, field_name: str, field: GraphQLField, argument_name, arg_type):
7575
self.criticality = Criticality.safe('Adding an optional argument is a safe change')\
7676
if not is_non_null_type(arg_type.type)\
7777
else Criticality.breaking("Adding a required argument to an existing field is a breaking "
7878
"change because it will break existing uses of this field")
7979
self.parent = parent
8080
self.field_name = field_name
81+
self.field = field
8182
self.argument_name = argument_name
8283
self.arg_type = arg_type
8384

schemadiff/changes/object.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
from graphql import GraphQLField, GraphQLObjectType
2+
13
from schemadiff.changes import Change, Criticality
24

35

46
class ObjectTypeFieldAdded(Change):
57

68
criticality = Criticality.safe()
79

8-
def __init__(self, parent, field_name):
10+
def __init__(self, parent: GraphQLObjectType, field_name, field: GraphQLField):
911
self.parent = parent
1012
self.field_name = field_name
13+
self.field = field
1114
self.description = parent.fields[field_name].description
1215

1316
@property

schemadiff/diff/field.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
class Field:
1212

13-
def __init__(self, type_, name, old_field, new_field):
14-
self.type_ = type_
13+
def __init__(self, parent, name, old_field, new_field):
14+
self.parent = parent
1515
self.field_name = name
1616
self.old_field = old_field
1717
self.new_field = new_field
@@ -22,31 +22,31 @@ def diff(self):
2222
changes = []
2323

2424
if self.old_field.description != self.new_field.description:
25-
changes.append(FieldDescriptionChanged(self.type_, self.field_name, self.old_field, self.new_field))
25+
changes.append(FieldDescriptionChanged(self.parent, self.field_name, self.old_field, self.new_field))
2626

2727
if self.old_field.deprecation_reason != self.new_field.deprecation_reason:
28-
changes.append(FieldDeprecationReasonChanged(self.type_, self.field_name, self.old_field, self.new_field))
28+
changes.append(FieldDeprecationReasonChanged(self.parent, self.field_name, self.old_field, self.new_field))
2929

3030
if str(self.old_field.type) != str(self.new_field.type):
31-
changes.append(FieldTypeChanged(self.type_, self.field_name, self.old_field, self.new_field))
31+
changes.append(FieldTypeChanged(self.parent, self.field_name, self.old_field, self.new_field))
3232

3333
added = self.new_args - self.old_args
3434
removed = self.old_args - self.new_args
3535

3636
changes.extend(
37-
FieldArgumentAdded(self.type_, self.field_name, arg_name, self.new_field.args[arg_name])
37+
FieldArgumentAdded(self.parent, self.field_name, self.new_field, arg_name, self.new_field.args[arg_name])
3838
for arg_name in added
3939
)
4040
changes.extend(
41-
FieldArgumentRemoved(self.type_, self.field_name, arg_name)
41+
FieldArgumentRemoved(self.parent, self.field_name, arg_name)
4242
for arg_name in removed
4343
)
4444

4545
common_arguments = self.common_arguments()
4646
for arg_name in common_arguments:
4747
old_arg = self.old_field.args[arg_name]
4848
new_arg = self.new_field.args[arg_name]
49-
changes += Argument(self.type_, self.field_name, arg_name, old_arg, new_arg).diff() or []
49+
changes += Argument(self.parent, self.field_name, arg_name, old_arg, new_arg).diff() or []
5050

5151
return changes
5252

schemadiff/diff/object_type.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ def __init__(self, old, new):
99
self.old = old
1010
self.new = new
1111

12-
self.old_fields = set(old.fields)
13-
self.new_fields = set(new.fields)
12+
self.old_field_names = set(old.fields)
13+
self.new_field_names = set(new.fields)
1414

1515
self.old_interfaces = set(old.interfaces)
1616
self.new_interfaces = set(new.interfaces)
@@ -19,9 +19,9 @@ def diff(self):
1919
changes = []
2020

2121
# Added and removed fields
22-
added = self.new_fields - self.old_fields
23-
removed = self.old_fields - self.new_fields
24-
changes.extend(ObjectTypeFieldAdded(self.new, field_name) for field_name in added)
22+
added = self.new_field_names - self.old_field_names
23+
removed = self.old_field_names - self.new_field_names
24+
changes.extend(ObjectTypeFieldAdded(self.new, field_name, self.new.fields[field_name]) for field_name in added)
2525
changes.extend(ObjectTypeFieldRemoved(self.new, field_name, self.old.fields[field_name])
2626
for field_name in removed)
2727

@@ -31,15 +31,15 @@ def diff(self):
3131
changes.extend(NewInterfaceImplemented(interface, self.new) for interface in added)
3232
changes.extend(DroppedInterfaceImplementation(interface, self.new) for interface in removed)
3333

34-
for field in self.common_fields():
35-
old_field = self.old.fields[field]
36-
new_field = self.new.fields[field]
37-
changes += Field(self.new, field, old_field, new_field).diff() or []
34+
for field_name in self.common_fields():
35+
old_field = self.old.fields[field_name]
36+
new_field = self.new.fields[field_name]
37+
changes += Field(self.new, field_name, old_field, new_field).diff() or []
3838

3939
return changes
4040

4141
def common_fields(self):
42-
return self.old_fields & self.new_fields
42+
return self.old_field_names & self.new_field_names
4343

4444
def added_interfaces(self):
4545
"""Compare interfaces equality by name. Internal diffs are solved later"""

schemadiff/validation_rules/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from graphql import GraphQLObjectType
55

66
from schemadiff.changes.enum import EnumValueAdded, EnumValueDescriptionChanged
7-
from schemadiff.changes.field import FieldDescriptionChanged
7+
from schemadiff.changes.field import FieldDescriptionChanged, FieldArgumentAdded
88
from schemadiff.changes.object import ObjectTypeFieldAdded
99
from schemadiff.changes.type import AddedType, TypeDescriptionChanged
1010

@@ -144,3 +144,28 @@ def is_valid(self) -> bool:
144144
@property
145145
def message(self):
146146
return f"Description for enum value `{self.change.name}` was removed (rule: `{self.name}`)"
147+
148+
149+
class MutationWithTooManyArguments(ValidationRule):
150+
"""Restrict adding fields with too many top level arguments"""
151+
152+
name = "field-has-too-many-arguments"
153+
limit = 10
154+
155+
def is_valid(self) -> bool:
156+
if not isinstance(self.change, (ObjectTypeFieldAdded, FieldArgumentAdded)):
157+
return True
158+
159+
if len(self.args) > self.limit:
160+
return False
161+
else:
162+
return True
163+
164+
@property
165+
def args(self):
166+
return self.change.field.args or {}
167+
168+
@property
169+
def message(self):
170+
return f"Field `{self.change.parent.name}.{self.change.field_name}` has too many arguments " \
171+
f"({len(self.args)}>{self.limit}). Rule: {self.name}"

tests/test_validation_rules.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,89 @@ def test_schema_added_field_no_desc():
230230
)
231231
assert diff[0].path == 'AddedType.other'
232232

233+
234+
def test_cant_create_mutation_with_more_than_10_arguments():
235+
schema_restrictions = ['field-has-too-many-arguments']
236+
237+
old_schema = schema("""
238+
schema {
239+
mutation: Mutation
240+
}
241+
242+
type Mutation {
243+
field: Int
244+
}
245+
""")
246+
247+
new_schema = schema("""
248+
schema {
249+
mutation: Mutation
250+
}
251+
252+
type Mutation {
253+
field: Int
254+
mutation_with_too_many_args(
255+
a1: Int, a2: Int, a3: Int, a4: Int, a5: Int, a6: Int, a7: Int, a8: Int, a9: Int, a10: Int, a11: Int
256+
): Int
257+
}
258+
""")
259+
260+
diff = Schema(old_schema, new_schema).diff()
261+
# Type Int was also added but its ignored because its a primitive.
262+
assert diff and len(diff) == 1
263+
error_msg = (
264+
'Field `Mutation.mutation_with_too_many_args` has too many arguments (11>10). '
265+
'Rule: field-has-too-many-arguments'
266+
)
267+
assert evaluate_rules(diff, schema_restrictions) == ValidationResult(
268+
ok=False,
269+
errors=[ValidationError(error_msg)]
270+
)
271+
assert diff[0].path == 'Mutation.mutation_with_too_many_args'
272+
273+
274+
def test_cant_add_arguments_to_mutation_if_exceeds_10_args():
275+
schema_restrictions = ['field-has-too-many-arguments']
276+
277+
old_schema = schema("""
278+
schema {
279+
mutation: Mutation
280+
}
281+
282+
type Mutation {
283+
field: Int
284+
mutation_with_too_many_args(
285+
a1: Int, a2: Int, a3: Int, a4: Int, a5: Int, a6: Int, a7: Int, a8: Int, a9: Int, a10: Int
286+
): Int
287+
}
288+
""")
289+
290+
new_schema = schema("""
291+
schema {
292+
mutation: Mutation
293+
}
294+
295+
type Mutation {
296+
field: Int
297+
mutation_with_too_many_args(
298+
a1: Int, a2: Int, a3: Int, a4: Int, a5: Int, a6: Int, a7: Int, a8: Int, a9: Int, a10: Int, a11: Int
299+
): Int
300+
}
301+
""")
302+
303+
diff = Schema(old_schema, new_schema).diff()
304+
# Type Int was also added but its ignored because its a primitive.
305+
assert diff and len(diff) == 1
306+
error_msg = (
307+
'Field `Mutation.mutation_with_too_many_args` has too many arguments (11>10). '
308+
'Rule: field-has-too-many-arguments'
309+
)
310+
assert evaluate_rules(diff, schema_restrictions) == ValidationResult(
311+
ok=False,
312+
errors=[ValidationError(error_msg)]
313+
)
314+
assert diff[0].path == 'Mutation.mutation_with_too_many_args'
315+
316+
317+
def test_mutation_input_fields_cant_share_prefix():
318+
pass

0 commit comments

Comments
 (0)