Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch Mutations for creating, updating, and deleting #438 #653

Merged
merged 3 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions docs/guide/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 expect that the mutations take and return a list.
keithhackbarth marked this conversation as resolved.
Show resolved Hide resolved

```python title="schema.py"
import strawberry
from strawberry_django import mutations

@strawberry.type
class Mutation:
createFruits: list[Fruit] = mutations.update(list[FruitPartialInput])
keithhackbarth marked this conversation as resolved.
Show resolved Hide resolved
updateFruits: list[Fruit] = mutations.update(list[FruitPartialInput])
deleteFruits: list[Fruit] = mutations.delete(list[FruitPartialInput])

schema = strawberry.Schema(mutation=Mutation)
```
23 changes: 19 additions & 4 deletions strawberry_django/mutations/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@
argument(
self.argument_name,
self.input_type,
is_list=self.is_list and isinstance(self, DjangoCreateMutation),
),
]

Expand Down Expand Up @@ -272,6 +271,10 @@
)


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],
*,
Expand Down Expand Up @@ -300,17 +303,29 @@
) -> 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 | None,
kwargs: dict[str, Any],
keithhackbarth marked this conversation as resolved.
Show resolved Hide resolved
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 {}

pk = get_pk(vdata, key_attr=self.key_attr)

if pk not in (None, UNSET): # noqa: PLR6201
instance = get_with_perms(

Check failure on line 326 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

No overloads for "get_with_perms" match the provided arguments (reportCallIssue)
pk,

Check failure on line 327 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

Argument of type "ID | GlobalID | Unknown" cannot be assigned to parameter "pk" of type "GlobalID" in function "get_with_perms"   Type "ID | GlobalID | Unknown" is not assignable to type "GlobalID"     "ID" is not assignable to "GlobalID" (reportArgumentType)
info,

Check failure on line 328 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

Argument of type "Info[Any, Any] | None" cannot be assigned to parameter "info" of type "Info[Any, Any]" in function "get_with_perms"   Type "Info[Any, Any] | None" is not assignable to type "Info[Any, Any]"     "None" is not assignable to "Info[Any, Any]" (reportArgumentType)
required=True,
model=model,
key_attr=self.key_attr,
Expand All @@ -319,14 +334,14 @@
instance = filter_with_perms(
self.get_queryset(
queryset=model._default_manager.all(),
info=info,

Check failure on line 337 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

Argument of type "Info[Any, Any] | None" cannot be assigned to parameter "info" of type "Info[Any, Any]" in function "get_queryset"   Type "Info[Any, Any] | None" is not assignable to type "Info[Any, Any]"     "None" is not assignable to "Info[Any, Any]" (reportArgumentType)
**kwargs,
),
info,

Check failure on line 340 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

Argument of type "Info[Any, Any] | None" cannot be assigned to parameter "info" of type "Info[Any, Any]" in function "filter_with_perms"   Type "Info[Any, Any] | None" is not assignable to type "Info[Any, Any]"     "None" is not assignable to "Info[Any, Any]" (reportArgumentType)
)

return self.update(
info, instance, resolvers.parse_input(info, vdata, key_attr=self.key_attr)

Check failure on line 344 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

Argument of type "Info[Any, Any] | None" cannot be assigned to parameter "info" of type "Info[Any, Any]" in function "update"   Type "Info[Any, Any] | None" is not assignable to type "Info[Any, Any]"     "None" is not assignable to "Info[Any, Any]" (reportArgumentType)

Check failure on line 344 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

No overloads for "parse_input" match the provided arguments (reportCallIssue)

Check failure on line 344 in strawberry_django/mutations/fields.py

View workflow job for this annotation

GitHub Actions / Typing

Argument of type "Info[Any, Any] | None" cannot be assigned to parameter "info" of type "Info[Any, Any]" in function "parse_input"   Type "Info[Any, Any] | None" is not assignable to type "Info[Any, Any]"     "None" is not assignable to "Info[Any, Any]" (reportArgumentType)
)

def update(
Expand Down Expand Up @@ -366,7 +381,7 @@
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
Expand Down
3 changes: 2 additions & 1 deletion tests/mutations/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
117 changes: 117 additions & 0 deletions tests/mutations/test_batch_mutations.py
Original file line number Diff line number Diff line change
@@ -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"},
]
keithhackbarth marked this conversation as resolved.
Show resolved Hide resolved
keithhackbarth marked this conversation as resolved.
Show resolved Hide resolved


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"},
]
67 changes: 0 additions & 67 deletions tests/mutations/test_filter_mutations.py

This file was deleted.

Loading