strawberry-orm 0.6.0__tar.gz → 0.7.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.6.0 → strawberry_orm-0.7.0}/PKG-INFO +215 -3
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/README.md +214 -2
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/pyproject.toml +3 -2
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/__init__.py +8 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/_async.py +2 -1
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/backends/_base.py +321 -1
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/backends/django.py +117 -40
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/backends/protocol.py +8 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/backends/sqlalchemy.py +138 -42
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/backends/tortoise.py +126 -48
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/core.py +20 -3
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/fields.py +2 -1
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/filters.py +50 -1
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/mutations.py +185 -42
- strawberry_orm-0.7.0/src/strawberry_orm/policy.py +140 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/relay/connection.py +1 -2
- strawberry_orm-0.7.0/src/strawberry_orm/repo.py +284 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/types.py +2 -1
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/backends/__init__.py +0 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/optimizer/__init__.py +0 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/optimizer/extension.py +0 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/optimizer/store.py +0 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/py.typed +0 -0
- {strawberry_orm-0.6.0 → strawberry_orm-0.7.0}/src/strawberry_orm/relay/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: strawberry-orm
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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>
|
|
@@ -46,6 +46,7 @@ Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.r
|
|
|
46
46
|
- [Backends](#backends)
|
|
47
47
|
- [Defining Types](#defining-types)
|
|
48
48
|
- [Filters and Ordering](#filters-and-ordering)
|
|
49
|
+
- [Custom Filters and Ordering](#custom-filters-and-ordering)
|
|
49
50
|
- [Mutations](#mutations)
|
|
50
51
|
- [Relay Integration](#relay-integration)
|
|
51
52
|
- [Query Optimization](#query-optimization)
|
|
@@ -129,7 +130,7 @@ class PostType:
|
|
|
129
130
|
|
|
130
131
|
CreatePostInput = orm.input(Post, include=["title", "body", "author_id"])
|
|
131
132
|
CreateTagInput = orm.input(Tag, include=["name"])
|
|
132
|
-
TagRef = orm.ref(Tag, create=CreateTagInput, delete=True)
|
|
133
|
+
TagRef = orm.ref(Tag, create=CreateTagInput, unlink=True, delete=True)
|
|
133
134
|
|
|
134
135
|
@strawberry.type
|
|
135
136
|
class Mutation:
|
|
@@ -517,6 +518,217 @@ Registration order matters: define related orders *before* the parent (e.g. `orm
|
|
|
517
518
|
|
|
518
519
|
---
|
|
519
520
|
|
|
521
|
+
## Custom Filters and Ordering
|
|
522
|
+
|
|
523
|
+
`orm.filter()` and `orm.order()` auto-generate types from model introspection. When you need filter logic that goes beyond column lookups — full-text search across multiple fields, subquery-based conditions, or ordering by computed values — use `orm.filter_type()` and `orm.order_type()` with the `@filter_field` and `@order_field` decorators.
|
|
524
|
+
|
|
525
|
+
### Custom Filter Types
|
|
526
|
+
|
|
527
|
+
`orm.filter_type(Model)` is a class decorator. Annotate fields with `auto` for standard lookups (identical to what `orm.filter()` generates). Add methods decorated with `@filter_field` for custom logic:
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
from strawberry_orm import StrawberryORM, filter_field, auto
|
|
531
|
+
|
|
532
|
+
orm = StrawberryORM("sqlalchemy", dialect="postgresql", session_getter=...)
|
|
533
|
+
|
|
534
|
+
@orm.filter_type(User)
|
|
535
|
+
class UserFilter:
|
|
536
|
+
name: auto # standard StringLookup
|
|
537
|
+
email: auto # standard StringLookup
|
|
538
|
+
|
|
539
|
+
@filter_field
|
|
540
|
+
def search(self, value: str, query):
|
|
541
|
+
"""Full-text search across name and email."""
|
|
542
|
+
from sqlalchemy import or_
|
|
543
|
+
return query.where(
|
|
544
|
+
or_(User.name.ilike(f"%{value}%"), User.email.ilike(f"%{value}%"))
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
@filter_field
|
|
548
|
+
def has_posts(self, value: bool, query):
|
|
549
|
+
"""Filter users who have (or lack) any posts."""
|
|
550
|
+
from sqlalchemy import func, select
|
|
551
|
+
subq = (
|
|
552
|
+
select(func.count(Post.id))
|
|
553
|
+
.where(Post.author_id == User.id)
|
|
554
|
+
.correlate(User)
|
|
555
|
+
.scalar_subquery()
|
|
556
|
+
)
|
|
557
|
+
if value:
|
|
558
|
+
return query.where(subq > 0)
|
|
559
|
+
return query.where(subq == 0)
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Each `@filter_field` method must:
|
|
563
|
+
|
|
564
|
+
- Have a `value` parameter with a **type annotation** — this becomes the GraphQL input type for the field.
|
|
565
|
+
- Have a `query` parameter — receives the backend's native query object (Django `QuerySet`, SQLAlchemy `Select`, or Tortoise `QuerySet`).
|
|
566
|
+
- Return the modified query.
|
|
567
|
+
- Optionally accept an `info` parameter to receive the Strawberry `Info` context.
|
|
568
|
+
|
|
569
|
+
The generated GraphQL input places custom fields as top-level keys alongside `field`, `object`, `all`, `any`, `not`, and `oneOf`:
|
|
570
|
+
|
|
571
|
+
```graphql
|
|
572
|
+
input UserFilter @oneOf {
|
|
573
|
+
field: UserField # auto-generated scalar lookups
|
|
574
|
+
object: UserObject # auto-generated relation lookups (if any)
|
|
575
|
+
search: String # custom
|
|
576
|
+
hasPosts: Boolean # custom
|
|
577
|
+
all: [UserFilter!]
|
|
578
|
+
any: [UserFilter!]
|
|
579
|
+
not: UserFilter
|
|
580
|
+
oneOf: [UserFilter!]
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Since filters are `@oneOf`, combine custom filters with standard lookups using `all` or `any`:
|
|
585
|
+
|
|
586
|
+
```graphql
|
|
587
|
+
{
|
|
588
|
+
users(filter: { all: [
|
|
589
|
+
{ search: "john" },
|
|
590
|
+
{ field: { email: { contains: "example.com" } } }
|
|
591
|
+
] }) {
|
|
592
|
+
name
|
|
593
|
+
email
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Custom Order Types
|
|
599
|
+
|
|
600
|
+
`orm.order_type(Model)` works the same way. `auto` fields get the standard `Ordering` enum. Methods decorated with `@order_field` receive a `value` of type `Ordering` (ASC, DESC, etc.) and return the modified query:
|
|
601
|
+
|
|
602
|
+
```python
|
|
603
|
+
from strawberry_orm import order_field
|
|
604
|
+
from strawberry_orm.types import Ordering
|
|
605
|
+
|
|
606
|
+
@orm.order_type(User)
|
|
607
|
+
class UserOrder:
|
|
608
|
+
name: auto # standard Ordering (ASC/DESC/...)
|
|
609
|
+
|
|
610
|
+
@order_field
|
|
611
|
+
def post_count(self, value: Ordering, query):
|
|
612
|
+
"""Order users by how many posts they have."""
|
|
613
|
+
from sqlalchemy import func
|
|
614
|
+
query = query.outerjoin(Post, Post.author_id == User.id).group_by(User.id)
|
|
615
|
+
col = func.count(Post.id)
|
|
616
|
+
if "DESC" in value.value:
|
|
617
|
+
return query.order_by(col.desc())
|
|
618
|
+
return query.order_by(col.asc())
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
The generated GraphQL input:
|
|
622
|
+
|
|
623
|
+
```graphql
|
|
624
|
+
input UserOrder @oneOf {
|
|
625
|
+
field: UserOrderField # auto-generated
|
|
626
|
+
object: UserOrderObject # auto-generated (if relations exist)
|
|
627
|
+
postCount: Ordering # custom
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Custom and standard orders compose naturally in the order list:
|
|
632
|
+
|
|
633
|
+
```graphql
|
|
634
|
+
{
|
|
635
|
+
users(order: [
|
|
636
|
+
{ postCount: DESC },
|
|
637
|
+
{ field: { name: ASC } }
|
|
638
|
+
]) {
|
|
639
|
+
name
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Using Custom Types
|
|
645
|
+
|
|
646
|
+
Custom filter and order types are used exactly like auto-generated ones:
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
@orm.type(User, filters=UserFilter, order=UserOrder)
|
|
650
|
+
class UserType:
|
|
651
|
+
id: auto
|
|
652
|
+
name: auto
|
|
653
|
+
email: auto
|
|
654
|
+
|
|
655
|
+
@strawberry.type
|
|
656
|
+
class Query:
|
|
657
|
+
@orm.field()
|
|
658
|
+
def users(self) -> list[UserType]:
|
|
659
|
+
return orm.get_default_queryset(User)
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
They also work with Relay connections and `orm.connection()`.
|
|
663
|
+
|
|
664
|
+
### Backend-Specific Examples
|
|
665
|
+
|
|
666
|
+
The query manipulation inside `@filter_field` and `@order_field` methods is backend-specific since it operates on native query objects. Here are equivalent examples for each backend:
|
|
667
|
+
|
|
668
|
+
<details>
|
|
669
|
+
<summary>Django</summary>
|
|
670
|
+
|
|
671
|
+
```python
|
|
672
|
+
from django.db.models import Q, Count, F
|
|
673
|
+
|
|
674
|
+
@orm.filter_type(User)
|
|
675
|
+
class UserFilter:
|
|
676
|
+
name: auto
|
|
677
|
+
|
|
678
|
+
@filter_field
|
|
679
|
+
def search(self, value: str, query):
|
|
680
|
+
return query.filter(Q(name__icontains=value) | Q(email__icontains=value))
|
|
681
|
+
|
|
682
|
+
@orm.order_type(User)
|
|
683
|
+
class UserOrder:
|
|
684
|
+
name: auto
|
|
685
|
+
|
|
686
|
+
@order_field
|
|
687
|
+
def post_count(self, value: Ordering, query):
|
|
688
|
+
query = query.annotate(_post_count=Count("posts"))
|
|
689
|
+
dir_value = value.value
|
|
690
|
+
if dir_value.startswith("DESC"):
|
|
691
|
+
return query.order_by(F("_post_count").desc())
|
|
692
|
+
return query.order_by(F("_post_count").asc())
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
</details>
|
|
696
|
+
|
|
697
|
+
<details>
|
|
698
|
+
<summary>Tortoise</summary>
|
|
699
|
+
|
|
700
|
+
```python
|
|
701
|
+
from tortoise.queryset import Q
|
|
702
|
+
from tortoise.functions import Count
|
|
703
|
+
|
|
704
|
+
@orm.filter_type(User)
|
|
705
|
+
class UserFilter:
|
|
706
|
+
name: auto
|
|
707
|
+
|
|
708
|
+
@filter_field
|
|
709
|
+
def search(self, value: str, query):
|
|
710
|
+
return query.filter(Q(name__icontains=value) | Q(email__icontains=value))
|
|
711
|
+
|
|
712
|
+
@orm.order_type(User)
|
|
713
|
+
class UserOrder:
|
|
714
|
+
name: auto
|
|
715
|
+
|
|
716
|
+
@order_field
|
|
717
|
+
def post_count(self, value: Ordering, query):
|
|
718
|
+
query = query.annotate(_post_count=Count("posts"))
|
|
719
|
+
if value.value.startswith("DESC"):
|
|
720
|
+
return query.order_by("-_post_count")
|
|
721
|
+
return query.order_by("_post_count")
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
</details>
|
|
725
|
+
|
|
726
|
+
### Combining with `orm.filter()` / `orm.order()`
|
|
727
|
+
|
|
728
|
+
`orm.filter()` and `orm.order()` remain available for fully auto-generated types. Use `orm.filter_type()` and `orm.order_type()` only when you need custom logic. The types produced by both APIs are interchangeable in all contexts — `orm.type(Model, filters=..., order=...)`, `orm.field(filters=..., order=...)`, and `orm.connection()`.
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
520
732
|
## Mutations
|
|
521
733
|
|
|
522
734
|
Write plain `@strawberry.mutation` resolvers and use `strawberry-orm` for generated input types:
|
|
@@ -857,7 +1069,7 @@ orm = StrawberryORM(
|
|
|
857
1069
|
|
|
858
1070
|
## Public Exports
|
|
859
1071
|
|
|
860
|
-
`StrawberryORM`, `auto`, `make_field`, `make_ref_type`, `Ordering`, `FieldDefinition`, `FieldHints`, `OptimizerExtension`, `OptimizerStore`, `UNSET`, and the built-in lookup input classes from `strawberry_orm.filters`.
|
|
1072
|
+
`StrawberryORM`, `auto`, `make_field`, `make_ref_type`, `Ordering`, `FieldDefinition`, `FieldHints`, `OptimizerExtension`, `OptimizerStore`, `UNSET`, `filter_field`, `order_field`, and the built-in lookup input classes from `strawberry_orm.filters`.
|
|
861
1073
|
|
|
862
1074
|
## License
|
|
863
1075
|
|
|
@@ -11,6 +11,7 @@ Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.r
|
|
|
11
11
|
- [Backends](#backends)
|
|
12
12
|
- [Defining Types](#defining-types)
|
|
13
13
|
- [Filters and Ordering](#filters-and-ordering)
|
|
14
|
+
- [Custom Filters and Ordering](#custom-filters-and-ordering)
|
|
14
15
|
- [Mutations](#mutations)
|
|
15
16
|
- [Relay Integration](#relay-integration)
|
|
16
17
|
- [Query Optimization](#query-optimization)
|
|
@@ -94,7 +95,7 @@ class PostType:
|
|
|
94
95
|
|
|
95
96
|
CreatePostInput = orm.input(Post, include=["title", "body", "author_id"])
|
|
96
97
|
CreateTagInput = orm.input(Tag, include=["name"])
|
|
97
|
-
TagRef = orm.ref(Tag, create=CreateTagInput, delete=True)
|
|
98
|
+
TagRef = orm.ref(Tag, create=CreateTagInput, unlink=True, delete=True)
|
|
98
99
|
|
|
99
100
|
@strawberry.type
|
|
100
101
|
class Mutation:
|
|
@@ -482,6 +483,217 @@ Registration order matters: define related orders *before* the parent (e.g. `orm
|
|
|
482
483
|
|
|
483
484
|
---
|
|
484
485
|
|
|
486
|
+
## Custom Filters and Ordering
|
|
487
|
+
|
|
488
|
+
`orm.filter()` and `orm.order()` auto-generate types from model introspection. When you need filter logic that goes beyond column lookups — full-text search across multiple fields, subquery-based conditions, or ordering by computed values — use `orm.filter_type()` and `orm.order_type()` with the `@filter_field` and `@order_field` decorators.
|
|
489
|
+
|
|
490
|
+
### Custom Filter Types
|
|
491
|
+
|
|
492
|
+
`orm.filter_type(Model)` is a class decorator. Annotate fields with `auto` for standard lookups (identical to what `orm.filter()` generates). Add methods decorated with `@filter_field` for custom logic:
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
from strawberry_orm import StrawberryORM, filter_field, auto
|
|
496
|
+
|
|
497
|
+
orm = StrawberryORM("sqlalchemy", dialect="postgresql", session_getter=...)
|
|
498
|
+
|
|
499
|
+
@orm.filter_type(User)
|
|
500
|
+
class UserFilter:
|
|
501
|
+
name: auto # standard StringLookup
|
|
502
|
+
email: auto # standard StringLookup
|
|
503
|
+
|
|
504
|
+
@filter_field
|
|
505
|
+
def search(self, value: str, query):
|
|
506
|
+
"""Full-text search across name and email."""
|
|
507
|
+
from sqlalchemy import or_
|
|
508
|
+
return query.where(
|
|
509
|
+
or_(User.name.ilike(f"%{value}%"), User.email.ilike(f"%{value}%"))
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
@filter_field
|
|
513
|
+
def has_posts(self, value: bool, query):
|
|
514
|
+
"""Filter users who have (or lack) any posts."""
|
|
515
|
+
from sqlalchemy import func, select
|
|
516
|
+
subq = (
|
|
517
|
+
select(func.count(Post.id))
|
|
518
|
+
.where(Post.author_id == User.id)
|
|
519
|
+
.correlate(User)
|
|
520
|
+
.scalar_subquery()
|
|
521
|
+
)
|
|
522
|
+
if value:
|
|
523
|
+
return query.where(subq > 0)
|
|
524
|
+
return query.where(subq == 0)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Each `@filter_field` method must:
|
|
528
|
+
|
|
529
|
+
- Have a `value` parameter with a **type annotation** — this becomes the GraphQL input type for the field.
|
|
530
|
+
- Have a `query` parameter — receives the backend's native query object (Django `QuerySet`, SQLAlchemy `Select`, or Tortoise `QuerySet`).
|
|
531
|
+
- Return the modified query.
|
|
532
|
+
- Optionally accept an `info` parameter to receive the Strawberry `Info` context.
|
|
533
|
+
|
|
534
|
+
The generated GraphQL input places custom fields as top-level keys alongside `field`, `object`, `all`, `any`, `not`, and `oneOf`:
|
|
535
|
+
|
|
536
|
+
```graphql
|
|
537
|
+
input UserFilter @oneOf {
|
|
538
|
+
field: UserField # auto-generated scalar lookups
|
|
539
|
+
object: UserObject # auto-generated relation lookups (if any)
|
|
540
|
+
search: String # custom
|
|
541
|
+
hasPosts: Boolean # custom
|
|
542
|
+
all: [UserFilter!]
|
|
543
|
+
any: [UserFilter!]
|
|
544
|
+
not: UserFilter
|
|
545
|
+
oneOf: [UserFilter!]
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Since filters are `@oneOf`, combine custom filters with standard lookups using `all` or `any`:
|
|
550
|
+
|
|
551
|
+
```graphql
|
|
552
|
+
{
|
|
553
|
+
users(filter: { all: [
|
|
554
|
+
{ search: "john" },
|
|
555
|
+
{ field: { email: { contains: "example.com" } } }
|
|
556
|
+
] }) {
|
|
557
|
+
name
|
|
558
|
+
email
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Custom Order Types
|
|
564
|
+
|
|
565
|
+
`orm.order_type(Model)` works the same way. `auto` fields get the standard `Ordering` enum. Methods decorated with `@order_field` receive a `value` of type `Ordering` (ASC, DESC, etc.) and return the modified query:
|
|
566
|
+
|
|
567
|
+
```python
|
|
568
|
+
from strawberry_orm import order_field
|
|
569
|
+
from strawberry_orm.types import Ordering
|
|
570
|
+
|
|
571
|
+
@orm.order_type(User)
|
|
572
|
+
class UserOrder:
|
|
573
|
+
name: auto # standard Ordering (ASC/DESC/...)
|
|
574
|
+
|
|
575
|
+
@order_field
|
|
576
|
+
def post_count(self, value: Ordering, query):
|
|
577
|
+
"""Order users by how many posts they have."""
|
|
578
|
+
from sqlalchemy import func
|
|
579
|
+
query = query.outerjoin(Post, Post.author_id == User.id).group_by(User.id)
|
|
580
|
+
col = func.count(Post.id)
|
|
581
|
+
if "DESC" in value.value:
|
|
582
|
+
return query.order_by(col.desc())
|
|
583
|
+
return query.order_by(col.asc())
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The generated GraphQL input:
|
|
587
|
+
|
|
588
|
+
```graphql
|
|
589
|
+
input UserOrder @oneOf {
|
|
590
|
+
field: UserOrderField # auto-generated
|
|
591
|
+
object: UserOrderObject # auto-generated (if relations exist)
|
|
592
|
+
postCount: Ordering # custom
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Custom and standard orders compose naturally in the order list:
|
|
597
|
+
|
|
598
|
+
```graphql
|
|
599
|
+
{
|
|
600
|
+
users(order: [
|
|
601
|
+
{ postCount: DESC },
|
|
602
|
+
{ field: { name: ASC } }
|
|
603
|
+
]) {
|
|
604
|
+
name
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Using Custom Types
|
|
610
|
+
|
|
611
|
+
Custom filter and order types are used exactly like auto-generated ones:
|
|
612
|
+
|
|
613
|
+
```python
|
|
614
|
+
@orm.type(User, filters=UserFilter, order=UserOrder)
|
|
615
|
+
class UserType:
|
|
616
|
+
id: auto
|
|
617
|
+
name: auto
|
|
618
|
+
email: auto
|
|
619
|
+
|
|
620
|
+
@strawberry.type
|
|
621
|
+
class Query:
|
|
622
|
+
@orm.field()
|
|
623
|
+
def users(self) -> list[UserType]:
|
|
624
|
+
return orm.get_default_queryset(User)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
They also work with Relay connections and `orm.connection()`.
|
|
628
|
+
|
|
629
|
+
### Backend-Specific Examples
|
|
630
|
+
|
|
631
|
+
The query manipulation inside `@filter_field` and `@order_field` methods is backend-specific since it operates on native query objects. Here are equivalent examples for each backend:
|
|
632
|
+
|
|
633
|
+
<details>
|
|
634
|
+
<summary>Django</summary>
|
|
635
|
+
|
|
636
|
+
```python
|
|
637
|
+
from django.db.models import Q, Count, F
|
|
638
|
+
|
|
639
|
+
@orm.filter_type(User)
|
|
640
|
+
class UserFilter:
|
|
641
|
+
name: auto
|
|
642
|
+
|
|
643
|
+
@filter_field
|
|
644
|
+
def search(self, value: str, query):
|
|
645
|
+
return query.filter(Q(name__icontains=value) | Q(email__icontains=value))
|
|
646
|
+
|
|
647
|
+
@orm.order_type(User)
|
|
648
|
+
class UserOrder:
|
|
649
|
+
name: auto
|
|
650
|
+
|
|
651
|
+
@order_field
|
|
652
|
+
def post_count(self, value: Ordering, query):
|
|
653
|
+
query = query.annotate(_post_count=Count("posts"))
|
|
654
|
+
dir_value = value.value
|
|
655
|
+
if dir_value.startswith("DESC"):
|
|
656
|
+
return query.order_by(F("_post_count").desc())
|
|
657
|
+
return query.order_by(F("_post_count").asc())
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
</details>
|
|
661
|
+
|
|
662
|
+
<details>
|
|
663
|
+
<summary>Tortoise</summary>
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
from tortoise.queryset import Q
|
|
667
|
+
from tortoise.functions import Count
|
|
668
|
+
|
|
669
|
+
@orm.filter_type(User)
|
|
670
|
+
class UserFilter:
|
|
671
|
+
name: auto
|
|
672
|
+
|
|
673
|
+
@filter_field
|
|
674
|
+
def search(self, value: str, query):
|
|
675
|
+
return query.filter(Q(name__icontains=value) | Q(email__icontains=value))
|
|
676
|
+
|
|
677
|
+
@orm.order_type(User)
|
|
678
|
+
class UserOrder:
|
|
679
|
+
name: auto
|
|
680
|
+
|
|
681
|
+
@order_field
|
|
682
|
+
def post_count(self, value: Ordering, query):
|
|
683
|
+
query = query.annotate(_post_count=Count("posts"))
|
|
684
|
+
if value.value.startswith("DESC"):
|
|
685
|
+
return query.order_by("-_post_count")
|
|
686
|
+
return query.order_by("_post_count")
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
</details>
|
|
690
|
+
|
|
691
|
+
### Combining with `orm.filter()` / `orm.order()`
|
|
692
|
+
|
|
693
|
+
`orm.filter()` and `orm.order()` remain available for fully auto-generated types. Use `orm.filter_type()` and `orm.order_type()` only when you need custom logic. The types produced by both APIs are interchangeable in all contexts — `orm.type(Model, filters=..., order=...)`, `orm.field(filters=..., order=...)`, and `orm.connection()`.
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
485
697
|
## Mutations
|
|
486
698
|
|
|
487
699
|
Write plain `@strawberry.mutation` resolvers and use `strawberry-orm` for generated input types:
|
|
@@ -822,7 +1034,7 @@ orm = StrawberryORM(
|
|
|
822
1034
|
|
|
823
1035
|
## Public Exports
|
|
824
1036
|
|
|
825
|
-
`StrawberryORM`, `auto`, `make_field`, `make_ref_type`, `Ordering`, `FieldDefinition`, `FieldHints`, `OptimizerExtension`, `OptimizerStore`, `UNSET`, and the built-in lookup input classes from `strawberry_orm.filters`.
|
|
1037
|
+
`StrawberryORM`, `auto`, `make_field`, `make_ref_type`, `Ordering`, `FieldDefinition`, `FieldHints`, `OptimizerExtension`, `OptimizerStore`, `UNSET`, `filter_field`, `order_field`, and the built-in lookup input classes from `strawberry_orm.filters`.
|
|
826
1038
|
|
|
827
1039
|
## License
|
|
828
1040
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "strawberry-orm"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Unified, backend-agnostic ORM abstraction for Strawberry GraphQL"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -67,7 +67,8 @@ select = [
|
|
|
67
67
|
"SIM", # flake8-simplify
|
|
68
68
|
]
|
|
69
69
|
ignore = [
|
|
70
|
-
"E501",
|
|
70
|
+
"E501", # line too long (handled by formatter)
|
|
71
|
+
"UP046", # Generic[M] needed for runtime introspection via get_original_bases
|
|
71
72
|
]
|
|
72
73
|
|
|
73
74
|
[tool.ruff.lint.isort]
|
|
@@ -12,9 +12,13 @@ from strawberry_orm.filters import (
|
|
|
12
12
|
StringLookup,
|
|
13
13
|
StringLookupNoRegex,
|
|
14
14
|
TimeComparisonLookup,
|
|
15
|
+
filter_field,
|
|
16
|
+
order_field,
|
|
15
17
|
)
|
|
16
18
|
from strawberry_orm.mutations import make_ref_type
|
|
17
19
|
from strawberry_orm.optimizer import FieldHints, OptimizerExtension, OptimizerStore
|
|
20
|
+
from strawberry_orm.policy import MutationPolicy
|
|
21
|
+
from strawberry_orm.repo import AbstractRepo
|
|
18
22
|
from strawberry_orm.types import (
|
|
19
23
|
UNSET,
|
|
20
24
|
FieldDefinition,
|
|
@@ -25,6 +29,7 @@ from strawberry_orm.types import (
|
|
|
25
29
|
)
|
|
26
30
|
|
|
27
31
|
__all__ = [
|
|
32
|
+
"AbstractRepo",
|
|
28
33
|
"BooleanLookup",
|
|
29
34
|
"DateComparisonLookup",
|
|
30
35
|
"DateTimeComparisonLookup",
|
|
@@ -33,6 +38,7 @@ __all__ = [
|
|
|
33
38
|
"FloatComparisonLookup",
|
|
34
39
|
"IDLookup",
|
|
35
40
|
"IntComparisonLookup",
|
|
41
|
+
"MutationPolicy",
|
|
36
42
|
"OperationInfo",
|
|
37
43
|
"OperationMessage",
|
|
38
44
|
"OptimizerExtension",
|
|
@@ -44,6 +50,8 @@ __all__ = [
|
|
|
44
50
|
"TimeComparisonLookup",
|
|
45
51
|
"UNSET",
|
|
46
52
|
"auto",
|
|
53
|
+
"filter_field",
|
|
47
54
|
"make_field",
|
|
48
55
|
"make_ref_type",
|
|
56
|
+
"order_field",
|
|
49
57
|
]
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
6
7
|
from functools import partial
|
|
7
8
|
from inspect import isawaitable
|
|
8
|
-
from typing import Any,
|
|
9
|
+
from typing import Any, TypeVar
|
|
9
10
|
|
|
10
11
|
T = TypeVar("T")
|
|
11
12
|
|