1
1
# Copyright 2019-present Kensho Technologies, LLC.
2
2
from typing import Any , Dict , List , Union
3
3
4
+ from graphql import GraphQLSchema
4
5
from graphql .language .ast import (
5
6
DocumentNode ,
6
7
FieldNode ,
8
+ InterfaceTypeDefinitionNode ,
7
9
NamedTypeNode ,
10
+ ObjectTypeDefinitionNode ,
8
11
OperationDefinitionNode ,
9
12
OperationType ,
10
13
SelectionSetNode ,
11
14
)
12
15
from graphql .language .visitor import Visitor , VisitorAction , visit
13
16
from graphql .validation import validate
14
17
18
+ from ..ast_manipulation import get_ast_with_non_null_and_list_stripped
15
19
from ..exceptions import GraphQLValidationError
16
20
from .rename_schema import RenamedSchemaDescriptor
17
21
from .utils import RenameQueryNodeTypesT , get_copy_of_node_with_new_name
20
24
def rename_query (
21
25
ast : DocumentNode , renamed_schema_descriptor : RenamedSchemaDescriptor
22
26
) -> DocumentNode :
23
- """Translate names of types using reverse_name_map of the input RenamedSchemaDescriptor.
27
+ """Translate names of types/fields using reverse_name_map of the input RenamedSchemaDescriptor.
24
28
25
- The direction in which types and fields are renamed is opposite of the process that
26
- produced the renamed schema descriptor. If a type X was renamed to Y in the schema, then
27
- any occurrences of type Y in the input query ast will be renamed to X.
29
+ The direction in which types/ fields are renamed is opposite of the process that
30
+ produced the renamed schema descriptor. If a type/field X was renamed to Y in the schema, then
31
+ any occurrences of Y in the input query AST will be renamed to X.
28
32
29
- All type names (including ones in type coercions), as well as root vertex fields (fields
30
- of the query type) will be renamed. No other field names will be renamed.
33
+ All type names (including ones in type coercions) and field names will be renamed.
31
34
32
35
Args:
33
36
ast: represents a query
34
37
renamed_schema_descriptor: namedtuple including the attribute reverse_name_map, which maps
35
- the new, renamed names of types to their original names. This
36
- function will revert these renamed types in the query ast back to
37
- their original names
38
+ the new, renamed names of types to their original names, and
39
+ reverse_field_name_map which has a similar role for fields. This
40
+ function will revert these renamed types/fields in the query AST
41
+ back to their original names
38
42
39
43
Returns:
40
44
New AST representing the renamed query
@@ -70,25 +74,43 @@ def rename_query(
70
74
'selection "{}"' .format (type (selection ).__name__ , selection )
71
75
)
72
76
73
- visitor = RenameQueryVisitor (renamed_schema_descriptor .reverse_name_map )
77
+ visitor = RenameQueryVisitor (
78
+ renamed_schema_descriptor .schema ,
79
+ renamed_schema_descriptor .reverse_name_map ,
80
+ renamed_schema_descriptor .reverse_field_name_map ,
81
+ )
74
82
renamed_ast = visit (ast , visitor )
75
83
76
84
return renamed_ast
77
85
78
86
79
87
class RenameQueryVisitor (Visitor ):
80
- def __init__ (self , type_renamings : Dict [str , str ]) -> None :
81
- """Create a visitor for renaming types and root vertex fields in a query AST.
88
+ def __init__ (
89
+ self ,
90
+ schema : GraphQLSchema ,
91
+ type_renamings : Dict [str , str ],
92
+ field_renamings : Dict [str , Dict [str , str ]],
93
+ ) -> None :
94
+ """Create a visitor for renaming types and fields in a query AST.
82
95
83
96
Args:
97
+ schema: The renamed schema that the original query is written against
84
98
type_renamings: Maps type or root field names to the new value in the dict.
85
99
Any name not in the dict will be unchanged
100
+ field_renamings: Maps type names to a dict mapping the field names to the new value.
101
+ Any names not in the dicts will be unchanged
86
102
"""
103
+ self .schema = schema
87
104
self .type_renamings = type_renamings
105
+ self .field_renamings = field_renamings
88
106
self .selection_set_level = 0
107
+ # Acts like a stack that records the types of the current scopes. The last item is the top
108
+ # of the stack. Each entry is the name of a type in the new schema, i.e. not the name of
109
+ # the type in the original schema if it was renamed.
110
+ self .current_type_name : List [str ] = []
89
111
90
112
def _rename_name (self , node : RenameQueryNodeTypesT ) -> RenameQueryNodeTypesT :
91
- """Change the name of the input node if necessary, according to type_renamings .
113
+ """Change the name of the input node if necessary, according to the renamings .
92
114
93
115
Args:
94
116
node: represents a field in an AST, containing a .name attribute. It is not modified
@@ -98,7 +120,21 @@ def _rename_name(self, node: RenameQueryNodeTypesT) -> RenameQueryNodeTypesT:
98
120
the name was not changed, the returned object is the exact same object as the input
99
121
"""
100
122
name_string = node .name .value
101
- new_name_string = self .type_renamings .get (name_string , name_string ) # Default use original
123
+ if isinstance (node , FieldNode ) and self .selection_set_level > 1 :
124
+ field_name = node .name .value
125
+ # The top item in the stack is the type of the field, and the one immediately after that
126
+ # is the type that contains this field in the schema
127
+ current_type_name = self .current_type_name [- 2 ]
128
+ current_type_name_in_original_schema = self .type_renamings .get (
129
+ current_type_name , current_type_name
130
+ )
131
+ new_name_string = self .field_renamings .get (
132
+ current_type_name_in_original_schema , {}
133
+ ).get (field_name , field_name )
134
+ else :
135
+ new_name_string = self .type_renamings .get (
136
+ name_string , name_string
137
+ ) # Default use original
102
138
if new_name_string == name_string :
103
139
return node
104
140
else :
@@ -110,6 +146,7 @@ def enter_named_type(
110
146
) -> Union [NamedTypeNode , VisitorAction ]:
111
147
"""Rename name of node."""
112
148
# NamedType nodes describe types in the schema, appearing in InlineFragments
149
+ self .current_type_name .append (node .name .value )
113
150
renamed_node = self ._rename_name (node )
114
151
if renamed_node is node : # Name unchanged, continue traversal
115
152
return None
@@ -131,18 +168,56 @@ def leave_selection_set(
131
168
def enter_field (
132
169
self , node : FieldNode , key : Any , parent : Any , path : List [Any ], ancestors : List [Any ]
133
170
) -> Union [FieldNode , VisitorAction ]:
134
- """Rename root vertex fields."""
171
+ """Rename fields."""
135
172
# For a Field to be a root vertex field, it needs to be the first level of
136
173
# selections (fields in more nested selections are normal fields that should not be
137
174
# modified)
138
175
# As FragmentDefinition is not allowed, the parent of the selection must be a query
139
176
# As a query may not start with an inline fragment, all first level selections are
140
177
# fields
141
178
if self .selection_set_level == 1 :
142
- renamed_node = self ._rename_name (node )
143
- if renamed_node is node : # Name unchanged, continue traversal
144
- return None
145
- else : # Name changed, return new node, `visit` will make shallow copies along path
146
- return renamed_node
179
+ self .current_type_name .append (node .name .value )
180
+ else :
181
+ # Entered a regular field and we want to find its type
182
+ current_type_name = self .current_type_name [- 1 ]
183
+ current_type = self .schema .get_type (current_type_name )
184
+ if current_type is None :
185
+ raise AssertionError (
186
+ f"Current type is { current_type_name } which doesn't exist in the schema. This "
187
+ f"is a bug."
188
+ )
189
+ if current_type .ast_node is None :
190
+ raise AssertionError (
191
+ f"Current type { current_type_name } should have non-null field ast_node, which "
192
+ f"contains information such as the current type's fields. However, ast_node "
193
+ f"was None. This is a bug."
194
+ )
195
+ if not isinstance (
196
+ current_type .ast_node , (ObjectTypeDefinitionNode , InterfaceTypeDefinitionNode )
197
+ ):
198
+ raise AssertionError (
199
+ f"Current type { current_type_name } 's ast_node field should be an "
200
+ f"ObjectTypeDefinitionNode. However, the actual type was "
201
+ f"{ type (current_type .ast_node )} . This is a bug."
202
+ )
203
+ current_type_fields = current_type .ast_node .fields
204
+ for field_node in current_type_fields :
205
+ # Unfortunately, fields is a list instead of some other datastructure so we actually
206
+ # have to loop through them all.
207
+ if field_node .name .value == node .name .value :
208
+ field_type_name = get_ast_with_non_null_and_list_stripped (
209
+ field_node .type
210
+ ).name .value
211
+ self .current_type_name .append (field_type_name )
212
+ break
213
+ renamed_node = self ._rename_name (node )
214
+ if renamed_node is node : # Name unchanged, continue traversal
215
+ return None
216
+ else : # Name changed, return new node, `visit` will make shallow copies along path
217
+ return renamed_node
147
218
148
- return None
219
+ def leave_field (
220
+ self , node : FieldNode , key : Any , parent : Any , path : List [Any ], ancestors : List [Any ]
221
+ ) -> None :
222
+ """Record that we left a field."""
223
+ self .current_type_name .pop ()
0 commit comments