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

Can't get relationships to work. Unexpected type ForwardRef? #97

Closed
dentatar opened this issue Dec 12, 2023 · 15 comments · Fixed by #106 or #137
Closed

Can't get relationships to work. Unexpected type ForwardRef? #97

dentatar opened this issue Dec 12, 2023 · 15 comments · Fixed by #106 or #137
Labels
bug Something isn't working

Comments

@dentatar
Copy link

dentatar commented Dec 12, 2023

python 3.11
sqlalchemy 2.0.23
strawberry-graphql 0.216.0
strawberry-sqlalchemy-mapper 0.4.0

Trying to use this mapper and was playing around with some basics but can't get relationships to work. It doesn't seem like i do something wrong, this is literally the example from readme:

models.py

from sqlalchemy import Column, UUID, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()


class Employee(Base):
    __tablename__ = "employee"
    id = Column(UUID, primary_key=True)
    name = Column(String, nullable=False)
    password_hash = Column(String, nullable=False)
    department_id = Column(UUID, ForeignKey("department.id"))
    department = relationship("Department", back_populates="employees")


class Department(Base):
    __tablename__ = "department"
    id = Column(UUID, primary_key=True)
    name = Column(String, nullable=False)
    employees = relationship("Employee", back_populates="department")

app.py

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
import strawberry, models
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper

strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()


@strawberry_sqlalchemy_mapper.type(models.Employee)
class Employee:
    __exclude__ = ["password_hash"]


@strawberry_sqlalchemy_mapper.type(models.Department)
class Department:
    pass


@strawberry.type
class Query:
    @strawberry.field
    def departments(self) -> Department:
        return Department(name="Test", employees=[Employee(name="1"), Employee(name="2")])


strawberry_sqlalchemy_mapper.finalize()
schema = strawberry.Schema(
    query=Query,
)
app = FastAPI()
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix='/graphql')

main.py

import uvicorn
from app import app

if __name__ == "__main__":
    uvicorn.run(app, port=5000, log_level="info")

Got this error:

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 808, in fields
    fields = resolve_thunk(self._fields)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 300, in resolve_thunk
    return thunk() if callable(thunk) else thunk
           ^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 514, in <lambda>
    fields=lambda: self.get_graphql_fields(object_type),
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 373, in get_graphql_fields
    return _get_thunk_mapping(
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 133, in _get_thunk_mapping
    thunk_mapping[name_converter(field)] = field_converter(
                                           ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 314, in from_field
    self.from_maybe_optional(
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 768, in from_maybe_optional
    return GraphQLNonNull(self.from_type(type_))
                          ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 801, in from_type
    raise TypeError(f"Unexpected type '{type_}'")
TypeError: Unexpected type 'ForwardRef('Employee')'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/app/app.py", line 28, in <module>
    schema = strawberry.Schema(
             ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema.py", line 141, in __init__
    self._schema = GraphQLSchema(
                   ^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 224, in __init__
    collect_referenced_types(query)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 432, in collect_referenced_types
    for field in named_type.fields.values():
                 ^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/functools.py", line 1001, in __get__
    val = self.func(instance)
          ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 811, in fields
    raise cls(f"{self.name} fields cannot be resolved. {error}") from error
TypeError: EmployeeEdge fields cannot be resolved. Unexpected type 'ForwardRef('Employee')'

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
@dentatar dentatar added the bug Something isn't working label Dec 12, 2023
@Gianfranco753
Copy link

+1

Have the same exception

1 similar comment
@0x587
Copy link

0x587 commented Dec 20, 2023

+1

Have the same exception

@johansigfrids
Copy link

I have the same issue. Downgrading to 0.3.1 fixed it, so I guess something in 0.4.0 broke it.

@erikwrede
Copy link
Member

Thanks for the report! I'll try to look into it this weekend. Has anyone gathered some more detail of what could have caused the issue? That would really help me dive straight into the fixing 🙂

@erikwrede
Copy link
Member

I've investigated further. The issue is this change, specifically L289. The node type names are now passed as a ForwardRef to the generic. This ref is not resolved correctly.

def _edge_type_for(self, type_name: str) -> Type[Any]:
"""
Get or create a corresponding Edge model for the given type
(to support future pagination)
"""
edge_name = f"{type_name}Edge"
if edge_name not in self.edge_types:
self.edge_types[edge_name] = edge_type = strawberry.type(
dataclasses.make_dataclass(
edge_name,
[],
bases=(relay.Edge[type_name],), # type: ignore[valid-type]
)
)
setattr(edge_type, _GENERATED_FIELD_KEYS_KEY, ["node"])
return self.edge_types[edge_name]

Since relay.Edge is generic, we save the value of the TypeVar for the node type inside of StrawberryField.specialized_type_var_map to later replace it in StrawberryField.resolve_type:

https://github.com/strawberry-graphql/strawberry/blob/aad305b0036de8bfd4d08ef9571aee68f04eb0d1/strawberry/field.py#L360-L375

The problem in this case: the mapped type is not a StrawberryType, but a ForwardRef equaling the type name we expect it to be. This cannot be resolved, because the generic resolver does not convert the ForwardRef into a StrawberryAnnotation to call evaluate on it.

I did this as a test, but this yielded another error: the type Employee is not defined in the scope. This is because we have no strawberry.lazy info about the actual location of the type Employee because we just expect that it will exist under this name later.

I'm still looking for a quick and easy fix, but long-term I see this being fixed through one of these options:

  • Have a type registry for all mapped types and a custom lazy type that utilizes this registry
  • Adopt strawberry-django's behavior /cc @bellini666 how are you implementing lazy refs in relationships?

For me personally, a central registry is the preferred approach since it gives full control over the way we access types. This is a mid-term effort and requires heavy refactoring, so having something that works short-term to fix the occurring issues would be preferable.

/cc @mattalbr

@bellini666
Copy link
Member

Adopt strawberry-django's behavior /cc @bellini666 how are you implementing lazy refs in relationships?

Actually in strawberry django all relations and the types to those relations should be created explicitly: https://strawberry-graphql.github.io/strawberry-graphql-django/guide/fields/#relationships , and thus, those can be done using Lazy types

If we try to map a relationship using auto or passing to the fields list, we map them to a type that only has a pk attribute (https://github.com/strawberry-graphql/strawberry-graphql-django/blob/main/strawberry_django/fields/types.py#L250) or to the Node interface when using relay. But we usually suggest users to map relations explicitly

Having said that, based on how this code works, a type registry is probably the way to go for this.

TBH, I'm not totally familiar with this code but doesn't something like that already exists when creating the Edge/Connection? i.e. relay.Edge[self.mapped_types[type_name]]

Also, it seems that I'm the one who caused this issue so I can try to fix it as well :)

bellini666 added a commit to bellini666/strawberry-sqlalchemy that referenced this issue Dec 26, 2023
Use a lazy type to properly resolve and specialize the `Edge`/`Connection`
types that are created from the model.

The reason we are using a subclass of `LazyType` is because the original
one expects a path to be passed to be used to import it later. We don't
have one here, but we do have a mapper containing all the created types
that can be retrieved later.

Fix strawberry-graphql#97
@bellini666
Copy link
Member

This should fix the issue: #106

bellini666 added a commit to bellini666/strawberry-sqlalchemy that referenced this issue Dec 26, 2023
Use a lazy type to properly resolve and specialize the `Edge`/`Connection`
types that are created from the model.

The reason we are using a subclass of `LazyType` is because the original
one expects a path to be passed to be used to import it later. We don't
have one here, but we do have a mapper containing all the created types
that can be retrieved later.

Fix strawberry-graphql#97
erikwrede pushed a commit that referenced this issue Dec 27, 2023
Use a lazy type to properly resolve and specialize the `Edge`/`Connection`
types that are created from the model.

The reason we are using a subclass of `LazyType` is because the original
one expects a path to be passed to be used to import it later. We don't
have one here, but we do have a mapper containing all the created types
that can be retrieved later.

Fix #97
@erikwrede erikwrede reopened this Dec 27, 2023
@erikwrede
Copy link
Member

erikwrede commented Dec 27, 2023

I've merged #106 to ensure a quick fix for the affected projects. We can keep this issue open until we are certain about the definitive future solution for this. The use of lazy types is exactly what I've envisioned, but I'm looking forward to hear matt's opinion on the matter.

@dentatar @Gianfranco753 @0x587 @johansigfrids please try out the new release 0.4.1 and let us know if you encounter any additional issues. Thanks for reporting the bug! 🙂

@mattalbr
Copy link
Contributor

I'm trying to catch up here, but you two (Erik and Thiago) understand strawberry's typing system WAY better than me. I really need to hop in and play around with Thiago's new relay code to get a better understanding. That said, I took a look at #106 and it looks good to me. I appreciate you both!

@johansigfrids
Copy link

Well, I finally had time to test it out and it is not quite working yet. The ForwardRef error is gone and it starts up fine, but attempting to query the connection results in the following error:

EmployeeEdge.__init__() missing 1 required keyword-only argument: 'cursor'

GraphQL request:4:5
3 |     name
4 |     employees {
  |     ^
5 |       edges {
Traceback (most recent call last):
  File "/Users/johansigfrids/mapper_test/venv/lib/python3.10/site-packages/graphql/execution/execute.py", line 528, in await_result
    return_type, field_nodes, info, path, await result
  File "/Users/johansigfrids/mapper_test/venv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 675, in _async_resolver
    return await await_maybe(
  File "/Users/johansigfrids/mapper_test/venv/lib/python3.10/site-packages/strawberry/utils/await_maybe.py", line 12, in await_maybe
    return await value
  File "/Users/johansigfrids/mapper_test/venv/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 467, in wrapper
    edges=[
  File "/Users/johansigfrids/mapper_test/venv/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 468, in <listcomp>
    edge_type(
TypeError: EmployeeEdge.__init__() missing 1 required keyword-only argument: 'cursor'

@Philaeux
Copy link

Philaeux commented Jan 8, 2024

Got the same issues in my project. A one-to-many relationship not working with missing 1 required keyword-only argument: 'cursor'.
My simple Schema:

class A(Base):
    __tablename__ = "a"

    id: Mapped[str] = mapped_column(primary_key=True, index=True)
    bs: Mapped[List["B"]] = relationship(back_populates="a")


class B(Base):
    __tablename__ = "b"

    id: Mapped[str] = mapped_column(primary_key=True, index=True)
    a_id: Mapped[str] = mapped_column(ForeignKey("a.id"), nullable=False)

    a: Mapped[A] = relationship(back_populates="bs")

Downgrading from 0.4.2 to 0.3.1 fixes relationships for the time being.
I don't know the project enough yet to help you on this.

@bellini666
Copy link
Member

Ok, here is what is going on here:

When trying to instantiate the connection and the edges in here we are missing some attributes:

  • edge_type is a subclass of strawberry's relay.Edge, meaning it requires a node and a cursor (we are missing the cursor)
  • connection_type is a subclass of strawberry's relay.Connection, meaning it requires a list of edges and a page_info (we are missing the page_info)

This is because in my relay PR I changed the Edge we create to use relay.Edge as its base and I also changed the Connection we create to use relay.Connection as its base

Because we didn't have any tests for the resolvers themselves I didn't even see that they were missing those.

We have 2 options to solve that:

  1. Remove the bases from those places so that the auto generated edges/connections will not require cursor/page_info. This is the easiest way to fix this but it would also make automatic connections a lot different from explicitly defined connections.

  2. Add that missing info, but where should we retrieve that from? From what I can tell the dataloader approach is not actually using any pagination information (i.e. it will just load everything from the database), meaning adding a cursor to it is mostly useless (unless we change how it works)

@mattalbr @erikwrede thoughts about this?

@y-nosa
Copy link

y-nosa commented Jan 16, 2024

Got the same issues in my project. A one-to-many relationship not working with missing 1 required keyword-only argument: 'cursor'. My simple Schema:

class A(Base):
    __tablename__ = "a"

    id: Mapped[str] = mapped_column(primary_key=True, index=True)
    bs: Mapped[List["B"]] = relationship(back_populates="a")


class B(Base):
    __tablename__ = "b"

    id: Mapped[str] = mapped_column(primary_key=True, index=True)
    a_id: Mapped[str] = mapped_column(ForeignKey("a.id"), nullable=False)

    a: Mapped[Cabinet] = relationship(back_populates="bs")

Downgrading from 0.4.2 to 0.3.1 fixes relationships for the time being. I don't know the project enough yet to help you on this.

Same problem, same resolution

@Philaeux
Copy link

Friendly bump, the issue prevents lib update.

@bellini666
Copy link
Member

Friendly bump, the issue prevents lib update.

Will take a look at this during the weekend.

@erikwrede @mattalbr could you take a look at my comment in #97 (comment)?

I'm leaning towards the first option in there, but I would appreciate your thoughts 😊

bellini666 added a commit to bellini666/strawberry-sqlalchemy that referenced this issue Mar 16, 2024
We changed auto generated edges/connections to inherit from
`relay.Edge`/`relay.Connection`, which requires some extra fields to be
instantiated.

Fix strawberry-graphql#97
bellini666 added a commit to bellini666/strawberry-sqlalchemy that referenced this issue Mar 16, 2024
We changed auto generated edges/connections to inherit from
`relay.Edge`/`relay.Connection`, which requires some extra fields to be
instantiated.

Fix strawberry-graphql#97
bellini666 added a commit to bellini666/strawberry-sqlalchemy that referenced this issue Mar 16, 2024
We changed auto generated edges/connections to inherit from
`relay.Edge`/`relay.Connection`, which requires some extra fields to be
instantiated.

Fix strawberry-graphql#97
yovel-clutch pushed a commit to clutchsecurity/strawberry-sqlalchemy that referenced this issue May 5, 2024
We changed auto generated edges/connections to inherit from
`relay.Edge`/`relay.Connection`, which requires some extra fields to be
instantiated.

Fix strawberry-graphql#97
patrick91 pushed a commit that referenced this issue May 22, 2024
We changed auto generated edges/connections to inherit from
`relay.Edge`/`relay.Connection`, which requires some extra fields to be
instantiated.

Fix #97
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
9 participants