ballerina-platform / ballerina-library

The Ballerina Library
https://ballerina.io/learn/api-docs/ballerina/
Apache License 2.0
136 stars 64 forks source link

Proposal: GraphQL Federation Gateway in Ballerina #4298

Closed Ishad-M-I-M closed 1 year ago

Ishad-M-I-M commented 1 year ago

Summary

To overcome the limitations in GraphQL, federation architecture is introduced. With this architecture, the GraphQL schema can be broken down into smaller microservices. To merge these microservices, a GraphQL federation gateway is required. The gateway will direct GraphQL requests to the relevant subgraph implementation, and it will support multiple programming languages to accommodate a broad range of GraphQL subgraphs.

Goals

Non-Goals

Motivation

It is a common practice in GraphQL 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 has a gateway module to interact with the subgraph implementations.

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 service needs to act as a gateway and route the request to correct graphql end-points to fetch data. This service will be generated as an executable (a .jar file) via the Ballerina GraphQL CLI tool.

Configuration and Initialization

The Ballerina federation gateway executable can be generated by the following command.

bal graphql -m federation-gateway -i <supergraph-schema-file> 

The supergraph schema file needs to be provided as a .graphql as an input. It can be composed and obtained by a third party tool such as rover cli. (Composing a supergraph schema can be a part of the Ballerina GraphQL CLI tool in the future).

Validation

The supergraph schema provided will be parsed and validated by a schema parser.

Building query plan

The plan to query the subgraphs is composed to a table by parsing the supergraph schema. For example to the below schema,

schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
  @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
  query: Query
}

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__field(
  graph: join__Graph
  requires: join__FieldSet
  provides: join__FieldSet
  type: String
  external: Boolean
  override: String
  usedOverridden: Boolean
) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__implements(
  graph: join__Graph!
  interface: String!
) repeatable on OBJECT | INTERFACE

directive @join__type(
  graph: join__Graph!
  key: join__FieldSet
  extension: Boolean! = false
  resolvable: Boolean! = true
  isInterfaceObject: Boolean! = false
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(
  graph: join__Graph!
  member: String!
) repeatable on UNION

directive @link(
  url: String
  as: String
  for: link__Purpose
  import: [link__Import]
) repeatable on SCHEMA

type Astronaut
  @join__type(graph: ASTRONAUTS, key: "id")
  @join__type(graph: MISSIONS, key: "id") {
  id: ID!
  name: String @join__field(graph: ASTRONAUTS)
  missions: [Mission] @join__field(graph: MISSIONS)
}

scalar join__FieldSet

enum join__Graph {
  ASTRONAUTS @join__graph(name: "astronauts", url: "http://localhost:4001")
  MISSIONS @join__graph(name: "missions", url: "http://localhost:4002")
}

scalar link__Import

enum link__Purpose {
  """
  `SECURITY` features provide metadata necessary to securely resolve fields.
  """
  SECURITY

  """
  `EXECUTION` features provide metadata necessary for operation execution.
  """
  EXECUTION
}

type Mission @join__type(graph: MISSIONS) {
  id: ID!
  crew: [Astronaut]
  designation: String!
  startDate: String
  endDate: String
}

type Query @join__type(graph: ASTRONAUTS) @join__type(graph: MISSIONS) {
  astronaut(id: ID!): Astronaut @join__field(graph: ASTRONAUTS)
  astronauts: [Astronaut] @join__field(graph: ASTRONAUTS)
  mission(id: ID!): Mission @join__field(graph: MISSIONS)
  missions: [Mission] @join__field(graph: MISSIONS)
}

Following kind of table will be generated and used for dynamically resolving the incoming query requests.

public const string ASTRONAUTS = "astronauts";
public const string MISSIONS = "missions";

public final readonly & table<QueryPlanEntry> key(typename) queryPlan = table [
    {
        typename: "Astronaut",
        keys: {
            "astronauts": "id",
            "missions": "id"
        },
        fields: table [
            {name: "name", 'type: "STRING", 'client: ASTRONAUTS},
            {name: "missions", 'type: "Mission", 'client: MISSIONS}
        ]
    },
    {
        typename: "Mission",
        keys: {
            "missions": "id"
        },
        fields: table [
            {name: "designation", 'type: "STRING", 'client: MISSIONS},
            {name: "crew", 'type: "Astronaut", 'client: MISSIONS},
            {name: "startDate", 'type: "STRING", 'client: MISSIONS},
            {name: "endDate", 'type: "STRING", 'client: MISSIONS}
        ]
    }
];

The common types ( ex: QueryPlanEntry ) and function used for gateway can be published as a repository for ballerina central and imported upon gateway generation. The query plan is straightforward from the supergraph SDL. The graphql request types and the fields that can be resolved from the gateway can be decided from the type definitions in the supergraph SDL. For example from the above scenario,

type Query @join__type(graph: ASTRONAUTS) @join__type(graph: MISSIONS) {
  astronaut(id: ID!): Astronaut @join__field(graph: ASTRONAUTS)
  astronauts: [Astronaut] @join__field(graph: ASTRONAUTS)
  mission(id: ID!): Mission @join__field(graph: MISSIONS)
  missions: [Mission] @join__field(graph: MISSIONS)
}

Above segment defines the graphql query requests that can handle only astronaut, astronauts, mission, missions arguments.

For resolving the query requests the gateway may generate and inject the resolver functions to the graphql while generating the executable.

For example to resolve

query{
    astronaut(id: <id>){
        # data need to be fetched
    }
}

requests the generated function will be as,

service on new graphql:Listener(5000){
    resource function get astronaut (int id) returns Astronaut {
        // using graphql clients data will be fetched from subgraphs
        // make use of the @join__field(graph: <GRAPH-ENUM-VALUE>) to identify the corresponding subgraph url
    }
}

Response generation

The resolver of the gateway core module will compose and return the response in the requested type. The response sent to the client is handled by the already existing response generator and formatter of the ballerina graphql module.

The location field in error messages will be removed from an interceptor before the response is delivered to the client. Since it is irrelevant for the response from the gateway.

Testing

Future plans

ThisaruGuruge commented 1 year ago

Closing as this is continued as a separate project under xlibb.