Skip to content

Commit

Permalink
Merge pull request #1 from loft-orbital/issue#50-fix_directive_declar…
Browse files Browse the repository at this point in the history
…ation

Issue preply#50 and preply#51 : fix various bugs and add unit tests
  • Loading branch information
tcleonard authored Nov 20, 2020
2 parents e90b2ee + 41ec6ef commit 0ea0711
Show file tree
Hide file tree
Showing 29 changed files with 1,807 additions and 238 deletions.
24 changes: 0 additions & 24 deletions .circleci/config.yml

This file was deleted.

2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = */tests/*
14 changes: 14 additions & 0 deletions .github/workflows/integration_tests.yml
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
20 changes: 20 additions & 0 deletions .github/workflows/lint.yml
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
30 changes: 30 additions & 0 deletions .github/workflows/tests.yml
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
14 changes: 14 additions & 0 deletions Dockerfile
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
22 changes: 20 additions & 2 deletions Makefile
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
216 changes: 136 additions & 80 deletions README.md
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)
Loading

0 comments on commit 0ea0711

Please sign in to comment.