igorbenav / fastcrud

FastCRUD is a Python package for FastAPI, offering robust async CRUD operations and flexible endpoint creation utilities.
MIT License
530 stars 32 forks source link

`nest_joins` only works if `join_prefix` is set #64

Closed waza-ari closed 1 month ago

waza-ari commented 2 months ago

Describe the bug or question Attributes from a joined model are not nested if no join_prefix is set

To Reproduce

Consider two simple models, Hero and Ability

class Ability(Base, UUIDMixin, TimestampMixin, SoftDeleteMixin):
    __tablename__ = "abilities"

    name: Mapped[str] = mapped_column(nullable=False)
    strength: Mapped[int] = mapped_column(nullable=False)
    heroes: Mapped[list["Hero"]] = relationship(back_populates="ability")

class Hero(Base, UUIDMixin, TimestampMixin, SoftDeleteMixin):
    __tablename__ = "heroes"

    name: Mapped[str] = mapped_column(nullable=False)
    ability_id: Mapped[int] = mapped_column(ForeignKey("abilities.id"))
    ability: Mapped["Ability"] = relationship(back_populates="heroes")

Then, this code does not work as expected:

heroes = await crud_hero.get_multi_joined(db, join_model=Ability, nest_joins=True)

Returns this (unrelated columns are removed):

{
    "data": [
        {
            "name": "Diana",
            "ability_id": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
            "id": UUID("8212bccb-ce20-489a-a675-45772ad60eb8"),
            "name_1": "Superstrength",
            "strength": 10,
            "id_1": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
        },
    ],
    "total_count": 2,
}

When adding a prefix, it (kinda) works:

heroes = await crud_hero.get_multi_joined(db, join_model=Ability, join_prefix="ability_", nest_joins=True)

Result is looking better

{
    "data": [
        {
            "name": "Diana",
            "ability": {
                "id": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
                "name": "Superstrength",
                "strength": 10,
                "id_1": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
            },
            "id": UUID("8212bccb-ce20-489a-a675-45772ad60eb8"),
        },
    ],
    "total_count": 2,
}
igorbenav commented 2 months ago

In _nest_join_data I use join.join_prefix as the key for the data, so it doesn't work if it's not passed. It could be just changed to the model's name or the join_prefix as well and it will fix this issue if join_prefix is None.

def _nest_join_data(
    data: dict[str, Any], join_definitions: list[JoinConfig]
) -> dict[str, Any]:
    nested_data: dict = {}
    for key, value in data.items():
        nested = False
        for join in join_definitions:
            if join.join_prefix and key.startswith(join.join_prefix):
                nested_key = join.join_prefix.rstrip("_") # here defining nested_key
                nested_field = key[len(join.join_prefix) :]
                if nested_key not in nested_data:
                    nested_data[nested_key] = {}
                nested_data[nested_key][nested_field] = value # and using nested_key as the key
                nested = True
                break
        if not nested:
            nested_data[key] = value
    return nested_data