diff --git a/docs/guide/mutations.md b/docs/guide/mutations.md index 3506de12..90186770 100644 --- a/docs/guide/mutations.md +++ b/docs/guide/mutations.md @@ -184,3 +184,20 @@ class Mutation: schema = strawberry.Schema(mutation=Mutation) ``` + +## Batching + +If you need to make multiple creates, updates, or deletes as part of one atomic mutation you can use batching. Batching has a similar syntax except that the mutations take and return a list. + +```python title="schema.py" +import strawberry +from strawberry_django import mutations + +@strawberry.type +class Mutation: + createFruits: list[Fruit] = mutations.create(list[FruitPartialInput]) + updateFruits: list[Fruit] = mutations.update(list[FruitPartialInput]) + deleteFruits: list[Fruit] = mutations.delete(list[FruitPartialInput]) + +schema = strawberry.Schema(mutation=Mutation) +``` diff --git a/strawberry_django/mutations/fields.py b/strawberry_django/mutations/fields.py index 5296cd37..09db62fe 100644 --- a/strawberry_django/mutations/fields.py +++ b/strawberry_django/mutations/fields.py @@ -215,7 +215,6 @@ def arguments(self): argument( self.argument_name, self.input_type, - is_list=self.is_list and isinstance(self, DjangoCreateMutation), ), ] @@ -272,6 +271,10 @@ def create(self, data: dict[str, Any], *, info: Info): ) +def get_vdata(data: Any) -> dict[str, Any]: + return vars(data).copy() if data is not None else {} + + def get_pk( data: dict[str, Any], *, @@ -300,13 +303,25 @@ def resolver( ) -> Any: assert info is not None + data: list[Any] | Any = kwargs.get(self.argument_name) + + if isinstance(data, list): + return [self.instance_level_update(info, kwargs, d) for d in data] + + return self.instance_level_update(info, kwargs, data) + + def instance_level_update( + self, + info: Info, + kwargs: dict[str, Any], + data: Any, + ) -> Any: model = self.django_model assert model is not None - data: Any = kwargs.get(self.argument_name) - vdata = vars(data).copy() if data is not None else {} - + vdata = get_vdata(data) pk = get_pk(vdata, key_attr=self.key_attr) + if pk not in (None, UNSET): # noqa: PLR6201 instance = get_with_perms( pk, @@ -366,7 +381,7 @@ def resolver( assert model is not None data: Any = kwargs.get(self.argument_name) - vdata = vars(data).copy() if data is not None else {} + vdata = get_vdata(data) pk = get_pk(vdata, key_attr=self.key_attr) if pk not in (None, UNSET): # noqa: PLR6201 diff --git a/tests/mutations/conftest.py b/tests/mutations/conftest.py index 16f1f437..d6ca439c 100644 --- a/tests/mutations/conftest.py +++ b/tests/mutations/conftest.py @@ -35,7 +35,8 @@ class FruitFilter: @strawberry.type class Mutation: create_fruit: Fruit = mutations.create(FruitInput) - create_fruits: list[Fruit] = mutations.create(FruitInput) + create_fruits: list[Fruit] = mutations.create(list[FruitInput]) + patch_fruits: list[Fruit] = mutations.update(list[FruitPartialInput], key_attr="id") update_fruits: list[Fruit] = mutations.update( FruitPartialInput, filters=FruitFilter, key_attr="id" ) diff --git a/tests/mutations/test_batch_mutations.py b/tests/mutations/test_batch_mutations.py new file mode 100644 index 00000000..48615a81 --- /dev/null +++ b/tests/mutations/test_batch_mutations.py @@ -0,0 +1,117 @@ +"""Tests for batched mutations. + +Batched mutations are mutations that mutate multiple objects at once. +Mutations with a filter function or accept a list of objects that return a list. +""" + + +def test_batch_create(mutation, fruits): + result = mutation( + """ + mutation { + fruits: createFruits( + data: [{ name: "banana" }, { name: "cherry" }] + ) { + id + name + } + } + """ + ) + assert not result.errors + assert result.data["fruits"] == [ + {"id": "4", "name": "banana"}, + {"id": "5", "name": "cherry"}, + ] + + +def test_batch_delete_with_filter(mutation, fruits): + result = mutation( + """ + mutation($ids: [ID!]) { + fruits: deleteFruits( + filters: {id: {inList: $ids}} + ) { + id + name + } + } + """, + {"ids": ["2"]}, + ) + assert not result.errors + assert result.data["fruits"] == [ + {"id": "2", "name": "raspberry"}, + ] + + +def test_batch_delete_with_filter_empty_list(mutation, fruits): + result = mutation( + """ + { + fruits: deleteFruits( + filters: {id: {inList: []}} + ) { + id + name + } + } + """ + ) + assert not result.errors + + +def test_batch_update_with_filter(mutation, fruits): + result = mutation( + """ + { + fruits: updateFruits( + data: { name: "orange" } + filters: {id: {inList: [1]}} + ) { + id + name + } + } + """ + ) + assert not result.errors + assert result.data["fruits"] == [ + {"id": "1", "name": "orange"}, + ] + + +def test_batch_update_with_filter_empty_list(mutation, fruits): + result = mutation( + """ + { + fruits: updateFruits( + data: { name: "orange" } + filters: {id: {inList: []}} + ) { + id + name + } + } + """ + ) + assert not result.errors + + +def test_batch_patch(mutation, fruits): + result = mutation( + """ + { + fruits: patchFruits( + data: [{ id: 2, name: "orange" }] + ) { + id + name + } + } + """ + ) + assert not result.errors + assert result.data["fruits"] == [ + {"id": "2", "name": "orange"}, + ] diff --git a/tests/mutations/test_filter_mutations.py b/tests/mutations/test_filter_mutations.py deleted file mode 100644 index 290e166d..00000000 --- a/tests/mutations/test_filter_mutations.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Tests for "batch mutations", mutations with a filter function that return a list.""" - - -def test_delete_with_filter(mutation, fruits): - result = mutation( - """ - { - fruits: deleteFruits( - filters: {id: {exact: 1}} - ) { - id - name - } - } - """ - ) - assert not result.errors - - -def test_delete_with_filter_empty_list(mutation, fruits): - result = mutation( - """ - { - fruits: deleteFruits( - filters: {id: {inList: []}} - ) { - id - name - } - } - """ - ) - assert not result.errors - - -def test_update_with_filter(mutation, fruits): - result = mutation( - """ - { - fruits: updateFruits( - data: { name: "orange" } - filters: {id: {exact: 1}} - ) { - id - name - } - } - """ - ) - assert not result.errors - - -def test_update_with_filter_empty_list(mutation, fruits): - result = mutation( - """ - { - fruits: updateFruits( - data: { name: "orange" } - filters: {id: {inList: []}} - ) { - id - name - } - } - """ - ) - assert not result.errors