Skip to content

Commit fe9df11

Browse files
gabor-lblsourcery-ai[bot]Ckk3
authored
Option to always "use_list" and not make relay Connections (#257)
* Option to always use_list and not make relay Connections * Update src/strawberry_sqlalchemy_mapper/mapper.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * RELEASE.md * moved always_use_list optinal param to last param. Defaults to False, instead of None. Updated README. * new unit test for always_use_list * Refactor always_use_list parameter to be non-optional and update related tests for mixed relationships * Refactor test_always_use_list_with_mixed_relationships to use clearer class names for Employee and Department * fix tests --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Ckk3 <[email protected]>
1 parent 6bb53b2 commit fe9df11

File tree

5 files changed

+100
-2
lines changed

5 files changed

+100
-2
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,25 @@ class ApiB(ApiA):
192192
# "extra_field" will be overridden and will be a float now instead of the String type declared in ModelB:
193193
extra_field: float = strawberry.field(name="extraField")
194194
```
195+
196+
### Relay connections
197+
198+
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:
199+
200+
1. Declare `__use_list__` in your models, for example:
201+
202+
```python
203+
@strawberry_sqlalchemy_mapper.type(models.Department)
204+
class Department:
205+
__use_list__ = ["employees"]
206+
```
207+
208+
2. Alternatively, you can disable relay style connections for all models via the `always_use_list` constructor parameter:
209+
210+
```python
211+
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper(always_use_list=True)
212+
```
213+
195214
## Limitations
196215

197216
### Supported Types

RELEASE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Release type: minor
2+
3+
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.
4+
5+
Example:
6+
mapper = StrawberrySQLAlchemyMapper(always_use_list=True)

src/strawberry_sqlalchemy_mapper/mapper.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ class StrawberrySQLAlchemyMapper(Generic[BaseModelType]):
204204
#: for a given (polymorphic base) model
205205
model_to_interface_name: Callable[[Type[BaseModelType]], str]
206206

207+
#: If set to true, don't create connections for list type
208+
#: relationships
209+
always_use_list: bool
210+
207211
#: Default mapping from sqlalchemy types to strawberry types
208212
_default_sqlalchemy_type_to_strawberry_type_map: Dict[
209213
Type[TypeEngine], Union[Type[Any], SkipTypeSentinelT]
@@ -251,12 +255,14 @@ def __init__(
251255
extra_sqlalchemy_type_to_strawberry_type_map: Optional[
252256
Mapping[Type[TypeEngine], Type[Any]]
253257
] = None,
258+
always_use_list: bool = False,
254259
) -> None:
255260
if model_to_type_name is None:
256261
model_to_type_name = self._default_model_to_type_name
257262
self.model_to_type_name = model_to_type_name
258263
if model_to_interface_name is None:
259264
model_to_interface_name = self._default_model_to_interface_name
265+
self.always_use_list = always_use_list
260266
self.model_to_interface_name = model_to_interface_name
261267
self.sqlalchemy_type_to_strawberry_type_map = (
262268
self._default_sqlalchemy_type_to_strawberry_type_map.copy()
@@ -401,7 +407,7 @@ def _convert_relationship_to_strawberry_type(
401407
self._related_type_models.add(relationship_model)
402408
if relationship.uselist:
403409
# Use list if excluding relay pagination
404-
if use_list:
410+
if use_list or self.always_use_list:
405411
return List[ForwardRef(type_name)] # type: ignore
406412

407413
return self._connection_type_for(type_name)
@@ -669,7 +675,7 @@ def connection_resolver_for(
669675
passed from the GraphQL query to the database query.
670676
"""
671677
relationship_resolver = self.relationship_resolver_for(relationship)
672-
if relationship.uselist and not use_list:
678+
if relationship.uselist and not use_list and not self.always_use_list:
673679
return self.make_connection_wrapper_resolver(
674680
relationship_resolver,
675681
relationship,

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,8 @@ def base():
118118
@pytest.fixture
119119
def mapper():
120120
return StrawberrySQLAlchemyMapper()
121+
122+
123+
@pytest.fixture
124+
def mapper_always_use_list():
125+
return StrawberrySQLAlchemyMapper(always_use_list=True)

tests/test_mapper.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,68 @@ class Department:
276276
assert isinstance(name.type, StrawberryList) is True
277277

278278

279+
def test_always_use_list(employee_and_department_tables, mapper_always_use_list):
280+
Employee, Department = employee_and_department_tables
281+
282+
@mapper_always_use_list.type(Employee)
283+
class Employee:
284+
pass
285+
286+
@mapper_always_use_list.type(Department)
287+
class Department:
288+
pass
289+
290+
mapper_always_use_list.finalize()
291+
additional_types = list(mapper_always_use_list.mapped_types.values())
292+
assert len(additional_types) == 2
293+
mapped_employee_type = additional_types[0]
294+
assert mapped_employee_type.__name__ == "Employee"
295+
mapped_department_type = additional_types[1]
296+
assert mapped_department_type.__name__ == "Department"
297+
assert len(mapped_department_type.__strawberry_definition__.fields) == 3
298+
department_type_fields = mapped_department_type.__strawberry_definition__.fields
299+
300+
name = next((field for field in department_type_fields if field.name == "employees"), None)
301+
assert name is not None
302+
assert isinstance(name.type, StrawberryOptional) is False
303+
assert isinstance(name.type, StrawberryList) is True
304+
305+
306+
def test_always_use_list_with_mixed_relationships(
307+
employee_and_department_tables, mapper_always_use_list
308+
):
309+
Employee, Department = employee_and_department_tables
310+
311+
@mapper_always_use_list.type(Employee)
312+
class Employee:
313+
pass
314+
315+
@mapper_always_use_list.type(Department)
316+
class Department:
317+
pass
318+
319+
mapper_always_use_list.finalize()
320+
additional_types = list(mapper_always_use_list.mapped_types.values())
321+
assert len(additional_types) == 2
322+
mapped_employee_type = additional_types[0]
323+
assert mapped_employee_type.__name__ == "Employee"
324+
mapped_department_type = additional_types[1]
325+
assert mapped_department_type.__name__ == "Department"
326+
327+
department_type_fields = mapped_department_type.__strawberry_definition__.fields
328+
employees_field = next((f for f in department_type_fields if f.name == "employees"), None)
329+
assert employees_field is not None
330+
# List relationship should be StrawberryList with always_use_list=True
331+
assert isinstance(employees_field.type, StrawberryList)
332+
333+
employee_type_fields = mapped_employee_type.__strawberry_definition__.fields
334+
department_field = next((f for f in employee_type_fields if f.name == "department"), None)
335+
assert department_field is not None
336+
# Single relationship should remain as Optional, not converted to a list
337+
assert not isinstance(department_field.type, StrawberryList)
338+
assert isinstance(department_field.type, StrawberryOptional)
339+
340+
279341
def test_type_relationships(employee_and_department_tables, mapper):
280342
Employee, _ = employee_and_department_tables
281343

0 commit comments

Comments
 (0)