|
1 | 1 | # graphene-federation |
2 | | -Federation support for graphene |
3 | 2 |
|
4 | | -Build: [](https://circleci.com/gh/preply/graphene-federation) |
| 3 | +Federation support for  [Graphene](http://graphene-python.org) following the [Federation specifications](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). |
5 | 4 |
|
| 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 |
6 | 12 |
|
7 | | -Federation specs implementation on top of Python graphene lib |
8 | | -https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ |
9 | 13 |
|
10 | 14 | Based on discussion: https://github.com/graphql-python/graphene/issues/953#issuecomment-508481652 |
11 | 15 |
|
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. |
43 | 44 |
|
| 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: |
44 | 47 |
|
45 | 48 | ```python |
46 | | -import graphene |
| 49 | +from graphene import Field, ID, ObjectType, String |
47 | 50 | from graphene_federation import build_schema, key |
48 | 51 |
|
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) |
53 | 56 |
|
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") |
56 | 62 |
|
57 | | - def resolve_name(self, info, **kwargs): |
58 | | - return self.name |
| 63 | +class Query(ObjectType): |
| 64 | + me = Field(User) |
59 | 65 |
|
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) |
62 | 67 | ``` |
63 | 68 |
|
| 69 | +### Product |
| 70 | +The product service exposes a `Product` type that can be used by other services via the `upc` field: |
64 | 71 |
|
65 | 72 | ```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() |
68 | 81 |
|
| 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}") |
69 | 87 |
|
70 | | -class Query(graphene.ObjectType): |
71 | | - ... |
72 | | - pass |
| 88 | +class Query(ObjectType): |
| 89 | + topProducts = List(Product, first=Argument(Int, default_value=5)) |
73 | 90 |
|
74 | | -schema = build_schema(Query) # add _service{sdl} field in Query |
| 91 | +schema = build_schema(query=Query) |
75 | 92 | ``` |
76 | 93 |
|
| 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. |
77 | 98 |
|
78 | 99 | ```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 | +``` |
81 | 131 |
|
82 | | -@extend(fields='id') |
83 | | -class Message(graphene.ObjectType): |
84 | | - id = external(graphene.Int(required=True)) |
| 132 | +### Federation |
85 | 133 |
|
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: |
88 | 136 |
|
| 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"]) |
89 | 150 | ``` |
90 | 151 |
|
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. |
99 | 153 |
|
| 154 | +You can find more examples in the unit / integration tests and [examples folder](examples/). |
100 | 155 |
|
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. |
105 | 157 |
|
106 | | ---------------------------- |
| 158 | +------------------------ |
107 | 159 |
|
108 | | -For more details see [examples](examples/) |
| 160 | +## Known issues |
109 | 161 |
|
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 | +------------------------ |
111 | 166 |
|
112 | | -Also cool [example](https://github.com/preply/graphene-federation/issues/1) of integration with Mongoengine |
| 167 | +## Contributing |
113 | 168 |
|
| 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. |
114 | 173 |
|
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. |
119 | 175 |
|
120 | 176 | --------------------------- |
121 | 177 |
|
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