diff --git a/README.md b/README.md index 538bdca..01bc519 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..c9a24a0 --- /dev/null +++ b/RELEASE.md @@ -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) diff --git a/src/strawberry_sqlalchemy_mapper/mapper.py b/src/strawberry_sqlalchemy_mapper/mapper.py index 94b0305..d522928 100644 --- a/src/strawberry_sqlalchemy_mapper/mapper.py +++ b/src/strawberry_sqlalchemy_mapper/mapper.py @@ -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] @@ -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() @@ -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: return List[ForwardRef(type_name)] # type: ignore return self._connection_type_for(type_name) @@ -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, diff --git a/tests/conftest.py b/tests/conftest.py index 09c9c6e..76d87f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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) diff --git a/tests/test_mapper.py b/tests/test_mapper.py index faa18a2..cde780f 100644 --- a/tests/test_mapper.py +++ b/tests/test_mapper.py @@ -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