Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,25 @@ class ApiB(ApiA):
# "extra_field" will be overridden and will be a float now instead of the String type declared in ModelB:
extra_field: float = strawberry.field(name="extraField")
```

### Relay connections

By default, StrawberrySQLAlchemyMapper() will create [Relay connections](https://relay.dev/graphql/connections.htm) for relationships to lists. If instead you want these relationships to present as plain lists, you have two options:

1. Declare `__use_list__` in your models, for example:

```python
@strawberry_sqlalchemy_mapper.type(models.Department)
class Department:
__use_list__ = ["employees"]
```

2. Alternatively, you can disable relay style connections for all models via the `always_use_list` constructor parameter:

```python
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper(always_use_list=True)
```

## Limitations

### Supported Types
Expand Down
6 changes: 6 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Release type: minor

Added a new optional constructor parameter to always use lists instead of relay Connections for relationships. Defaults to False, maintaining current functionality. If set to True, all relationships will be handled as lists.

Example:
mapper = StrawberrySQLAlchemyMapper(always_use_list=True)
10 changes: 8 additions & 2 deletions src/strawberry_sqlalchemy_mapper/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ class StrawberrySQLAlchemyMapper(Generic[BaseModelType]):
#: for a given (polymorphic base) model
model_to_interface_name: Callable[[Type[BaseModelType]], str]

#: If set to true, don't create connections for list type
#: relationships
always_use_list: bool

#: Default mapping from sqlalchemy types to strawberry types
_default_sqlalchemy_type_to_strawberry_type_map: Dict[
Type[TypeEngine], Union[Type[Any], SkipTypeSentinelT]
Expand Down Expand Up @@ -251,12 +255,14 @@ def __init__(
extra_sqlalchemy_type_to_strawberry_type_map: Optional[
Mapping[Type[TypeEngine], Type[Any]]
] = None,
always_use_list: Optional[bool] = False,
) -> None:
if model_to_type_name is None:
model_to_type_name = self._default_model_to_type_name
self.model_to_type_name = model_to_type_name
if model_to_interface_name is None:
model_to_interface_name = self._default_model_to_interface_name
self.always_use_list = always_use_list or False
self.model_to_interface_name = model_to_interface_name
self.sqlalchemy_type_to_strawberry_type_map = (
self._default_sqlalchemy_type_to_strawberry_type_map.copy()
Expand Down Expand Up @@ -401,7 +407,7 @@ def _convert_relationship_to_strawberry_type(
self._related_type_models.add(relationship_model)
if relationship.uselist:
# Use list if excluding relay pagination
if use_list:
if use_list or self.always_use_list:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo (blocking)
I think we should create tests to validate this changes, what do you think @gabor-lbl ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. When I run tox I get an error though:

❯ tox
.pkg: _optional_hooks> python /Users/gtorok/dev/strawberry-sqlalchemy/aaa/lib/python3.9/site-packages/pyproject_api/_backend.py True poetry.core.masonry.api
.pkg: get_requires_for_build_sdist> python /Users/gtorok/dev/strawberry-sqlalchemy/aaa/lib/python3.9/site-packages/pyproject_api/_backend.py True poetry.core.masonry.api
.pkg: get_requires_for_build_wheel> python /Users/gtorok/dev/strawberry-sqlalchemy/aaa/lib/python3.9/site-packages/pyproject_api/_backend.py True poetry.core.masonry.api
.pkg: prepare_metadata_for_build_wheel> python /Users/gtorok/dev/strawberry-sqlalchemy/aaa/lib/python3.9/site-packages/pyproject_api/_backend.py True poetry.core.masonry.api
.pkg: build_sdist> python /Users/gtorok/dev/strawberry-sqlalchemy/aaa/lib/python3.9/site-packages/pyproject_api/_backend.py True poetry.core.masonry.api
default: install_package> python -I -m pip install --force-reinstall --no-deps /Users/gtorok/dev/strawberry-sqlalchemy/.tox/.tmp/package/6/strawberry_sqlalchemy_mapper-0.7.0.tar.gz
default: commands[0]> pytest
default: failed with pytest is not allowed, use allowlist_externals to allow it
.pkg: _exit> python /Users/gtorok/dev/strawberry-sqlalchemy/aaa/lib/python3.9/site-packages/pyproject_api/_backend.py True poetry.core.masonry.api
  default: FAIL code 1 (1.53 seconds)
  evaluation failed :( (1.54 seconds)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never used tox or poetry before so please let me know if I'm doing something wrong.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gabor-lbl Dont need to bother with this, if it runs with pytest in your machine you can just push the changes that our CI will setup and run the tests again using nox in every python version we support 😉

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do I need to install to run pytest? I ran pytest on its own and it needed testing.postgres. I installed that but it still won't run:

❯ pytest
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --emoji --mypy-ini-file=mypy.ini
  inifile: /Users/gtorok/dev/strawberry-sqlalchemy/pyproject.toml
  rootdir: /Users/gtorok/dev/strawberry-sqlalchemy

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gabor-lbl Oh you need to have a postgres soo it can create the database.
I recommend you use a devcontainer since it automatically runs a postgres container
, we have one in our project just open your vscode, press f1 and select "Reopen in Container"
image

If you prefer, we can also talk in strawberry discord server, you can find me there as @gustavom0ta

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thanks - I added a new test to check the always_use_list param.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gabor-lbl , I'll review it later

return List[ForwardRef(type_name)] # type: ignore

return self._connection_type_for(type_name)
Expand Down Expand Up @@ -669,7 +675,7 @@ def connection_resolver_for(
passed from the GraphQL query to the database query.
"""
relationship_resolver = self.relationship_resolver_for(relationship)
if relationship.uselist and not use_list:
if relationship.uselist and not use_list and not self.always_use_list:
return self.make_connection_wrapper_resolver(
relationship_resolver,
relationship,
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ def base():
@pytest.fixture
def mapper():
return StrawberrySQLAlchemyMapper()


@pytest.fixture
def mapper_always_use_list():
return StrawberrySQLAlchemyMapper(always_use_list=True)
27 changes: 27 additions & 0 deletions tests/test_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,33 @@ class Department:
assert isinstance(name.type, StrawberryList) is True


def test_always_use_list(employee_and_department_tables, mapper_always_use_list):
Employee, Department = employee_and_department_tables

@mapper_always_use_list.type(Employee)
class Employee:
pass

@mapper_always_use_list.type(Department)
class Department:
pass

mapper_always_use_list.finalize()
additional_types = list(mapper_always_use_list.mapped_types.values())
assert len(additional_types) == 2
mapped_employee_type = additional_types[0]
assert mapped_employee_type.__name__ == "Employee"
mapped_department_type = additional_types[1]
assert mapped_department_type.__name__ == "Department"
assert len(mapped_department_type.__strawberry_definition__.fields) == 3
department_type_fields = mapped_department_type.__strawberry_definition__.fields

name = next((field for field in department_type_fields if field.name == "employees"), None)
assert name is not None
assert isinstance(name.type, StrawberryOptional) is False
assert isinstance(name.type, StrawberryList) is True


def test_type_relationships(employee_and_department_tables, mapper):
Employee, _ = employee_and_department_tables

Expand Down
Loading