def _get_property_field(existing_field, field_name, directives_from_edge): """Return a FieldNode with field_name, sharing directives with any such existing FieldNode. Any valid directives in directives_on_edge will be transferred over to the new FieldNode. If there is an existing FieldNode in selection with field_name, the returned new FieldNode will also contain all directives of the existing field with that name. Args: existing_field: FieldNode or None. If it's not None, it is a FieldNode 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[DirectiveNode], the directives of a vertex field. The output field will contain all @filter and any @optional directives from this list Returns: FieldNode, 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_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, DirectiveNode ) 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) ) new_field = FieldNode(name=NameNode(value=field_name), directives=new_field_directives,) return new_field
def _get_out_name_optionally_add_output( field: FieldNode, name_assigner: IntermediateOutNameAssigner) -> Tuple[FieldNode, str]: """Return out_name of @output on field, creating new @output if needed. Args: field: FieldNode that may need an added an @output directive. name_assigner: Object used to generate and keep track of names of newly created @output directives. Returns: Tuple containing: - Either the original field or a new FieldNode that has the same properties as the original field, but with an added @output directive. - Name of the out_name of the @output directive, either pre-existing or newly generated. """ # Check for existing directive output_directive = try_get_ast_by_name_and_type(field.directives, OutputDirective.name, DirectiveNode) if output_directive is None: # Create and add new directive to field out_name = name_assigner.assign_and_return_out_name() output_directive = _get_output_directive(out_name) if field.directives is None: new_directives = [] else: new_directives = list(field.directives) new_directives.append(output_directive) new_field = FieldNode( alias=field.alias, name=field.name, arguments=field.arguments, selection_set=field.selection_set, directives=new_directives, loc=field.loc, ) return new_field, out_name else: if not isinstance(output_directive, DirectiveNode): raise AssertionError( "output_directive needs to be type DirectiveNode, but was type " f"{type(output_directive)}. This should be impossible and is a bug." ) argument_value = output_directive.arguments[0].value if not isinstance(argument_value, ScalarConstantValueNodes) or isinstance( argument_value, BooleanValueNode): raise AssertionError( "argument_value must be of type ScalarConstantValueNodes, but " f"was of type {type(argument_value)}.") return field, argument_value.value # Location of value of out_name
def _get_query_document(root_vertex_field_name, root_selections): """Return a Document representing a query with the specified name and selections.""" return DocumentNode(definitions=[ OperationDefinitionNode( operation=OperationType.QUERY, selection_set=SelectionSetNode(selections=[ FieldNode( name=NameNode(value=root_vertex_field_name), selection_set=SelectionSetNode(selections=root_selections, ), directives=[], ) ]), ) ])
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, DIRECTIVES_REQUIRED_IN_MACRO_EDGE_DEFINITION) # We will add this output directive to make the ast a valid query output_directive = DirectiveNode( name=NameNode(value="output"), arguments=[ ArgumentNode(name=NameNode(value="out_name"), value=StringValueNode(value="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 = list( 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, FieldNode): 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, FieldNode(name=NameNode(value="__typename"), directives=[output_directive])) # Propagate the changes back to the result_ast root_level_selection.selection_set = SelectionSetNode( selections=first_level_selections) query_ast.selection_set = SelectionSetNode( selections=[root_level_selection]) return DocumentNode(definitions=[query_ast])
def _get_out_name_optionally_add_output(field, name_assigner): """Return out_name of @output on field, creating new @output if needed. Args: field: FieldNode, a field that may need an added an @output directive name_assigner: IntermediateOutNameAssigner, object used to generate and keep track of names of newly created @output directives Returns: tuple of (field, out_name) with the following information: field: FieldNode, either the original field or a new FieldNode that has the same properties as the original field, but with an added @output directive out_name: str, name of the out_name of the @output directive, either pre-existing or newly generated """ # Check for existing directive output_directive = try_get_ast_by_name_and_type(field.directives, OutputDirective.name, DirectiveNode) if output_directive is None: # Create and add new directive to field out_name = name_assigner.assign_and_return_out_name() output_directive = _get_output_directive(out_name) if field.directives is None: new_directives = [] else: new_directives = list(field.directives) new_directives.append(output_directive) new_field = FieldNode( alias=field.alias, name=field.name, arguments=field.arguments, selection_set=field.selection_set, directives=new_directives, loc=field.loc, ) return new_field, out_name else: return field, output_directive.arguments[ 0].value.value # Location of value of out_name
def _add_pagination_filter_at_node( query_analysis: QueryPlanningAnalysis, node_vertex_path: VertexPath, node_ast: DocumentNode, pagination_field: str, directive_to_add: DirectiveNode, extended_parameters: Dict[str, Any], ) -> Tuple[DocumentNode, Dict[str, Any]]: """Add the filter to the target field, returning a query and its new parameters. Args: query_analysis: the entire query with any query analysis needed for pagination node_vertex_path: path to the node_ast from the query root node_ast: part of the entire query, rooted at the location where we are adding a filter. pagination_field: field on which we are adding a filter directive_to_add: filter directive to add extended_parameters: original parameters of the query along with the parameter used in directive_to_add Returns: tuple (new_ast, removed_parameters) new_ast: A query with the filter inserted, and any filters on the same location with the same operation removed. new_parameters: The parameters to use with the new_ast """ if not isinstance( node_ast, (FieldNode, InlineFragmentNode, OperationDefinitionNode)): raise AssertionError( f'Input AST is of type "{type(node_ast).__name__}", which should not be a selection.' ) new_directive_operation = _get_filter_node_operation(directive_to_add) new_directive_parameter_name = _get_binary_filter_node_parameter( directive_to_add) new_directive_parameter_value = extended_parameters[ new_directive_parameter_name] # If the field exists, add the new filter and remove redundant filters. new_parameters = dict(extended_parameters) new_selections = [] found_field = False for selection_ast in node_ast.selection_set.selections: new_selection_ast = selection_ast field_name = get_ast_field_name(selection_ast) if field_name == pagination_field: found_field = True new_selection_ast = copy(selection_ast) new_selection_ast.directives = copy(selection_ast.directives) new_directives = [] for directive in selection_ast.directives: operation = _get_filter_node_operation(directive) if _are_filter_operations_equal_and_possible_to_eliminate( new_directive_operation, operation): parameter_name = _get_binary_filter_node_parameter( directive) parameter_value = new_parameters[parameter_name] if not _is_new_filter_stronger( query_analysis, PropertyPath(node_vertex_path, pagination_field), operation, new_directive_parameter_value, parameter_value, ): logger.error( "Pagination filter %(new_filter)s on %(pagination_field)s with param " "%(new_filter_param)s is not stronger than existing filter " "%(existing_filter)s with param %(existing_param)s. This is either a " "bug in parameter generation, or this assertion is outdated. " "Query: %(query)s", { "new_filter": print_ast(directive_to_add), "pagination_field": pagination_field, "new_filter_param": new_directive_parameter_value, "existing_filter": print_ast(directive), "existing_param": parameter_value, "query": query_analysis.query_string_with_parameters, }, ) del new_parameters[parameter_name] else: new_directives.append(directive) new_directives.append(directive_to_add) new_selection_ast.directives = new_directives new_selections.append(new_selection_ast) # If field didn't exist, create it and add the new directive to it. if not found_field: new_selections.insert( 0, FieldNode(name=NameNode(value=pagination_field), directives=[directive_to_add])) new_ast = copy(node_ast) new_ast.selection_set = SelectionSetNode(selections=new_selections) return new_ast, new_parameters