Skip to content
This repository has been archived by the owner on Oct 21, 2022. It is now read-only.

Support for multiple keys #12

Merged
merged 9 commits into from
Nov 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,20 @@ Based on discussion: https://github.com/graphql-python/graphene/issues/953#issue

Supports now:
* sdl (_service fields) # make possible to add schema in federation (as is)
* @key decorator (entity support) # to perform Queries across service boundaries
* `@key` decorator (entity support) # to perform Queries across service boundaries
* You can use multiple `@key` per each ObjectType
```python
@key('id')
@key('email')
class User(ObjectType):
id = Int(required=True)
email = String()

def __resolve_reference(self, info, **kwargs):
if self.id is not None:
return User(id=self.id, email=f'name_{self.id}@gmail.com')
return User(id=123, email=self.email)
```
* extend # extend remote types
* external # mark field as external

Expand Down
8 changes: 7 additions & 1 deletion graphene_federation/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def build_schema(query, mutation=None, **kwargs):
def key(fields: str):
def decorator(Type):
register_entity(Type.__name__, Type)
setattr(Type, '_sdl', '@key(fields: "%s")' % fields)

existing = getattr(Type, "_sdl", "")

key_sdl = f'@key(fields: "{fields}")'
updated = f"{key_sdl} {existing}" if existing else key_sdl

setattr(Type, '_sdl', updated)
return Type
return decorator
17 changes: 14 additions & 3 deletions integration_tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ services:
timeout: 10s
retries: 10

service_c:
build: service_c/.
volumes:
- ./service_c/src:/project/src
- ../:/project/federation_deps
command: sh -c "pip install ./federation_deps && python ./src/app.py"
healthcheck:
test: ["CMD", "curl", "-XGET", "http://0.0.0.0:5000/graphql"]
interval: 1s
timeout: 10s
retries: 10

federation:
build: federation/.
volumes:
- ./federation/src:/project/src
depends_on:
- service_a
- service_b
ports:
- 3000:3000
healthcheck:
Expand All @@ -43,6 +52,8 @@ services:
condition: service_healthy
service_b:
condition: service_healthy
service_c:
condition: service_healthy

proxy_dep:
image: busybox
Expand Down
2 changes: 2 additions & 0 deletions integration_tests/federation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import {ApolloGateway} from '@apollo/gateway'

const serviceA_url: string = 'http://service_a:5000/graphql';
const serviceB_url: string = 'http://service_b:5000/graphql';
const serviceC_url: string = 'http://service_c:5000/graphql';

const gateway = new ApolloGateway({
serviceList: [
{ name: 'service_a', url: serviceA_url },
{ name: 'service_b', url: serviceB_url },
{ name: 'service_c', url: serviceC_url },
],
});

Expand Down
7 changes: 7 additions & 0 deletions integration_tests/service_a/src/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,17 @@ def resolve_color(self, info, **kwargs):
return self.id + 2


@extend(fields='email')
class User(ObjectType):
email = external(String())


class Post(ObjectType):
id = Int(required=True)
title = String(required=True)
text = Field(lambda: FunnyText)
files = List(NonNull(FileNode))
author = Field(lambda: User)


class Query(ObjectType):
Expand All @@ -49,6 +55,7 @@ def resolve_posts(root, info):
Post(id=1, title='title1', text=FunnyText(id=1), files=[FileNode(id=1)]),
Post(id=2, title='title2', text=FunnyText(id=2), files=[FileNode(id=2), FileNode(id=3)]),
Post(id=3, title='title3', text=FunnyText(id=3)),
Post(id=4, title='title4', text=FunnyText(id=4), author=User(email="frank@frank.com")),
]

def resolve_goodbye(root, info):
Expand Down
32 changes: 27 additions & 5 deletions integration_tests/service_b/src/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,27 @@ def __resolve_reference(self, info, **kwargs):
return FileNode(id=self.id, name=f'file_{self.id}')


@key('id')
@key('email')
class User(ObjectType):
id = Int(required=True)
email = String()

def __resolve_reference(self, info, **kwargs):
if self.id is not None:
return User(id=self.id, email=f'name_{self.id}@gmail.com')

user_id = 1001 if self.email == "frank@frank.com" else hash(self.email) % 10000000

return User(id=user_id, email=self.email)


# to test that @key applied only to FileNode, but not to FileNodeAnother
class FileNodeAnother(ObjectType):
id = Int(required=True)
name = String(required=True)


class Query(ObjectType):
file = Field(lambda: FileNode)


class FunnyMutation(Mutation):
result = String(required=True)

Expand All @@ -48,4 +59,15 @@ class Mutation(ObjectType):
funny_mutation = FunnyMutation.Field()


schema = build_schema(Query, Mutation, types=[FileNode, FunnyText, FileNodeAnother])
class Query(ObjectType):
file = Field(lambda: FileNode)


types = [
FileNode,
FunnyText,
FileNodeAnother,
User
]

schema = build_schema(Query, Mutation, types=types)
10 changes: 10 additions & 0 deletions integration_tests/service_c/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.6-alpine3.9

WORKDIR project
RUN apk add curl

COPY ./requirements.txt ./
RUN pip install -r requirements.txt

EXPOSE 5000
CMD [ "python", "./src/app.py"]
3 changes: 3 additions & 0 deletions integration_tests/service_c/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
graphene==2.1.7
flask==1.1.1
flask_graphql==2.0.0
Empty file.
12 changes: 12 additions & 0 deletions integration_tests/service_c/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask

from flask_graphql import GraphQLView
from schema import schema

app = Flask(__name__)
app.debug = True

app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True))

if __name__ == '__main__':
app.run(host='0.0.0.0')
25 changes: 25 additions & 0 deletions integration_tests/service_c/src/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from graphene import ObjectType, String, Int, List, NonNull, Field
from graphene_federation import build_schema, extend, external


@extend(fields='id')
class User(ObjectType):
id = external(Int(required=True))


class Article(ObjectType):
id = Int(required=True)
text = String(required=True)
author = Field(lambda: User)


class Query(ObjectType):
articles = List(NonNull(lambda: Article))

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


schema = build_schema(Query)
35 changes: 31 additions & 4 deletions integration_tests/tests/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ def test_external_types():
id
name
}
author {
id
email
}
}
articles {
id
text
author {
id
email
}
}
}
""",
Expand All @@ -42,17 +54,24 @@ def test_external_types():
json=query,
)
assert response.status_code == 200
posts = json.loads(response.content)['data']['posts']
assert 3 == len(posts)
data = json.loads(response.content)['data']
posts = data['posts']
articles = data['articles']

assert 4 == len(posts)
assert [{'id': 1, 'name': 'file_1'}] == posts[0]['files']
assert {'id': 1, 'body': 'funny_text_1', 'color': 3} == posts[0]['text']
assert [{'id': 2, 'name': 'file_2'}, {'id': 3, 'name': 'file_3'}] == posts[1]['files']
assert {'id': 2, 'body': 'funny_text_2', 'color': 4} == posts[1]['text']
assert posts[2]['files'] is None
assert {'id': 3, 'body': 'funny_text_3', 'color': 5} == posts[2]['text']
assert {'id': 1001, 'email': 'frank@frank.com', } == posts[3]['author']

assert articles == [
{'id': 1, 'text': 'some text', 'author': {'id': 5, 'email': 'name_5@gmail.com'}}]

def test_key_decorator_applied_by_exact_match_only():

def fetch_sdl():
query = {
'query': """
query {
Expand All @@ -65,7 +84,11 @@ def test_key_decorator_applied_by_exact_match_only():
}
response = requests.post('http://service_b:5000/graphql', json=query)
assert response.status_code == 200
sdl = response.json()['data']['_service']['sdl']
return response.json()['data']['_service']['sdl']


def test_key_decorator_applied_by_exact_match_only():
sdl = fetch_sdl()
assert 'type FileNode @key(fields: "id")' in sdl
assert 'type FileNodeAnother @key(fields: "id")' not in sdl

Expand All @@ -86,3 +109,7 @@ def test_mutation_is_accessible_in_federation():
assert 'errors' not in response.json()
assert response.json()['data']['funnyMutation']['result'] == 'Funny'


def test_multiple_key_decorators_apply_multiple_key_annotations():
sdl = fetch_sdl()
assert 'type User @key(fields: "id") @key(fields: "email")' in sdl