Skip to content

Commit 0ea0711

Browse files
authored
Merge pull request #1 from loft-orbital/issue#50-fix_directive_declaration
Issue #50 and #51 : fix various bugs and add unit tests
2 parents e90b2ee + 41ec6ef commit 0ea0711

29 files changed

+1807
-238
lines changed

.circleci/config.yml

Lines changed: 0 additions & 24 deletions
This file was deleted.

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
omit = */tests/*
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Integration Tests
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v1
11+
- name: Build environment
12+
run: make integration-build
13+
- name: Run Integration Tests
14+
run: make integration-test

.github/workflows/lint.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Lint
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v1
11+
- name: Set up Python 3.8
12+
uses: actions/setup-python@v1
13+
with:
14+
python-version: 3.8
15+
- name: Install dependencies
16+
run: |
17+
python -m pip install --upgrade pip
18+
pip install -e ".[dev]"
19+
- name: Run lint 💅
20+
run: black graphene_federation --check

.github/workflows/tests.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Unit Tests
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
max-parallel: 4
10+
matrix:
11+
python-version: ["3.6", "3.7", "3.8"]
12+
13+
steps:
14+
- uses: actions/checkout@v1
15+
- name: Set up Python ${{ matrix.python-version }}
16+
uses: actions/setup-python@v1
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
- name: Install dependencies
20+
run: |
21+
python -m pip install --upgrade pip
22+
pip install -e ".[test]"
23+
- name: Run Unit Tests
24+
run: py.test graphene_federation --cov=graphene_federation -vv
25+
- name: Upload Coverage
26+
env:
27+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28+
run: |
29+
pip install coveralls
30+
coveralls

Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM python:3.8-slim
2+
3+
# Disable Python buffering in order to see the logs immediatly
4+
ENV PYTHONUNBUFFERED=1
5+
6+
# Set the default working directory
7+
WORKDIR /workdir
8+
9+
COPY . /workdir
10+
11+
# Install dependencies
12+
RUN pip install -e ".[dev]"
13+
14+
CMD tail -f /dev/null

Makefile

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
1-
build:
1+
# -------------------------
2+
# Integration testing
3+
# -------------------------
4+
5+
.PHONY: integration-build ## Build environment for integration tests
6+
integration-build:
27
cd integration_tests && docker-compose build
38

4-
test:
9+
.PHONY: integration-test ## Run integration tests
10+
integration-test:
511
cd integration_tests && docker-compose down && docker-compose run --rm tests
12+
13+
# -------------------------
14+
# Development and unit testing
15+
# -------------------------
16+
17+
.PHONY: dev-setup ## Install development dependencies
18+
dev-setup:
19+
docker-compose up -d && docker-compose exec graphene_federation bash
20+
21+
.PHONY: tests ## Run unit tests
22+
tests:
23+
docker-compose run graphene_federation py.test graphene_federation --cov=graphene_federation -vv

README.md

Lines changed: 136 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,178 @@
11
# graphene-federation
2-
Federation support for graphene
32

4-
Build: [![CircleCI](https://circleci.com/gh/preply/graphene-federation.svg?style=svg)](https://circleci.com/gh/preply/graphene-federation)
3+
Federation support for ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) following the [Federation specifications](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/).
54

5+
[![Build Status][build-image]][build-url]
6+
[![Coverage Status][coveralls-image]][coveralls-url]
7+
8+
[build-image]: https://github.com/loft-orbital/graphene-federation/workflows/Unit%20Tests/badge.svg?branch=loft-master
9+
[build-url]: https://github.com/loft-orbital/graphene-federation/actions
10+
[coveralls-image]: https://coveralls.io/repos/github/loft-orbital/graphene-federation/badge.svg?branch=loft-master
11+
[coveralls-url]: https://coveralls.io/github/loft-orbital/graphene-federation?branch=loft-master
612

7-
Federation specs implementation on top of Python graphene lib
8-
https://www.apollographql.com/docs/apollo-server/federation/federation-spec/
913

1014
Based on discussion: https://github.com/graphql-python/graphene/issues/953#issuecomment-508481652
1115

12-
Supports now:
13-
* sdl (_service fields) # make possible to add schema in federation (as is)
14-
* `@key` decorator (entity support) # to perform Queries across service boundaries
15-
* You can use multiple `@key` per each ObjectType
16-
```python
17-
@key('id')
18-
@key('email')
19-
class User(ObjectType):
20-
id = Int(required=True)
21-
email = String()
22-
23-
def __resolve_reference(self, info, **kwargs):
24-
if self.id is not None:
25-
return User(id=self.id, email=f'name_{self.id}@gmail.com')
26-
return User(id=123, email=self.email)
27-
```
28-
* extend # extend remote types
29-
* external # mark field as external
30-
* requires # mark that field resolver requires other fields to be pre-fetched
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-
```
16+
------------------------
17+
18+
## Supported Features
19+
20+
At the moment it supports:
21+
22+
* `sdl` (`_service` on field): enable to add schema in federation (as is)
23+
* `@key` decorator (entity support): enable to perform queries across service boundaries (you can have more than one key per type)
24+
* `@extend`: extend remote types
25+
* `external()`: mark a field as external
26+
* `requires()`: mark that field resolver requires other fields to be pre-fetched
27+
* `provides()`/`@provides`: annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway.
28+
29+
Each type which is decorated with `@key` or `@extend` is added to the `_Entity` union.
30+
The [`__resolve_reference` method](https://www.apollographql.com/docs/federation/api/apollo-federation/#__resolvereference) can be defined for each type that is an entity.
31+
This method is called whenever an entity is requested as part of the fulfilling a query plan.
32+
If not explicitly defined, the default resolver is used.
33+
The default resolver just creates instance of type with passed fieldset as kwargs, see [`entity.get_entity_query`](graphene_federation/entity.py) for more details
34+
* You should define `__resolve_reference`, if you need to extract object before passing it to fields resolvers (example: [FileNode](integration_tests/service_b/src/schema.py))
35+
* You should not define `__resolve_reference`, if fields resolvers need only data passed in fieldset (example: [FunnyText](integration_tests/service_a/src/schema.py))
36+
Read more in [official documentation](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference).
37+
38+
------------------------
39+
40+
## Example
41+
42+
Here is an example of implementation based on the [Apollo Federation introduction example](https://www.apollographql.com/docs/federation/).
43+
It implements a federation schema for a basic e-commerce application over three services: accounts, products, reviews.
4344

45+
### Accounts
46+
First add an account service that expose a `User` type that can then be referenced in other services by its `id` field:
4447

4548
```python
46-
import graphene
49+
from graphene import Field, ID, ObjectType, String
4750
from graphene_federation import build_schema, key
4851

49-
@key(fields='id') # mark File as Entity and add in EntityUnion https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#key
50-
class File(graphene.ObjectType):
51-
id = graphene.Int(required=True)
52-
name = graphene.String()
52+
@key("id")
53+
class User(ObjectType):
54+
id = Int(required=True)
55+
username = String(required=True)
5356

54-
def resolve_id(self, info, **kwargs):
55-
return 1
57+
def __resolve_reference(self, info, **kwargs):
58+
"""
59+
Here we resolve the reference of the user entity referenced by its `id` field.
60+
"""
61+
return User(id=self.id, email=f"user_{self.id}@mail.com")
5662

57-
def resolve_name(self, info, **kwargs):
58-
return self.name
63+
class Query(ObjectType):
64+
me = Field(User)
5965

60-
def __resolve_reference(self, info, **kwargs): # https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference
61-
return get_file_by_id(self.id)
66+
schema = build_schema(query=Query)
6267
```
6368

69+
### Product
70+
The product service exposes a `Product` type that can be used by other services via the `upc` field:
6471

6572
```python
66-
import graphene
67-
from graphene_federation import build_schema
73+
from graphene import Argument, ID, Int, List, ObjectType, String
74+
from graphene_federation import build_schema, key
75+
76+
@key("upc")
77+
class Product(ObjectType):
78+
upc = String(required=True)
79+
name = String(required=True)
80+
price = Int()
6881

82+
def __resolve_reference(self, info, **kwargs):
83+
"""
84+
Here we resolve the reference of the product entity referenced by its `upc` field.
85+
"""
86+
return User(upc=self.upc, name=f"product {self.upc}")
6987

70-
class Query(graphene.ObjectType):
71-
...
72-
pass
88+
class Query(ObjectType):
89+
topProducts = List(Product, first=Argument(Int, default_value=5))
7390

74-
schema = build_schema(Query) # add _service{sdl} field in Query
91+
schema = build_schema(query=Query)
7592
```
7693

94+
### Reviews
95+
The reviews service exposes a `Review` type which has a link to both the `User` and `Product` types.
96+
It also has the ability to provide the username of the `User`.
97+
On top of that it adds to the `User`/`Product` types (that are both defined in other services) the ability to get their reviews.
7798

7899
```python
79-
import graphene
80-
from graphene_federation import external, extend
100+
from graphene import Field, ID, Int, List, ObjectType, String
101+
from graphene_federation import build_schema, extend, external, provides
102+
103+
@extend("id")
104+
class User(ObjectType):
105+
id = external(Int(required=True))
106+
reviews = List(lambda: Review)
107+
108+
def resolve_reviews(self, info, *args, **kwargs):
109+
"""
110+
Get all the reviews of a given user. (not implemented here)
111+
"""
112+
return []
113+
114+
@extend("upc")
115+
class Product(ObjectType):
116+
upc = external(String(required=True))
117+
reviews = List(lambda: Review)
118+
119+
# Note that both the base type and the field need to be decorated with `provides` (on the field itself you need to specify which fields get provided).
120+
@provides
121+
class Review(ObjectType):
122+
body = String()
123+
author = provides(Field(User), fields="username")
124+
product = Field(Product)
125+
126+
class Query(ObjectType):
127+
review = Field(Review)
128+
129+
schema = build_schema(query=Query)
130+
```
81131

82-
@extend(fields='id')
83-
class Message(graphene.ObjectType):
84-
id = external(graphene.Int(required=True))
132+
### Federation
85133

86-
def resolve_id(self, **kwargs):
87-
return 1
134+
Note that each schema declaration for the services is a valid graphql schema (it only adds the `_Entity` and `_Service` types).
135+
The best way to check that the decorator are set correctly is to request the service sdl:
88136

137+
```python
138+
from graphql import graphql
139+
140+
query = """
141+
query {
142+
_service {
143+
sdl
144+
}
145+
}
146+
"""
147+
148+
result = graphql(schema, query)
149+
print(result.data["_service"]["sdl"])
89150
```
90151

91-
### __resolve_reference
92-
* Each type which is decorated with `@key` or `@extend` is added to `_Entity` union
93-
* `__resolve_reference` method can be defined for each type that is an entity. This method is called whenever an entity is requested as part of the fulfilling a query plan.
94-
If not explicitly defined, default resolver is used. Default resolver just creates instance of type with passed fieldset as kwargs, see [`entity.get_entity_query`](graphene_federation/entity.py) for more details
95-
* You should define `__resolve_reference`, if you need to extract object before passing it to fields resolvers (example: [FileNode](integration_tests/service_b/src/schema.py))
96-
* You should not define `__resolve_reference`, if fileds resolvers need only data passed in fieldset (example: [FunnyText](integration_tests/service_a/src/schema.py))
97-
* read more in [official documentation](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference)
98-
------------------------
152+
Those can then be used in a federated schema.
99153

154+
You can find more examples in the unit / integration tests and [examples folder](examples/).
100155

101-
### Known issues:
102-
1. decorators will not work properly
103-
* on fields with capitalised letters with `auto_camelcase=True`, for example: `my_ABC_field = String()`
104-
* on fields with custom names for example `some_field = String(name='another_name')`
156+
There is also a cool [example](https://github.com/preply/graphene-federation/issues/1) of integration with Mongoengine.
105157

106-
---------------------------
158+
------------------------
107159

108-
For more details see [examples](examples/)
160+
## Known issues
109161

110-
Or better check [integration_tests](integration_tests/)
162+
1. decorators will not work properly on fields with custom names for example `some_field = String(name='another_name')`
163+
1. `@key` decorator will not work on [compound primary key](https://www.apollographql.com/docs/federation/entities/#defining-a-compound-primary-key)
164+
165+
------------------------
111166

112-
Also cool [example](https://github.com/preply/graphene-federation/issues/1) of integration with Mongoengine
167+
## Contributing
113168

169+
* You can run the unit tests by doing: `make tests`.
170+
* You can run the integration tests by doing `make integration-build && make integration-test`.
171+
* You can get a development environment (on a Docker container) with `make dev-setup`.
172+
* You should use `black` to format your code.
114173

115-
### For contribution:
116-
#### Run tests:
117-
* `make test`
118-
* if you've changed Dockerfile or requirements run `make build` before `make test`
174+
The tests are automatically run on Travis CI on push to GitHub.
119175

120176
---------------------------
121177

122-
Also, you can read about how we've come to federation at Preply [here](https://medium.com/preply-engineering/apollo-federation-support-in-graphene-761a0512456d)
178+
Also, you can read about how we've come to federation at Preply [here](https://medium.com/preply-engineering/apollo-federation-support-in-graphene-761a0512456d)

0 commit comments

Comments
 (0)