strawberry-orm 0.3.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.3.0 → strawberry_orm-0.4.0}/PKG-INFO +99 -1
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/README.md +98 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/pyproject.toml +1 -1
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/_base.py +122 -10
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/django.py +20 -1
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/sqlalchemy.py +14 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/tortoise.py +20 -1
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/mutations.py +25 -11
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/__init__.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/_async.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/__init__.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/protocol.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/core.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/fields.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/filters.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/__init__.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/extension.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/store.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/py.typed +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/relay/__init__.py +0 -0
- {strawberry_orm-0.3.0 → strawberry_orm-0.4.0}/src/strawberry_orm/relay/connection.py +0 -0
- {strawberry_orm-0.3.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>
|
|
@@ -425,6 +425,104 @@ Typical string lookups: `exact`, `neq`, `contains`, `iContains`, `startsWith`, `
|
|
|
425
425
|
|
|
426
426
|
Regex lookups (`regex`, `iRegex`) are disabled by default. Enable with `enable_regex_filters=True`.
|
|
427
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
|
+
|
|
428
526
|
### Ordering
|
|
429
527
|
|
|
430
528
|
Generate an order input:
|
|
@@ -390,6 +390,104 @@ Typical string lookups: `exact`, `neq`, `contains`, `iContains`, `startsWith`, `
|
|
|
390
390
|
|
|
391
391
|
Regex lookups (`regex`, `iRegex`) are disabled by default. Enable with `enable_regex_filters=True`.
|
|
392
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
|
+
|
|
393
491
|
### Ordering
|
|
394
492
|
|
|
395
493
|
Generate an order input:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
import re
|
|
6
7
|
import typing
|
|
7
8
|
import warnings
|
|
@@ -59,6 +60,8 @@ class BaseBackend:
|
|
|
59
60
|
self._filter_overrides: dict[type, type] = kwargs.get("filter_overrides") or {}
|
|
60
61
|
self._type_registry: dict[str, type] = {}
|
|
61
62
|
self._graphql_type_registry: dict[type, type] = {}
|
|
63
|
+
self._filter_registry: dict[type, type] = {}
|
|
64
|
+
self._projected_filter_cache: dict[tuple[type, Any], type] = {}
|
|
62
65
|
self._type_querysets: dict[type, Any] = {}
|
|
63
66
|
self._warn_sensitive: bool = kwargs.get("warn_sensitive", True)
|
|
64
67
|
self._exclude_sensitive_fields: bool = kwargs.get(
|
|
@@ -137,15 +140,40 @@ class BaseBackend:
|
|
|
137
140
|
model = model_or_type
|
|
138
141
|
include = kwargs.get("include")
|
|
139
142
|
exclude = kwargs.get("exclude")
|
|
143
|
+
project: dict[str, Any] | None = kwargs.get("project")
|
|
140
144
|
|
|
141
145
|
enable_regex = getattr(self, "_enable_regex_filters", False)
|
|
142
146
|
|
|
143
147
|
fields_meta = self._introspect_model(model)
|
|
144
148
|
|
|
149
|
+
if project is not None:
|
|
150
|
+
relation_names = {
|
|
151
|
+
fname for fname, _, is_rel, _ in fields_meta if is_rel
|
|
152
|
+
}
|
|
153
|
+
unknown = set(project.keys()) - relation_names
|
|
154
|
+
if unknown:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Unknown relation(s) in project for {model.__name__}: "
|
|
157
|
+
f"{', '.join(sorted(unknown))}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Check the projected-filter cache for projected (non-default) calls.
|
|
161
|
+
if project is not None:
|
|
162
|
+
cache_key = (model, self._filter_project_signature(project))
|
|
163
|
+
cached = self._projected_filter_cache.get(cache_key)
|
|
164
|
+
if cached is not None:
|
|
165
|
+
return cached
|
|
166
|
+
|
|
167
|
+
suffix = self._filter_project_suffix(project)
|
|
168
|
+
|
|
145
169
|
field_annotations: dict[str, Any] = {}
|
|
146
170
|
field_defaults: dict[str, Any] = {}
|
|
147
171
|
|
|
148
|
-
|
|
172
|
+
object_annotations: dict[str, Any] = {}
|
|
173
|
+
object_defaults: dict[str, Any] = {}
|
|
174
|
+
relation_models: dict[str, type] = {}
|
|
175
|
+
|
|
176
|
+
for fname, ftype, is_relation, rel_model in fields_meta:
|
|
149
177
|
if include and fname not in include:
|
|
150
178
|
continue
|
|
151
179
|
if exclude and fname in exclude:
|
|
@@ -153,6 +181,19 @@ class BaseBackend:
|
|
|
153
181
|
if self._exclude_generated_sensitive_field(fname, include):
|
|
154
182
|
continue
|
|
155
183
|
if is_relation:
|
|
184
|
+
if rel_model is None:
|
|
185
|
+
continue
|
|
186
|
+
if project is not None and fname not in project:
|
|
187
|
+
continue
|
|
188
|
+
sub_project = project[fname] if project is not None else None
|
|
189
|
+
if sub_project is not None:
|
|
190
|
+
rel_filter = self._get_projected_filter(rel_model, sub_project)
|
|
191
|
+
else:
|
|
192
|
+
rel_filter = self._filter_registry.get(rel_model)
|
|
193
|
+
if rel_filter is not None:
|
|
194
|
+
object_annotations[fname] = Optional[rel_filter]
|
|
195
|
+
object_defaults[fname] = strawberry.UNSET
|
|
196
|
+
relation_models[fname] = rel_model
|
|
156
197
|
continue
|
|
157
198
|
lookup_type = self._filter_overrides.get(ftype) or TYPE_TO_LOOKUP.get(ftype)
|
|
158
199
|
if lookup_type is not None:
|
|
@@ -161,15 +202,20 @@ class BaseBackend:
|
|
|
161
202
|
field_annotations[fname] = Optional[lookup_type]
|
|
162
203
|
field_defaults[fname] = strawberry.UNSET
|
|
163
204
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
205
|
+
# Reuse the existing FieldType when creating a projected variant.
|
|
206
|
+
base_filter = self._filter_registry.get(model)
|
|
207
|
+
if project is not None and base_filter is not None and hasattr(base_filter, "_field_type"):
|
|
208
|
+
FieldType = base_filter._field_type
|
|
209
|
+
else:
|
|
210
|
+
field_type_name = f"{model.__name__}Field{suffix}"
|
|
211
|
+
field_ns: dict[str, Any] = {
|
|
212
|
+
"__annotations__": field_annotations,
|
|
213
|
+
**field_defaults,
|
|
214
|
+
}
|
|
215
|
+
field_cls = type(field_type_name, (), field_ns)
|
|
216
|
+
FieldType = strawberry.input(field_cls, one_of=True)
|
|
217
|
+
|
|
218
|
+
filter_type_name = f"{model.__name__}Filter{suffix}"
|
|
173
219
|
filter_ns: dict[str, Any] = {
|
|
174
220
|
"__annotations__": {"field": Optional[FieldType]},
|
|
175
221
|
"field": strawberry.UNSET,
|
|
@@ -185,11 +231,77 @@ class BaseBackend:
|
|
|
185
231
|
FilterCls.not_ = strawberry.field(default=strawberry.UNSET, name="not")
|
|
186
232
|
FilterCls.one_of = strawberry.UNSET
|
|
187
233
|
|
|
234
|
+
if object_annotations:
|
|
235
|
+
obj_type_name = f"{model.__name__}Object{suffix}"
|
|
236
|
+
obj_ns: dict[str, Any] = {
|
|
237
|
+
"__annotations__": object_annotations,
|
|
238
|
+
**object_defaults,
|
|
239
|
+
}
|
|
240
|
+
obj_cls = type(obj_type_name, (), obj_ns)
|
|
241
|
+
ObjectType = strawberry.input(obj_cls, one_of=True)
|
|
242
|
+
FilterCls.__annotations__["object"] = Optional[ObjectType]
|
|
243
|
+
FilterCls.object = strawberry.UNSET
|
|
244
|
+
|
|
188
245
|
FilterType = strawberry.input(FilterCls, one_of=True)
|
|
189
246
|
FilterType._field_type = FieldType # type: ignore[attr-defined]
|
|
190
247
|
FilterType.__orm_model__ = model # type: ignore[attr-defined]
|
|
248
|
+
if object_annotations:
|
|
249
|
+
FilterType._object_type = ObjectType # type: ignore[attr-defined]
|
|
250
|
+
FilterType._relation_models = relation_models # type: ignore[attr-defined]
|
|
251
|
+
|
|
252
|
+
if project is None:
|
|
253
|
+
self._filter_registry[model] = FilterType
|
|
254
|
+
else:
|
|
255
|
+
self._projected_filter_cache[cache_key] = FilterType
|
|
191
256
|
return FilterType
|
|
192
257
|
|
|
258
|
+
# -- Filter project helpers ------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def _get_projected_filter(self, model: type, sub_project: dict[str, Any]) -> Any:
|
|
261
|
+
"""Return a filter type for *model* constrained by *sub_project*.
|
|
262
|
+
|
|
263
|
+
If *sub_project* is empty the returned filter has no ``object`` type
|
|
264
|
+
(leaf). Otherwise only the relations listed in *sub_project* are
|
|
265
|
+
exposed, each recursively constrained by their own sub-project.
|
|
266
|
+
"""
|
|
267
|
+
sig = self._filter_project_signature(sub_project)
|
|
268
|
+
cache_key = (model, sig)
|
|
269
|
+
cached = self._projected_filter_cache.get(cache_key)
|
|
270
|
+
if cached is not None:
|
|
271
|
+
return cached
|
|
272
|
+
if not sub_project:
|
|
273
|
+
base = self._filter_registry.get(model)
|
|
274
|
+
if base is not None and not hasattr(base, "_object_type"):
|
|
275
|
+
self._projected_filter_cache[cache_key] = base
|
|
276
|
+
return base
|
|
277
|
+
result = self.filter(model, project=sub_project)
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def _filter_project_signature(project: dict[str, Any] | None) -> Any:
|
|
282
|
+
"""Turn a project dict into a hashable, comparable value."""
|
|
283
|
+
if project is None:
|
|
284
|
+
return None
|
|
285
|
+
if not project:
|
|
286
|
+
return ()
|
|
287
|
+
return tuple(
|
|
288
|
+
sorted(
|
|
289
|
+
(k, BaseBackend._filter_project_signature(v))
|
|
290
|
+
for k, v in project.items()
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def _filter_project_suffix(project: dict[str, Any] | None) -> str:
|
|
296
|
+
"""Short hash suffix for projected type names (empty for default)."""
|
|
297
|
+
sig = BaseBackend._filter_project_signature(project)
|
|
298
|
+
if sig is None:
|
|
299
|
+
return ""
|
|
300
|
+
if not sig:
|
|
301
|
+
return "_leaf"
|
|
302
|
+
digest = hashlib.sha1(repr(sig).encode("utf-8")).hexdigest()[:8]
|
|
303
|
+
return f"_{digest}"
|
|
304
|
+
|
|
193
305
|
def order(self, model_or_type: type, **kwargs: Any) -> Any:
|
|
194
306
|
model = model_or_type
|
|
195
307
|
include = kwargs.get("include")
|
|
@@ -636,6 +636,7 @@ def _build_django_filter(
|
|
|
636
636
|
enable_regex: bool = False,
|
|
637
637
|
max_in_list_size: int = 500,
|
|
638
638
|
_depth: int = 0,
|
|
639
|
+
_prefix: str = "",
|
|
639
640
|
) -> Any:
|
|
640
641
|
from django.db.models import Q
|
|
641
642
|
|
|
@@ -652,6 +653,7 @@ def _build_django_filter(
|
|
|
652
653
|
enable_regex=enable_regex,
|
|
653
654
|
max_in_list_size=max_in_list_size,
|
|
654
655
|
_depth=_depth + 1,
|
|
656
|
+
_prefix=_prefix,
|
|
655
657
|
)
|
|
656
658
|
|
|
657
659
|
for key in fields:
|
|
@@ -662,9 +664,25 @@ def _build_django_filter(
|
|
|
662
664
|
if key == "field":
|
|
663
665
|
return _build_django_field_clause(
|
|
664
666
|
val,
|
|
667
|
+
prefix=_prefix,
|
|
665
668
|
enable_regex=enable_regex,
|
|
666
669
|
max_in_list_size=max_in_list_size,
|
|
667
670
|
)
|
|
671
|
+
elif key == "object":
|
|
672
|
+
obj_fields = val.__class__.__dataclass_fields__
|
|
673
|
+
for rel_name in obj_fields:
|
|
674
|
+
nested_filter = getattr(val, rel_name)
|
|
675
|
+
if nested_filter is strawberry.UNSET or nested_filter is None:
|
|
676
|
+
continue
|
|
677
|
+
return _build_django_filter(
|
|
678
|
+
nested_filter,
|
|
679
|
+
max_depth=max_depth,
|
|
680
|
+
max_branches=max_branches,
|
|
681
|
+
enable_regex=enable_regex,
|
|
682
|
+
max_in_list_size=max_in_list_size,
|
|
683
|
+
_depth=_depth + 1,
|
|
684
|
+
_prefix=f"{_prefix}{rel_name}__",
|
|
685
|
+
)
|
|
668
686
|
elif key == "all":
|
|
669
687
|
if len(val) > max_branches:
|
|
670
688
|
raise ValueError(
|
|
@@ -708,6 +726,7 @@ def _build_django_filter(
|
|
|
708
726
|
def _build_django_field_clause(
|
|
709
727
|
field_input: Any,
|
|
710
728
|
*,
|
|
729
|
+
prefix: str = "",
|
|
711
730
|
enable_regex: bool = False,
|
|
712
731
|
max_in_list_size: int = 500,
|
|
713
732
|
) -> Any:
|
|
@@ -722,7 +741,7 @@ def _build_django_field_clause(
|
|
|
722
741
|
continue
|
|
723
742
|
|
|
724
743
|
col_q = _build_django_lookup(
|
|
725
|
-
col_name,
|
|
744
|
+
f"{prefix}{col_name}",
|
|
726
745
|
lookup,
|
|
727
746
|
enable_regex=enable_regex,
|
|
728
747
|
max_in_list_size=max_in_list_size,
|
|
@@ -880,6 +880,20 @@ def _build_sa_filter(
|
|
|
880
880
|
enable_regex=enable_regex,
|
|
881
881
|
max_in_list_size=max_in_list_size,
|
|
882
882
|
)
|
|
883
|
+
elif key == "object":
|
|
884
|
+
obj_fields = val.__class__.__dataclass_fields__
|
|
885
|
+
for rel_name in obj_fields:
|
|
886
|
+
nested_filter = getattr(val, rel_name)
|
|
887
|
+
if nested_filter is strawberry.UNSET or nested_filter is None:
|
|
888
|
+
continue
|
|
889
|
+
relationship_prop = getattr(model, rel_name)
|
|
890
|
+
rel_model = relationship_prop.property.mapper.class_
|
|
891
|
+
inner = _build_sa_filter(nested_filter, rel_model, **recurse_kw)
|
|
892
|
+
if inner is not None:
|
|
893
|
+
if relationship_prop.property.uselist:
|
|
894
|
+
return relationship_prop.any(inner)
|
|
895
|
+
else:
|
|
896
|
+
return relationship_prop.has(inner)
|
|
883
897
|
elif key == "all":
|
|
884
898
|
if len(val) > max_branches:
|
|
885
899
|
raise ValueError(
|
|
@@ -803,6 +803,7 @@ def _build_tortoise_filter(
|
|
|
803
803
|
enable_regex: bool = False,
|
|
804
804
|
max_in_list_size: int = 500,
|
|
805
805
|
_depth: int = 0,
|
|
806
|
+
_prefix: str = "",
|
|
806
807
|
) -> Any:
|
|
807
808
|
"""Recursively translate a filter input into a Tortoise Q object."""
|
|
808
809
|
from tortoise.queryset import Q
|
|
@@ -820,6 +821,7 @@ def _build_tortoise_filter(
|
|
|
820
821
|
enable_regex=enable_regex,
|
|
821
822
|
max_in_list_size=max_in_list_size,
|
|
822
823
|
_depth=_depth + 1,
|
|
824
|
+
_prefix=_prefix,
|
|
823
825
|
)
|
|
824
826
|
|
|
825
827
|
for key in fields:
|
|
@@ -830,9 +832,25 @@ def _build_tortoise_filter(
|
|
|
830
832
|
if key == "field":
|
|
831
833
|
return _build_tortoise_field_clause(
|
|
832
834
|
val,
|
|
835
|
+
prefix=_prefix,
|
|
833
836
|
enable_regex=enable_regex,
|
|
834
837
|
max_in_list_size=max_in_list_size,
|
|
835
838
|
)
|
|
839
|
+
elif key == "object":
|
|
840
|
+
obj_fields = val.__class__.__dataclass_fields__
|
|
841
|
+
for rel_name in obj_fields:
|
|
842
|
+
nested_filter = getattr(val, rel_name)
|
|
843
|
+
if nested_filter is strawberry.UNSET or nested_filter is None:
|
|
844
|
+
continue
|
|
845
|
+
return _build_tortoise_filter(
|
|
846
|
+
nested_filter,
|
|
847
|
+
max_depth=max_depth,
|
|
848
|
+
max_branches=max_branches,
|
|
849
|
+
enable_regex=enable_regex,
|
|
850
|
+
max_in_list_size=max_in_list_size,
|
|
851
|
+
_depth=_depth + 1,
|
|
852
|
+
_prefix=f"{_prefix}{rel_name}__",
|
|
853
|
+
)
|
|
836
854
|
elif key == "all":
|
|
837
855
|
if len(val) > max_branches:
|
|
838
856
|
raise ValueError(
|
|
@@ -882,6 +900,7 @@ def _build_tortoise_filter(
|
|
|
882
900
|
def _build_tortoise_field_clause(
|
|
883
901
|
field_input: Any,
|
|
884
902
|
*,
|
|
903
|
+
prefix: str = "",
|
|
885
904
|
enable_regex: bool = False,
|
|
886
905
|
max_in_list_size: int = 500,
|
|
887
906
|
) -> Any:
|
|
@@ -896,7 +915,7 @@ def _build_tortoise_field_clause(
|
|
|
896
915
|
if lookup is strawberry.UNSET or lookup is None:
|
|
897
916
|
continue
|
|
898
917
|
col_q = _build_tortoise_lookup(
|
|
899
|
-
col_name,
|
|
918
|
+
f"{prefix}{col_name}",
|
|
900
919
|
lookup,
|
|
901
920
|
enable_regex=enable_regex,
|
|
902
921
|
max_in_list_size=max_in_list_size,
|
|
@@ -170,30 +170,39 @@ class MutationNamespace:
|
|
|
170
170
|
*,
|
|
171
171
|
models: Iterable[type] | None = None,
|
|
172
172
|
project: dict[str, Any] | None = None,
|
|
173
|
+
name: str | None = None,
|
|
173
174
|
) -> type:
|
|
174
175
|
selected_models = self._resolve_root_models(models)
|
|
175
176
|
root_projects = self._normalize_root_project(selected_models, project)
|
|
176
|
-
return self._root_input_type(
|
|
177
|
+
return self._root_input_type(
|
|
178
|
+
"create", selected_models, root_projects, name=name
|
|
179
|
+
)
|
|
177
180
|
|
|
178
181
|
def update_node_input(
|
|
179
182
|
self,
|
|
180
183
|
*,
|
|
181
184
|
models: Iterable[type] | None = None,
|
|
182
185
|
project: dict[str, Any] | None = None,
|
|
186
|
+
name: str | None = None,
|
|
183
187
|
) -> type:
|
|
184
188
|
selected_models = self._resolve_root_models(models)
|
|
185
189
|
root_projects = self._normalize_root_project(selected_models, project)
|
|
186
|
-
return self._root_input_type(
|
|
190
|
+
return self._root_input_type(
|
|
191
|
+
"update", selected_models, root_projects, name=name
|
|
192
|
+
)
|
|
187
193
|
|
|
188
194
|
def create_node(
|
|
189
195
|
self,
|
|
190
196
|
*,
|
|
191
197
|
models: Iterable[type] | None = None,
|
|
192
198
|
project: dict[str, Any] | None = None,
|
|
199
|
+
input_name: str | None = None,
|
|
193
200
|
description: str | None = None,
|
|
194
201
|
) -> Any:
|
|
195
202
|
factory = self
|
|
196
|
-
input_type = self.create_node_input(
|
|
203
|
+
input_type = self.create_node_input(
|
|
204
|
+
models=models, project=project, name=input_name
|
|
205
|
+
)
|
|
197
206
|
|
|
198
207
|
if self._backend.__class__.__name__ == "TortoiseBackend":
|
|
199
208
|
|
|
@@ -221,10 +230,13 @@ class MutationNamespace:
|
|
|
221
230
|
*,
|
|
222
231
|
models: Iterable[type] | None = None,
|
|
223
232
|
project: dict[str, Any] | None = None,
|
|
233
|
+
input_name: str | None = None,
|
|
224
234
|
description: str | None = None,
|
|
225
235
|
) -> Any:
|
|
226
236
|
factory = self
|
|
227
|
-
input_type = self.update_node_input(
|
|
237
|
+
input_type = self.update_node_input(
|
|
238
|
+
models=models, project=project, name=input_name
|
|
239
|
+
)
|
|
228
240
|
|
|
229
241
|
if self._backend.__class__.__name__ == "TortoiseBackend":
|
|
230
242
|
|
|
@@ -409,16 +421,19 @@ class MutationNamespace:
|
|
|
409
421
|
operation: str,
|
|
410
422
|
models: tuple[type, ...],
|
|
411
423
|
root_projects: dict[str, Any],
|
|
424
|
+
*,
|
|
425
|
+
name: str | None = None,
|
|
412
426
|
) -> type:
|
|
413
427
|
cache = (
|
|
414
428
|
self._root_create_inputs
|
|
415
429
|
if operation == "create"
|
|
416
430
|
else self._root_update_inputs
|
|
417
431
|
)
|
|
418
|
-
|
|
419
|
-
(model, self._project_signature(root_projects[_model_key(model)]))
|
|
432
|
+
root_signature = tuple(
|
|
433
|
+
(model.__name__, self._project_signature(root_projects[_model_key(model)]))
|
|
420
434
|
for model in models
|
|
421
435
|
)
|
|
436
|
+
cache_key = (root_signature, name)
|
|
422
437
|
if cache_key in cache:
|
|
423
438
|
return cache[cache_key]
|
|
424
439
|
|
|
@@ -433,13 +448,12 @@ class MutationNamespace:
|
|
|
433
448
|
) | None
|
|
434
449
|
defaults[key] = strawberry.UNSET
|
|
435
450
|
|
|
436
|
-
root_signature = tuple(
|
|
437
|
-
(model.__name__, self._project_signature(root_projects[_model_key(model)]))
|
|
438
|
-
for model in models
|
|
439
|
-
)
|
|
440
451
|
root_suffix = self._signature_suffix(root_signature)
|
|
452
|
+
class_name = name or (
|
|
453
|
+
f"{'Create' if operation == 'create' else 'Update'}NodeInput{root_suffix}"
|
|
454
|
+
)
|
|
441
455
|
cls = type(
|
|
442
|
-
|
|
456
|
+
class_name,
|
|
443
457
|
(),
|
|
444
458
|
{"__annotations__": annotations, **defaults},
|
|
445
459
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|