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.
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/PKG-INFO +258 -13
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/README.md +253 -9
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/pyproject.toml +7 -5
- strawberry_orm-0.3.0/src/strawberry_orm/_async.py +51 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/_base.py +7 -11
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/django.py +276 -237
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/protocol.py +11 -2
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/sqlalchemy.py +223 -77
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/tortoise.py +233 -117
- strawberry_orm-0.3.0/src/strawberry_orm/core.py +609 -0
- strawberry_orm-0.3.0/src/strawberry_orm/mutations.py +1330 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/optimizer/extension.py +26 -5
- strawberry_orm-0.1.1/src/strawberry_orm/core.py +0 -311
- strawberry_orm-0.1.1/src/strawberry_orm/mutations.py +0 -59
- strawberry_orm-0.1.1/src/strawberry_orm/relay/resolvers.py +0 -95
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/__init__.py +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/backends/__init__.py +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/fields.py +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/filters.py +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/optimizer/__init__.py +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/optimizer/store.py +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/py.typed +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/relay/__init__.py +0 -0
- {strawberry_orm-0.1.1 → strawberry_orm-0.3.0}/src/strawberry_orm/relay/connection.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
>
|
|
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.
|
|
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
|
|
128
|
-
| Tortoise | `StrawberryORM("tortoise")` | Async ORM; use Strawberry
|
|
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
|
-
>
|
|
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.
|
|
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
|
|
94
|
-
| Tortoise | `StrawberryORM("tortoise")` | Async ORM; use Strawberry
|
|
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
|