Skip to content

Commit 32413d7

Browse files
authored
Merge pull request #24 from preply/provides-support
Provides support
2 parents 583c097 + daea0c6 commit 32413d7

File tree

7 files changed

+116
-4
lines changed

7 files changed

+116
-4
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@ Supports now:
2828
* extend # extend remote types
2929
* external # mark field as external
3030
* requires # mark that field resolver requires other fields to be pre-fetched
31-
32-
Todo implement:
33-
* @provides
31+
* provides # to annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway.
32+
* **Base class should be decorated with `@provides`** as well as field on a base type that provides. Check example bellow:
33+
```python
34+
import graphene
35+
from graphene_federation import provides
36+
37+
@provides
38+
class ArticleThatProvideAuthorAge(graphene.ObjectType):
39+
id = Int(required=True)
40+
text = String(required=True)
41+
author = provides(Field(User), fields='age')
42+
```
3443

3544

3645
```python

graphene_federation/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .main import build_schema
22
from .entity import key
33
from .extend import extend, external, requires
4+
from .provides import provides

graphene_federation/provides.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from graphene import Field
2+
3+
provides_parent_types = set()
4+
5+
6+
def provides(field, fields: str = None):
7+
"""
8+
9+
:param field: base type (when used as decorator) or field of base type
10+
:param fields:
11+
:return:
12+
"""
13+
if fields is None: # used as decorator on base type
14+
if isinstance(field, Field):
15+
raise RuntimeError("Please specify fields")
16+
provides_parent_types.add(field)
17+
else: # used as wrapper over field
18+
field._provides = fields
19+
return field

graphene_federation/service.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from graphene.utils.str_converters import to_camel_case
55

66
from graphene_federation.extend import extended_types
7+
from graphene_federation.provides import provides_parent_types
78
from .entity import custom_entities
89

910

@@ -37,6 +38,13 @@ def _mark_requires(entity_name, entity, schema, auto_camelcase):
3738
)
3839

3940

41+
def _mark_provides(entity_name, entity, schema, auto_camelcase):
42+
return _mark_field(
43+
entity_name, entity, schema, '_provides', lambda fields: f'@provides(fields: "{fields}")',
44+
auto_camelcase
45+
)
46+
47+
4048
def get_sdl(schema, custom_entities):
4149
string_schema = str(schema)
4250
string_schema = string_schema.replace("\n", " ")
@@ -51,6 +59,10 @@ def get_sdl(schema, custom_entities):
5159
pattern = re.compile(type_def_re)
5260
string_schema = pattern.sub(repl_str, string_schema)
5361

62+
for entity in provides_parent_types:
63+
string_schema = _mark_provides(
64+
entity.__name__, entity, string_schema, schema.auto_camelcase)
65+
5466
for entity_name, entity in extended_types.items():
5567
string_schema = _mark_external(entity_name, entity, string_schema, schema.auto_camelcase)
5668
string_schema = _mark_requires(entity_name, entity, string_schema, schema.auto_camelcase)

integration_tests/service_b/src/schema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def __resolve_reference(self, info, **kwargs):
3131
class User(ObjectType):
3232
id = Int(required=True)
3333
primary_email = String()
34+
age = Int()
35+
36+
def resolve_age(self, info):
37+
return 17
3438

3539
def __resolve_reference(self, info, **kwargs):
3640
if self.id is not None:

integration_tests/service_c/src/schema.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
from graphene import ObjectType, String, Int, List, NonNull, Field
2-
from graphene_federation import build_schema, extend, external, requires, key
2+
from graphene_federation import build_schema, extend, external, requires, key, provides
33

44

55
@extend(fields='id')
66
class User(ObjectType):
77
id = external(Int(required=True))
88
primary_email = external(String())
99
uppercase_email = requires(String(), fields='primaryEmail')
10+
age = external(Int())
1011

1112
def resolve_uppercase_email(self, info):
1213
return self.primary_email.upper() if self.primary_email else self.primary_email
1314

15+
def resolve_age(self, info):
16+
return 18
17+
1418

1519
@key(fields='id')
1620
class Article(ObjectType):
@@ -22,13 +26,32 @@ def __resolve_reference(self, info, **kwargs):
2226
return Article(id=self.id, text=f'text_{self.id}')
2327

2428

29+
@provides
30+
class ArticleThatProvideAuthorAge(ObjectType):
31+
"""
32+
should not contain other graphene-federation decorators to proper test test-case
33+
"""
34+
id = Int(required=True)
35+
text = String(required=True)
36+
author = provides(Field(User), fields='age')
37+
38+
def __resolve_reference(self, info, **kwargs):
39+
return Article(id=self.id, text=f'text_{self.id}')
40+
41+
2542
class Query(ObjectType):
2643
articles = List(NonNull(lambda: Article))
44+
articles_with_author_age_provide = List(NonNull(lambda: ArticleThatProvideAuthorAge))
2745

2846
def resolve_articles(self, info):
2947
return [
3048
Article(id=1, text='some text', author=User(id=5))
3149
]
3250

51+
def resolve_articles_with_author_age_provide(self, info):
52+
return [
53+
ArticleThatProvideAuthorAge(id=1, text='some text', author=User(id=5))
54+
]
55+
3356

3457
schema = build_schema(Query)

integration_tests/tests/tests/test_main.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,47 @@ def test_requires():
145145

146146
assert articles == [
147147
{'id': 1, 'text': 'some text', 'author': {'uppercaseEmail': '[email protected]'}}]
148+
149+
150+
def test_provides():
151+
"""
152+
articles -> w/o provide (get age value from service b)
153+
articlesWithAuthorAgeProvide -> w/ provide (get age value from service c)
154+
155+
:return:
156+
"""
157+
query = {
158+
'query': """
159+
query {
160+
articles {
161+
id
162+
text
163+
author {
164+
age
165+
}
166+
}
167+
articlesWithAuthorAgeProvide {
168+
id
169+
text
170+
author {
171+
age
172+
}
173+
}
174+
}
175+
""",
176+
'variables': {}
177+
}
178+
response = requests.post(
179+
'http://federation:3000/graphql/',
180+
json=query,
181+
)
182+
assert response.status_code == 200
183+
data = json.loads(response.content)['data']
184+
articles = data['articles']
185+
articles_with_age_provide = data['articlesWithAuthorAgeProvide']
186+
187+
assert articles == [
188+
{'id': 1, 'text': 'some text', 'author': {'age': 17}}]
189+
190+
assert articles_with_age_provide == [
191+
{'id': 1, 'text': 'some text', 'author': {'age': 18}}]

0 commit comments

Comments
 (0)