Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Support upto Federation v2.7 #27

Open
wants to merge 39 commits into
base: main
Choose a base branch
from

Conversation

mak626
Copy link

@mak626 mak626 commented Feb 23, 2024

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

  • v1.0
  • v2.0
  • v2.1
  • v2.2
  • v2.3
  • v2.4
  • v2.5
  • v2.6 STABLE_VERSION . Rover dev supports only upto v2.6
  • 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 at DIRECTIVE_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

abhinand-c and others added 30 commits June 13, 2023 18:40
… marked directives to be composed into supergraph
@mak626 mak626 marked this pull request as draft February 23, 2024 09:48
@arunsureshkumar arunsureshkumar self-assigned this Feb 23, 2024
@mak626 mak626 marked this pull request as ready for review February 23, 2024 09:50
@coveralls
Copy link

coveralls commented Feb 23, 2024

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%
Files with Coverage Reduction New Missed Lines %
graphene_federation/entity.py 3 52.94%
Totals Coverage Status
Change from base Build 5063747249: -17.1%
Covered Lines: 644
Relevant Lines: 875

💛 - Coveralls

@mak626 mak626 changed the title [FEAT] Support upto Federation v2.6 [FEAT] Support upto Federation v2.7 Mar 8, 2024
@mak626
Copy link
Author

mak626 commented Mar 8, 2024

Added support for federation v2.7

@mak626 mak626 force-pushed the federation-v2_3 branch 2 times, most recently from 689357c to 3e4013b Compare April 22, 2024 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants