def _get_minimal_query_ast_from_macro_ast(macro_ast): """Get a query that should successfully compile to IR if the macro is valid.""" ast_without_macro_directives = remove_directives_from_ast(macro_ast, { directive.name for directive in DIRECTIVES_REQUIRED_IN_MACRO_EDGE_DEFINITION }) # We will add this output directive to make the ast a valid query output_directive = Directive(Name('output'), arguments=[ Argument(Name('out_name'), StringValue('dummy_output_name')) ]) # Shallow copy everything on the path to the first level selection list query_ast = copy(ast_without_macro_directives) root_level_selection = copy(get_only_selection_from_ast(query_ast, GraphQLInvalidMacroError)) first_level_selections = copy(root_level_selection.selection_set.selections) # Add an output to a new or existing __typename field existing_typename_field = None for idx, selection in enumerate(first_level_selections): if isinstance(selection, Field): if selection.name.value == '__typename': # We have a copy of the list, but the elements are references to objects # in macro_ast that we don't want to mutate. So the following copy is necessary. existing_typename_field = copy(selection) existing_typename_field.directives = copy(existing_typename_field.directives) existing_typename_field.directives.append(output_directive) first_level_selections[idx] = existing_typename_field if existing_typename_field is None: first_level_selections.insert(0, Field(Name('__typename'), directives=[output_directive])) # Propagate the changes back to the result_ast root_level_selection.selection_set = SelectionSet(first_level_selections) query_ast.selection_set = SelectionSet([root_level_selection]) return Document([query_ast])
def _get_output_directive(out_name): """Return a Directive representing an @output with the input out_name.""" return Directive( name=Name(value=OutputDirective.name), arguments=[ Argument( name=Name(value=u'out_name'), value=StringValue(value=out_name), ), ], )
def get_schema_with_macros(macro_registry): """Get a new GraphQLSchema with fields where macro edges can be used. Preconditions: 1. No macro in the registry has the same name as a field on the vertex where it applies. 2. Members of a union type do not have outgoing macros with the same name. An easy way to satisfy the preconditions is to create the macro_registry using create_macro_registry, and only update it with register_macro_edge, which does all the necessary validation. Postconditions: 1. Every GraphQLQuery that uses macros from this registry appropriately should successfully type-check against the schema generated from this function. 2. A GraphQLQuery that uses macros not present in the registry, or uses valid macros but on types they are not defined at should fail schema validation with the schema generated from this function. 3. This function is total -- A valid macro registry should not fail to create a GraphQL schema with macros. Args: macro_registry: MacroRegistry object containing a schema and macro descriptors we want to add to the schema. Returns: GraphQLSchema with additional fields where macroe edges can be used. """ # The easiest way to manipulate the schema is through its AST. The easiest # way to get an AST is to print it and parse it. schema_ast = parse(print_schema(macro_registry.schema_without_macros)) definitions_by_name = {} for definition in schema_ast.definitions: if isinstance(definition, (ObjectTypeDefinition, InterfaceTypeDefinition)): definitions_by_name[definition.name.value] = definition for class_name, macros_for_class in six.iteritems( macro_registry.macro_edges_at_class): for macro_edge_name, macro_edge_descriptor in six.iteritems( macros_for_class): list_type_at_target = ListType( NamedType(Name(macro_edge_descriptor.target_class_name))) arguments = [] directives = [Directive(Name(MacroEdgeDirective.name))] definitions_by_name[class_name].fields.append( FieldDefinition(Name(macro_edge_name), arguments, list_type_at_target, directives=directives)) return build_ast_schema(schema_ast)
def _replace_tag_names_in_tag_directive(name_change_map, tag_directive): """Apply tag parameter renaming to the given tag directive. Args: name_change_map: Dict[str, str] mapping tag names to new names tag_directive: GraphQL library tag directive whose name is in the name_change_map. This ast is not mutated. Returns: GraphQL library directive object, equivalent to the input one, with its name changed according to the name_change_map. If no changes were made, this is the same object as the input tag directive. """ # Schema validation has ensured this exists current_name = tag_directive.arguments[0].value.value new_name = name_change_map[current_name] if new_name == current_name: # No changes are necessary, return the original input object. return tag_directive renamed_tag_directive = copy(tag_directive) renamed_tag_directive.arguments = [ Argument(Name('tag_name'), StringValue(new_name)) ] return renamed_tag_directive
def test_visits_edited_node(): # type: () -> None added_field = Field(name=Name(value="__typename")) ast = parse("{ a { x } }") class TestVisitor(Visitor): def __init__(self): # type: () -> None self.did_visit_added_field = False def enter(self, node, *args): # type: (Any, *Any) -> Optional[Field] if isinstance(node, Field) and node.name.value == "a": selection_set = node.selection_set selections = [] if selection_set: selections = selection_set.selections new_selection_set = SelectionSet(selections=[added_field] + selections) return Field(name=None, selection_set=new_selection_set) if node is added_field: self.did_visit_added_field = True visitor = TestVisitor() visit(ast, visitor) assert visitor.did_visit_added_field
def enter(self, node, key, parent, *args): # type: (Any, Union[None, int, str], Any, *List[Any]) -> Optional[Any] parent_type = type_info.get_parent_type() _type = type_info.get_type() input_type = type_info.get_input_type() visited.append( [ "enter", type(node).__name__, node.value if type(node).__name__ == "Name" else None, str(parent_type) if parent_type else None, str(_type) if _type else None, str(input_type) if input_type else None, ] ) # Make a query valid by adding missing selection sets. if ( type(node).__name__ == "Field" and not node.selection_set and is_composite_type(get_named_type(_type)) ): return Field( alias=node.alias, name=node.name, arguments=node.arguments, directives=node.directives, selection_set=SelectionSet([Field(name=Name(value="__typename"))]), )
def get_copy_of_node_with_new_name(node, new_name): """Return a node with new_name as its name and otherwise identical to the input node. Args: node: type Node, with a .name attribute. Not modified by this function new_name: str, name to give to the output node Returns: Node, with new_name as its name and otherwise identical to the input node """ node_type = type(node).__name__ allowed_types = frozenset(( 'EnumTypeDefinition', 'Field', 'FieldDefinition', 'InterfaceTypeDefinition', 'NamedType', 'ObjectTypeDefinition', 'UnionTypeDefinition', )) if node_type not in allowed_types: raise AssertionError( u'Input node {} of type {} is not allowed, only {} are allowed.'. format(node, node_type, allowed_types)) node_with_new_name = copy(node) # shallow copy is enough node_with_new_name.name = Name(value=new_name) return node_with_new_name
def _get_in_collection_filter_directive(input_filter_name): """Create a @filter directive with in_collecion operation and the desired variable name.""" return Directive( name=Name(value=FilterDirective.name), arguments=[ Argument( name=Name(value='op_name'), value=StringValue(value='in_collection'), ), Argument( name=Name(value='value'), value=ListValue(values=[ StringValue(value=u'$' + input_filter_name), ], ), ), ], )
def _replace_tag_names_in_filter_directive(name_change_map, filter_directive): """Apply tag parameter renaming to the given filter directive. Args: name_change_map: Dict[str, str] mapping tag names to new names filter_directive: GraphQL library filter directive object that potentially uses tagged parameters. All such tagged parameters should be in the name_change_map. This directive object is not mutated, and if no changes are necessary then it will be returned Returns: GraphQL library directive object, equivalent to the input one, with any tagged parameters it uses replaced according to the name_change_map. If no changes were made, this is the same object as the input filter directive. """ made_changes = False new_arguments = [] for argument in filter_directive.arguments: if argument.name.value == 'op_name': new_arguments.append(argument) elif argument.name.value == 'value': new_value_list = [] for value in argument.value.values: parameter = value.value new_value = value # Rewrite tagged parameter names if necessary. if is_tagged_parameter(parameter): current_name = get_parameter_name(parameter) new_name = name_change_map[current_name] if new_name != current_name: made_changes = True new_value = StringValue('%' + new_name) new_value_list.append(new_value) if made_changes: new_argument = Argument(Name('value'), value=ListValue(new_value_list)) else: new_argument = argument new_arguments.append(new_argument) else: raise AssertionError( u'Unknown argument name {} in filter directive {}, this should ' u'have been caught in an earlier validation step.'.format( argument.name.value, filter_directive)) if not made_changes: # No changes were made, return the original input object. return filter_directive filter_with_renamed_args = copy(filter_directive) filter_with_renamed_args.arguments = new_arguments return filter_with_renamed_args
def build_default_schema(document: Document) -> SchemaDefinition: defined_types = [ td.name.value for td in document.definitions if isinstance(td, ObjectTypeDefinition) ] operations = [] if "Query" in defined_types: operations.append( OperationTypeDefinition("query", type=NamedType(name=Name("Query")))) if "Mutation" in defined_types: operations.append( OperationTypeDefinition("mutation", type=NamedType(name=Name("Mutation")))) if "Subscription" in defined_types: operations.append( OperationTypeDefinition("subscription", type=NamedType(name=Name("Subscription")))) return SchemaDefinition(operation_types=operations, directives=[])
def _get_property_field(existing_field, field_name, directives_from_edge): """Return a Field object with field_name, sharing directives with any such existing field. Any valid directives in directives_on_edge will be transferred over to the new field. If there is an existing Field in selection with field_name, the returned new Field will also contain all directives of the existing field with that name. Args: existing_field: Field or None. If it's not None, it is a field with field_name. The directives of this field will carry output to the output field field_name: str, the name of the output field directives_from_edge: List[Directive], the directives of a vertex field. The output field will contain all @filter and any @optional directives from this list Returns: Field object, with field_name as its name, containing directives from any field in the input selections with the same name and directives from the input list of directives """ new_field = Field( name=Name(value=field_name), directives=[], ) # Transfer directives from existing field of the same name if existing_field is not None: # Existing field, add all its directives directives_from_existing_field = existing_field.directives if directives_from_existing_field is not None: new_field.directives.extend(directives_from_existing_field) # Transfer directives from edge if directives_from_edge is not None: for directive in directives_from_edge: if directive.name.value == OutputDirective.name: # output illegal on vertex field raise GraphQLValidationError( u'Directive "{}" is not allowed on a vertex field, as @output directives ' u'can only exist on property fields.'.format(directive)) elif directive.name.value == OptionalDirective.name: if try_get_ast_by_name_and_type(new_field.directives, OptionalDirective.name, Directive) is None: # New optional directive new_field.directives.append(directive) elif directive.name.value == FilterDirective.name: new_field.directives.append(directive) else: raise AssertionError( u'Unreachable code reached. Directive "{}" is of an unsupported type, and ' u'was not caught in a prior validation step.'.format( directive)) return new_field
def _get_query_document(root_vertex_field_name, root_selections): """Return a Document representing a query with the specified name and selections.""" return Document(definitions=[ OperationDefinition(operation='query', selection_set=SelectionSet(selections=[ Field( name=Name(value=root_vertex_field_name), selection_set=SelectionSet( selections=root_selections, ), directives=[], ) ])) ])
def test_visits_edited_node(): added_field = Field(name=Name(value='__typename')) ast = parse('{ a { x } }') class TestVisitor(Visitor): def __init__(self): self.did_visit_added_field = False def enter(self, node, *args): if isinstance(node, Field) and node.name.value == 'a': selection_set = node.selection_set selections = [] if selection_set: selections = selection_set.selections new_selection_set = SelectionSet(selections=[added_field] + selections) return Field(name=None, selection_set=new_selection_set) if node is added_field: self.did_visit_added_field = True visitor = TestVisitor() visit(ast, visitor) assert visitor.did_visit_added_field
def enter(self, node, key, parent, *args): parent_type = type_info.get_parent_type() _type = type_info.get_type() input_type = type_info.get_input_type() visited.append([ 'enter', type(node).__name__, node.value if type(node).__name__ == "Name" else None, str(parent_type) if parent_type else None, str(_type) if _type else None, str(input_type) if input_type else None ]) # Make a query valid by adding missing selection sets. if type( node ).__name__ == "Field" and not node.selection_set and is_composite_type( get_named_type(_type)): return Field(alias=node.alias, name=node.name, arguments=node.arguments, directives=node.directives, selection_set=SelectionSet( [Field(name=Name(value='__typename'))]))
def test_prints_minimal_ast(): # type: () -> None ast = Field(name=Name(loc=None, value="foo")) assert print_ast(ast) == "foo"