strawberry-graphql 0.229.2.dev1715881453__py3-none-any.whl → 0.230.0__py3-none-any.whl
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/federation/object_type.py +9 -0
- strawberry/object_type.py +8 -0
- strawberry/permission.py +25 -181
- strawberry/schema/execute.py +5 -0
- strawberry/schema/schema.py +11 -0
- strawberry/schema/schema_converter.py +23 -0
- strawberry/schema/validation_rules/__init__.py +0 -0
- strawberry/schema/validation_rules/one_of.py +80 -0
- strawberry/schema_directives.py +8 -0
- strawberry/type.py +4 -0
- strawberry/types/types.py +9 -0
- strawberry/utils/typing.py +1 -1
- {strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/METADATA +1 -1
- {strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/RECORD +17 -14
- {strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/LICENSE +0 -0
- {strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/WHEEL +0 -0
- {strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/entry_points.txt +0 -0
@@ -31,6 +31,7 @@ def _impl_type(
|
|
31
31
|
*,
|
32
32
|
name: Optional[str] = None,
|
33
33
|
description: Optional[str] = None,
|
34
|
+
one_of: Optional[bool] = None,
|
34
35
|
directives: Iterable[object] = (),
|
35
36
|
authenticated: bool = False,
|
36
37
|
keys: Iterable[Union["Key", str]] = (),
|
@@ -54,6 +55,7 @@ def _impl_type(
|
|
54
55
|
Shareable,
|
55
56
|
Tag,
|
56
57
|
)
|
58
|
+
from strawberry.schema_directives import OneOf
|
57
59
|
|
58
60
|
directives = list(directives)
|
59
61
|
|
@@ -83,6 +85,9 @@ def _impl_type(
|
|
83
85
|
if is_interface_object:
|
84
86
|
directives.append(InterfaceObject())
|
85
87
|
|
88
|
+
if one_of:
|
89
|
+
directives.append(OneOf())
|
90
|
+
|
86
91
|
return base_type( # type: ignore
|
87
92
|
cls,
|
88
93
|
name=name,
|
@@ -180,6 +185,7 @@ def input(
|
|
180
185
|
cls: T,
|
181
186
|
*,
|
182
187
|
name: Optional[str] = None,
|
188
|
+
one_of: Optional[bool] = None,
|
183
189
|
description: Optional[str] = None,
|
184
190
|
directives: Sequence[object] = (),
|
185
191
|
inaccessible: bool = UNSET,
|
@@ -197,6 +203,7 @@ def input(
|
|
197
203
|
*,
|
198
204
|
name: Optional[str] = None,
|
199
205
|
description: Optional[str] = None,
|
206
|
+
one_of: Optional[bool] = None,
|
200
207
|
directives: Sequence[object] = (),
|
201
208
|
inaccessible: bool = UNSET,
|
202
209
|
tags: Iterable[str] = (),
|
@@ -207,6 +214,7 @@ def input(
|
|
207
214
|
cls: Optional[T] = None,
|
208
215
|
*,
|
209
216
|
name: Optional[str] = None,
|
217
|
+
one_of: Optional[bool] = None,
|
210
218
|
description: Optional[str] = None,
|
211
219
|
directives: Sequence[object] = (),
|
212
220
|
inaccessible: bool = UNSET,
|
@@ -219,6 +227,7 @@ def input(
|
|
219
227
|
directives=directives,
|
220
228
|
inaccessible=inaccessible,
|
221
229
|
is_input=True,
|
230
|
+
one_of=one_of,
|
222
231
|
tags=tags,
|
223
232
|
)
|
224
233
|
|
strawberry/object_type.py
CHANGED
@@ -293,6 +293,7 @@ def input(
|
|
293
293
|
cls: T,
|
294
294
|
*,
|
295
295
|
name: Optional[str] = None,
|
296
|
+
one_of: Optional[bool] = None,
|
296
297
|
description: Optional[str] = None,
|
297
298
|
directives: Optional[Sequence[object]] = (),
|
298
299
|
) -> T: ...
|
@@ -305,6 +306,7 @@ def input(
|
|
305
306
|
def input(
|
306
307
|
*,
|
307
308
|
name: Optional[str] = None,
|
309
|
+
one_of: Optional[bool] = None,
|
308
310
|
description: Optional[str] = None,
|
309
311
|
directives: Optional[Sequence[object]] = (),
|
310
312
|
) -> Callable[[T], T]: ...
|
@@ -314,6 +316,7 @@ def input(
|
|
314
316
|
cls: Optional[T] = None,
|
315
317
|
*,
|
316
318
|
name: Optional[str] = None,
|
319
|
+
one_of: Optional[bool] = None,
|
317
320
|
description: Optional[str] = None,
|
318
321
|
directives: Optional[Sequence[object]] = (),
|
319
322
|
):
|
@@ -324,6 +327,11 @@ def input(
|
|
324
327
|
>>> field_abc: str = "ABC"
|
325
328
|
"""
|
326
329
|
|
330
|
+
from strawberry.schema_directives import OneOf
|
331
|
+
|
332
|
+
if one_of:
|
333
|
+
directives = (*(directives or ()), OneOf())
|
334
|
+
|
327
335
|
return type( # type: ignore # not sure why mypy complains here
|
328
336
|
cls,
|
329
337
|
name=name,
|
strawberry/permission.py
CHANGED
@@ -8,15 +8,12 @@ from typing import (
|
|
8
8
|
TYPE_CHECKING,
|
9
9
|
Any,
|
10
10
|
Awaitable,
|
11
|
+
Dict,
|
11
12
|
List,
|
12
|
-
Literal,
|
13
13
|
Optional,
|
14
|
-
Tuple,
|
15
14
|
Type,
|
16
|
-
TypedDict,
|
17
15
|
Union,
|
18
16
|
)
|
19
|
-
from typing_extensions import deprecated
|
20
17
|
|
21
18
|
from strawberry.exceptions import StrawberryGraphQLError
|
22
19
|
from strawberry.exceptions.permission_fail_silently_requires_optional import (
|
@@ -38,15 +35,6 @@ if TYPE_CHECKING:
|
|
38
35
|
from strawberry.types import Info
|
39
36
|
|
40
37
|
|
41
|
-
def unpack_maybe(
|
42
|
-
value: Union[object, Tuple[bool, object]], default: object = None
|
43
|
-
) -> Tuple[object, object]:
|
44
|
-
if isinstance(value, tuple) and len(value) == 2:
|
45
|
-
return value
|
46
|
-
else:
|
47
|
-
return value, default
|
48
|
-
|
49
|
-
|
50
38
|
class BasePermission(abc.ABC):
|
51
39
|
"""
|
52
40
|
Base class for creating permissions
|
@@ -62,41 +50,18 @@ class BasePermission(abc.ABC):
|
|
62
50
|
|
63
51
|
@abc.abstractmethod
|
64
52
|
def has_permission(
|
65
|
-
self, source: Any, info: Info, **kwargs:
|
66
|
-
) -> Union[
|
67
|
-
bool,
|
68
|
-
Awaitable[bool],
|
69
|
-
Tuple[Literal[False], dict],
|
70
|
-
Awaitable[Tuple[Literal[False], dict]],
|
71
|
-
]:
|
72
|
-
"""
|
73
|
-
This method is a required override in the permission class. It checks if the user has the necessary permissions to access a specific field.
|
74
|
-
|
75
|
-
The method should return a boolean value:
|
76
|
-
- True: The user has the necessary permissions.
|
77
|
-
- False: The user does not have the necessary permissions. In this case, the `on_unauthorized` method will be invoked.
|
78
|
-
|
79
|
-
Avoid raising exceptions in this method. Instead, use the `on_unauthorized` method to handle errors and customize the error response.
|
80
|
-
|
81
|
-
If there's a need to pass additional information to the `on_unauthorized` method, return a tuple. The first element should be False, and the second element should be a dictionary containing the additional information.
|
82
|
-
|
83
|
-
Args:
|
84
|
-
source (Any): The source field that the permission check is being performed on.
|
85
|
-
info (Info): The GraphQL resolve info associated with the field.
|
86
|
-
**kwargs (Any): Additional arguments that are typically passed to the field resolver.
|
87
|
-
|
88
|
-
Returns:
|
89
|
-
bool or tuple: Returns True if the user has the necessary permissions. Returns False or a tuple (False, additional_info) if the user does not have the necessary permissions. In the latter case, the `on_unauthorized` method will be invoked.
|
90
|
-
"""
|
53
|
+
self, source: Any, info: Info, **kwargs: Any
|
54
|
+
) -> Union[bool, Awaitable[bool]]:
|
91
55
|
raise NotImplementedError(
|
92
56
|
"Permission classes should override has_permission method"
|
93
57
|
)
|
94
58
|
|
95
|
-
def on_unauthorized(self
|
59
|
+
def on_unauthorized(self) -> None:
|
96
60
|
"""
|
97
61
|
Default error raising for permissions.
|
98
62
|
This can be overridden to customize the behavior.
|
99
63
|
"""
|
64
|
+
|
100
65
|
# Instantiate error class
|
101
66
|
error = self.error_class(self.message or "")
|
102
67
|
|
@@ -109,9 +74,6 @@ class BasePermission(abc.ABC):
|
|
109
74
|
raise error
|
110
75
|
|
111
76
|
@property
|
112
|
-
@deprecated(
|
113
|
-
"@schema_directive is deprecated and will be disabled by default on 31.12.2024 with future removal planned. Use the new @permissions directive instead."
|
114
|
-
)
|
115
77
|
def schema_directive(self) -> object:
|
116
78
|
if not self._schema_directive:
|
117
79
|
|
@@ -127,111 +89,6 @@ class BasePermission(abc.ABC):
|
|
127
89
|
|
128
90
|
return self._schema_directive
|
129
91
|
|
130
|
-
@cached_property
|
131
|
-
def is_async(self) -> bool:
|
132
|
-
return iscoroutinefunction(self.has_permission)
|
133
|
-
|
134
|
-
def __and__(self, other: BasePermission):
|
135
|
-
return AndPermission([self, other])
|
136
|
-
|
137
|
-
def __or__(self, other: BasePermission):
|
138
|
-
return OrPermission([self, other])
|
139
|
-
|
140
|
-
|
141
|
-
class CompositePermissionContext(TypedDict):
|
142
|
-
failed_permissions: List[Tuple[BasePermission, dict]]
|
143
|
-
|
144
|
-
|
145
|
-
class CompositePermission(BasePermission, abc.ABC):
|
146
|
-
def __init__(self, child_permissions: List[BasePermission]):
|
147
|
-
self.child_permissions = child_permissions
|
148
|
-
|
149
|
-
def on_unauthorized(self, **kwargs: object) -> Any:
|
150
|
-
failed_permissions = kwargs.get("failed_permissions", [])
|
151
|
-
for permission, context in failed_permissions:
|
152
|
-
permission.on_unauthorized(**context)
|
153
|
-
|
154
|
-
@cached_property
|
155
|
-
def is_async(self) -> bool:
|
156
|
-
return any(x.is_async for x in self.child_permissions)
|
157
|
-
|
158
|
-
|
159
|
-
class AndPermission(CompositePermission):
|
160
|
-
def has_permission(
|
161
|
-
self, source: Any, info: Info, **kwargs: object
|
162
|
-
) -> Union[
|
163
|
-
bool,
|
164
|
-
Awaitable[bool],
|
165
|
-
Tuple[Literal[False], CompositePermissionContext],
|
166
|
-
Awaitable[Tuple[Literal[False], CompositePermissionContext]],
|
167
|
-
]:
|
168
|
-
if self.is_async:
|
169
|
-
return self._has_permission_async(source, info, **kwargs)
|
170
|
-
|
171
|
-
for permission in self.child_permissions:
|
172
|
-
has_permission, context = unpack_maybe(
|
173
|
-
permission.has_permission(source, info, **kwargs), {}
|
174
|
-
)
|
175
|
-
if not has_permission:
|
176
|
-
return False, {"failed_permissions": [(permission, context)]}
|
177
|
-
return True
|
178
|
-
|
179
|
-
async def _has_permission_async(
|
180
|
-
self, source: Any, info: Info, **kwargs: object
|
181
|
-
) -> Union[bool, Tuple[Literal[False], CompositePermissionContext]]:
|
182
|
-
for permission in self.child_permissions:
|
183
|
-
permission_response = await await_maybe(
|
184
|
-
permission.has_permission(source, info, **kwargs)
|
185
|
-
)
|
186
|
-
has_permission, context = unpack_maybe(permission_response, {})
|
187
|
-
if not has_permission:
|
188
|
-
return False, {"failed_permissions": [(permission, context)]}
|
189
|
-
return True
|
190
|
-
|
191
|
-
def __and__(self, other: BasePermission):
|
192
|
-
return AndPermission([*self.child_permissions, other])
|
193
|
-
|
194
|
-
|
195
|
-
class OrPermission(CompositePermission):
|
196
|
-
def has_permission(
|
197
|
-
self, source: Any, info: Info, **kwargs: object
|
198
|
-
) -> Union[
|
199
|
-
bool,
|
200
|
-
Awaitable[bool],
|
201
|
-
Tuple[Literal[False], dict],
|
202
|
-
Awaitable[Tuple[Literal[False], dict]],
|
203
|
-
]:
|
204
|
-
if self.is_async:
|
205
|
-
return self._has_permission_async(source, info, **kwargs)
|
206
|
-
failed_permissions = []
|
207
|
-
for permission in self.child_permissions:
|
208
|
-
has_permission, context = unpack_maybe(
|
209
|
-
permission.has_permission(source, info, **kwargs), {}
|
210
|
-
)
|
211
|
-
if has_permission:
|
212
|
-
return True
|
213
|
-
failed_permissions.append((permission, context))
|
214
|
-
|
215
|
-
return False, {"failed_permissions": failed_permissions}
|
216
|
-
|
217
|
-
async def _has_permission_async(
|
218
|
-
self, source: Any, info: Info, **kwargs: object
|
219
|
-
) -> Union[bool, Tuple[Literal[False], dict]]:
|
220
|
-
failed_permissions = []
|
221
|
-
for permission in self.child_permissions:
|
222
|
-
permission_response = await await_maybe(
|
223
|
-
permission.has_permission(source, info, **kwargs)
|
224
|
-
)
|
225
|
-
has_permission, context = unpack_maybe(permission_response, {})
|
226
|
-
if has_permission:
|
227
|
-
return True
|
228
|
-
failed_permissions.append((permission, context))
|
229
|
-
|
230
|
-
return False, {"failed_permissions": failed_permissions}
|
231
|
-
|
232
|
-
def __or__(self, other: BasePermission):
|
233
|
-
return OrPermission([*self.child_permissions, other])
|
234
|
-
|
235
92
|
|
236
93
|
class PermissionExtension(FieldExtension):
|
237
94
|
"""
|
@@ -243,8 +100,8 @@ class PermissionExtension(FieldExtension):
|
|
243
100
|
|
244
101
|
NOTE:
|
245
102
|
Currently, this is automatically added to the field, when using
|
246
|
-
field.permission_classes
|
247
|
-
|
103
|
+
field.permission_classes
|
104
|
+
This is deprecated behavior, please manually add the extension to field.extensions
|
248
105
|
"""
|
249
106
|
|
250
107
|
def __init__(
|
@@ -260,16 +117,12 @@ class PermissionExtension(FieldExtension):
|
|
260
117
|
|
261
118
|
def apply(self, field: StrawberryField) -> None:
|
262
119
|
"""
|
263
|
-
Applies all the permission directives to the schema
|
120
|
+
Applies all of the permission directives to the schema
|
264
121
|
and sets up silent permissions
|
265
122
|
"""
|
266
123
|
if self.use_directives:
|
267
124
|
field.directives.extend(
|
268
|
-
|
269
|
-
p.schema_directive
|
270
|
-
for p in self.permissions
|
271
|
-
if not isinstance(p, CompositePermission)
|
272
|
-
]
|
125
|
+
p.schema_directive for p in self.permissions if p.schema_directive
|
273
126
|
)
|
274
127
|
# We can only fail silently if the field is optional or a list
|
275
128
|
if self.fail_silently:
|
@@ -279,36 +132,28 @@ class PermissionExtension(FieldExtension):
|
|
279
132
|
elif isinstance(field.type, StrawberryList):
|
280
133
|
self.return_empty_list = True
|
281
134
|
else:
|
282
|
-
|
135
|
+
errror = PermissionFailSilentlyRequiresOptionalError(field)
|
136
|
+
raise errror
|
283
137
|
|
284
|
-
def _on_unauthorized(self, permission: BasePermission
|
138
|
+
def _on_unauthorized(self, permission: BasePermission) -> Any:
|
285
139
|
if self.fail_silently:
|
286
140
|
return [] if self.return_empty_list else None
|
287
|
-
|
288
|
-
if kwargs in (None, {}):
|
289
|
-
return permission.on_unauthorized()
|
290
|
-
return permission.on_unauthorized(**kwargs)
|
141
|
+
return permission.on_unauthorized()
|
291
142
|
|
292
143
|
def resolve(
|
293
144
|
self,
|
294
145
|
next_: SyncExtensionResolver,
|
295
146
|
source: Any,
|
296
147
|
info: Info,
|
297
|
-
**kwargs:
|
148
|
+
**kwargs: Dict[str, Any],
|
298
149
|
) -> Any:
|
299
150
|
"""
|
300
151
|
Checks if the permission should be accepted and
|
301
152
|
raises an exception if not
|
302
153
|
"""
|
303
|
-
|
304
154
|
for permission in self.permissions:
|
305
|
-
has_permission,
|
306
|
-
|
307
|
-
)
|
308
|
-
|
309
|
-
if not has_permission:
|
310
|
-
return self._on_unauthorized(permission, **context)
|
311
|
-
|
155
|
+
if not permission.has_permission(source, info, **kwargs):
|
156
|
+
return self._on_unauthorized(permission)
|
312
157
|
return next_(source, info, **kwargs)
|
313
158
|
|
314
159
|
async def resolve_async(
|
@@ -316,21 +161,15 @@ class PermissionExtension(FieldExtension):
|
|
316
161
|
next_: AsyncExtensionResolver,
|
317
162
|
source: Any,
|
318
163
|
info: Info,
|
319
|
-
**kwargs:
|
164
|
+
**kwargs: Dict[str, Any],
|
320
165
|
) -> Any:
|
321
166
|
for permission in self.permissions:
|
322
|
-
|
167
|
+
has_permission = await await_maybe(
|
323
168
|
permission.has_permission(source, info, **kwargs)
|
324
169
|
)
|
325
170
|
|
326
|
-
context = {}
|
327
|
-
if isinstance(permission_response, tuple):
|
328
|
-
has_permission, context = permission_response
|
329
|
-
else:
|
330
|
-
has_permission = permission_response
|
331
|
-
|
332
171
|
if not has_permission:
|
333
|
-
return self._on_unauthorized(permission
|
172
|
+
return self._on_unauthorized(permission)
|
334
173
|
next = next_(source, info, **kwargs)
|
335
174
|
if inspect.isasyncgen(next):
|
336
175
|
return next
|
@@ -340,4 +179,9 @@ class PermissionExtension(FieldExtension):
|
|
340
179
|
def supports_sync(self) -> bool:
|
341
180
|
"""The Permission extension always supports async checking using await_maybe,
|
342
181
|
but only supports sync checking if there are no async permissions"""
|
343
|
-
|
182
|
+
async_permissions = [
|
183
|
+
True
|
184
|
+
for permission in self.permissions
|
185
|
+
if iscoroutinefunction(permission.has_permission)
|
186
|
+
]
|
187
|
+
return len(async_permissions) == 0
|
strawberry/schema/execute.py
CHANGED
@@ -23,6 +23,7 @@ from graphql.validation import validate
|
|
23
23
|
|
24
24
|
from strawberry.exceptions import MissingQueryError
|
25
25
|
from strawberry.extensions.runner import SchemaExtensionsRunner
|
26
|
+
from strawberry.schema.validation_rules.one_of import OneOfInputValidationRule
|
26
27
|
from strawberry.types import ExecutionResult
|
27
28
|
|
28
29
|
from .exceptions import InvalidOperationTypeError
|
@@ -55,6 +56,10 @@ def validate_document(
|
|
55
56
|
document: DocumentNode,
|
56
57
|
validation_rules: Tuple[Type[ASTValidationRule], ...],
|
57
58
|
) -> List[GraphQLError]:
|
59
|
+
validation_rules = (
|
60
|
+
*validation_rules,
|
61
|
+
OneOfInputValidationRule,
|
62
|
+
)
|
58
63
|
return validate(
|
59
64
|
schema,
|
60
65
|
document,
|
strawberry/schema/schema.py
CHANGED
@@ -16,6 +16,8 @@ from typing import (
|
|
16
16
|
)
|
17
17
|
|
18
18
|
from graphql import (
|
19
|
+
GraphQLBoolean,
|
20
|
+
GraphQLField,
|
19
21
|
GraphQLNamedType,
|
20
22
|
GraphQLNonNull,
|
21
23
|
GraphQLSchema,
|
@@ -168,6 +170,7 @@ class Schema(BaseSchema):
|
|
168
170
|
|
169
171
|
self._warn_for_federation_directives()
|
170
172
|
self._resolve_node_ids()
|
173
|
+
self._extend_introspection()
|
171
174
|
|
172
175
|
# Validate schema early because we want developers to know about
|
173
176
|
# possible issues as soon as possible
|
@@ -375,6 +378,14 @@ class Schema(BaseSchema):
|
|
375
378
|
stacklevel=3,
|
376
379
|
)
|
377
380
|
|
381
|
+
def _extend_introspection(self):
|
382
|
+
def _resolve_is_one_of(obj: Any, info: Any) -> bool:
|
383
|
+
return obj.extensions["strawberry-definition"].is_one_of
|
384
|
+
|
385
|
+
instrospection_type = self._schema.type_map["__Type"]
|
386
|
+
instrospection_type.fields["isOneOf"] = GraphQLField(GraphQLBoolean) # type: ignore[attr-defined]
|
387
|
+
instrospection_type.fields["isOneOf"].resolve = _resolve_is_one_of # type: ignore[attr-defined]
|
388
|
+
|
378
389
|
def as_str(self) -> str:
|
379
390
|
return print_schema(self)
|
380
391
|
|
@@ -26,6 +26,7 @@ from graphql import (
|
|
26
26
|
GraphQLDirective,
|
27
27
|
GraphQLEnumType,
|
28
28
|
GraphQLEnumValue,
|
29
|
+
GraphQLError,
|
29
30
|
GraphQLField,
|
30
31
|
GraphQLInputField,
|
31
32
|
GraphQLInputObjectType,
|
@@ -411,6 +412,27 @@ class GraphQLCoreConverter:
|
|
411
412
|
assert isinstance(graphql_object_type, GraphQLInputObjectType) # For mypy
|
412
413
|
return graphql_object_type
|
413
414
|
|
415
|
+
def check_one_of(value: dict[str, Any]) -> dict[str, Any]:
|
416
|
+
if len(value) != 1:
|
417
|
+
raise GraphQLError(
|
418
|
+
f"OneOf Input Object '{type_name}' must specify exactly one key."
|
419
|
+
)
|
420
|
+
|
421
|
+
first_key, first_value = next(iter(value.items()))
|
422
|
+
|
423
|
+
if first_value is None or first_value is UNSET:
|
424
|
+
raise GraphQLError(
|
425
|
+
f"Value for member field '{first_key}' must be non-null"
|
426
|
+
)
|
427
|
+
|
428
|
+
return value
|
429
|
+
|
430
|
+
out_type = (
|
431
|
+
check_one_of
|
432
|
+
if type_definition.is_input and type_definition.is_one_of
|
433
|
+
else None
|
434
|
+
)
|
435
|
+
|
414
436
|
graphql_object_type = GraphQLInputObjectType(
|
415
437
|
name=type_name,
|
416
438
|
fields=lambda: self.get_graphql_input_fields(type_definition),
|
@@ -418,6 +440,7 @@ class GraphQLCoreConverter:
|
|
418
440
|
extensions={
|
419
441
|
GraphQLCoreConverter.DEFINITION_BACKREF: type_definition,
|
420
442
|
},
|
443
|
+
out_type=out_type,
|
421
444
|
)
|
422
445
|
|
423
446
|
self.type_map[type_name] = ConcreteType(
|
File without changes
|
@@ -0,0 +1,80 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from graphql import (
|
4
|
+
ExecutableDefinitionNode,
|
5
|
+
GraphQLError,
|
6
|
+
GraphQLNamedType,
|
7
|
+
ObjectValueNode,
|
8
|
+
ValidationContext,
|
9
|
+
ValidationRule,
|
10
|
+
VariableDefinitionNode,
|
11
|
+
get_named_type,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
class OneOfInputValidationRule(ValidationRule):
|
16
|
+
def __init__(self, validation_context: ValidationContext) -> None:
|
17
|
+
super().__init__(validation_context)
|
18
|
+
|
19
|
+
def enter_operation_definition(
|
20
|
+
self, node: ExecutableDefinitionNode, *_args: Any
|
21
|
+
) -> None:
|
22
|
+
self.variable_definitions: dict[str, VariableDefinitionNode] = {}
|
23
|
+
|
24
|
+
def enter_variable_definition(
|
25
|
+
self, node: VariableDefinitionNode, *_args: Any
|
26
|
+
) -> None:
|
27
|
+
self.variable_definitions[node.variable.name.value] = node
|
28
|
+
|
29
|
+
def enter_object_value(self, node: ObjectValueNode, *_args: Any) -> None:
|
30
|
+
type_ = get_named_type(self.context.get_input_type())
|
31
|
+
|
32
|
+
if not type_:
|
33
|
+
return
|
34
|
+
|
35
|
+
strawberry_type = type_.extensions.get("strawberry-definition")
|
36
|
+
|
37
|
+
if strawberry_type and strawberry_type.is_one_of:
|
38
|
+
self.validate_one_of(node, type_)
|
39
|
+
|
40
|
+
def validate_one_of(self, node: ObjectValueNode, type: GraphQLNamedType) -> None:
|
41
|
+
field_node_map = {field.name.value: field for field in node.fields}
|
42
|
+
keys = list(field_node_map.keys())
|
43
|
+
is_not_exactly_one_field = len(keys) != 1
|
44
|
+
|
45
|
+
if is_not_exactly_one_field:
|
46
|
+
self.report_error(
|
47
|
+
GraphQLError(
|
48
|
+
f"OneOf Input Object '{type.name}' must specify exactly one key.",
|
49
|
+
nodes=[node],
|
50
|
+
)
|
51
|
+
)
|
52
|
+
|
53
|
+
return
|
54
|
+
|
55
|
+
value = field_node_map[keys[0]].value
|
56
|
+
is_null_literal = not value or value.kind == "null_value"
|
57
|
+
is_variable = value.kind == "variable"
|
58
|
+
|
59
|
+
if is_null_literal:
|
60
|
+
self.report_error(
|
61
|
+
GraphQLError(
|
62
|
+
f"Field '{type.name}.{keys[0]}' must be non-null.",
|
63
|
+
nodes=[node],
|
64
|
+
)
|
65
|
+
)
|
66
|
+
|
67
|
+
return
|
68
|
+
|
69
|
+
if is_variable:
|
70
|
+
variable_name = value.name.value # type: ignore
|
71
|
+
definition = self.variable_definitions[variable_name]
|
72
|
+
is_nullable_variable = definition.type.kind != "non_null_type"
|
73
|
+
|
74
|
+
if is_nullable_variable:
|
75
|
+
self.report_error(
|
76
|
+
GraphQLError(
|
77
|
+
f"Variable '{variable_name}' must be non-nullable to be used for OneOf Input Object '{type.name}'.",
|
78
|
+
nodes=[node],
|
79
|
+
)
|
80
|
+
)
|
strawberry/type.py
CHANGED
strawberry/types/types.py
CHANGED
@@ -225,6 +225,15 @@ class StrawberryObjectDefinition(StrawberryType):
|
|
225
225
|
# All field mappings succeeded. This is a match
|
226
226
|
return True
|
227
227
|
|
228
|
+
@property
|
229
|
+
def is_one_of(self) -> bool:
|
230
|
+
from strawberry.schema_directives import OneOf
|
231
|
+
|
232
|
+
if not self.is_input or not self.directives:
|
233
|
+
return False
|
234
|
+
|
235
|
+
return any(isinstance(directive, OneOf) for directive in self.directives)
|
236
|
+
|
228
237
|
|
229
238
|
# TODO: remove when deprecating _type_definition
|
230
239
|
if TYPE_CHECKING:
|
strawberry/utils/typing.py
CHANGED
@@ -302,7 +302,7 @@ def _get_namespace_from_ast(
|
|
302
302
|
# here to resolve lazy types by execing the annotated args, resolving the
|
303
303
|
# type directly and then adding it to extra namespace, so that _eval_type
|
304
304
|
# can properly resolve it later
|
305
|
-
type_name = args[0].strip()
|
305
|
+
type_name = args[0].strip(" '\"\n")
|
306
306
|
for arg in args[1:]:
|
307
307
|
evaled_arg = eval(arg, globalns, localns) # noqa: PGH001, S307
|
308
308
|
if isinstance(evaled_arg, StrawberryLazyReference):
|
{strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/RECORD
RENAMED
@@ -130,7 +130,7 @@ strawberry/federation/argument.py,sha256=5qyJYlQGEZd6iXWseQ7dnnejCYj5HyglfK10jOC
|
|
130
130
|
strawberry/federation/enum.py,sha256=riVfcf2Qe4ERE7SqPqFJ-yaKNG3ve47DniVC0IHZj2A,2969
|
131
131
|
strawberry/federation/field.py,sha256=eEzEyNSrkbwHRsdT27Ti9RucuDI0hPoDT9ohrBCuI8o,6172
|
132
132
|
strawberry/federation/mutation.py,sha256=0lV5HJwgw4HYR_59pwxWqnPs342HwakTNMc98w5Hb-c,43
|
133
|
-
strawberry/federation/object_type.py,sha256=
|
133
|
+
strawberry/federation/object_type.py,sha256=qY4LgXRc0bRXtOeaGruvv8fJDJ1wD73uB7RmFHu0Hrc,9270
|
134
134
|
strawberry/federation/scalar.py,sha256=_dwlQyiDGQQF-Qz-ACSOT-LikxjXgIVz9i0HWlUjTSc,3799
|
135
135
|
strawberry/federation/schema.py,sha256=9g7jp6eUTTP3atW81dLMtaqeY0tQB4YGdR8beKZ-JX8,13715
|
136
136
|
strawberry/federation/schema_directive.py,sha256=V_8ytK_cbVoVRhFle0o9DTQkrP1k-xwCBTaideJOYag,1723
|
@@ -160,9 +160,9 @@ strawberry/litestar/controller.py,sha256=BCntaDTmQNoBRHINi8TztuF7G4-FiED-KxEPTZ6
|
|
160
160
|
strawberry/litestar/handlers/graphql_transport_ws_handler.py,sha256=q_erlgzPsxarohRQXGp1yK0mjKyS8vsWntMYEmrQx4s,2008
|
161
161
|
strawberry/litestar/handlers/graphql_ws_handler.py,sha256=vVpjd5rJOldF8aQWEGjmmNd60WE1p6q6hFmB_DtCzDU,2250
|
162
162
|
strawberry/mutation.py,sha256=NROPvHJU1BBxZB7Wj4Okxw4hDIYM59MCpukAGEmSNYA,255
|
163
|
-
strawberry/object_type.py,sha256=
|
163
|
+
strawberry/object_type.py,sha256=KQhSHqu4DvOLJX3Znr3_hjfRkFCW5fOLTc2yJ48vQgQ,12764
|
164
164
|
strawberry/parent.py,sha256=rmedKjN4zdg4KTnNV8DENrzgNYVL67rXpHjHoBofMS4,825
|
165
|
-
strawberry/permission.py,sha256=
|
165
|
+
strawberry/permission.py,sha256=dcKx4Zlg4ZhcxEDBOSWzz0CUN4WPkcc_kJUVuvLLs6w,5925
|
166
166
|
strawberry/printer/__init__.py,sha256=DmepjmgtkdF5RxK_7yC6qUyRWn56U-9qeZMbkztYB9w,62
|
167
167
|
strawberry/printer/ast_from_value.py,sha256=MFIX2V51d9ocRvD0Njemjk8YIzKh2BB1g2iUcX6a3d8,4946
|
168
168
|
strawberry/printer/printer.py,sha256=HUUecFETXWgfQtMzxjx9pNOXP3tDevqlU3sG7TC3AD8,17449
|
@@ -186,16 +186,19 @@ strawberry/schema/base.py,sha256=lQBJyzG2ZhKc544oLbXEbpYOPOjaXBop3lxp68h_lfI,297
|
|
186
186
|
strawberry/schema/compat.py,sha256=n0r3UPUcGemMqK8vklgtCkkuCA1p6tWAYbc6Vl4iNOw,1684
|
187
187
|
strawberry/schema/config.py,sha256=XkWwmxEsmecscH29o4qU0iSe-hgwJJ2X0DPBVlka2OE,640
|
188
188
|
strawberry/schema/exceptions.py,sha256=T-DsvBtjx9svkegIm1YrVPGPswpVEpMTFc0_7flLEkM,542
|
189
|
-
strawberry/schema/execute.py,sha256=
|
189
|
+
strawberry/schema/execute.py,sha256=dwxMrJrR2Qdd1nPGcIS9-Viq7h5qtPfsD6qdujALoMw,11153
|
190
190
|
strawberry/schema/name_converter.py,sha256=UdNyd-QtqF2HsDCQK-nsOcLGxDTj4hJwYFNvMtZnpq4,6533
|
191
|
-
strawberry/schema/schema.py,sha256=
|
192
|
-
strawberry/schema/schema_converter.py,sha256=
|
191
|
+
strawberry/schema/schema.py,sha256=bfQdLmFXR_BQd80IGO8qa0431cQMUDr22XLNnBL7Tu0,14252
|
192
|
+
strawberry/schema/schema_converter.py,sha256=hxA1PKo8MwurxVC6-CscMnhQSSKyiD8B5jAaiqXc4JM,36605
|
193
193
|
strawberry/schema/types/__init__.py,sha256=oHO3COWhL3L1KLYCJNY1XFf5xt2GGtHiMC-UaYbFfnA,68
|
194
194
|
strawberry/schema/types/base_scalars.py,sha256=Z_BmgwLicNexLipGyw6MmZ7OBnkGJU3ySgaY9SwBWrw,1837
|
195
195
|
strawberry/schema/types/concrete_type.py,sha256=HB30G1hMUuuvjAvfSe6ADS35iI_T_wKO-EprVOWTMSs,746
|
196
196
|
strawberry/schema/types/scalar.py,sha256=SVJ8HiKncCvOw2xwABI5xYaHcC7KkGHG-tx2WDtSoCA,2802
|
197
|
+
strawberry/schema/validation_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
198
|
+
strawberry/schema/validation_rules/one_of.py,sha256=hO0Re1fQQWT9zIqo_7BHLM2bdSsmNJDbKia8AUTSXQQ,2588
|
197
199
|
strawberry/schema_codegen/__init__.py,sha256=PcCVXjS0Y5Buxadm07YAZOVunkQ_DmmwsBrgsH1T4ds,24375
|
198
200
|
strawberry/schema_directive.py,sha256=XGKwcsxRpHXJQ_qXXMi1gEXlZOG22SbDf4Phxf4tbQ0,1967
|
201
|
+
strawberry/schema_directives.py,sha256=KGKFWCODjm1Ah9qNV_bBwbic7Mld4qLWnWQkev-PG8A,175
|
199
202
|
strawberry/starlite/__init__.py,sha256=v209swT8H9MljVL-npvANhEO1zz3__PSfxb_Ix-NoeE,134
|
200
203
|
strawberry/starlite/controller.py,sha256=moo2HlaJT6w9AijwjHvXi4UibUydaZfA75JMFZVMJPw,12191
|
201
204
|
strawberry/starlite/handlers/graphql_transport_ws_handler.py,sha256=WhfFVWdjRSk4A48MaBLGWqZdi2OnHajxdQlA_Gc4XBE,1981
|
@@ -216,7 +219,7 @@ strawberry/test/client.py,sha256=N_AkxLp-kWVTgQDcL8FkruJpAd6NpkChIDptzneu_uM,575
|
|
216
219
|
strawberry/tools/__init__.py,sha256=pdGpZx8wpq03VfUZJyF9JtYxZhGqzzxCiipsalWxJX4,127
|
217
220
|
strawberry/tools/create_type.py,sha256=QxLRT9-DrSgo6vsu_L818wtlbO3wRtI0dOos0gmn1xk,1593
|
218
221
|
strawberry/tools/merge_types.py,sha256=YZ2GSMoDD82bfuvCT0COpN6SLFVRZpTXFFm9LHUyi10,1014
|
219
|
-
strawberry/type.py,sha256=
|
222
|
+
strawberry/type.py,sha256=MS2y7VNugCIHkndJmU5V7VZA9QRcmf76JMzP3TH1jws,6587
|
220
223
|
strawberry/types/__init__.py,sha256=APb1Cjy6bxqFxIfDfempP6eb9NE3LYDwQ3gX7r07lXI,139
|
221
224
|
strawberry/types/execution.py,sha256=Dz4Y_M1ysKz3UxnBK_-h103DjvP6NwLElXXOEpQCkgw,2783
|
222
225
|
strawberry/types/fields/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -225,7 +228,7 @@ strawberry/types/graphql.py,sha256=3SWZEsa0Zy1eVW6vy75BnB7t9_lJVi6TBV3_1j3RNBs,6
|
|
225
228
|
strawberry/types/info.py,sha256=b1ZWW_wUop6XrGNcGHKBQeUYjlX-y8u3s2Wm_XhKPYI,3412
|
226
229
|
strawberry/types/nodes.py,sha256=5tTYmxGpVDshbydicHTTBWEiUe8A7p7mdiaSV8Ry80Y,5027
|
227
230
|
strawberry/types/type_resolver.py,sha256=wuAYCbEjdov0IrnTvkFMNtSwb3lruQsbYI11x35ADeU,6542
|
228
|
-
strawberry/types/types.py,sha256=
|
231
|
+
strawberry/types/types.py,sha256=FWCZxclY47BgPDVtcYqA0kxTIN6DQ1uyjIbfBU9p4Z8,8387
|
229
232
|
strawberry/union.py,sha256=bQ3QBOLLugGvN434vHTZRuelgJdblfy8aJMrUpLgG_g,9585
|
230
233
|
strawberry/unset.py,sha256=4zYRN8vUD7lHQLLpulBFqEPfyvzpx8fl7ZDBUyfMqqk,1112
|
231
234
|
strawberry/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -240,9 +243,9 @@ strawberry/utils/inspect.py,sha256=6z-tJpiWWm6E4-O6OUfhu689W9k1uY0m3FDwAfVCiNs,2
|
|
240
243
|
strawberry/utils/logging.py,sha256=flS7hV0JiIOEdXcrIjda4WyIWix86cpHHFNJL8gl1y4,713
|
241
244
|
strawberry/utils/operation.py,sha256=Um-tBCPl3_bVFN2Ph7o1mnrxfxBes4HFCj6T0x4kZxE,1135
|
242
245
|
strawberry/utils/str_converters.py,sha256=avIgPVLg98vZH9mA2lhzVdyyjqzLsK2NdBw9mJQ02Xk,813
|
243
|
-
strawberry/utils/typing.py,sha256=
|
244
|
-
strawberry_graphql-0.
|
245
|
-
strawberry_graphql-0.
|
246
|
-
strawberry_graphql-0.
|
247
|
-
strawberry_graphql-0.
|
248
|
-
strawberry_graphql-0.
|
246
|
+
strawberry/utils/typing.py,sha256=u9vUmf0F57s9OL_qBK4oLlQtkI__XuLRAQ8ryNBnPgQ,13491
|
247
|
+
strawberry_graphql-0.230.0.dist-info/LICENSE,sha256=m-XnIVUKqlG_AWnfi9NReh9JfKhYOB-gJfKE45WM1W8,1072
|
248
|
+
strawberry_graphql-0.230.0.dist-info/METADATA,sha256=sHc-beSwgtdbMtNpTDGVXG4-Y0XKiNHyTlkJXe0Jh3I,7821
|
249
|
+
strawberry_graphql-0.230.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
250
|
+
strawberry_graphql-0.230.0.dist-info/entry_points.txt,sha256=Nk7-aT3_uEwCgyqtHESV9H6Mc31cK-VAvhnQNTzTb4k,49
|
251
|
+
strawberry_graphql-0.230.0.dist-info/RECORD,,
|
{strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/LICENSE
RENAMED
File without changes
|
{strawberry_graphql-0.229.2.dev1715881453.dist-info → strawberry_graphql-0.230.0.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|