graphene-federation
graphene-federation copied to clipboard
[FEAT] Support upto Federation v2.7
Supported versions, v1.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7
All directives are purely based on apollo-specs.
Some non existent directive like@extend
which acted like @key
was deprecated.
Migration Guide
For V1 @extend
can be replaced by @extends
and @key
combination
For V2 just @key
is enough.
Deprecate enable_federation_2
in favour of federation_version
Updated Docs
...
Supported Features
-
sdl
(_service
on field): enable to add schema in federation (as is)
Apollo Spec Supported
- [x] v1.0
- [x] v2.0
- [x] v2.1
- [x] v2.2
- [x] v2.3
- [x] v2.4
- [x] v2.5
- [x] v2.6
STABLE_VERSION
. Rover dev supports only upto v2.6 - [x] v2.7
LATEST_VERSION
All directives could be easily integrated with the help of graphene-directives. Now every directive's values are validated at run time itself by graphene-directives.
Directives (v2.7)
directive @composeDirective(name: String!) repeatable on SCHEMA
directive @extends on OBJECT | INTERFACE
directive @external on OBJECT | FIELD_DEFINITION
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
directive @inaccessible on
| FIELD_DEFINITION
| OBJECT
| INTERFACE
| UNION
| ENUM
| ENUM_VALUE
| SCALAR
| INPUT_OBJECT
| INPUT_FIELD_DEFINITION
| ARGUMENT_DEFINITION
directive @interfaceObject on OBJECT
directive @override(from: String!, label: String) on FIELD_DEFINITION
directive @provides(fields: FieldSet!) on FIELD_DEFINITION
directive @requires(fields: FieldSet!) on FIELD_DEFINITION
directive @shareable repeatable on FIELD_DEFINITION | OBJECT
directive @tag(name: String!) repeatable on
| FIELD_DEFINITION
| INTERFACE
| OBJECT
| UNION
| ARGUMENT_DEFINITION
| SCALAR
| ENUM
| ENUM_VALUE
| INPUT_OBJECT
| INPUT_FIELD_DEFINITION
directive @authenticated on
FIELD_DEFINITION
| OBJECT
| INTERFACE
| SCALAR
| ENUM
directive @requiresScopes(scopes: [[federation__Scope!]!]!) on
FIELD_DEFINITION
| OBJECT
| INTERFACE
| SCALAR
| ENUM
directive @policy(policies: [[federation__Policy!]!]!) on
| FIELD_DEFINITION
| OBJECT
| INTERFACE
| SCALAR
| ENUM
scalar federation__Policy
scalar federation__Scope
scalar FieldSet
Read about directives in official documentation
Each type which is decorated with @key
or @extends
is added to the _Entity
union.
The __resolve_reference
method can be defined for each type that is an entity.
Note that since the notation with double underscores can be problematic in Python for model inheritance this resolver method can also be named _resolve_reference
(the __resolve_reference
method will take precedence if both are declared).
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
for more details
- You should define
__resolve_reference
, if you need to extract object before passing it to fields resolvers (example: FileNode) - You should not define
__resolve_reference
, if fields resolvers need only data passed in fieldset (example: FunnyText) Read more in official documentation.
Example
Here is an example of implementation based on the Apollo Federation introduction example. 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:
from graphene import Field, Int, ObjectType, String
from graphene_federation import build_schema, key
@key("id")
class User(ObjectType):
id = Int(required=True)
username = String(required=True)
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")
class Query(ObjectType):
me = Field(User)
schema = build_schema(query=Query, enable_federation_2=True)
Product
The product service exposes a Product
type that can be used by other services via the upc
field:
from graphene import Argument, 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 Product(upc=self.upc, name=f"product {self.upc}")
class Query(ObjectType):
topProducts = List(Product, first=Argument(Int, default_value=5))
schema = build_schema(query=Query, enable_federation_2=True)
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.
from graphene import Field, Int, List, ObjectType, String
from graphene_federation import build_schema, external, key, provides
@key("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 []
@key("upc")
class Product(ObjectType):
upc = external(String(required=True))
reviews = List(lambda: Review)
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, enable_federation_2=True)
Federation
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:
from graphql import graphql
query = """
query {
_service {
sdl
}
}
"""
result = graphql(schema, query)
print(result.data["_service"]["sdl"])
Those can then be used in a federated schema.
You can find more examples in the unit / integration tests and examples folder.
There is also a cool example of integration with Mongoengine.
Other Notes
build_schema new arguments
-
schema_directives
(Collection[SchemaDirective]
): Directives that can be defined atDIRECTIVE_LOCATION.SCHEMA
with their argument values. -
include_graphql_spec_directives
(bool
): Includes directives defined by GraphQL spec (@include
,@skip
,@deprecated
,@specifiedBy
) -
enable_federation_2
(bool
): Whether to enable federation 2 directives (default False) -
federation_version
(FederationVersion
): Specify the version explicit (default LATEST_VERSION)
In case both enable_federation_2 and federation_version are specified, federation_version is given higher priority
Directives Additional arguments
-
federation_version
: (FederationVersion
=LATEST_VERSION
) : You can use this to take a directive from a particular federation version
Note: The federation_version
in build_schema
is given higher priority. If the directive you have chosen is not compatible, it will raise an error
Custom Directives
You can define custom directives as follows
from graphene import Field, ObjectType, String
from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull
from graphene_federation import DirectiveLocation, FederationDirective
from graphene_federation import build_schema
CacheDirective = FederationDirective(
name="cache",
locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT],
args={
"maxAge": GraphQLArgument(
GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds."
),
},
description="Caching directive to control cache behavior.",
spec_url="https://specs.example.dev/directives/v1.0",
)
cache = CacheDirective.decorator()
@cache(max_age=20)
class Review(ObjectType):
body = cache(field=String(),max_age=100)
class Query(ObjectType):
review = Field(Review)
schema = build_schema(
query=Query,
directives=(CacheDirective,),
enable_federation_2=True,
)
This will automatically add @link and @composeDirective to schema
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@composeDirective"])
@link(url: "https://specs.example.dev/directives/v1.0", import: ["@cache"])
@composeDirective(name: "@cache")
"""Caching directive to control cache behavior."""
directive @cache(
"""Specifies the maximum age for cache in seconds."""
maxAge: Int!
) on FIELD_DEFINITION | OBJECT
type Query {
review: Review
_service: _Service!
}
type Review @cache(maxAge: 20) {
body: String @cache(maxAge: 100)
}
If you wish to add the schema_directives @link
@composeDirective
manually.
You can pass the add_to_schema_directives
as False
from graphene import Field, ObjectType, String
from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull
from graphene_federation import DirectiveLocation, FederationDirective, build_schema, compose_directive, link_directive
CacheDirective = FederationDirective(
name="cache",
locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT],
args={
"maxAge": GraphQLArgument(
GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds."
),
},
description="Caching directive to control cache behavior.",
add_to_schema_directives=False
)
cache = CacheDirective.decorator()
@cache(max_age=20)
class Review(ObjectType):
body = cache(field=String(), max_age=100)
class Query(ObjectType):
review = Field(Review)
schema = build_schema(
query=Query,
directives=(CacheDirective,),
schema_directives=(
link_directive(url="https://specs.example.dev/directives/v1.0", import_=['@cache']),
compose_directive(name='@cache'),
),
enable_federation_2=True,
)
Custom field name
When using decorator on a field with custom name
Case 1 (auto_camelcase=False)
@key("identifier")
@key("validEmail")
class User(ObjectType):
identifier = ID()
email = String(name="validEmail")
class Query(ObjectType):
user = Field(User)
schema = build_schema(query=Query, enable_federation_2=True, auto_camelcase=False) # Disable auto_camelcase
This works correctly.
By default fields
of @key
,@requires
and @provides
are not converted to camel case if auto_camelcase
is set to False
Case 2 (auto_camelcase=True)
@key("identifier")
@key("valid_email")
class User(ObjectType):
identifier = ID()
email = String(name="valid_email")
class Query(ObjectType):
user = Field(User)
schema = build_schema(query=Query, enable_federation_2=True) # auto_camelcase Enabled
This will raise an error @key, field "validEmail" does not exist on type "User"
.
Because The decorator auto camel-cased the field
value of key, as schema has auto_camelcase=True
(default)
To fix this, pass auto_case=False
in the @key
, @requires
or @provides
argument
@key("identifier")
@key("valid_email", auto_case=False)
class User(ObjectType):
identifier = ID()
email = String(name="valid_email")
class Query(ObjectType):
user = Field(User)
schema = build_schema(query=Query, enable_federation_2=True) # auto_camelcase=True
Pull Request Test Coverage Report for Build 8788451656
Details
- 604 of 819 (73.75%) changed or added relevant lines in 49 files are covered.
- 3 unchanged lines in 1 file lost coverage.
- Overall coverage decreased (-17.1%) to 73.6%
Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
---|---|---|---|
graphene_federation/directives/inaccessible.py | 13 | 14 | 92.86% |
graphene_federation/directives/key.py | 15 | 16 | 93.75% |
graphene_federation/directives/shareable.py | 13 | 14 | 92.86% |
graphene_federation/validators/provides.py | 18 | 19 | 94.74% |
graphene_federation/directives/extends.py | 12 | 14 | 85.71% |
graphene_federation/directives/external.py | 12 | 14 | 85.71% |
graphene_federation/directives/override.py | 12 | 14 | 85.71% |
graphene_federation/directives/provides.py | 17 | 19 | 89.47% |
graphene_federation/directives/requires.py | 17 | 19 | 89.47% |
graphene_federation/schema_directives/compose_directive.py | 3 | 5 | 60.0% |
<!-- | Total: | 604 | 819 |
Files with Coverage Reduction | New Missed Lines | % |
---|---|---|
graphene_federation/entity.py | 3 | 52.94% |
<!-- | Total: | 3 |
Totals | |
---|---|
Change from base Build 5063747249: | -17.1% |
Covered Lines: | 644 |
Relevant Lines: | 875 |
💛 - Coveralls
Added support for federation v2.7
This has been tagged and released as patch while it's actually a major. Indeed this introduced a breaking change by removing the extend
directive.
Maybe we need to adjust the release tag to reflect this?
This has been tagged and released as patch while it's actually a major. Indeed this introduced a breaking change by removing the
extend
directive. Maybe we need to adjust the release tag to reflect this?
I agree. @arunsureshkumar @erikwrede @patrick91 can we release this under major version. Is it possible to rollback the previous release too?
+1 on this being a breaking change.
Assuming this project intends to support semver, this should definitely be a major version release.
If no, could we make clear that this package does not follow semver?
Hey @tcleonard, @mak626 @lyndsysimon. This is a delicate issue, as Graphene / GraphQL Core are not really semver compliant themselves, and most Graphene libraries try to follow the 3.x pattern to signify compatibility with the newest Graphene 3 version.
Since this really is a breaking change, let's bump the version to 4.0
here. I've yanked the current release from PyPi, meaning users can only access it when pinning to 3.1.5
to not add more confusion and will push a new 4.0
release if you all agree. How does that sound?
Hey @tcleonard, @mak626 @lyndsysimon. This is a delicate issue, as Graphene / GraphQL Core are not really semver compliant themselves, and most Graphene libraries try to follow the 3.x pattern to signify compatibility with the newest Graphene 3 version.
Since this really is a breaking change, let's bump the version to
4.0
here. I've yanked the current release from PyPi, meaning users can only access it when pinning to3.1.5
to not add more confusion and will push a new4.0
release if you all agree. How does that sound?
@erikwrede Since the latest graphene version is 3.3
. I think we should bump graphene-federation
to 3.3.0
from 3.1.4
. I agree a major change should be reflected as 4.x.x based on semver conventions, but since all packages of graphql-python
do not follow semver. We should stick with graphene versioning.
Let's also add the following notes in the release.
Breaking Changes
-
@extend
decorator is deprecated due to not being officially present in GraphQL spec. - Deprecate
enable_federation_2
argument ofbuild_schema(...)
in favour offederation_version
argument
Migration Guide
-
@extend
decorator- For
V1
@extend
can be replaced by@extends
and@key
combination - For
V2
just@key
is enough.
- For
-
enable_federation_2
argument ofbuild_schema
- For
V1
setfederation_version=FederationVersion.VERSION_1_0
inbuild_schema(...)
- For
V2
you can omitfederation_version
to use the latest stable v2 version inbuild_schema(...)
- For
Hey @tcleonard, @mak626 @lyndsysimon. This is a delicate issue, as Graphene / GraphQL Core are not really semver compliant themselves, and most Graphene libraries try to follow the 3.x pattern to signify compatibility with the newest Graphene 3 version.
Since this really is a breaking change, let's bump the version to
4.0
here. I've yanked the current release from PyPi, meaning users can only access it when pinning to3.1.5
to not add more confusion and will push a new4.0
release if you all agree. How does that sound?
@erikwrede - In my view, the best possible option for us here is to bump to 3.2.0.