Skip to content

Mapper cannot access relationship direction attribute #243

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

Open
david-1792 opened this issue Apr 30, 2025 · 2 comments
Open

Mapper cannot access relationship direction attribute #243

david-1792 opened this issue Apr 30, 2025 · 2 comments
Labels
awaiting author response Awaiting response from issue opener bug Something isn't working

Comments

@david-1792
Copy link

I am running the mapper using async SQLAlchemy with a simple one-to-many relationship like the following.

# app.api.teams.models
class Team(Base):
    __tablename__ = 'team'

    id: Mapped[uuid.UUID] = mapped_column(types.UUID, primary_key=True, default=uuid.uuid4)

    name: Mapped[str]
    headquarters: Mapped[str]

# app.api.teams.graphql.types
@graphql_mapper.type(m.Team)
class Team:
    pass

# app.api.heroes.models
class Hero(Base):
    __tablename__ = 'hero'

    id: Mapped[uuid.UUID] = mapped_column(types.UUID, primary_key=True, default=uuid.uuid4)

    name: Mapped[str]
    secret_name: Mapped[str]
    age: Mapped[int]

    team_id: Mapped[uuid.UUID] = mapped_column(types.UUID, ForeignKey('team.id'))
    team: Mapped["Team"] = relationship()

# app.api.heroes.graphql.types
@graphql_mapper.type(m.Hero)
class Hero:
    __exclude__ = ['secret_name']

This example works fine and I can access the Team type through the Hero type. The problem arises when I create the heroes attribute on the Team class (to make the relationship bi-lateral)

# app.api.teams.models
def resolve_hero():
    from app.api.heroes.models import Hero
    return Hero

class Team(Base):
    __tablename__ = 'team'

    id: Mapped[uuid.UUID] = mapped_column(types.UUID, primary_key=True, default=uuid.uuid4)

    name: Mapped[str]
    headquarters: Mapped[str]

    heroes: Mapped[List["Hero"]] = relationship(resolve_hero, back_populates='team')

# app.api.heroes.models
class Hero(Base):
    __tablename__ = 'hero'

    id: Mapped[uuid.UUID] = mapped_column(types.UUID, primary_key=True, default=uuid.uuid4)

    name: Mapped[str]
    secret_name: Mapped[str]
    age: Mapped[int]

    team_id: Mapped[uuid.UUID] = mapped_column(types.UUID, ForeignKey('team.id'))
    team: Mapped["Team"] = relationship("Team", back_populates='heroes')

At this moment, the app does not deploy and I get the following error message.

  File "/app/app/api/heroes/graphql/types.py", line 13, in <module>
    @graphql_mapper.type(m.Hero)
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.12/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 699, in convert
    strawberry_type = self._convert_relationship_to_strawberry_type(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.12/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 397, in _convert_relationship_to_strawberry_type
    if self._get_relationship_is_optional(relationship):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.12/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 406, in _get_relationship_is_optional
    if relationship.direction in [ONETOMANY, MANYTOMANY]:
       ^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.12/site-packages/sqlalchemy/util/langhelpers.py", line 1332, in __getattr__
    return self._fallback_getattr(key)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/.venv/lib/python3.12/site-packages/sqlalchemy/util/langhelpers.py", line 1301, in _fallback_getattr
    raise AttributeError(key)
AttributeError: direction

I am using strawberry v0.266.0 and strawberry-sqlalchemy-mapper v.0.6.0

@david-1792 david-1792 added the bug Something isn't working label Apr 30, 2025
@Ckk3
Copy link
Contributor

Ckk3 commented May 6, 2025

Hi, @david-1792 , thanks so much for your description it really helps the investigation!
Please fell free to work on a PR if you find the solution, I will check it on the weekend 😉

@Ckk3
Copy link
Contributor

Ckk3 commented May 23, 2025

Hello, @david-1792 , I couldn't got success replicating this issue, I think that maybe you forgot to add the async_bind_factorry on the context_value , please take a look at the test I've made:
I tested on both 0.6.0 and 0.6.1 version.

import uuid
from sqlalchemy import types
from sqlalchemy import orm
from sqlalchemy.orm import (
    relationship,
    Mapped,
    mapped_column,
)
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyLoader


@pytest.fixture
def base():
    return orm.declarative_base()


@pytest.fixture
def mapper():
    return StrawberrySQLAlchemyMapper()


@pytest.fixture
def direction_relationship_table(base):
    class Hero(base):
        __tablename__ = "hero"

        id: Mapped[uuid.UUID] = mapped_column(
            types.UUID, primary_key=True, default=uuid.uuid4
        )

        name: Mapped[str]
        secret_name: Mapped[str]
        age: Mapped[int]

        team_id: Mapped[uuid.UUID] = mapped_column(types.UUID, ForeignKey("team.id"))
        team: Mapped["Team"] = relationship("Team", back_populates="heroes")

    def resolve_hero():
        return Hero

    class Team(base):
        __tablename__ = "team"

        id: Mapped[uuid.UUID] = mapped_column(
            types.UUID, primary_key=True, default=uuid.uuid4
        )

        name: Mapped[str]
        headquarters: Mapped[str]

        heroes: Mapped[List["Hero"]] = relationship(resolve_hero, back_populates="team")

    return Team, Hero


async def test_direction_relationship(
    direction_relationship_table, mapper, async_engine, base, async_sessionmaker
):
    # async_engine and async_sessionmaker are fixture from tests/conftest.py 
    async with async_engine.begin() as conn:
        await conn.run_sync(base.metadata.create_all)

    TeamModel, HeroModel = direction_relationship_table

    @mapper.type(TeamModel)
    class Team:
        pass

    @mapper.type(HeroModel)
    class Hero:
        __exclude__ = ["secret_name"]

    @strawberry.type
    class Query:
        @strawberry.field
        async def heroes(self) -> Hero:
            session = async_sessionmaker()
            return await session.get(
                HeroModel, uuid.UUID("1832dd83-79b0-499b-a6d4-42797f60e72a")
            )

    mapper.finalize()
    schema = strawberry.Schema(query=Query)

Its good to you know that I've tested a query with this code and it run as expected:

query = """\
    query GetHero {
        heroes {
            id
            name
            age
            team {
                id
                name
                headquarters
                heroes {
                    pageInfo {
                        hasNextPage
                        hasPreviousPage
                        startCursor
                        endCursor
                    }
                    edges {
                        cursor
                        node {
                            id
                            name
                            age
                        }
                    }
                }
            }
        }
    }
    """

    async with async_sessionmaker(expire_on_commit=False) as session:
        team = TeamModel(name="Avengers", headquarters="New York")
        hero = HeroModel(
            id=uuid.UUID("1832dd83-79b0-499b-a6d4-42797f60e72a"),
            name="Iron Man",
            secret_name="Tony Stark",
            age=45,
            team=team,  # Associate Hero with the Team
        )

        session.add_all([team, hero])
        await session.commit()
        # breakpoint()
        result = await schema.execute(
            query,
            context_value={
                "sqlalchemy_loader": StrawberrySQLAlchemyLoader(
                    async_bind_factory=async_sessionmaker
                )
            },
        )
        assert result.errors is None
        # result.data = {'heroes': {'id': '1832dd83-79b0-499b-a6d4-42797f60e72a', 'name': 'Iron Man', 'age': 45, 'team': {'id': 'b6a57fbd-bd1d-4029-80be-df33fed989db', 'name': 'Avengers', 'headquarters': 'New York', 'heroes': {'pageInfo': {'hasNextPage': False, 'hasPreviousPage': False, 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjA='}, 'edges': [{'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'node': {'id': '1832dd83-79b0-499b-a6d4-42797f60e72a', 'name': 'Iron Man', 'age': 45}}]}}}}

@Ckk3 Ckk3 added question Further information is requested awaiting author response Awaiting response from issue opener and removed question Further information is requested labels May 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting author response Awaiting response from issue opener bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants