strawberry-orm 0.2.0__tar.gz → 0.4.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.2.0 → strawberry_orm-0.4.0}/PKG-INFO +242 -4
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/README.md +240 -3
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/pyproject.toml +3 -1
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/_base.py +126 -21
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/django.py +44 -8
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/protocol.py +1 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/sqlalchemy.py +51 -11
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/tortoise.py +204 -36
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/core.py +35 -4
- strawberry_orm-0.4.0/src/strawberry_orm/mutations.py +1344 -0
- strawberry_orm-0.2.0/src/strawberry_orm/mutations.py +0 -59
- strawberry_orm-0.2.0/src/strawberry_orm/relay/resolvers.py +0 -119
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/__init__.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/_async.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/__init__.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/fields.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/filters.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/__init__.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/extension.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/store.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/py.typed +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/relay/__init__.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/relay/connection.py +0 -0
- {strawberry_orm-0.2.0 → strawberry_orm-0.4.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.4.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>
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
14
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'
|
|
@@ -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,6 +66,8 @@ 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
73
|
- Python `>=3.12`
|
|
@@ -422,6 +425,104 @@ Typical string lookups: `exact`, `neq`, `contains`, `iContains`, `startsWith`, `
|
|
|
422
425
|
|
|
423
426
|
Regex lookups (`regex`, `iRegex`) are disabled by default. Enable with `enable_regex_filters=True`.
|
|
424
427
|
|
|
428
|
+
#### Object Traversal
|
|
429
|
+
|
|
430
|
+
When filters are registered for related models, the generated filter gains an `object` key that lets you filter parent rows based on conditions on their related objects:
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
UserFilter = orm.filter(User)
|
|
434
|
+
PostFilter = orm.filter(Post) # Post has an "author" relation to User
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```graphql
|
|
438
|
+
{
|
|
439
|
+
posts(filter: {
|
|
440
|
+
object: { author: { field: { name: { exact: "Alice" } } } }
|
|
441
|
+
}) {
|
|
442
|
+
title
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Object traversal composes with boolean operators:
|
|
448
|
+
|
|
449
|
+
```graphql
|
|
450
|
+
{
|
|
451
|
+
posts(filter: {
|
|
452
|
+
all: [
|
|
453
|
+
{ field: { isPublished: { exact: true } } }
|
|
454
|
+
{ object: { author: { field: { name: { exact: "Alice" } } } } }
|
|
455
|
+
]
|
|
456
|
+
}) { title }
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Multi-level traversal works when the intermediate models also have registered filters:
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
UserFilter = orm.filter(User)
|
|
464
|
+
PostFilter = orm.filter(Post)
|
|
465
|
+
CommentFilter = orm.filter(Comment) # Comment -> Post -> User
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
```graphql
|
|
469
|
+
# Find comments on posts written by Alice
|
|
470
|
+
{
|
|
471
|
+
comments(filter: {
|
|
472
|
+
object: { post: {
|
|
473
|
+
object: { author: { field: { name: { exact: "Alice" } } } }
|
|
474
|
+
} }
|
|
475
|
+
}) { body }
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The `object` type is `@oneOf`, so each filter entry names exactly one relation.
|
|
480
|
+
|
|
481
|
+
Relations only appear in `object` if their target model already has a registered filter at the time `orm.filter()` is called. Register child model filters before parent model filters.
|
|
482
|
+
|
|
483
|
+
#### Filter Projection
|
|
484
|
+
|
|
485
|
+
By default every relation with a registered filter appears in `object`. Pass `project={...}` to `orm.filter()` to control exactly which relations are exposed and how deep traversal can go:
|
|
486
|
+
|
|
487
|
+
```python
|
|
488
|
+
UserFilter = orm.filter(User)
|
|
489
|
+
TagFilter = orm.filter(Tag)
|
|
490
|
+
CommentFilter = orm.filter(Comment)
|
|
491
|
+
|
|
492
|
+
# Only expose "author" in the object type — tags and comments are excluded
|
|
493
|
+
PostFilter = orm.filter(Post, project={"author": {}})
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
```graphql
|
|
497
|
+
# This works (author is projected)
|
|
498
|
+
{ posts(filter: { object: { author: { field: { name: { exact: "Alice" } } } } }) { title } }
|
|
499
|
+
|
|
500
|
+
# This would be a schema error (tags is not projected)
|
|
501
|
+
{ posts(filter: { object: { tags: { field: { name: { exact: "python" } } } } }) { title } }
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Sub-project dicts control nested traversal. An empty dict `{}` means "include this relation but don't allow further object traversal from it". A non-empty dict lists which of the related model's relations are reachable:
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
# Allow Comment -> post, and from post -> author (but not post -> tags)
|
|
508
|
+
CommentFilter = orm.filter(Comment, project={
|
|
509
|
+
"post": {
|
|
510
|
+
"author": {},
|
|
511
|
+
},
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Summary:
|
|
516
|
+
|
|
517
|
+
| `project` value | Behavior |
|
|
518
|
+
| --- | --- |
|
|
519
|
+
| `None` (default) | Auto-include all relations with registered filters |
|
|
520
|
+
| `{}` | No `object` type at all (scalar lookups only) |
|
|
521
|
+
| `{"rel": {}}` | Include `rel` as a leaf (no further traversal from it) |
|
|
522
|
+
| `{"rel": {"nested": {}}}` | Include `rel` and allow traversal to `nested` from it |
|
|
523
|
+
|
|
524
|
+
Projected filters are cached internally and do not overwrite the global filter registry, so you can create multiple projected variants of the same model's filter for different schema entry points.
|
|
525
|
+
|
|
425
526
|
### Ordering
|
|
426
527
|
|
|
427
528
|
Generate an order input:
|
|
@@ -640,6 +741,143 @@ In practice:
|
|
|
640
741
|
|
|
641
742
|
---
|
|
642
743
|
|
|
744
|
+
### Recursive Node Mutations
|
|
745
|
+
|
|
746
|
+
`orm.mutations.create_node(...)` and `orm.mutations.update_node(...)` generate catch-all Relay `Node` mutations with recursive nested inputs.
|
|
747
|
+
|
|
748
|
+
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(...)`:
|
|
749
|
+
|
|
750
|
+
```python
|
|
751
|
+
import strawberry
|
|
752
|
+
from strawberry import relay
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@orm.type(User)
|
|
756
|
+
class UserNode(relay.Node):
|
|
757
|
+
id: relay.NodeID[int]
|
|
758
|
+
name: auto
|
|
759
|
+
email: auto
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@orm.type(Post)
|
|
763
|
+
class PostNode(relay.Node):
|
|
764
|
+
id: relay.NodeID[int]
|
|
765
|
+
title: auto
|
|
766
|
+
body: auto
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
CreateNodeInput = orm.mutations.create_node_input()
|
|
770
|
+
UpdateNodeInput = orm.mutations.update_node_input()
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@strawberry.type
|
|
774
|
+
class Mutation:
|
|
775
|
+
create_node = orm.mutations.create_node()
|
|
776
|
+
update_node = orm.mutations.update_node()
|
|
777
|
+
|
|
778
|
+
@strawberry.field
|
|
779
|
+
def custom_create_node(self, input: CreateNodeInput) -> str:
|
|
780
|
+
return "implement your own create logic here"
|
|
781
|
+
|
|
782
|
+
@strawberry.field
|
|
783
|
+
def custom_update_node(self, input: UpdateNodeInput) -> str:
|
|
784
|
+
return "implement your own update logic here"
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
List relations use `items`, while singular relations use explicit `create` / `update` branches:
|
|
788
|
+
|
|
789
|
+
```graphql
|
|
790
|
+
mutation {
|
|
791
|
+
createNode(input: {
|
|
792
|
+
post: {
|
|
793
|
+
title: "Hello"
|
|
794
|
+
body: "World"
|
|
795
|
+
author: {
|
|
796
|
+
create: {
|
|
797
|
+
name: "Alice"
|
|
798
|
+
email: "alice@example.com"
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
tags: {
|
|
802
|
+
items: [{ create: { name: "python" } }]
|
|
803
|
+
mode: REPLACE
|
|
804
|
+
onRemove: DELETE
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}) {
|
|
808
|
+
__typename
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
#### Projection And Policy Config
|
|
814
|
+
|
|
815
|
+
Pass `project={...}` to restrict recursion depth and configure relation semantics in one dict:
|
|
816
|
+
|
|
817
|
+
```python
|
|
818
|
+
project = {
|
|
819
|
+
"post": {
|
|
820
|
+
"author": {
|
|
821
|
+
"_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
|
|
822
|
+
},
|
|
823
|
+
"comments": {
|
|
824
|
+
"_meta": {
|
|
825
|
+
"mode": ["PATCH", "REPLACE"],
|
|
826
|
+
"onRemove": ["DISCONNECT", "DELETE"],
|
|
827
|
+
},
|
|
828
|
+
"author": {
|
|
829
|
+
"_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
"tags": {
|
|
833
|
+
"_meta": {
|
|
834
|
+
"mode": "REPLACE",
|
|
835
|
+
"onRemove": "DELETE",
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
"comment": {
|
|
840
|
+
"author": {
|
|
841
|
+
"_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
@strawberry.type
|
|
848
|
+
class Mutation:
|
|
849
|
+
create_node = orm.mutations.create_node(project=project)
|
|
850
|
+
update_node = orm.mutations.update_node(project=project)
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
Rules for the config object:
|
|
854
|
+
|
|
855
|
+
- Root keys are model names (`post`, `comment`, `user`, ...).
|
|
856
|
+
- Nested keys are relation names available on that model.
|
|
857
|
+
- `_meta` is optional and configures behavior for that relation subtree.
|
|
858
|
+
- Omitted relations still exist as shallow nested inputs, but recursion stops after one more level.
|
|
859
|
+
|
|
860
|
+
`_meta` supports:
|
|
861
|
+
|
|
862
|
+
- `mode`: list relation merge strategy (`PATCH` or `REPLACE`)
|
|
863
|
+
- `onRemove`: what to do with removed items from a list relation (`DISCONNECT` or `DELETE`)
|
|
864
|
+
- `onReplace`: what to do with the previous object when replacing a singular relation (`DISCONNECT` or `DELETE`)
|
|
865
|
+
|
|
866
|
+
The `_meta` values can be either:
|
|
867
|
+
|
|
868
|
+
- an array of enum strings, which means the GraphQL input exposes that field and the caller may choose from those options
|
|
869
|
+
- a single enum string, which fixes that behavior for the relation and omits the corresponding GraphQL field
|
|
870
|
+
|
|
871
|
+
Default behavior when a field is exposed but omitted in the mutation input:
|
|
872
|
+
|
|
873
|
+
- `mode` defaults to `PATCH` when allowed
|
|
874
|
+
- `onRemove` defaults to `DISCONNECT` when allowed
|
|
875
|
+
- `onReplace` defaults to `DISCONNECT` when allowed
|
|
876
|
+
|
|
877
|
+
If the preferred default is not included in the allowed array, the first configured value is used.
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
643
881
|
## Query Optimization
|
|
644
882
|
|
|
645
883
|
Add the optimizer extension to your schema:
|
|
@@ -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,6 +31,8 @@ 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
38
|
- Python `>=3.12`
|
|
@@ -388,6 +390,104 @@ Typical string lookups: `exact`, `neq`, `contains`, `iContains`, `startsWith`, `
|
|
|
388
390
|
|
|
389
391
|
Regex lookups (`regex`, `iRegex`) are disabled by default. Enable with `enable_regex_filters=True`.
|
|
390
392
|
|
|
393
|
+
#### Object Traversal
|
|
394
|
+
|
|
395
|
+
When filters are registered for related models, the generated filter gains an `object` key that lets you filter parent rows based on conditions on their related objects:
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
UserFilter = orm.filter(User)
|
|
399
|
+
PostFilter = orm.filter(Post) # Post has an "author" relation to User
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```graphql
|
|
403
|
+
{
|
|
404
|
+
posts(filter: {
|
|
405
|
+
object: { author: { field: { name: { exact: "Alice" } } } }
|
|
406
|
+
}) {
|
|
407
|
+
title
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Object traversal composes with boolean operators:
|
|
413
|
+
|
|
414
|
+
```graphql
|
|
415
|
+
{
|
|
416
|
+
posts(filter: {
|
|
417
|
+
all: [
|
|
418
|
+
{ field: { isPublished: { exact: true } } }
|
|
419
|
+
{ object: { author: { field: { name: { exact: "Alice" } } } } }
|
|
420
|
+
]
|
|
421
|
+
}) { title }
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Multi-level traversal works when the intermediate models also have registered filters:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
UserFilter = orm.filter(User)
|
|
429
|
+
PostFilter = orm.filter(Post)
|
|
430
|
+
CommentFilter = orm.filter(Comment) # Comment -> Post -> User
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
```graphql
|
|
434
|
+
# Find comments on posts written by Alice
|
|
435
|
+
{
|
|
436
|
+
comments(filter: {
|
|
437
|
+
object: { post: {
|
|
438
|
+
object: { author: { field: { name: { exact: "Alice" } } } }
|
|
439
|
+
} }
|
|
440
|
+
}) { body }
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The `object` type is `@oneOf`, so each filter entry names exactly one relation.
|
|
445
|
+
|
|
446
|
+
Relations only appear in `object` if their target model already has a registered filter at the time `orm.filter()` is called. Register child model filters before parent model filters.
|
|
447
|
+
|
|
448
|
+
#### Filter Projection
|
|
449
|
+
|
|
450
|
+
By default every relation with a registered filter appears in `object`. Pass `project={...}` to `orm.filter()` to control exactly which relations are exposed and how deep traversal can go:
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
UserFilter = orm.filter(User)
|
|
454
|
+
TagFilter = orm.filter(Tag)
|
|
455
|
+
CommentFilter = orm.filter(Comment)
|
|
456
|
+
|
|
457
|
+
# Only expose "author" in the object type — tags and comments are excluded
|
|
458
|
+
PostFilter = orm.filter(Post, project={"author": {}})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
```graphql
|
|
462
|
+
# This works (author is projected)
|
|
463
|
+
{ posts(filter: { object: { author: { field: { name: { exact: "Alice" } } } } }) { title } }
|
|
464
|
+
|
|
465
|
+
# This would be a schema error (tags is not projected)
|
|
466
|
+
{ posts(filter: { object: { tags: { field: { name: { exact: "python" } } } } }) { title } }
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Sub-project dicts control nested traversal. An empty dict `{}` means "include this relation but don't allow further object traversal from it". A non-empty dict lists which of the related model's relations are reachable:
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
# Allow Comment -> post, and from post -> author (but not post -> tags)
|
|
473
|
+
CommentFilter = orm.filter(Comment, project={
|
|
474
|
+
"post": {
|
|
475
|
+
"author": {},
|
|
476
|
+
},
|
|
477
|
+
})
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Summary:
|
|
481
|
+
|
|
482
|
+
| `project` value | Behavior |
|
|
483
|
+
| --- | --- |
|
|
484
|
+
| `None` (default) | Auto-include all relations with registered filters |
|
|
485
|
+
| `{}` | No `object` type at all (scalar lookups only) |
|
|
486
|
+
| `{"rel": {}}` | Include `rel` as a leaf (no further traversal from it) |
|
|
487
|
+
| `{"rel": {"nested": {}}}` | Include `rel` and allow traversal to `nested` from it |
|
|
488
|
+
|
|
489
|
+
Projected filters are cached internally and do not overwrite the global filter registry, so you can create multiple projected variants of the same model's filter for different schema entry points.
|
|
490
|
+
|
|
391
491
|
### Ordering
|
|
392
492
|
|
|
393
493
|
Generate an order input:
|
|
@@ -606,6 +706,143 @@ In practice:
|
|
|
606
706
|
|
|
607
707
|
---
|
|
608
708
|
|
|
709
|
+
### Recursive Node Mutations
|
|
710
|
+
|
|
711
|
+
`orm.mutations.create_node(...)` and `orm.mutations.update_node(...)` generate catch-all Relay `Node` mutations with recursive nested inputs.
|
|
712
|
+
|
|
713
|
+
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(...)`:
|
|
714
|
+
|
|
715
|
+
```python
|
|
716
|
+
import strawberry
|
|
717
|
+
from strawberry import relay
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@orm.type(User)
|
|
721
|
+
class UserNode(relay.Node):
|
|
722
|
+
id: relay.NodeID[int]
|
|
723
|
+
name: auto
|
|
724
|
+
email: auto
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
@orm.type(Post)
|
|
728
|
+
class PostNode(relay.Node):
|
|
729
|
+
id: relay.NodeID[int]
|
|
730
|
+
title: auto
|
|
731
|
+
body: auto
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
CreateNodeInput = orm.mutations.create_node_input()
|
|
735
|
+
UpdateNodeInput = orm.mutations.update_node_input()
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@strawberry.type
|
|
739
|
+
class Mutation:
|
|
740
|
+
create_node = orm.mutations.create_node()
|
|
741
|
+
update_node = orm.mutations.update_node()
|
|
742
|
+
|
|
743
|
+
@strawberry.field
|
|
744
|
+
def custom_create_node(self, input: CreateNodeInput) -> str:
|
|
745
|
+
return "implement your own create logic here"
|
|
746
|
+
|
|
747
|
+
@strawberry.field
|
|
748
|
+
def custom_update_node(self, input: UpdateNodeInput) -> str:
|
|
749
|
+
return "implement your own update logic here"
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
List relations use `items`, while singular relations use explicit `create` / `update` branches:
|
|
753
|
+
|
|
754
|
+
```graphql
|
|
755
|
+
mutation {
|
|
756
|
+
createNode(input: {
|
|
757
|
+
post: {
|
|
758
|
+
title: "Hello"
|
|
759
|
+
body: "World"
|
|
760
|
+
author: {
|
|
761
|
+
create: {
|
|
762
|
+
name: "Alice"
|
|
763
|
+
email: "alice@example.com"
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
tags: {
|
|
767
|
+
items: [{ create: { name: "python" } }]
|
|
768
|
+
mode: REPLACE
|
|
769
|
+
onRemove: DELETE
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}) {
|
|
773
|
+
__typename
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
#### Projection And Policy Config
|
|
779
|
+
|
|
780
|
+
Pass `project={...}` to restrict recursion depth and configure relation semantics in one dict:
|
|
781
|
+
|
|
782
|
+
```python
|
|
783
|
+
project = {
|
|
784
|
+
"post": {
|
|
785
|
+
"author": {
|
|
786
|
+
"_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
|
|
787
|
+
},
|
|
788
|
+
"comments": {
|
|
789
|
+
"_meta": {
|
|
790
|
+
"mode": ["PATCH", "REPLACE"],
|
|
791
|
+
"onRemove": ["DISCONNECT", "DELETE"],
|
|
792
|
+
},
|
|
793
|
+
"author": {
|
|
794
|
+
"_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
"tags": {
|
|
798
|
+
"_meta": {
|
|
799
|
+
"mode": "REPLACE",
|
|
800
|
+
"onRemove": "DELETE",
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
"comment": {
|
|
805
|
+
"author": {
|
|
806
|
+
"_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
@strawberry.type
|
|
813
|
+
class Mutation:
|
|
814
|
+
create_node = orm.mutations.create_node(project=project)
|
|
815
|
+
update_node = orm.mutations.update_node(project=project)
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
Rules for the config object:
|
|
819
|
+
|
|
820
|
+
- Root keys are model names (`post`, `comment`, `user`, ...).
|
|
821
|
+
- Nested keys are relation names available on that model.
|
|
822
|
+
- `_meta` is optional and configures behavior for that relation subtree.
|
|
823
|
+
- Omitted relations still exist as shallow nested inputs, but recursion stops after one more level.
|
|
824
|
+
|
|
825
|
+
`_meta` supports:
|
|
826
|
+
|
|
827
|
+
- `mode`: list relation merge strategy (`PATCH` or `REPLACE`)
|
|
828
|
+
- `onRemove`: what to do with removed items from a list relation (`DISCONNECT` or `DELETE`)
|
|
829
|
+
- `onReplace`: what to do with the previous object when replacing a singular relation (`DISCONNECT` or `DELETE`)
|
|
830
|
+
|
|
831
|
+
The `_meta` values can be either:
|
|
832
|
+
|
|
833
|
+
- an array of enum strings, which means the GraphQL input exposes that field and the caller may choose from those options
|
|
834
|
+
- a single enum string, which fixes that behavior for the relation and omits the corresponding GraphQL field
|
|
835
|
+
|
|
836
|
+
Default behavior when a field is exposed but omitted in the mutation input:
|
|
837
|
+
|
|
838
|
+
- `mode` defaults to `PATCH` when allowed
|
|
839
|
+
- `onRemove` defaults to `DISCONNECT` when allowed
|
|
840
|
+
- `onReplace` defaults to `DISCONNECT` when allowed
|
|
841
|
+
|
|
842
|
+
If the preferred default is not included in the allowed array, the first configured value is used.
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
609
846
|
## Query Optimization
|
|
610
847
|
|
|
611
848
|
Add the optimizer extension to your schema:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "strawberry-orm"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Unified, backend-agnostic ORM abstraction for Strawberry GraphQL"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -33,6 +33,7 @@ django = ["django>=4.2"]
|
|
|
33
33
|
sqlalchemy = ["sqlalchemy>=2.0"]
|
|
34
34
|
tortoise = ["tortoise-orm>=0.25.0"]
|
|
35
35
|
dev = [
|
|
36
|
+
"greenlet>=3.3.2",
|
|
36
37
|
"pytest>=8.0",
|
|
37
38
|
"pytest-asyncio>=0.24",
|
|
38
39
|
"pytest-django>=4.8",
|
|
@@ -46,6 +47,7 @@ build-backend = "uv_build"
|
|
|
46
47
|
|
|
47
48
|
[dependency-groups]
|
|
48
49
|
dev = [
|
|
50
|
+
"pytest-cov>=7.0.0",
|
|
49
51
|
"starlette>=0.52.1",
|
|
50
52
|
"typer>=0.24.1",
|
|
51
53
|
"uvicorn>=0.41.0",
|