Skip to content
Merged
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)
Comment thread
Ckk3 marked this conversation as resolved.
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,
Comment thread
Ckk3 marked this conversation as resolved.
always_use_list: 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
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:
Comment thread
Ckk3 marked this conversation as resolved.
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)
62 changes: 62 additions & 0 deletions tests/test_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,68 @@ 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_always_use_list_with_mixed_relationships(
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"

department_type_fields = mapped_department_type.__strawberry_definition__.fields
employees_field = next((f for f in department_type_fields if f.name == "employees"), None)
assert employees_field is not None
# List relationship should be StrawberryList with always_use_list=True
assert isinstance(employees_field.type, StrawberryList)

employee_type_fields = mapped_employee_type.__strawberry_definition__.fields
department_field = next((f for f in employee_type_fields if f.name == "department"), None)
assert department_field is not None
# Single relationship should remain as Optional, not converted to a list
assert not isinstance(department_field.type, StrawberryList)
assert isinstance(department_field.type, StrawberryOptional)


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

Expand Down
Loading