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

gql function should accept additional DocumentNode args #414

Closed
markedwards opened this issue May 5, 2023 · 11 comments
Closed

gql function should accept additional DocumentNode args #414

markedwards opened this issue May 5, 2023 · 11 comments
Labels
type: question or discussion Issue discussing or asking a question about gql

Comments

@markedwards
Copy link

I'd like to use the gql() function in a way that's similar to the common JS/TS pattern, where fragments can be nested to construct queries in a modular fashion.

In other words, I'm looking for a signature like this:

def gql(literal: str, *args: DocumentNode | str) -> DocumentNode:

I also would like the functionality from JS/TS which deduplicates fragments from the AST, so this is not a bother when building queries with common fragments like pageInfo.

Is such functionality already present and I'm missing it? I've already got part of this working, and I'd be happy to contribute the change as well. But if it's already there then I don't want to reinvent the wheel.

@leszekhanusz leszekhanusz added type: feature A new feature type: question or discussion Issue discussing or asking a question about gql and removed type: feature A new feature labels May 6, 2023
@leszekhanusz
Copy link
Collaborator

You could use the DSL module to compose queries dynamically.

@markedwards
Copy link
Author

Thanks, @leszekhanusz. I don’t really want to drop SDL though. Being able to directly copy query strings from a playground environment, for instance, is very useful.

What do you think about incorporating features from graphql-tag into the gql function? It can be done in a backwards-compatible way I think. Is that something you would consider?

@leszekhanusz
Copy link
Collaborator

It's better not to mix different goals in the same method.

If I understand correctly you would like to merge queries like described in that stackoverflow question.

So another method would be preferable, with a signature looking like this:

def merge(*documents: DocumentNode) -> DocumentNode:

@markedwards
Copy link
Author

My goal is to define reusable fragments, not to merge queries. This is a common workflow with graphql-tag: https://github.com/apollographql/graphql-tag#fragments

@leszekhanusz
Copy link
Collaborator

leszekhanusz commented May 6, 2023

One way to do this would be to concatenate the strings:

from gql import gql                                                                                                     
from graphql import print_ast                                                                                           
                                                                                                                        
userFragment = """                                                                                                      
  fragment User_user on User {                                                                                          
    firstName                                                                                                           
    lastName                                                                                                            
  }                                                                                                                     
"""                                                                                                                     
                                                                                                                        
query = gql(                                                                                                            
    """                                                                                                                 
  {                                                                                                                     
    user(id: 5) {                                                                                                       
      ...User_user                                                                                                      
    }                                                                                                                   
  }                                                                                                                     
"""                                                                                                                     
    + userFragment                                                                                                      
)                                                                                                                       
                                                                                                                        
print(print_ast(query))

@markedwards
Copy link
Author

markedwards commented May 7, 2023

Yes, that would work, and actually the graphql-tag interface supports both DocumentNode and str. One advantage of DocumentNode is that a parsing error in a given fragment would be traced to the offending file, whereas with str the error would be deferred to wherever gql() is ultimately executed. There may be other advantages that I'm not clear on (static analysis perhaps).

Its not just a question of this, though. The graphql-tag implementation does other things to aid composition, like deduplicating fragments. This is useful for common fragments like pageInfo that might show up in lots of places.

This composition flow is very well-established in the Apollo/JS world. I'm not trying to invent anything here, I'm simply proposing including this functionality because it's a useful approach in my experience. Specifically, I'm proposing a rough port of this code: https://github.com/apollographql/graphql-tag/blob/main/src/index.ts#L114

I'll just close this thread if there's no interest here, however.

Thanks!

@leszekhanusz
Copy link
Collaborator

leszekhanusz commented May 8, 2023

You can make such a method yourself with two lines of code:

from gql import gql as _gql                                                                                             
from graphql import print_ast, DocumentNode                                                                             
                                                                                                                        
                                                                                                                        
def gql(*args):                                                                                                         
    texts = [print_ast(arg) if isinstance(arg, DocumentNode) else arg for arg in args]                                  
    return _gql("\n".join(texts))                                                                                       
                                                                                                                        
                                                                                                                        
userFragment = gql(                                                                                                     
    """                                                                                                                 
  fragment User_user on User {                                                                                          
    firstName                                                                                                           
    lastName                                                                                                            
  }                                                                                                                     
"""                                                                                                                     
)                                                                                                                       
                                                                                                                        
query = gql(                                                                                                            
    userFragment,                                                                                                       
    """                                                                                                                 
  {                                                                                                                     
    user(id: 5) {                                                                                                       
      ...User_user                                                                                                      
    }                                                                                                                   
  }                                                                                                                     
""",                                                                                                                    
)                                                                                                                       
                                                                                                                        
print(print_ast(query))

@markedwards
Copy link
Author

The above example only solves for concatenating additional str or DocumentNode objects, which as I said in the original ticket I've already built. In order for this to be a useful workflow, other things from the graphql-tag implementation need to be implemented, such as fragment deduplication.

I've explained this more than once above though, and there's clearly no interest in expanding the scope of this project in this direction. So I'll leave it at that.

@leszekhanusz
Copy link
Collaborator

Alright, so your main problem is not related to accepting more args to the gql method but instead is to prevent gql to make documents which are invalid according to the GraphQL spec because they contains the same fragment definition multiple times.

What you would like is that:

gql("""
query {
  ...SomeFragment
}
fragment SomeFragment on SomeResource {
  field
}
fragment SomeFragment on SomeResource {
  field
}""")

would make a document with the second fragment definition ignored:

query {
  ...SomeFragment
}
fragment SomeFragment on SomeResource {
  field
}

as was explained in graphql-tag issue #27

You have to understand that the gql method in graphql-python/gql is currently a thin wrapper around the parse method of graphql-core which itself should be equivalent to the parse method of graphql-js.
I'm actually surprised that this was not already managed by the parse method of graphql-core / graphql-js as this creates invalid documents.

I'm not sure if this should do the trick:

from gql import gql as _gql
from typing import Tuple
from graphql import print_ast, DocumentNode, FragmentDefinitionNode, DefinitionNode


def deduplicate_fragments(
    definitions: Tuple[DefinitionNode, ...]
) -> Tuple[DefinitionNode]:

    fragment_names = []
    dedup_definitions = []

    for definition in definitions:
        if isinstance(definition, FragmentDefinitionNode):
            fragment_name = definition.name.value

            if fragment_name not in fragment_names:
                fragment_names.append(fragment_name)
                dedup_definitions.append(definition)
        else:
            dedup_definitions.append(definition)

    return tuple(dedup_definitions)


def gql(*args):
    texts = [print_ast(arg) if isinstance(arg, DocumentNode) else arg for arg in args]
    document = _gql("\n".join(texts))
    document.definitions = deduplicate_fragments(document.definitions)
    return document


userFragment = gql(
    """
  fragment User_user on User {
    firstName
    lastName
  }
"""
)

query = gql(
    userFragment,
    userFragment,
    """
  {
    user(id: 5) {
      ...User_user
    }
  }
""",
)

print(print_ast(query))

@markedwards
Copy link
Author

I've already ported the relevant graphql-tag code and it's working. My question was really whether this was already built somewhere in gql, and if not would you want to extend gql to include this functionality (in which case I'm willing to contribute it). But it seems like the intention is to keep gql.gql a thin wrapper.

@Sytten
Copy link

Sytten commented Jul 29, 2024

Could we make this a first class citizen? I had the same exact problem.
It would be super nice if we could use f strings with this, this is what JS does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question or discussion Issue discussing or asking a question about gql
Projects
None yet
Development

No branches or pull requests

3 participants