Beispiel #1
0
def _get_basic_schema_ast(query_type: str) -> DocumentNode:
    """Create a basic AST Document representing a nearly blank schema.

    The output AST contains a single query type, whose name is the input string. The query type
    is guaranteed to be the second entry of Document definitions, after the schema definition.
    The query type has no fields.

    Args:
        query_type: name of the query type for the schema

    Returns:
        DocumentNode, representing a nearly blank schema
    """
    blank_ast = DocumentNode(definitions=[
        SchemaDefinitionNode(
            operation_types=[
                OperationTypeDefinitionNode(
                    operation=OperationType.QUERY,
                    type=NamedTypeNode(name=NameNode(value=query_type)),
                )
            ],
            directives=[],
        ),
        ObjectTypeDefinitionNode(
            name=NameNode(value=query_type),
            fields=[],
            interfaces=[],
            directives=[],
        ),
    ])
    return blank_ast
def _get_output_directive(out_name):
    """Return a Directive representing an @output with the input out_name."""
    return DirectiveNode(
        name=NameNode(value=OutputDirective.name),
        arguments=[
            ArgumentNode(name=NameNode(value=u"out_name"), value=StringValueNode(value=out_name),),
        ],
    )
Beispiel #3
0
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(
        (
            "EnumTypeDefinitionNode",
            "FieldNode",
            "FieldDefinitionNode",
            "InterfaceTypeDefinitionNode",
            "NamedTypeNode",
            "ObjectTypeDefinitionNode",
            "UnionTypeDefinitionNode",
        )
    )
    if node_type not in allowed_types:
        raise AssertionError(
            "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 = NameNode(value=new_name)
    return node_with_new_name
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 = [
        ArgumentNode(name=NameNode(value="tag_name"),
                     value=StringValueNode(value=new_name))
    ]
    return renamed_tag_directive
Beispiel #5
0
def _build_stitch_directive(source_field_name: str,
                            sink_field_name: str) -> DirectiveNode:
    """Build a Directive node for the stitch directive."""
    return DirectiveNode(
        name=NameNode(value="stitch"),
        arguments=[
            ArgumentNode(
                name=NameNode(value="source_field"),
                value=StringValueNode(value=source_field_name),
            ),
            ArgumentNode(
                name=NameNode(value="sink_field"),
                value=StringValueNode(value=sink_field_name),
            ),
        ],
    )
def _make_binary_filter_directive_node(op_name: str,
                                       param_name: str) -> DirectiveNode:
    """Make a binary filter directive node with the given binary op_name and parameter name."""
    return DirectiveNode(
        name=NameNode(value="filter"),
        arguments=[
            ArgumentNode(
                name=NameNode(value="op_name"),
                value=StringValueNode(value=op_name),
            ),
            ArgumentNode(
                name=NameNode(value="value"),
                value=ListValueNode(
                    values=[StringValueNode(value="$" + param_name)]),
            ),
        ],
    )
def _get_in_collection_filter_directive(input_filter_name):
    """Create a @filter directive with in_collecion operation and the desired variable name."""
    return DirectiveNode(
        name=NameNode(value=FilterDirective.name),
        arguments=[
            ArgumentNode(
                name=NameNode(value="op_name"),
                value=StringValueNode(value="in_collection"),
            ),
            ArgumentNode(
                name=NameNode(value="value"),
                value=ListValueNode(values=[
                    StringValueNode(value=u"$" + input_filter_name),
                ], ),
            ),
        ],
    )
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 _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 = StringValueNode(value="%" + new_name)

                new_value_list.append(new_value)

            if made_changes:
                new_argument = ArgumentNode(
                    name=NameNode(value="value"),
                    value=ListValueNode(values=new_value_list))
            else:
                new_argument = argument
            new_arguments.append(new_argument)
        else:
            raise AssertionError(
                "Unknown argument name {} in filter directive {}, this should "
                "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 _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
Beispiel #11
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=[],
                )
            ]),
        )
    ])
Beispiel #12
0
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 macro 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))

    fields_by_definition_name = {}
    for definition in schema_ast.definitions:
        if isinstance(definition, (ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode)):
            # Cast to list (from FrozenList) to allow for updates.
            fields_by_definition_name[definition.name.value] = list(definition.fields)

    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 = ListTypeNode(
                type=NamedTypeNode(name=NameNode(value=macro_edge_descriptor.target_class_name))
            )
            arguments = []
            directives = [DirectiveNode(name=NameNode(value=MacroEdgeDirective.name))]
            fields_by_definition_name[class_name].append(
                FieldDefinitionNode(
                    name=NameNode(value=macro_edge_name),
                    arguments=arguments,
                    type=list_type_at_target,
                    directives=directives,
                )
            )

    new_definitions = []
    for definition in schema_ast.definitions:
        # Create new (Object)/(Interface)TypeDefinitionNode based on the updated fields.
        if isinstance(definition, ObjectTypeDefinitionNode):
            new_definitions.append(
                ObjectTypeDefinitionNode(
                    interfaces=definition.interfaces,
                    description=definition.description,
                    name=definition.name,
                    directives=definition.directives,
                    loc=definition.loc,
                    fields=FrozenList(fields_by_definition_name[definition.name.value]),
                )
            )
        elif isinstance(definition, InterfaceTypeDefinitionNode):
            new_definitions.append(
                InterfaceTypeDefinitionNode(
                    description=definition.description,
                    name=definition.name,
                    directives=definition.directives,
                    loc=definition.loc,
                    fields=FrozenList(fields_by_definition_name[definition.name.value]),
                )
            )
        else:
            new_definitions.append(definition)

    new_schema_ast = DocumentNode(definitions=new_definitions)
    return build_ast_schema(new_schema_ast)
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
Beispiel #14
0
print(document)
print(print_ast(document))

# ast

from graphql.language.ast import (
    DocumentNode,
    ObjectTypeDefinitionNode,
    NameNode,
    FieldDefinitionNode,
    NamedTypeNode,
)

document = DocumentNode(definitions=[
    ObjectTypeDefinitionNode(
        name=NameNode(value="Query"),
        fields=[
            FieldDefinitionNode(
                name=NameNode(value="me"),
                type=NamedTypeNode(name=NameNode(value="User")),
                arguments=[],
                directives=[],
            )
        ],
        directives=[],
        interfaces=[],
    ),
    ObjectTypeDefinitionNode(
        name=NameNode(value="User"),
        fields=[
            FieldDefinitionNode(
Beispiel #15
0
def _add_edge_field(
    source_type_node: Union[InterfaceTypeDefinitionNode,
                            ObjectTypeDefinitionNode],
    sink_type_name: str,
    source_field_name: str,
    sink_field_name: str,
    edge_name: str,
    direction: str,
) -> Union[InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode]:
    """Add one direction of the specified edge as a field of the source type.

    Args:
        source_type_node: new field representing one direction of the edge will be added.
        sink_type_name: name of the type that the edge leads to.
        source_field_name: name of the source side field that will be stitched.
        sink_field_name: name of the sink side field that will be stitched.
        edge_name: name of the edge that will be used to name the new field.
        direction: either OUTBOUND_EDGE_DIRECTION or INBOUND_EDGE_DIRECTION ('out' or 'in').

    Returns:
        updated version of source_type_node.

    Raises:
        - SchemaNameConflictError if the new cross-schema edge name causes a name conflict with
          existing fields, or fields created by previous cross-schema edges
    """
    type_fields = source_type_node.fields

    if direction not in (OUTBOUND_EDGE_DIRECTION, INBOUND_EDGE_DIRECTION):
        raise AssertionError(
            'Input "direction" must be either "{}" or "{}".'.format(
                OUTBOUND_EDGE_DIRECTION, INBOUND_EDGE_DIRECTION))
    new_edge_field_name = direction + "_" + edge_name

    # Error if new edge causes a field name clash
    if any(field.name.value == new_edge_field_name for field in type_fields):
        raise SchemaMergeNameConflictError(
            'New field "{}" under type "{}" created by the {}bound field of edge named '
            '"{}" clashes with an existing field of the same name. Consider changing the '
            "name of your edge to avoid name conflicts.".format(
                new_edge_field_name, source_type_node.name.value, direction,
                edge_name))

    new_edge_field_node = FieldDefinitionNode(
        name=NameNode(value=new_edge_field_name),
        arguments=[],
        type=ListTypeNode(type=NamedTypeNode(
            name=NameNode(value=sink_type_name), ), ),
        directives=[
            _build_stitch_directive(source_field_name, sink_field_name),
        ],
    )

    new_type_fields = list(type_fields)
    new_type_fields.append(new_edge_field_node)
    new_source_type_node: Union[ObjectTypeDefinitionNode,
                                InterfaceTypeDefinitionNode]
    if type(source_type_node) == ObjectTypeDefinitionNode:
        new_source_type_node = ObjectTypeDefinitionNode(
            description=source_type_node.description,
            name=source_type_node.name,
            directives=source_type_node.directives,
            fields=new_type_fields,
            interfaces=source_type_node.interfaces,
        )
    elif type(source_type_node) == InterfaceTypeDefinitionNode:
        new_source_type_node = InterfaceTypeDefinitionNode(
            description=source_type_node.description,
            name=source_type_node.name,
            directives=source_type_node.directives,
            fields=new_type_fields,
        )
    else:
        raise AssertionError(
            'Input "source_type_node" must be of type {} or {}. Received type {}'
            .format(
                ObjectTypeDefinitionNode,
                InterfaceTypeDefinitionNode,
                type(source_type_node),
            ))
    return new_source_type_node