forked from preply/graphene-federation
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from loft-orbital/issue#50-fix_directive_declar…
- Loading branch information
Showing
29 changed files
with
1,807 additions
and
238 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[run] | ||
omit = */tests/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
name: Integration Tests | ||
|
||
on: [push, pull_request] | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v1 | ||
- name: Build environment | ||
run: make integration-build | ||
- name: Run Integration Tests | ||
run: make integration-test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
name: Lint | ||
|
||
on: [push, pull_request] | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v1 | ||
- name: Set up Python 3.8 | ||
uses: actions/setup-python@v1 | ||
with: | ||
python-version: 3.8 | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install -e ".[dev]" | ||
- name: Run lint 💅 | ||
run: black graphene_federation --check |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
name: Unit Tests | ||
|
||
on: [push, pull_request] | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
max-parallel: 4 | ||
matrix: | ||
python-version: ["3.6", "3.7", "3.8"] | ||
|
||
steps: | ||
- uses: actions/checkout@v1 | ||
- name: Set up Python ${{ matrix.python-version }} | ||
uses: actions/setup-python@v1 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install -e ".[test]" | ||
- name: Run Unit Tests | ||
run: py.test graphene_federation --cov=graphene_federation -vv | ||
- name: Upload Coverage | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: | | ||
pip install coveralls | ||
coveralls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
FROM python:3.8-slim | ||
|
||
# Disable Python buffering in order to see the logs immediatly | ||
ENV PYTHONUNBUFFERED=1 | ||
|
||
# Set the default working directory | ||
WORKDIR /workdir | ||
|
||
COPY . /workdir | ||
|
||
# Install dependencies | ||
RUN pip install -e ".[dev]" | ||
|
||
CMD tail -f /dev/null |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,23 @@ | ||
build: | ||
# ------------------------- | ||
# Integration testing | ||
# ------------------------- | ||
|
||
.PHONY: integration-build ## Build environment for integration tests | ||
integration-build: | ||
cd integration_tests && docker-compose build | ||
|
||
test: | ||
.PHONY: integration-test ## Run integration tests | ||
integration-test: | ||
cd integration_tests && docker-compose down && docker-compose run --rm tests | ||
|
||
# ------------------------- | ||
# Development and unit testing | ||
# ------------------------- | ||
|
||
.PHONY: dev-setup ## Install development dependencies | ||
dev-setup: | ||
docker-compose up -d && docker-compose exec graphene_federation bash | ||
|
||
.PHONY: tests ## Run unit tests | ||
tests: | ||
docker-compose run graphene_federation py.test graphene_federation --cov=graphene_federation -vv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,122 +1,178 @@ | ||
# graphene-federation | ||
Federation support for graphene | ||
|
||
Build: [![CircleCI](https://circleci.com/gh/preply/graphene-federation.svg?style=svg)](https://circleci.com/gh/preply/graphene-federation) | ||
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/). | ||
|
||
[![Build Status][build-image]][build-url] | ||
[![Coverage Status][coveralls-image]][coveralls-url] | ||
|
||
[build-image]: https://github.com/loft-orbital/graphene-federation/workflows/Unit%20Tests/badge.svg?branch=loft-master | ||
[build-url]: https://github.com/loft-orbital/graphene-federation/actions | ||
[coveralls-image]: https://coveralls.io/repos/github/loft-orbital/graphene-federation/badge.svg?branch=loft-master | ||
[coveralls-url]: https://coveralls.io/github/loft-orbital/graphene-federation?branch=loft-master | ||
|
||
Federation specs implementation on top of Python graphene lib | ||
https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ | ||
|
||
Based on discussion: https://github.com/graphql-python/graphene/issues/953#issuecomment-508481652 | ||
|
||
Supports now: | ||
* sdl (_service fields) # make possible to add schema in federation (as is) | ||
* `@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 | ||
* requires # mark that field resolver requires other fields to be pre-fetched | ||
* provides # to annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | ||
* **Base class should be decorated with `@provides`** as well as field on a base type that provides. Check example bellow: | ||
```python | ||
import graphene | ||
from graphene_federation import provides | ||
|
||
@provides | ||
class ArticleThatProvideAuthorAge(graphene.ObjectType): | ||
id = Int(required=True) | ||
text = String(required=True) | ||
author = provides(Field(User), fields='age') | ||
``` | ||
------------------------ | ||
|
||
## Supported Features | ||
|
||
At the moment it supports: | ||
|
||
* `sdl` (`_service` on field): enable to add schema in federation (as is) | ||
* `@key` decorator (entity support): enable to perform queries across service boundaries (you can have more than one key per type) | ||
* `@extend`: extend remote types | ||
* `external()`: mark a field as external | ||
* `requires()`: mark that field resolver requires other fields to be pre-fetched | ||
* `provides()`/`@provides`: annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | ||
|
||
Each type which is decorated with `@key` or `@extend` is added to the `_Entity` union. | ||
The [`__resolve_reference` method](https://www.apollographql.com/docs/federation/api/apollo-federation/#__resolvereference) 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. | ||
If not explicitly defined, the default resolver is used. | ||
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 | ||
* 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)) | ||
* You should not define `__resolve_reference`, if fields resolvers need only data passed in fieldset (example: [FunnyText](integration_tests/service_a/src/schema.py)) | ||
Read more in [official documentation](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference). | ||
|
||
------------------------ | ||
|
||
## Example | ||
|
||
Here is an example of implementation based on the [Apollo Federation introduction example](https://www.apollographql.com/docs/federation/). | ||
It implements a federation schema for a basic e-commerce application over three services: accounts, products, reviews. | ||
|
||
### Accounts | ||
First add an account service that expose a `User` type that can then be referenced in other services by its `id` field: | ||
|
||
```python | ||
import graphene | ||
from graphene import Field, ID, ObjectType, String | ||
from graphene_federation import build_schema, key | ||
|
||
@key(fields='id') # mark File as Entity and add in EntityUnion https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#key | ||
class File(graphene.ObjectType): | ||
id = graphene.Int(required=True) | ||
name = graphene.String() | ||
@key("id") | ||
class User(ObjectType): | ||
id = Int(required=True) | ||
username = String(required=True) | ||
|
||
def resolve_id(self, info, **kwargs): | ||
return 1 | ||
def __resolve_reference(self, info, **kwargs): | ||
""" | ||
Here we resolve the reference of the user entity referenced by its `id` field. | ||
""" | ||
return User(id=self.id, email=f"user_{self.id}@mail.com") | ||
|
||
def resolve_name(self, info, **kwargs): | ||
return self.name | ||
class Query(ObjectType): | ||
me = Field(User) | ||
|
||
def __resolve_reference(self, info, **kwargs): # https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference | ||
return get_file_by_id(self.id) | ||
schema = build_schema(query=Query) | ||
``` | ||
|
||
### Product | ||
The product service exposes a `Product` type that can be used by other services via the `upc` field: | ||
|
||
```python | ||
import graphene | ||
from graphene_federation import build_schema | ||
from graphene import Argument, ID, Int, List, ObjectType, String | ||
from graphene_federation import build_schema, key | ||
|
||
@key("upc") | ||
class Product(ObjectType): | ||
upc = String(required=True) | ||
name = String(required=True) | ||
price = Int() | ||
|
||
def __resolve_reference(self, info, **kwargs): | ||
""" | ||
Here we resolve the reference of the product entity referenced by its `upc` field. | ||
""" | ||
return User(upc=self.upc, name=f"product {self.upc}") | ||
|
||
class Query(graphene.ObjectType): | ||
... | ||
pass | ||
class Query(ObjectType): | ||
topProducts = List(Product, first=Argument(Int, default_value=5)) | ||
|
||
schema = build_schema(Query) # add _service{sdl} field in Query | ||
schema = build_schema(query=Query) | ||
``` | ||
|
||
### Reviews | ||
The reviews service exposes a `Review` type which has a link to both the `User` and `Product` types. | ||
It also has the ability to provide the username of the `User`. | ||
On top of that it adds to the `User`/`Product` types (that are both defined in other services) the ability to get their reviews. | ||
|
||
```python | ||
import graphene | ||
from graphene_federation import external, extend | ||
from graphene import Field, ID, Int, List, ObjectType, String | ||
from graphene_federation import build_schema, extend, external, provides | ||
|
||
@extend("id") | ||
class User(ObjectType): | ||
id = external(Int(required=True)) | ||
reviews = List(lambda: Review) | ||
|
||
def resolve_reviews(self, info, *args, **kwargs): | ||
""" | ||
Get all the reviews of a given user. (not implemented here) | ||
""" | ||
return [] | ||
|
||
@extend("upc") | ||
class Product(ObjectType): | ||
upc = external(String(required=True)) | ||
reviews = List(lambda: Review) | ||
|
||
# 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). | ||
@provides | ||
class Review(ObjectType): | ||
body = String() | ||
author = provides(Field(User), fields="username") | ||
product = Field(Product) | ||
|
||
class Query(ObjectType): | ||
review = Field(Review) | ||
|
||
schema = build_schema(query=Query) | ||
``` | ||
|
||
@extend(fields='id') | ||
class Message(graphene.ObjectType): | ||
id = external(graphene.Int(required=True)) | ||
### Federation | ||
|
||
def resolve_id(self, **kwargs): | ||
return 1 | ||
Note that each schema declaration for the services is a valid graphql schema (it only adds the `_Entity` and `_Service` types). | ||
The best way to check that the decorator are set correctly is to request the service sdl: | ||
|
||
```python | ||
from graphql import graphql | ||
|
||
query = """ | ||
query { | ||
_service { | ||
sdl | ||
} | ||
} | ||
""" | ||
|
||
result = graphql(schema, query) | ||
print(result.data["_service"]["sdl"]) | ||
``` | ||
|
||
### __resolve_reference | ||
* Each type which is decorated with `@key` or `@extend` is added to `_Entity` union | ||
* `__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. | ||
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 | ||
* 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)) | ||
* You should not define `__resolve_reference`, if fileds resolvers need only data passed in fieldset (example: [FunnyText](integration_tests/service_a/src/schema.py)) | ||
* read more in [official documentation](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference) | ||
------------------------ | ||
Those can then be used in a federated schema. | ||
|
||
You can find more examples in the unit / integration tests and [examples folder](examples/). | ||
|
||
### Known issues: | ||
1. decorators will not work properly | ||
* on fields with capitalised letters with `auto_camelcase=True`, for example: `my_ABC_field = String()` | ||
* on fields with custom names for example `some_field = String(name='another_name')` | ||
There is also a cool [example](https://github.com/preply/graphene-federation/issues/1) of integration with Mongoengine. | ||
|
||
--------------------------- | ||
------------------------ | ||
|
||
For more details see [examples](examples/) | ||
## Known issues | ||
|
||
Or better check [integration_tests](integration_tests/) | ||
1. decorators will not work properly on fields with custom names for example `some_field = String(name='another_name')` | ||
1. `@key` decorator will not work on [compound primary key](https://www.apollographql.com/docs/federation/entities/#defining-a-compound-primary-key) | ||
|
||
------------------------ | ||
|
||
Also cool [example](https://github.com/preply/graphene-federation/issues/1) of integration with Mongoengine | ||
## Contributing | ||
|
||
* You can run the unit tests by doing: `make tests`. | ||
* You can run the integration tests by doing `make integration-build && make integration-test`. | ||
* You can get a development environment (on a Docker container) with `make dev-setup`. | ||
* You should use `black` to format your code. | ||
|
||
### For contribution: | ||
#### Run tests: | ||
* `make test` | ||
* if you've changed Dockerfile or requirements run `make build` before `make test` | ||
The tests are automatically run on Travis CI on push to GitHub. | ||
|
||
--------------------------- | ||
|
||
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) | ||
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) |
Oops, something went wrong.