Skip to content

Commit 7154b96

Browse files
committed
Extract check for unique field names into separate rule
Replicates graphql/graphql-js@6be63f0
1 parent aa0d616 commit 7154b96

File tree

6 files changed

+446
-23
lines changed

6 files changed

+446
-23
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ a query language for APIs created by Facebook.
1313
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
1414

1515
The current version 1.0.1 of GraphQL-core-next is up-to-date with GraphQL.js version
16-
14.0.2. All parts of the API are covered by an extensive test suite of currently 1679
16+
14.0.2. All parts of the API are covered by an extensive test suite of currently 1690
1717
unit tests.
1818

1919

graphql/utilities/extend_schema.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,7 @@ def extend_input_field_map(type_: GraphQLInputObjectType) -> GraphQLInputFieldMa
230230
if extensions:
231231
for extension in extensions:
232232
for field in extension.fields:
233-
field_name = field.name.value
234-
if field_name in old_field_map:
235-
raise GraphQLError(
236-
f"Field '{type_.name}.{field_name}' already"
237-
" exists in the schema. It cannot also be defined"
238-
" in this type extension.",
239-
[field],
240-
)
241-
new_field_map[field_name] = ast_builder.build_input_field(field)
233+
new_field_map[field.name.value] = build_input_field(field)
242234

243235
return new_field_map
244236

@@ -444,15 +436,7 @@ def extend_field_map(
444436
# If there are any extensions to the fields, apply those here.
445437
for extension in type_extensions_map[type_.name]:
446438
for field in extension.fields:
447-
field_name = field.name.value
448-
if field_name in old_field_map:
449-
raise GraphQLError(
450-
f"Field '{type_.name}.{field_name}'"
451-
" already exists in the schema."
452-
" It cannot also be defined in this type extension.",
453-
[field],
454-
)
455-
new_field_map[field_name] = build_field(field)
439+
new_field_map[field.name.value] = build_field(field)
456440

457441
return new_field_map
458442

@@ -475,6 +459,7 @@ def resolve_type(type_name: str) -> GraphQLNamedType:
475459
type_definition_map, assume_valid=assume_valid, resolve_type=resolve_type
476460
)
477461
build_field = ast_builder.build_field
462+
build_input_field = ast_builder.build_input_field
478463
build_type = ast_builder.build_type
479464

480465
extend_type_cache: Dict[str, GraphQLNamedType] = {}
@@ -492,7 +477,7 @@ def resolve_type(type_name: str) -> GraphQLNamedType:
492477
# Note: While this could make early assertions to get the correctly typed
493478
# values, that would throw immediately while type system validation with
494479
# `validate_schema()` will produce more actionable results.
495-
operation_types[operation] = ast_builder.build_type(operation_type.type)
480+
operation_types[operation] = build_type(operation_type.type)
496481

497482
# Then, incorporate schema definition and all schema extensions.
498483
for schema_extension in schema_extensions:
@@ -503,7 +488,7 @@ def resolve_type(type_name: str) -> GraphQLNamedType:
503488
# typed values, that would throw immediately while type system
504489
# validation with `validate_schema()` will produce more actionable
505490
# results.
506-
operation_types[operation] = ast_builder.build_type(operation_type.type)
491+
operation_types[operation] = build_type(operation_type.type)
507492

508493
schema_extension_ast_nodes = (
509494
schema.extension_ast_nodes or cast(Tuple[SchemaExtensionNode], ())
@@ -513,7 +498,7 @@ def resolve_type(type_name: str) -> GraphQLNamedType:
513498
# any type not directly referenced by a value will get created.
514499
types = list(map(extend_named_type, schema.type_map.values()))
515500
# do the same with new types
516-
types.extend(map(ast_builder.build_type, type_definition_map.values()))
501+
types.extend(map(build_type, type_definition_map.values()))
517502

518503
# Then produce and return a Schema with these types.
519504
return GraphQLSchema( # type: ignore
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from typing import Any, Dict
2+
3+
from ...error import GraphQLError
4+
from ...language import NameNode, ObjectTypeDefinitionNode
5+
from ...type import is_object_type, is_interface_type, is_input_object_type
6+
from . import SDLValidationContext, SDLValidationRule
7+
8+
__all__ = [
9+
"UniqueFieldDefinitionNamesRule",
10+
"duplicate_field_definition_name_message",
11+
"existed_field_definition_name_message",
12+
]
13+
14+
15+
def duplicate_field_definition_name_message(type_name: str, field_name: str) -> str:
16+
return f"Field '{type_name}.{field_name}' can only be defined once."
17+
18+
19+
def existed_field_definition_name_message(type_name: str, field_name: str) -> str:
20+
return (
21+
f"Field '{type_name}.{field_name}' already exists in the schema."
22+
" It cannot also be defined in this type extension."
23+
)
24+
25+
26+
class UniqueFieldDefinitionNamesRule(SDLValidationRule):
27+
"""Unique field definition names
28+
29+
A GraphQL complex type is only valid if all its fields are uniquely named.
30+
"""
31+
32+
def __init__(self, context: SDLValidationContext) -> None:
33+
super().__init__(context)
34+
schema = context.schema
35+
self.existing_type_map = schema.type_map if schema else {}
36+
self.known_field_names: Dict[str, Dict[str, NameNode]] = {}
37+
38+
def check_field_uniqueness(self, node: ObjectTypeDefinitionNode, *_args):
39+
type_name = node.name.value
40+
41+
known_field_names = self.known_field_names
42+
if type_name not in known_field_names:
43+
known_field_names[type_name] = {}
44+
45+
if node.fields:
46+
field_names = known_field_names[type_name]
47+
existing_type_map = self.existing_type_map
48+
49+
for field_def in node.fields:
50+
field_name = field_def.name.value
51+
52+
if has_field(existing_type_map.get(type_name), field_name):
53+
self.report_error(
54+
GraphQLError(
55+
existed_field_definition_name_message(
56+
type_name, field_name
57+
),
58+
[field_def.name],
59+
)
60+
)
61+
elif field_name in field_names:
62+
self.report_error(
63+
GraphQLError(
64+
duplicate_field_definition_name_message(
65+
type_name, field_name
66+
),
67+
[field_names[field_name], field_def.name],
68+
)
69+
)
70+
else:
71+
field_names[field_name] = field_def.name
72+
73+
return self.SKIP
74+
75+
enter_input_object_type_definition = check_field_uniqueness
76+
enter_input_object_type_extension = check_field_uniqueness
77+
enter_interface_type_definition = check_field_uniqueness
78+
enter_interface_type_extension = check_field_uniqueness
79+
enter_object_type_definition = check_field_uniqueness
80+
enter_object_type_extension = check_field_uniqueness
81+
82+
83+
def has_field(type_: Any, field_name: str) -> bool:
84+
if is_object_type(type_) or is_interface_type(type_) or is_input_object_type(type_):
85+
return field_name in type_.fields
86+
return False

graphql/validation/specified_rules.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from .rules.lone_schema_definition import LoneSchemaDefinitionRule
8585
from .rules.unique_operation_types import UniqueOperationTypesRule
8686
from .rules.unique_type_names import UniqueTypeNamesRule
87+
from .rules.unique_field_definition_names import UniqueFieldDefinitionNamesRule
8788
from .rules.unique_directive_names import UniqueDirectiveNamesRule
8889
from .rules.known_argument_names import KnownArgumentNamesOnDirectivesRule
8990
from .rules.provided_required_arguments import ProvidedRequiredArgumentsOnDirectivesRule
@@ -129,6 +130,7 @@
129130
LoneSchemaDefinitionRule,
130131
UniqueOperationTypesRule,
131132
UniqueTypeNamesRule,
133+
UniqueFieldDefinitionNamesRule,
132134
UniqueDirectiveNamesRule,
133135
KnownTypeNamesRule,
134136
KnownDirectivesRule,

tests/utilities/test_extend_schema.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,6 @@ def extends_different_types_multiple_times():
774774
}
775775
776776
extend type Biz {
777-
newFieldA: Int
778777
newFieldB: Float
779778
}
780779

0 commit comments

Comments
 (0)