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

Federated schema beaks when referring to the Query type from another type #3682

Open
lirsacc-mns opened this issue Oct 29, 2024 · 0 comments
Labels
bug Something isn't working

Comments

@lirsacc-mns
Copy link

lirsacc-mns commented Oct 29, 2024

While it's a bit niche, we have a use case where we want to return Query from an object field (specifically this is in a mutation result to allow callers to request arbitrary parts of the schema after a mutation).

This works fine on a standard strawberry.Schema, but breaks down when using strawberry.federation.Schema.

Reproduction

This works fine without federation, for example this minimal test case:

from __future__ import annotations

import strawberry


@strawberry.type
class Query:
    @strawberry.field
    def foo(self) -> Foo:
        return Foo()


@strawberry.type
class Foo:
    bar: int = 42

    @strawberry.field
    def query(self) -> Query:
        return Query()


schema = strawberry.Schema(query=Query)

result = schema.execute_sync("query { foo { query { foo { bar } } } }")

assert result.data == {'foo': {'query': {'foo': {'bar': 42}}}}

Works fine (either run python <file> or strawberry export-schema <module>:schema).

However if you use the federation integration:

diff --git a/test/schema.py b/test/schema_federated.py
index f3b1a0d485eb..1bb2c1f5d101 100644
--- a/test/schema.py
+++ b/test/schema_federated.py
@@ -19,7 +19,7 @@ class Foo:
        return Query()


-schema = strawberry.Schema(query=Query)
+strawberry.federation.Schema(query=Query)

result = schema.execute_sync("query { foo { query { foo { bar } } } }")
from __future__ import annotations

import strawberry


@strawberry.type
class Query:
    @strawberry.field
    def foo(self) -> Foo:
        return Foo()


@strawberry.type
class Foo:
    bar: int = 42

    @strawberry.field
    def query(self) -> Query:
        return Query()


schema = strawberry.federation.Schema(query=Query)

result = schema.execute_sync("query { foo { query { foo { bar } } } }")

assert result.data == {'foo': {'query': {'foo': {'bar': 42}}}}

You get:

error: Type `Query` is defined multiple times in the schema                               
                                                                                            
       @ test/schema_federated.py:7                                                         
                                                                                            
     6 | @strawberry.type                                                                   
  ❱  7 | class Query:                                                                       
               ^^^^^ first class defined here                                               
     8 |     @strawberry.field                                                              
     9 |     def foo(self) -> Foo:                                                          
    10 |         return Foo()                                                               
                                                                                            
                                                                                            
  To fix this error you should either rename the type or remove the duplicated definition.  
                                                                                            
  Read more about this error on https://errors.strawberry.rocks/duplicated-type-name        

(the link isn't useful here as the issue isn't non unique names)

I've tracked it down to how the federation schema works, specifically _get_federation_query_type (1) which creates a new type that's purely internal and when the converter tries to match them up in validate_same_type_definition (2) the types aren't the same anymore, one is the type as defined in the consumer python module and the other is the internal type to which the federation fields have been added. When we reach the raise statement (3), the values extracted from the rich formatted exception are:

│ │  first_type_definition = StrawberryObjectDefinition(                                                                                                      │ │
│ │                          │   name='Query',                                                                                                                │ │
│ │                          │   is_input=False,                                                                                                              │ │
│ │                          │   is_interface=False,                                                                                                          │ │
│ │                          │   origin=<class 'strawberry.tools.merge_types.Query'>,                                                                         │ │
│ │                          │   description=None,                                                                                                            │ │
│ │                          │   interfaces=[],                                                                                                               │ │
│ │                          │   extend=False,                                                                                                                │ │
│ │                          │   directives=(),                                                                                                               │ │
│ │                          │   is_type_of=None,                                                                                                             │ │
│ │                          │   resolve_type=None,                                                                                                           │ │
│ │                          │   fields=[                                                                                                                     │ │
│ │                          │   │   Field(name='service',type=<class                                                                                         │ │
│ │                          'strawberry.federation.schema.Schema._get_federation_query_type.<locals>.Service'>,default=<dataclasses._MISSING_TYPE object at  │ │
│ │                          0x10487f2c0>,default_factory=<dataclasses._MISSING_TYPE object at                                                                │ │
│ │                          0x10487f2c0>,init=False,repr=False,hash=None,compare=False,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD),           │ │
│ │                          │   │   Field(name='foo',type=<class 'test.schema_federated.Foo'>,default=<dataclasses._MISSING_TYPE object at                   │ │
│ │                          0x10487f2c0>,default_factory=<dataclasses._MISSING_TYPE object at                                                                │ │
│ │                          0x10487f2c0>,init=False,repr=False,hash=None,compare=False,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD)            │ │
│ │                          │   ],                                                                                                                           │ │
│ │                          │   concrete_of=None,                                                                                                            │ │
│ │                          │   type_var_map={}                                                                                                              │ │
│ │                          )                                                                                                                                                                                                                                                   

and:

│ │ second_type_definition = StrawberryObjectDefinition(                                                                                                      │ │
│ │                          │   name='Query',                                                                                                                │ │
│ │                          │   is_input=False,                                                                                                              │ │
│ │                          │   is_interface=False,                                                                                                          │ │
│ │                          │   origin=<class 'test.schema_federated.Query'>,                                                                                │ │
│ │                          │   description=None,                                                                                                            │ │
│ │                          │   interfaces=[],                                                                                                               │ │
│ │                          │   extend=False,                                                                                                                │ │
│ │                          │   directives=(),                                                                                                               │ │
│ │                          │   is_type_of=None,                                                                                                             │ │
│ │                          │   resolve_type=None,                                                                                                           │ │
│ │                          │   fields=[                                                                                                                     │ │
│ │                          │   │   Field(name='foo',type=<class 'test.schema_federated.Foo'>,default=<dataclasses._MISSING_TYPE object at                   │ │
│ │                          0x10487f2c0>,default_factory=<dataclasses._MISSING_TYPE object at                                                                │ │
│ │                          0x10487f2c0>,init=False,repr=False,hash=None,compare=False,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD)            │ │
│ │                          │   ],                                                                                                                           │ │
│ │                          │   concrete_of=None,                                                                                                            │ │
│ │                          │   type_var_map={}                                                                                                              │ │
│ │                          )                                                                                                                                │ │

This feels like it should be supported with or without federation but as far as I could find, there's no currently supported way to have a field type be a forward reference to "the final Query type".

We haven't found a solution we're happy with yet, but it looks like either extracting the bits creating the new type so we can get a hold of a reference to the type or possibly a custom SchemaConverter could work as a workaround (will update when we have something working, hoping there's something that can be turned into a PR but reporting for now for tracking).

Also to note using lazy here doesn't help as it requires an importable type, but a variation/expansion on the lazy concept could work to express "reference to the final schema type", although probably can't be made type safe.

System Information

  • Operating system: Mac OS (but don't think that will matter here)
  • Strawberry version (if applicable): Latest version

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@lirsacc-mns lirsacc-mns added the bug Something isn't working label Oct 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant