Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify query programmatically #149

Closed
2 tasks
abitrolly opened this issue Dec 4, 2021 · 7 comments
Closed
2 tasks

Modify query programmatically #149

abitrolly opened this issue Dec 4, 2021 · 7 comments

Comments

@abitrolly
Copy link

abitrolly commented Dec 4, 2021

Sent here from graphql-python/gql#272

There is a way in gql to construct queries programmatically with DSL module or by parsing a string into AST with gql.gql() and then using print_ast from graphql to get back the string.

import gql

dg = gql.gql("""
    query getContinents {
      continents {
        code
        name
      }
    }
""")

from graphql import print_ast
print(print_ast(dg))

What is not clear is how to actually find nodes in AST and edit or expand them. For example, finding a parent of node in the query below (continents) and adding an attribute to it ((code:"AF")).

query getContinents {
  continents {
    code
    name
  }
}

So that the query becomes.

    query getContinents {
      continents (code:"AF") {
        code
        name
      }
    }

I looked into the docs, but it doesn't actually explain.

  • How to find AST node that needs modification?
  • How to modify it (upsert attributes)?

The documentation container chapter about schemas https://graphql-core-3.readthedocs.io/en/latest/usage/extension.html?highlight=modify%20ast#extending-a-schema and as I am new to GraphQL I am not yet sure if schema and query are the same things.

Feature requests

I am not sure GraphQL.js includes this too and not sure that fundamentally changes the way GraphQL works.

@Cito
Copy link
Member

Cito commented Dec 4, 2021

Hi @abitrolly. You can modify the query AST manually, but it is not something you would normally do.

Please check the language reference again, sorry it had been broken in Read the Docs lately.

Here is how you could solve your two tasks from above:

from graphql import (parse, print_ast,
                     ArgumentNode, NameNode, StringValueNode)

query = """
query getContinents {
  continents {
    code
    name
  }
}
"""

# Create AST from source

doc = parse(query)

# Find AST node that needs modification

field = doc.definitions[0].selection_set.selections[0]

# Modify it (add attributes)

field.arguments = [*field.arguments, ArgumentNode(
    name=NameNode(value='code'), value=StringValueNode(value='AF'))]

# Print modified AST

new_query = print_ast(doc)

print(new_query)

@abitrolly
Copy link
Author

abitrolly commented Dec 5, 2021

@Cito thanks. I still have troubles trying to figure out how to find out the position of element by name and then traverse up the hierarchy. In this particular example I need to find the element named 'code' and then add an attribute to its parent.

@Cito
Copy link
Member

Cito commented Dec 5, 2021

@abitrolly You can use a Visitor to traverse the AST. This will also give you access to the immediate parent and full list of ancestors of the current node.

Here is an example of a visitor that searches for a field node with the name 'code', and then adds another field node with the name 'newField' to its parent.

from graphql import FieldNode, NameNode, Visitor, parse, print_ast, visit
from graphql.pyutils import FrozenList

query = """
query getContinents {
  continents {
    code
    name
  }
}"""

doc = parse(query)

class ExampleVisitor(Visitor):

    def enter_name(self, node, key, parent, path, ancestors):
        if node.value == 'code' and parent.kind == 'field':
            parent = ancestors[-2]  # selection set
            parent.selections = FrozenList(
                (*parent.selections,
                 FieldNode(name=NameNode(value='newField'))))
            return True  # stop visiting

visitor = ExampleVisitor()
visit(doc, visitor)

new_query = print_ast(doc)

print(new_query)

Note: It is recommended to use a FrozenList instead of an ordinary list to make the nodes hashable.

@abitrolly
Copy link
Author

I expected some kind of DOM/AST traverse methods, like doc.findfirst('code').parent() and then .setattr('code', 'AF'). That might work too.

So the code overwrites .selections attribute for the parent, and appends "FieldNode" to it, which would be the next sibling to name element. Need to test it and combine with attribute setter.

parent = ancestors[-2]  # selection set

Not -1, because there is an additional wrapping container like selection set?

Not sure why I need hashable nodes.

@Cito
Copy link
Member

Cito commented Dec 6, 2021

I expected some kind of DOM/AST traverse methods, like doc.findfirst('code').parent() and then .setattr('code', 'AF').

No, something like that doesn't exist in the API. As explained above, the AST is not really meant to be analyzed or modified by users of the API. The visit method is mainly used to implement the various validation rules.

Not -1, because there is an additional wrapping container like selection set?

Exactly. The ancestor at -1 (parent) is just the list of field nodes (the selections attribute). You want its parent object which owns this list (selection set node).

Not sure why I need hashable nodes.

It helps to cache nodes during execution. It might also help to detect AST changes and avoid re-validation if that is not the case.

@Cito Cito closed this as completed Dec 27, 2021
@abitrolly
Copy link
Author

I still need time to validate how it is going to work.

@Cito
Copy link
Member

Cito commented Dec 27, 2021

@abitrolly I'm currently going through the old tickets before creating the next release. Feel free to reopen or open a new issue if you have concrete questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants