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
Beispiel #3
0
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])
Beispiel #5
0
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