graphql-python/gql

Validation error using '@include' directive if schema comes from introspection

SuperBo opened this issue · 10 comments

When I try to use "@include" directive in my string query, I got this error:
graphql.error.graphql_error.GraphQLError: Unknown directive '@include'.

I think "@include" and "@Skip" are in standards of GraphQL https://graphql.org/learn/queries/#directives

Version in my environment

gql[requests]==3.0.0b1
graphql-core==3.1.6; python_version >= '3.6' and python_version < '4'

GraphQL Query

query fetchPool($with_swap: Boolean!) {
  pool(id: "0x83abecf7204d5afc1bea5df734f085f2535a9976") {
    id
    createdAtTimestamp
    swaps (first: 2) @include(if: $with_swap ) {
      id
      timestamp
    }
  }
}

Full stack trace:

Traceback (most recent call last):
File "test.py", line 27, in
result = client.execute(q, variable_values= {'with_swap': True})
File "/Users/ynguyen/.local/share/virtualenvs/activelp_v1_prod-cEysbnrO/lib/python3.8/site-packages/gql/client.py", line 186, in execute
return self.execute_sync(document, *args, **kwargs)
File "/Users/ynguyen/.local/share/virtualenvs/activelp_v1_prod-cEysbnrO/lib/python3.8/site-packages/gql/client.py", line 134, in execute_sync
return session.execute(document, *args, **kwargs)
File "/Users/ynguyen/.local/share/virtualenvs/activelp_v1_prod-cEysbnrO/lib/python3.8/site-packages/gql/client.py", line 389, in execute
result = self._execute(
File "/Users/ynguyen/.local/share/virtualenvs/activelp_v1_prod-cEysbnrO/lib/python3.8/site-packages/gql/client.py", line 328, in _execute
self.client.validate(document)
File "/Users/ynguyen/.local/share/virtualenvs/activelp_v1_prod-cEysbnrO/lib/python3.8/site-packages/gql/client.py", line 129, in validate
raise validation_errors[0]
graphql.error.graphql_error.GraphQLError: Unknown directive '@include'.

GraphQL request:6:22
5 | createdAtTimestamp
6 | swaps (first: 2) @include(if: $with_swap ) {
| ^
7 | id

Hi,

Which versions of gql and graphql-core are you using ?
Could you please post your query and the full stack trace ?

Hi @leszekhanusz , I've just updated my OP.

How did you provide your schema? By using introspection or by providing your own schema.graphql file?
Does the schema include the @include directive? (I don't know if it is needed)

I set "fetch_schema_from_transport" in client to True. I found out that when I switch "fetch_schema_from_transport" to False, the query can work now.

This my full test code if you interest

import gql
from gql.transport.requests import RequestsHTTPTransport

query = """
query fetchPool($with_swap: Boolean!) {
  pool(id: "0x83abecf7204d5afc1bea5df734f085f2535a9976") {
    id
    createdAtTimestamp
    swaps (first: 2) @include(if: $with_swap ) {
      id
      timestamp
    }
  }
} 
"""

UNISWAP_V3_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'

transport = RequestsHTTPTransport(
    url=UNISWAP_V3_ENDPOINT, verify=True, retries=3,
)
client = gql.Client(transport=transport, fetch_schema_from_transport=True)

q = gql.gql(query)

result = client.execute(q, variable_values= {'with_swap': True})

print(result)

I tried to valide your query using a provided schema.graphql file but I had the following error:

graphql.error.graphql_error.GraphQLError: Field 'pool' argument 'subgraphError' of type '_SubgraphErrorPolicy_!' is required, but it was not provided.

GraphQL request:3:3
2 | query fetchPool($with_swap: Boolean!) {
3 |   pool(id: "0x83abecf7204d5afc1bea5df734f085f2535a9976") {
  |   ^
4 |     id

This was solved by adding the required subgraphError field:

import gql                                                                                                              
from gql.transport.requests import RequestsHTTPTransport                                                                
                                                                                                                        
query = """                                                                                                             
query fetchPool($with_swap: Boolean!) {                                                                                 
  pool(id: "0x83abecf7204d5afc1bea5df734f085f2535a9976", subgraphError: allow) {                                        
    id                                                                                                                  
    createdAtTimestamp                                                                                                  
    swaps (first: 2) @include(if: $with_swap ) {                                                                        
      id                                                                                                                
      timestamp                                                                                                         
    }                                                                                                                   
  }                                                                                                                     
}                                                                                                                       
"""                                                                                                                     
                                                                                                                        
UNISWAP_V3_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'                                      
                                                                                                                        
with open("./schema.graphql") as f:                                                                                     
    schema_str = f.read()                                                                                               
                                                                                                                        
transport = RequestsHTTPTransport(                                                                                      
    url=UNISWAP_V3_ENDPOINT, verify=True, retries=3,                                                                    
)                                                                                                                       
client = gql.Client(transport=transport, schema=schema_str)                                                             
                                                                                                                        
q = gql.gql(query)                                                                                                      
                                                                                                                        
result = client.execute(q, variable_values= {'with_swap': True})                                                        
                                                                                                                        
print(result)

schema.graphql.zip

This is probably a bug in graphql-core. It is not normal that the include directive is not supported if the schema is fetched from the backend using introspection.

Thanks for your report!

@Cito could you please take a look at this bug?

@leszekhanusz I don't see where the problem is. I can run your example without error.

@Cito I have received your message in a notification email but it does not appear here. GitHub having some issues perhaps?

Anyway to answer your question:
Yes, that's the point, it works if the schema is built from a file but not if the schema is built from introspection.
Ultimately, the problem lies in differences between the build_ast_schema and build_client_schema of graphql-core.

In build_ast_schema, there is this code to add the specified directives:

    # If specified directives were not explicitly declared, add them.                                                   
    if not any(directive.name == "skip" for directive in directives):                                                   
        directives.append(GraphQLSkipDirective)                                                                         
    if not any(directive.name == "include" for directive in directives):                                                
        directives.append(GraphQLIncludeDirective)                                                                      
    if not any(directive.name == "deprecated" for directive in directives):                                             
        directives.append(GraphQLDeprecatedDirective)                                                                   
    if not any(directive.name == "specifiedBy" for directive in directives):                                            
        directives.append(GraphQLSpecifiedByDirective)

There is nothing corresponding in the build_client_schema function.

Cito commented

Hi @leszekhanusz - yes, I understood the problem after reading again and therefore deleted my comment.

As you already noticed, the problem is the different behavior of build_client_schema and build_ast_schema which is inherited from GraphQL.js. The default directives like include are not added automatically with build_client_schema. As a workaround, you could do something like this to "normalize" the schema after creating it from introspection:

self.client.schema = build_ast_schema(parse(print_schema(self.client.schema)))

Or, you could somehow fumble the missing default directives into introspection['__schema']['directives'] before calling build_client_schema.
All of this is ugly, that's why I opened that ticket with GraphQL.js now. If it is fixed there, I will port it to GraphQL-core then.