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

Proposal: Ballerina GraphQL Federation Support #3504

Closed
MohamedSabthar opened this issue Oct 13, 2022 · 5 comments · Fixed by ballerina-platform/module-ballerina-graphql#1229
Assignees
Labels
module/graphql Issues related to Ballerina GraphQL module Points/9 Status/Active Proposals that are under review Team/PCM Protocol connector packages related issues Type/Proposal

Comments

@MohamedSabthar
Copy link
Member

MohamedSabthar commented Oct 13, 2022

Summary

The current implementation of the ballerina graphql module doesn’t support graphql federation. To enable the graphql federation the ballerina graphql module should support implementation of subgraphs specified by Apollo subgraph specification. This proposal focuses on the API design of ballerina graphql subgraphs.

Goals

  • Make GraphQL services federation friendly
  • Provide a way to implement subgraphs using ballerina-graphql

Non-Goals

  • Implement a GraphQL federation gateway

Motivation

It is a common practice to create a collection of subgraphs and then merge them into a single supergraph when performing large deployments. This practice offers means of splitting monolithic GraphQL servers into independent microservices (subgraphs) which can be independently scaled and managed by responsible teams. This is commonly done following Apollo Federation specifications. The gateway can resolve a client query by connecting to these subgraphs and executing a query plan. It would be ideal if the ballerina graphql module could provide a straightforward method for implementing these subgraphs.

Description

GraphQL Federation helps to split monolithic GraphQL servers into independent microservices. The federation consists of two main components.

  1. Independent micro services
  2. A gateway

Each independent microservice holds a part of the GraphQL schema and the gateway merges the schema into a single composed schema. In other words, these independent microservices create subgraphs which are combined by the gateway to create a supergraph. This supergraph can be consumed by the client application by connecting to the gateway.

A ballerina graphql service must do all of the following three steps to operate as a apollo federation 2 subgraph.

1. Automatically extend its schema with all definitions listed here

This can be done by introducing a @graphql:Subgraph annotation

public annotation Subgraph on service;

This annotation can be checked at runtime, and the schema can be extended with the additional types/directives necessary while the service is attached to the listener. If the @graphql:Subgraph annotation is not added to the service, the schema extension procedure can be bypassed. Minimum functionality to support Apollo federation at least requires implementation of @link, @key directives and implementation of following types

union _Entity <entity types>  # dynamically generated

type _Service {
  sdl: String!
}

extend type Query {
  _entities(representations: [_Any!]!): [_Entity]!
  _service: _Service!
}

2. Correctly resolve the Query._service enhanced introspection field

Some federated graph routers can compose their supergraph schema dynamically at runtime. To allow routers to compose their schema in runtime following types should be added or modified in the generated schema.

extend type Query {
  _service: _Service!
}

type _Service {
  sdl: String!
}

The aforementioned types can be added on demand by checking the proposed isSubgraph field in the service configuration annotation. No additional API needs to be added for this step.

Querying the _service.sdl field should return the schema string in the SDL language. This string should contain all instances of federation directives such as @key, @sharable, and so on. This improved introspection can be implemented into ballerina graphql by extending the SDLFileGenerator class's existing implementation. The current implementation of this class does not take federation directives into account while producing the SDL schema. As a result, the extension of this class will include additional logic to incorporate the federated directives.

3. Provide a mechanism to resolve entity fields via the Query._entities field

Entity

An entity is an object type that can resolve its fields across multiple subgraphs. Each subgraph can contribute different fields to the entity and is responsible for resolving only the fields that it contributes.

To fully define an entity within a subgraph, following 2 steps are required.

  1. Assign a @key directive to a type to make it as an entity- This directive defines the entity's primary key, which consists of one or more of the type's fields. This directive effectively tells the router, "This subgraph can resolve an instance of this entity if you provide its primary key."
  2. Define the entity's reference resolver - This resolver resolves an instance of the entity when the router query the instance by providing a primary key. In other words, the reference resolver is responsible for returning all of the entity fields that a subgraph defines.

These two steps can be incorporated in ballerina graphql using the following proposed API

API

The first step can be done by introducing a new @graphql:Entity annotation in graphql module

# Annotation that designates an object type as an entity  
# + key - GraphQL fields and subfields that contribute to the entity's primary key/keys
# + resolveReference - Function pointer to resolve the entity. if set to nil,
#                      indicates the graph router that this subgraph dones not define
#                      a reference resolver for this entity.
type FederatedEntity record {|
   string|string[] key;
   ReferenceResolver? resolveReference;
|}

public annotation FederatedEntity Entity on class, type;

# Denotes the entity representation outlined in the federation specification.
# + __typename - GraphQL typename of the entity beign resolved
public type Representation record {
    string __typename;
};

# Represents the type of entity resolver
public type ReferenceResolver function (Representation representation) returns record {}|service object {}|error?;

This annotation can be attached to a service class (GraphQL OBJECT) or service object (GraphQL INTERFACE)

Step two can be achieved by supplying a function pointer to the resolveReference field of the @graphql:Entity annotation.

Ex:

@graphql:Entity {
    key: "email",
    resolveReference: resolveUser
}
distinct service class User {
    resource function get email() returns string {
        return "sabthar@wso2.com";
    }
}

function resolveUser(Representation representation) returns User|error? {
    string email = check representation["email"].ensureType(); // key field name is "email"
    return fetchUserByEmail(email); 
}
  • A reference resolver can be a the type of ReferenceResolver or nil (if not resolvable).
  • A reference resolver's parameter is a representation of the entity being resolved
  • An entity representation is an object that contains the entity's key fields, plus its __typename field. These values are provided by the router.
  • Every subgraph that contributes at least one unique field to an entity must implement a reference resolver for that entity.
  • Router can resolve the entity by querying Query._entities of the subgraph. The router provides __typename and primary-key. The engine will invoke the referenceResolver of the entity specified by __typename.

Ex: Defining an Entity in ballerina graphql

import ballerina/graphql;

@graphql:Subgraph
service /subgraph_a on new graphql:Listener(9001) {
    resource function get product() returns Product {
        return new ("001", "one", 5000);
    }
}

@graphql:Entity {
    key: "id",
    resolveReference: resolveProduct
}
service class Product {
    private final string id;
    private final string name;
    private final int price;

    function init(string id, string name, int price) {
        self.id = id;
        self.name = name;
        self.price = price;
    }

    resource function get id() returns string {
        return self.id;
    }

    resource function get name() returns string {
        return self.name;
    }

    resource function get price() returns int {
        return self.price;
    }
}

function resolveProduct(Representation representation) returns Product|error? {
    string id = check representation["id"].ensureType();
    return fetchProductByID(id);
}

Any number of different subgraphs can contribute fields to an entity definition.

Ex: Same Product entity defined in a different ballerina graphql subgraph.

import ballerina/graphql;

@graphql:Subgraph
service /subgraph_b on new graphql:Listener(9002) {
    resource function get product() returns Product {
        return new ("001", false);
    }
}

@graphql:Entity {
    key: "id",
    resolveReference: resolveProduct
}
service class Product {
    private final string id;
    private final boolean inStock; // a new field called inStock is introduced in this subgraph

    function init(string id, boolean inStock) {
        self.id = id;
        self.inStock = inStock;
    }

    resource function get id() returns string {
        return self.id;
    }

    resource function get inStock() returns boolean {
        return self.inStock;
    }
}

function resolveProduct(Representation representation) returns Product|error? {
    string id = check representation["id"].ensureType();
    return fetchProductByID(id);
}

Subgraphs can use an entity as the return type of a field without adding any fields to that entity.
In this situation, a stub implementation of the entity must be included to build a valid schema, as seen below.

Ex: Entity stub implementation using class

import ballerina/graphql;

@graphql:Subgraph
service /graphql on new graphql:Listener(5001) {
    resource function get latestReviews() returns Review[] {
        return [
            new (20, "this is a product", new ("1"))
        ];
    }
}

public service class Review {
    final Product product;
    final int score;
    final string description;

    public function init(int score, string description, Product product) {
        self.score = score;
        self.description = description;
        self.product = product;
    }

    // This Review type uses Product entity as a return type.
    // Therefore, we need to provide a stub implementation for this Product entity. 
    resource function get product() returns Product {
        return self.product;
    }

    resource function get score() returns int {
        return self.score;
    }

    resource function get description() returns string {
        return self.description;
    }
}

// This stub definition includes only the @key fields of Product (just id in this case).
// It also includes resolvable: false in the @key directive to indicate that this 
// subgraph doesn't even define a reference resolver for the Product entity.
@graphql:Entity {
    key: "id",
    resolveReference: () // Note that this entity doesn't define a reference resolver
}
public service class Product {
    private final string id;

    function init(string id) {
        self.id = id;
    }

    resource function get id() returns string {
        return self.id;
    }
}

A record type can also be used as an entity

Ex: Entity stub implementation using record

@graphql:Entity {
   key: "id",
   resolvable: ()
}
public type Product record {
   int id;
}

The GraphQL compiler plugin collects all entities annotated with @graphql:Entity, then modifies the user code by creating a union type called Entity. Additionally, the produced GraphQL schema will include a GraphQL union type named _Entity, as required by the router in the Apollo subgraph specification.

Additionally, the GraphQL compiler plugin (code modifier) modifies the root GraphQL service by adding the following resource function which includes the logic for invoking the reference resolvers of each entity.

resource function get _entities(Representation[] representations) returns Entity?[]|error {
    map<typedesc<Entity>> typedescs = {"User": User}; // build this map using compiler plugin
    Entity?[] entities = [];
    foreach Representation rep in representations {
        if !typedescs.hasKey(rep.__typename) {
            entities.push(());
            continue;
        }
        typedesc entityTypedesc = typedescs.get(rep.__typename);
        FederatedEntity? federatedEntity = entityTypedesc.@Entity;
        if federatedEntity is () {
            entities.push(());
            continue;
        }
        ReferenceResolver? resolve = federatedEntity.resolveReference;
        if resolve is () {
            return error(string `No resolvers defined for ${rep.__typename}`);
        }
        Entity entity = check resolve(rep).ensureType();
        entities.push(entity);
    }
    return entities;
}

Future Plans

With the notable exception of @key fields, each subgraph must contribute different fields by default. Otherwise, a composition error occurs. To override this behavior, Ballerina graphql might introduce the following annotations, which can be applied to objects/class methods.

  • @graphql:Shareable
  • @graphql:Provides
  • @graphql:External

For more details about the corresponding directives of the above annotations see resolving-another-subgraphs-field.

Following example show the usage of @graphql:Shareable annotation

// Entity in subgraph A
service class Product {
    private final string id;
    private final string name;
    private final int price;

    function init(string id, string name, int price) {
        self.id = id;
        self.name = name;
        self.price = price;
    }

    resource function get id() returns string {
        return self.id;
    }

    @graphql:Shareable // This field can be resolved by router by querying subgraph A or B
    resource function get name() returns string {
        return self.name;
    }

    resource function get price() returns int {
        return self.price;
    }
}
// Entity in subgraph B
service class Product {
    private final string id;
    private final string name;
    private final boolean inStock;

    function init(string id, string name, int price) {
        self.id = id;
        self.name = name;
        self.price = price;
    }

    resource function get id() returns string {
        return self.id;
    }

    @graphql:Shareable // This field can be resolved by router by querying subgraph A or B
    resource function get name() returns string {
        return self.name;
    }

    resource function get inStock() returns boolean {
        return self.inStock;
    }
}

Following are a few additional federated directives defined in the apollo subgraph specification. These are useful for advanced use cases. These directives could also be implemented using ballerina annotations.

  • @requires(fields: FieldSet!)
  • @inaccessible
  • @override(from: String!)
  • @requires(fields: FieldSet!)
  • @tag(name: String!)
  • repeatable @key

Testing

A ballerina graphql subgraph might be tested by performing introspection on individual subgraphs. Further testing can be done using apollo-federation-subgraph-compatibility testing-suite.

Dependencies

#3286

@MohamedSabthar MohamedSabthar added Type/Proposal module/graphql Issues related to Ballerina GraphQL module Team/PCM Protocol connector packages related issues Status/Draft In circulation by the author for initial review and consensus-building labels Oct 13, 2022
@MohamedSabthar MohamedSabthar changed the title Ballerina GraphQL Federation Support Proposal: Ballerina GraphQL Federation Support Oct 17, 2022
@MohamedSabthar MohamedSabthar added Status/Active Proposals that are under review and removed Status/Draft In circulation by the author for initial review and consensus-building labels Jan 23, 2023
@MohamedSabthar MohamedSabthar moved this from Prioritized to In Progress in Ballerina Team Main Board Jan 23, 2023
@MohamedSabthar MohamedSabthar self-assigned this Jan 23, 2023
@MohamedSabthar
Copy link
Member Author

The proposal has been modified with a different approach to resolve entity reference as ballerina language doesn't allow the invocation of instance methods without an instance

@MohamedSabthar
Copy link
Member Author

The proposal has been modified with a different approach to resolve entity reference as ballerina language doesn't allow the invocation of instance methods without an instance

@shafreenAnfar @ThisaruGuruge FYI

@shafreenAnfar
Copy link
Contributor

@MohamedSabthar @ThisaruGuruge why aren't @graphql:Entity related configuration are not part of @graphql:ServiceConfig ?

@MohamedSabthar
Copy link
Member Author

It seems that I am having difficulty understanding the question. Just to clarify, are you asking if it is possible to enable entities in a subgraph by providing a boolean value through a configuration, or are you asking if it's possible to configure entities in a subgraph without using an additional @graphql:Entity directive?

@ThisaruGuruge
Copy link
Member

@MohamedSabthar @ThisaruGuruge why aren't @graphql:Entity related configuration are not part of @graphql:ServiceConfig ?

@graphql:Entity should be part of the type, not the entire service. Therefore, we can't add it as a part of the service config. If you're asking about the @subgraph annotation, the issue was that the compiler plugin cannot correctly process a field value since the values can be reference pointers such as a variable or a function call. To avoid that limitation, we introduced a separate annotation.

One alternative is to add it as a @graphql:serviceConfig field, and limit to allow only direct value assignation. But we thought of going this way since we do not have any other solution to support other scenarios in the foreseeable future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module/graphql Issues related to Ballerina GraphQL module Points/9 Status/Active Proposals that are under review Team/PCM Protocol connector packages related issues Type/Proposal
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

3 participants