strawberry-orm 0.1.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/PKG-INFO +258 -13
  2. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/README.md +253 -9
  3. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/pyproject.toml +7 -5
  4. strawberry_orm-0.3.0/src/strawberry_orm/_async.py +51 -0
  5. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/_base.py +7 -11
  6. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/django.py +276 -237
  7. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/protocol.py +11 -2
  8. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/sqlalchemy.py +223 -77
  9. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/tortoise.py +233 -117
  10. strawberry_orm-0.3.0/src/strawberry_orm/core.py +609 -0
  11. strawberry_orm-0.3.0/src/strawberry_orm/mutations.py +1330 -0
  12. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/optimizer/extension.py +26 -5
  13. strawberry_orm-0.1.1/src/strawberry_orm/core.py +0 -311
  14. strawberry_orm-0.1.1/src/strawberry_orm/mutations.py +0 -59
  15. strawberry_orm-0.1.1/src/strawberry_orm/relay/resolvers.py +0 -95
  16. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/__init__.py +0 -0
  17. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/__init__.py +0 -0
  18. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/fields.py +0 -0
  19. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/filters.py +0 -0
  20. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/optimizer/__init__.py +0 -0
  21. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/optimizer/store.py +0 -0
  22. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/py.typed +0 -0
  23. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/relay/__init__.py +0 -0
  24. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/relay/connection.py +0 -0
  25. {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strawberry-orm
3
- Version: 0.1.1
3
+ Version: 0.3.0
4
4
  Summary: Unified, backend-agnostic ORM abstraction for Strawberry GraphQL
5
5
  Author: James Davidson, Patrick Arminio
6
6
  Author-email: James Davidson <jamie.t.davidson@gmail.com>, Patrick Arminio <patrick.arminio@gmail.com>
@@ -9,11 +9,12 @@ Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3 :: Only
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Requires-Dist: strawberry-graphql>=0.311.0
17
+ Requires-Dist: greenlet>=3.3.2 ; extra == 'dev'
17
18
  Requires-Dist: pytest>=8.0 ; extra == 'dev'
18
19
  Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
19
20
  Requires-Dist: pytest-django>=4.8 ; extra == 'dev'
@@ -22,7 +23,7 @@ Requires-Dist: uvicorn>=0.30 ; extra == 'dev'
22
23
  Requires-Dist: django>=4.2 ; extra == 'django'
23
24
  Requires-Dist: sqlalchemy>=2.0 ; extra == 'sqlalchemy'
24
25
  Requires-Dist: tortoise-orm>=0.25.0 ; extra == 'tortoise'
25
- Requires-Python: >=3.10
26
+ Requires-Python: >=3.12
26
27
  Project-URL: Homepage, https://github.com/strawberry-graphql/strawberry-orm
27
28
  Project-URL: Repository, https://github.com/strawberry-graphql/strawberry-orm
28
29
  Project-URL: Issues, https://github.com/strawberry-graphql/strawberry-orm/issues
@@ -36,8 +37,8 @@ Description-Content-Type: text/markdown
36
37
 
37
38
  Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.rocks/) on top of Django ORM, SQLAlchemy, and Tortoise ORM.
38
39
 
39
- > **WARNING**
40
- > `strawberry-orm` is still in **alpha**. Expect breaking changes, incomplete APIs, and release-to-release churn while the package stabilizes.
40
+ > ## WARNING
41
+ > 🟧🟨 `strawberry-orm` is still in **alpha**. Expect breaking changes, incomplete APIs, and release-to-release churn while the package stabilizes. 🟨🟧
41
42
 
42
43
  `strawberry-orm` helps you keep one Strawberry schema style across multiple ORMs. It focuses on:
43
44
 
@@ -54,9 +55,9 @@ Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.r
54
55
  uv add strawberry-orm
55
56
 
56
57
  # With a backend
57
- uv add strawberry-orm[django]
58
- uv add strawberry-orm[sqlalchemy]
59
- uv add strawberry-orm[tortoise]
58
+ uv add "strawberry-orm[django]"
59
+ uv add "strawberry-orm[sqlalchemy]"
60
+ uv add "strawberry-orm[tortoise]"
60
61
  ```
61
62
 
62
63
  You can do the same with `pip`:
@@ -65,9 +66,11 @@ You can do the same with `pip`:
65
66
  pip install "strawberry-orm[sqlalchemy]"
66
67
  ```
67
68
 
69
+ If you are using `zsh`, keep the quotes around extras such as `"strawberry-orm[sqlalchemy]"`. Unquoted square brackets are treated as shell glob syntax before `uv` or `pip` sees the package name.
70
+
68
71
  Requirements:
69
72
 
70
- - Python `>=3.10`
73
+ - Python `>=3.12`
71
74
  - `strawberry-graphql>=0.311.0`
72
75
 
73
76
  ## Quick Start
@@ -121,11 +124,24 @@ That single `users` field will:
121
124
 
122
125
  ## Backends
123
126
 
127
+ `strawberry-orm` follows Strawberry's mixed execution model:
128
+
129
+ - sync and async schema execution are both supported
130
+ - a resolver can return plain values, backend query objects, or awaitables
131
+ - the optimizer extension handles both sync and async execution paths
132
+ - direct helper APIs such as `apply_ref_list(...)` may be sync or awaitable depending on the backend/session in use
133
+
134
+ As a rule of thumb:
135
+
136
+ - Django works in both sync and async Strawberry execution, but custom async resolvers still need `sync_to_async(...)` around direct Django ORM access
137
+ - SQLAlchemy supports both sync `Session` and `AsyncSession`
138
+ - Tortoise is async-first; use async Strawberry execution there
139
+
124
140
  | Backend | Constructor | Notes |
125
141
  | --- | --- | --- |
126
- | Django | `StrawberryORM("django")` | Uses Django querysets directly. |
127
- | SQLAlchemy | `StrawberryORM("sqlalchemy", dialect="postgresql", session_getter=...)` | Requires a SQLAlchemy session at resolve time. |
128
- | Tortoise | `StrawberryORM("tortoise")` | Async ORM; use Strawberry in async mode. |
142
+ | Django | `StrawberryORM("django")` | Uses Django querysets directly; async execution is supported via Strawberry's mixed sync/async model. |
143
+ | SQLAlchemy | `StrawberryORM("sqlalchemy", dialect="postgresql", session_getter=...)` | Requires a SQLAlchemy `Session` or `AsyncSession` at resolve time. |
144
+ | Tortoise | `StrawberryORM("tortoise")` | Async ORM; use Strawberry's async execution path. |
129
145
 
130
146
  ### Django
131
147
 
@@ -133,6 +149,8 @@ That single `users` field will:
133
149
  orm = StrawberryORM("django")
134
150
  ```
135
151
 
152
+ When executing the schema asynchronously, custom resolvers that touch Django models directly should still wrap those ORM calls with `sync_to_async(...)`, following the same guidance as `strawberry-django`.
153
+
136
154
  ### SQLAlchemy
137
155
 
138
156
  ```python
@@ -152,12 +170,48 @@ SQLAlchemy needs a session when a query is executed. `strawberry-orm` can obtain
152
170
 
153
171
  If your context stores a callable session factory, pass a `session_getter` instead of putting the callable directly on `info.context`.
154
172
 
173
+ Both sync and async sessions are supported:
174
+
175
+ ```python
176
+ # Sync session
177
+ orm = StrawberryORM(
178
+ "sqlalchemy",
179
+ dialect="postgresql",
180
+ session_getter=lambda info: info.context["session"],
181
+ )
182
+
183
+ # Async session
184
+ orm = StrawberryORM(
185
+ "sqlalchemy",
186
+ dialect="postgresql",
187
+ session_getter=lambda info: info.context["session"],
188
+ )
189
+
190
+ @strawberry.type
191
+ class Query:
192
+ @strawberry.field
193
+ def users(self) -> list[UserType]:
194
+ return select(User)
195
+
196
+ # context["session"] can be either Session or AsyncSession
197
+ ```
198
+
155
199
  ### Tortoise
156
200
 
157
201
  ```python
158
202
  orm = StrawberryORM("tortoise")
159
203
  ```
160
204
 
205
+ Tortoise resolvers, mutations, and related-list helpers should be used from async Strawberry execution:
206
+
207
+ ```python
208
+ @strawberry.type
209
+ class Query:
210
+ @strawberry.field
211
+ async def users(self) -> list[UserType]:
212
+ return await User.all()
213
+ ```
214
+
161
215
  ### Backend Options
162
216
 
163
217
  Shared options:
@@ -453,6 +507,17 @@ class Query:
453
507
 
454
508
  This works with the optimizer extension and with type-level `get_queryset` hooks.
455
509
 
510
+ If you execute your schema asynchronously, the same pattern works with async resolvers too:
511
+
512
+ ```python
513
+ @strawberry.type
514
+ class Query:
515
+ @strawberry.field
516
+ async def active_users(self, info: strawberry.types.Info) -> list[UserType]:
517
+ return await User.filter(is_active=True) # Tortoise
518
+ # return await sync_to_async(list)(User.objects.filter(is_active=True)) # Django
519
+ ```
520
+
456
521
  ---
457
522
 
458
523
  ## Mutations
@@ -483,6 +548,24 @@ class Mutation:
483
548
  ...
484
549
  ```
485
550
 
551
+ Async mutations work too. Use async ORM calls in the resolver body, and await backend helpers when the active backend/session requires it:
552
+
553
+ ```python
554
+ @strawberry.type
555
+ class Mutation:
556
+ @strawberry.mutation
557
+ async def create_post(
558
+ self,
559
+ info: strawberry.types.Info,
560
+ input: CreatePostInput,
561
+ ) -> PostType:
562
+ return await Post.create(
563
+ title=input.title,
564
+ body=input.body,
565
+ author_id=input.author_id,
566
+ )
567
+ ```
568
+
486
569
  ### Related List Inputs (`orm.ref`)
487
570
 
488
571
  `orm.ref(...)` generates a `@oneOf` input for managing related lists:
@@ -517,6 +600,24 @@ def set_post_tags(self, info: strawberry.types.Info, post_id: int, tags: list[Ta
517
600
  return post
518
601
  ```
519
602
 
603
+ Async backends can use the same helper from async mutations:
604
+
605
+ ```python
606
+ @strawberry.mutation
607
+ async def set_post_tags(
608
+ self,
609
+ info: strawberry.types.Info,
610
+ post_id: int,
611
+ tags: list[TagRef],
612
+ ) -> PostType | None:
613
+ post = await Post.get_or_none(pk=post_id)
614
+ if post is None:
615
+ return None
616
+
617
+ await orm.apply_ref_list(post, "tags", tags, info)
618
+ return post
619
+ ```
620
+
520
621
  ```graphql
521
622
  mutation {
522
623
  setPostTags(postId: 1, tags: [
@@ -533,6 +634,150 @@ mutation {
533
634
 
534
635
  `apply_ref_list` supports `mode="replace"` (default, replaces the entire list) and `mode="patch"` (only touches mentioned items). An optional `authorize` callback receives `(action, model, obj_id, info)` and returns `bool`.
535
636
 
637
+ In practice:
638
+
639
+ - use it directly in sync Django / sync SQLAlchemy mutations
640
+ - `await` it for Tortoise
641
+ - `await` it for SQLAlchemy when your request context carries an `AsyncSession`
642
+ - in custom async Django resolvers, prefer the same async-safe pattern you already use for direct ORM calls
643
+
644
+ ---
645
+
646
+ ### Recursive Node Mutations
647
+
648
+ `orm.mutations.create_node(...)` and `orm.mutations.update_node(...)` generate catch-all Relay `Node` mutations with recursive nested inputs.
649
+
650
+ If you want to implement the resolver logic yourself, you can generate the root input types directly with `orm.mutations.create_node_input(...)` and `orm.mutations.update_node_input(...)`:
651
+
652
+ ```python
653
+ import strawberry
654
+ from strawberry import relay
655
+
656
+
657
+ @orm.type(User)
658
+ class UserNode(relay.Node):
659
+ id: relay.NodeID[int]
660
+ name: auto
661
+ email: auto
662
+
663
+
664
+ @orm.type(Post)
665
+ class PostNode(relay.Node):
666
+ id: relay.NodeID[int]
667
+ title: auto
668
+ body: auto
669
+
670
+
671
+ CreateNodeInput = orm.mutations.create_node_input()
672
+ UpdateNodeInput = orm.mutations.update_node_input()
673
+
674
+
675
+ @strawberry.type
676
+ class Mutation:
677
+ create_node = orm.mutations.create_node()
678
+ update_node = orm.mutations.update_node()
679
+
680
+ @strawberry.field
681
+ def custom_create_node(self, input: CreateNodeInput) -> str:
682
+ return "implement your own create logic here"
683
+
684
+ @strawberry.field
685
+ def custom_update_node(self, input: UpdateNodeInput) -> str:
686
+ return "implement your own update logic here"
687
+ ```
688
+
689
+ List relations use `items`, while singular relations use explicit `create` / `update` branches:
690
+
691
+ ```graphql
692
+ mutation {
693
+ createNode(input: {
694
+ post: {
695
+ title: "Hello"
696
+ body: "World"
697
+ author: {
698
+ create: {
699
+ name: "Alice"
700
+ email: "alice@example.com"
701
+ }
702
+ }
703
+ tags: {
704
+ items: [{ create: { name: "python" } }]
705
+ mode: REPLACE
706
+ onRemove: DELETE
707
+ }
708
+ }
709
+ }) {
710
+ __typename
711
+ }
712
+ }
713
+ ```
714
+
715
+ #### Projection And Policy Config
716
+
717
+ Pass `project={...}` to restrict recursion depth and configure relation semantics in one dict:
718
+
719
+ ```python
720
+ project = {
721
+ "post": {
722
+ "author": {
723
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
724
+ },
725
+ "comments": {
726
+ "_meta": {
727
+ "mode": ["PATCH", "REPLACE"],
728
+ "onRemove": ["DISCONNECT", "DELETE"],
729
+ },
730
+ "author": {
731
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
732
+ },
733
+ },
734
+ "tags": {
735
+ "_meta": {
736
+ "mode": "REPLACE",
737
+ "onRemove": "DELETE",
738
+ },
739
+ },
740
+ },
741
+ "comment": {
742
+ "author": {
743
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
744
+ },
745
+ },
746
+ }
747
+
748
+
749
+ @strawberry.type
750
+ class Mutation:
751
+ create_node = orm.mutations.create_node(project=project)
752
+ update_node = orm.mutations.update_node(project=project)
753
+ ```
754
+
755
+ Rules for the config object:
756
+
757
+ - Root keys are model names (`post`, `comment`, `user`, ...).
758
+ - Nested keys are relation names available on that model.
759
+ - `_meta` is optional and configures behavior for that relation subtree.
760
+ - Omitted relations still exist as shallow nested inputs, but recursion stops after one more level.
761
+
762
+ `_meta` supports:
763
+
764
+ - `mode`: list relation merge strategy (`PATCH` or `REPLACE`)
765
+ - `onRemove`: what to do with removed items from a list relation (`DISCONNECT` or `DELETE`)
766
+ - `onReplace`: what to do with the previous object when replacing a singular relation (`DISCONNECT` or `DELETE`)
767
+
768
+ The `_meta` values can be either:
769
+
770
+ - an array of enum strings, which means the GraphQL input exposes that field and the caller may choose from those options
771
+ - a single enum string, which fixes that behavior for the relation and omits the corresponding GraphQL field
772
+
773
+ Default behavior when a field is exposed but omitted in the mutation input:
774
+
775
+ - `mode` defaults to `PATCH` when allowed
776
+ - `onRemove` defaults to `DISCONNECT` when allowed
777
+ - `onReplace` defaults to `DISCONNECT` when allowed
778
+
779
+ If the preferred default is not included in the allowed array, the first configured value is used.
780
+
536
781
  ---
537
782
 
538
783
  ## Query Optimization
@@ -2,8 +2,8 @@
2
2
 
3
3
  Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.rocks/) on top of Django ORM, SQLAlchemy, and Tortoise ORM.
4
4
 
5
- > **WARNING**
6
- > `strawberry-orm` is still in **alpha**. Expect breaking changes, incomplete APIs, and release-to-release churn while the package stabilizes.
5
+ > ## WARNING
6
+ > 🟧🟨 `strawberry-orm` is still in **alpha**. Expect breaking changes, incomplete APIs, and release-to-release churn while the package stabilizes. 🟨🟧
7
7
 
8
8
  `strawberry-orm` helps you keep one Strawberry schema style across multiple ORMs. It focuses on:
9
9
 
@@ -20,9 +20,9 @@ Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.r
20
20
  uv add strawberry-orm
21
21
 
22
22
  # With a backend
23
- uv add strawberry-orm[django]
24
- uv add strawberry-orm[sqlalchemy]
25
- uv add strawberry-orm[tortoise]
23
+ uv add "strawberry-orm[django]"
24
+ uv add "strawberry-orm[sqlalchemy]"
25
+ uv add "strawberry-orm[tortoise]"
26
26
  ```
27
27
 
28
28
  You can do the same with `pip`:
@@ -31,9 +31,11 @@ You can do the same with `pip`:
31
31
  pip install "strawberry-orm[sqlalchemy]"
32
32
  ```
33
33
 
34
+ If you are using `zsh`, keep the quotes around extras such as `"strawberry-orm[sqlalchemy]"`. Unquoted square brackets are treated as shell glob syntax before `uv` or `pip` sees the package name.
35
+
34
36
  Requirements:
35
37
 
36
- - Python `>=3.10`
38
+ - Python `>=3.12`
37
39
  - `strawberry-graphql>=0.311.0`
38
40
 
39
41
  ## Quick Start
@@ -87,11 +89,24 @@ That single `users` field will:
87
89
 
88
90
  ## Backends
89
91
 
92
+ `strawberry-orm` follows Strawberry's mixed execution model:
93
+
94
+ - sync and async schema execution are both supported
95
+ - a resolver can return plain values, backend query objects, or awaitables
96
+ - the optimizer extension handles both sync and async execution paths
97
+ - direct helper APIs such as `apply_ref_list(...)` may be sync or awaitable depending on the backend/session in use
98
+
99
+ As a rule of thumb:
100
+
101
+ - Django works in both sync and async Strawberry execution, but custom async resolvers still need `sync_to_async(...)` around direct Django ORM access
102
+ - SQLAlchemy supports both sync `Session` and `AsyncSession`
103
+ - Tortoise is async-first; use async Strawberry execution there
104
+
90
105
  | Backend | Constructor | Notes |
91
106
  | --- | --- | --- |
92
- | Django | `StrawberryORM("django")` | Uses Django querysets directly. |
93
- | SQLAlchemy | `StrawberryORM("sqlalchemy", dialect="postgresql", session_getter=...)` | Requires a SQLAlchemy session at resolve time. |
94
- | Tortoise | `StrawberryORM("tortoise")` | Async ORM; use Strawberry in async mode. |
107
+ | Django | `StrawberryORM("django")` | Uses Django querysets directly; async execution is supported via Strawberry's mixed sync/async model. |
108
+ | SQLAlchemy | `StrawberryORM("sqlalchemy", dialect="postgresql", session_getter=...)` | Requires a SQLAlchemy `Session` or `AsyncSession` at resolve time. |
109
+ | Tortoise | `StrawberryORM("tortoise")` | Async ORM; use Strawberry's async execution path. |
95
110
 
96
111
  ### Django
97
112
 
@@ -99,6 +114,8 @@ That single `users` field will:
99
114
  orm = StrawberryORM("django")
100
115
  ```
101
116
 
117
+ When executing the schema asynchronously, custom resolvers that touch Django models directly should still wrap those ORM calls with `sync_to_async(...)`, following the same guidance as `strawberry-django`.
118
+
102
119
  ### SQLAlchemy
103
120
 
104
121
  ```python
@@ -118,12 +135,48 @@ SQLAlchemy needs a session when a query is executed. `strawberry-orm` can obtain
118
135
 
119
136
  If your context stores a callable session factory, pass a `session_getter` instead of putting the callable directly on `info.context`.
120
137
 
138
+ Both sync and async sessions are supported:
139
+
140
+ ```python
141
+ # Sync session
142
+ orm = StrawberryORM(
143
+ "sqlalchemy",
144
+ dialect="postgresql",
145
+ session_getter=lambda info: info.context["session"],
146
+ )
147
+
148
+ # Async session
149
+ orm = StrawberryORM(
150
+ "sqlalchemy",
151
+ dialect="postgresql",
152
+ session_getter=lambda info: info.context["session"],
153
+ )
154
+
155
+ @strawberry.type
156
+ class Query:
157
+ @strawberry.field
158
+ def users(self) -> list[UserType]:
159
+ return select(User)
160
+
161
+ # context["session"] can be either Session or AsyncSession
162
+ ```
163
+
121
164
  ### Tortoise
122
165
 
123
166
  ```python
124
167
  orm = StrawberryORM("tortoise")
125
168
  ```
126
169
 
170
+ Tortoise resolvers, mutations, and related-list helpers should be used from async Strawberry execution:
171
+
172
+ ```python
173
+ @strawberry.type
174
+ class Query:
175
+ @strawberry.field
176
+ async def users(self) -> list[UserType]:
177
+ return await User.all()
178
+ ```
179
+
127
180
  ### Backend Options
128
181
 
129
182
  Shared options:
@@ -419,6 +472,17 @@ class Query:
419
472
 
420
473
  This works with the optimizer extension and with type-level `get_queryset` hooks.
421
474
 
475
+ If you execute your schema asynchronously, the same pattern works with async resolvers too:
476
+
477
+ ```python
478
+ @strawberry.type
479
+ class Query:
480
+ @strawberry.field
481
+ async def active_users(self, info: strawberry.types.Info) -> list[UserType]:
482
+ return await User.filter(is_active=True) # Tortoise
483
+ # return await sync_to_async(list)(User.objects.filter(is_active=True)) # Django
484
+ ```
485
+
422
486
  ---
423
487
 
424
488
  ## Mutations
@@ -449,6 +513,24 @@ class Mutation:
449
513
  ...
450
514
  ```
451
515
 
516
+ Async mutations work too. Use async ORM calls in the resolver body, and await backend helpers when the active backend/session requires it:
517
+
518
+ ```python
519
+ @strawberry.type
520
+ class Mutation:
521
+ @strawberry.mutation
522
+ async def create_post(
523
+ self,
524
+ info: strawberry.types.Info,
525
+ input: CreatePostInput,
526
+ ) -> PostType:
527
+ return await Post.create(
528
+ title=input.title,
529
+ body=input.body,
530
+ author_id=input.author_id,
531
+ )
532
+ ```
533
+
452
534
  ### Related List Inputs (`orm.ref`)
453
535
 
454
536
  `orm.ref(...)` generates a `@oneOf` input for managing related lists:
@@ -483,6 +565,24 @@ def set_post_tags(self, info: strawberry.types.Info, post_id: int, tags: list[Ta
483
565
  return post
484
566
  ```
485
567
 
568
+ Async backends can use the same helper from async mutations:
569
+
570
+ ```python
571
+ @strawberry.mutation
572
+ async def set_post_tags(
573
+ self,
574
+ info: strawberry.types.Info,
575
+ post_id: int,
576
+ tags: list[TagRef],
577
+ ) -> PostType | None:
578
+ post = await Post.get_or_none(pk=post_id)
579
+ if post is None:
580
+ return None
581
+
582
+ await orm.apply_ref_list(post, "tags", tags, info)
583
+ return post
584
+ ```
585
+
486
586
  ```graphql
487
587
  mutation {
488
588
  setPostTags(postId: 1, tags: [
@@ -499,6 +599,150 @@ mutation {
499
599
 
500
600
  `apply_ref_list` supports `mode="replace"` (default, replaces the entire list) and `mode="patch"` (only touches mentioned items). An optional `authorize` callback receives `(action, model, obj_id, info)` and returns `bool`.
501
601
 
602
+ In practice:
603
+
604
+ - use it directly in sync Django / sync SQLAlchemy mutations
605
+ - `await` it for Tortoise
606
+ - `await` it for SQLAlchemy when your request context carries an `AsyncSession`
607
+ - in custom async Django resolvers, prefer the same async-safe pattern you already use for direct ORM calls
608
+
609
+ ---
610
+
611
+ ### Recursive Node Mutations
612
+
613
+ `orm.mutations.create_node(...)` and `orm.mutations.update_node(...)` generate catch-all Relay `Node` mutations with recursive nested inputs.
614
+
615
+ If you want to implement the resolver logic yourself, you can generate the root input types directly with `orm.mutations.create_node_input(...)` and `orm.mutations.update_node_input(...)`:
616
+
617
+ ```python
618
+ import strawberry
619
+ from strawberry import relay
620
+
621
+
622
+ @orm.type(User)
623
+ class UserNode(relay.Node):
624
+ id: relay.NodeID[int]
625
+ name: auto
626
+ email: auto
627
+
628
+
629
+ @orm.type(Post)
630
+ class PostNode(relay.Node):
631
+ id: relay.NodeID[int]
632
+ title: auto
633
+ body: auto
634
+
635
+
636
+ CreateNodeInput = orm.mutations.create_node_input()
637
+ UpdateNodeInput = orm.mutations.update_node_input()
638
+
639
+
640
+ @strawberry.type
641
+ class Mutation:
642
+ create_node = orm.mutations.create_node()
643
+ update_node = orm.mutations.update_node()
644
+
645
+ @strawberry.field
646
+ def custom_create_node(self, input: CreateNodeInput) -> str:
647
+ return "implement your own create logic here"
648
+
649
+ @strawberry.field
650
+ def custom_update_node(self, input: UpdateNodeInput) -> str:
651
+ return "implement your own update logic here"
652
+ ```
653
+
654
+ List relations use `items`, while singular relations use explicit `create` / `update` branches:
655
+
656
+ ```graphql
657
+ mutation {
658
+ createNode(input: {
659
+ post: {
660
+ title: "Hello"
661
+ body: "World"
662
+ author: {
663
+ create: {
664
+ name: "Alice"
665
+ email: "alice@example.com"
666
+ }
667
+ }
668
+ tags: {
669
+ items: [{ create: { name: "python" } }]
670
+ mode: REPLACE
671
+ onRemove: DELETE
672
+ }
673
+ }
674
+ }) {
675
+ __typename
676
+ }
677
+ }
678
+ ```
679
+
680
+ #### Projection And Policy Config
681
+
682
+ Pass `project={...}` to restrict recursion depth and configure relation semantics in one dict:
683
+
684
+ ```python
685
+ project = {
686
+ "post": {
687
+ "author": {
688
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
689
+ },
690
+ "comments": {
691
+ "_meta": {
692
+ "mode": ["PATCH", "REPLACE"],
693
+ "onRemove": ["DISCONNECT", "DELETE"],
694
+ },
695
+ "author": {
696
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
697
+ },
698
+ },
699
+ "tags": {
700
+ "_meta": {
701
+ "mode": "REPLACE",
702
+ "onRemove": "DELETE",
703
+ },
704
+ },
705
+ },
706
+ "comment": {
707
+ "author": {
708
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
709
+ },
710
+ },
711
+ }
712
+
713
+
714
+ @strawberry.type
715
+ class Mutation:
716
+ create_node = orm.mutations.create_node(project=project)
717
+ update_node = orm.mutations.update_node(project=project)
718
+ ```
719
+
720
+ Rules for the config object:
721
+
722
+ - Root keys are model names (`post`, `comment`, `user`, ...).
723
+ - Nested keys are relation names available on that model.
724
+ - `_meta` is optional and configures behavior for that relation subtree.
725
+ - Omitted relations still exist as shallow nested inputs, but recursion stops after one more level.
726
+
727
+ `_meta` supports:
728
+
729
+ - `mode`: list relation merge strategy (`PATCH` or `REPLACE`)
730
+ - `onRemove`: what to do with removed items from a list relation (`DISCONNECT` or `DELETE`)
731
+ - `onReplace`: what to do with the previous object when replacing a singular relation (`DISCONNECT` or `DELETE`)
732
+
733
+ The `_meta` values can be either:
734
+
735
+ - an array of enum strings, which means the GraphQL input exposes that field and the caller may choose from those options
736
+ - a single enum string, which fixes that behavior for the relation and omits the corresponding GraphQL field
737
+
738
+ Default behavior when a field is exposed but omitted in the mutation input:
739
+
740
+ - `mode` defaults to `PATCH` when allowed
741
+ - `onRemove` defaults to `DISCONNECT` when allowed
742
+ - `onReplace` defaults to `DISCONNECT` when allowed
743
+
744
+ If the preferred default is not included in the allowed array, the first configured value is used.
745
+
502
746
  ---
503
747
 
504
748
  ## Query Optimization