rfcs/apollo-federation.md
Original issue #3064.
Apollo requires a bunch of fields and types to be exposed in schema in order to use the graphql server as a federated subgraph. The complete requirement can be found here.
We want the following things:
Apollo federation (AF) support should be enabled via an env variable (or a global metadata field? TBD). When AF is enabled, it will add the required AF schema objects to the final generated schema. This will allow point 1. in requirements above.
Secondly, you will have to resolve entities for each type that you want to
federate so that it can be used in other subgraphs. For this, you will have to
enable AF through table metadata for the individual table (i.e. via
*_track_table API). The *_track_table API's args will look something like
the following:
{
"source": "default",
"table": "Author",
"configuration": {},
"apollo_federation_config": {
"enable": "v1"
}
}
This API design will enable us to add more features (by adding new key-value
pairs in apollo_federation_config) such as:
@sharable
to some columns.@key directive fields (or primary key for apollo federation in
simple terms).When a table is tracked with enable AF option, the type of the table in the
schema will have the @key directive added. This will allow point 2. in the
requirements above. In the first version, the @key field will be populated by
the table’s primary key. For example:
type Review @key(fields: "id") {
id: Integer!
body: String
author: User
product: Product
}
@key(fields: “id”) will be automatically added where id is the primary key
for a table called Review.
This feature will be released as an experimental feature first, so that users can use the feature only if they want to explore this. Using the user feedback, we can incrementally improve the feature by adding more supports, fixing bugs and improving performance.
According to the apollo spec:
To make a GraphQL service subgraph-capable, it needs the following:
- Implementation of the federation schema specification
- Support for fetching service capabilities
- Implementation of stub type generation for references
- Implementation of request resolving for entities.
To use HGE as a federated subgraph, we need to expose _service field, which
is of type _Service, which contains field called sdl of type String. So,
first, we need to generate SDL (schema definition language) for the HGE schema
while building the schema field parsers.
For generating the SDL, we can use the Printer.schemaDocument from the
graphql-parser-hs library on SchemaDocument. We can easily generate the
SchemaDocument from SchemaIntrospection using the following:
getSchemaDocument:: G.SchemaIntrospection -> G.SchemaDocument
getSchemaDocument (G.SchemaIntrospection typeDefMap) =
G.SchemaDocument completeSchema
where
allTypeDefns = map G.TypeSystemDefinitionType (Map.elems typeDefMap)
rootOpTypeDefns = getRootOpTypeDefns -- define this
completeSchema = rootOpTypeDefns : allTypeDefns
Then we can easily generate the SDL by:
generateSDL :: G.SchemaIntrospection -> Text
generateSDL = Builder.run . Printer.schemaDocument . getSchemaDocument
To add this new field and type in the schema, we would have to create
FieldParser for each of the fields _service and sdl after creating the
SchemaIntrospection and then expose these in the GQL context. This should be
a trivial change. Please note that the schema introspection will be role
dependent, thus the SDL will not expose fields/types that the current role
doesn't have access to.
An example for the SDL generated for the following setup is:
A tracked table, called users with fields id and name using the
graphql-default naming convention.
(Shortened version)
schema {
query: query_root
mutation: mutation_root
subscription: subscription_root
}
type query_root {
usersAggregate(
where: UsersBoolExp
orderBy: [UsersOrderBy!]
limit: Int
offset: Int
distinctOn: [UsersSelectColumn!]
): UsersAggregate!
users(
where: UsersBoolExp
orderBy: [UsersOrderBy!]
limit: Int
offset: Int
distinctOn: [UsersSelectColumn!]
): [Users!]!
usersByPk(id: Int!): Users
}
type Users @key(fields: "id") {
id: Int!
name: String!
}
.
.
.
schema {
query: query_root
mutation: mutation_root
subscription: subscription_root
}
scalar Float
scalar Int
scalar String
type __Directive {
args: __InputValue
description: String!
isRepeatable: String!
locations: String!
name: String!
}
type __EnumValue {
deprecationReason: String!
description: String!
isDeprecated: String!
name: String!
}
type __Field {
args: __InputValue
deprecationReason: String!
description: String!
isDeprecated: String!
name: String!
type: __Type
}
type __InputValue {
defaultValue: String!
description: String!
name: String!
type: __Type
}
type __Schema {
description: String!
directives: __Directive
mutationType: __Type
queryType: __Type
subscriptionType: __Type
types: __Type
}
type __Type {
description: String!
enumValues(includeDeprecated: Boolean = false): __EnumValue
fields(includeDeprecated: Boolean = false): __Field
inputFields: __InputValue
interfaces: __Type
kind: __TypeKind!
name: String!
ofType: __Type
possibleTypes: __Type
}
type query_root {
usersAggregate(
where: UsersBoolExp
orderBy: [UsersOrderBy!]
limit: Int
offset: Int
distinctOn: [UsersSelectColumn!]
): UsersAggregate!
users(
where: UsersBoolExp
orderBy: [UsersOrderBy!]
limit: Int
offset: Int
distinctOn: [UsersSelectColumn!]
): [Users!]!
usersByPk(id: Int!): Users
}
type subscription_root {
usersAggregate(
where: UsersBoolExp
orderBy: [UsersOrderBy!]
limit: Int
offset: Int
distinctOn: [UsersSelectColumn!]
): UsersAggregate!
users(
where: UsersBoolExp
orderBy: [UsersOrderBy!]
limit: Int
offset: Int
distinctOn: [UsersSelectColumn!]
): [Users!]!
usersByPk(id: Int!): Users
}
type UsersAvgFields {
id: Float
}
type UsersAggregateFields {
avg: UsersAvgFields
count(distinct: Boolean, columns: [UsersSelectColumn!]): Int!
max: UsersMaxFields
min: UsersMinFields
stddev: UsersStddevFields
stddevPop: UsersStddevPopFields
stddevSamp: UsersStddevSampFields
sum: UsersSumFields
varPop: UsersVarPopFields
varSamp: UsersVarSampFields
variance: UsersVarianceFields
}
type UsersMaxFields {
id: Int
name: String
}
type UsersMinFields {
id: Int
name: String
}
type UsersStddevFields {
id: Float
}
type UsersStddevPopFields {
id: Float
}
type UsersStddevSampFields {
id: Float
}
type UsersSumFields {
id: Int
}
type UsersVarPopFields {
id: Float
}
type UsersVarSampFields {
id: Float
}
type UsersVarianceFields {
id: Float
}
type UsersAggregate {
aggregate: UsersAggregateFields
nodes: [Users!]!
}
type Users @key(fields: "id") {
id: Int!
name: String!
}
type mutation_root {
deleteUsers(where: UsersBoolExp!): UsersMutationResponse
deleteUsersByPk(id: Int!): Users
insertUsersOne(onConflict: UsersOnConflict, object: UsersInsertInput!): Users
insertUsers(
onConflict: UsersOnConflict
objects: [UsersInsertInput!]!
): UsersMutationResponse
updateUsers(
_set: UsersSetInput
_inc: UsersIncInput
where: UsersBoolExp!
): UsersMutationResponse
updateUsersByPk(
_set: UsersSetInput
_inc: UsersIncInput
pk_columns: UsersPkColumnsInput!
): Users
}
type UsersMutationResponse {
returning: [Users!]!
affected_rows: Int!
}
enum __TypeKind {
ENUM
INPUT_OBJECT
INTERFACE
LIST
NON_NULL
OBJECT
SCALAR
UNION
}
enum orderBy {
ascNullsFirst
asc
ascNullsLast
desc
descNullsFirst
descNullsLast
}
enum UsersSelectColumn {
id
name
}
enum UsersConstraint {
users_pkey
}
enum UsersUpdateColumn {
id
name
}
input Int_cast_exp {
String: StringComparisonExp
}
input IntComparisonExp {
_cast: Int_cast_exp
_eq: Int
_gt: Int
_gte: Int
_in: [Int!]
_isNull: Boolean
_lt: Int
_lte: Int
_neq: Int
_nin: [Int!]
}
input StringComparisonExp {
_eq: String
_gt: String
_gte: String
_in: [String!]
_isNull: Boolean
_lt: String
_lte: String
_neq: String
_nin: [String!]
_niregex: String
_nregex: String
_nsimilar: String
_nilike: String
_nlike: String
_iregex: String
_regex: String
_similar: String
_ilike: String
_like: String
}
input UsersBoolExp {
_and: [UsersBoolExp!]
_not: UsersBoolExp
_or: [UsersBoolExp!]
id: IntComparisonExp
name: StringComparisonExp
}
input UsersOrderBy {
id: orderBy
name: orderBy
}
input UsersIncInput {
id: Int
}
input UsersInsertInput {
id: Int
name: String
}
input UsersSetInput {
id: Int
name: String
}
input UsersOnConflict {
constraint: UsersConstraint!
update_columns: [UsersUpdateColumn!]! = []
where: UsersBoolExp
}
input UsersPkColumnsInput {
id: Int!
}
Apollo federation needs an _entities field as part of the specification,
which is defined as follows:
# a union of all types that use the @key directive
scalar _Any
union _Entity
extend type Query {
_entities(representations: [_Any!]!): [_Entity]!
}
To add this field, first we need to create an union of all the types that
have the directive key. Please note that the types might be defined in the
HGE (from DB sources or action types) or they might be exposed via the remote
schema.
For DB sources, we will add key directive to the select type with field set
to the primary key of the table (we can allow users to select the field that
they want to specify in the key directive).
For actions, we need to modify the set_custom_types API to include the
directives first.
For remote schema we already store the directives that are coming from the upstream, so we can use them.
Now, to create the union of the types, we already have a function called
selectionSetUnion which creates Parser for the union:
selectionSetUnion ::
(MonadParse n, Traversable t) =>
Name ->
Maybe Description ->
-- | The member object types.
t (Parser 'Output n b) ->
Parser 'Output n (t b)
We need to be a little careful here as we would want to remove the Parsers
based on the role access, for example if a role doesn't have access to the
@key directive feields, then it makes sense to omit those Parsers.
Next, we need to evaluate the _entities query. Consider the following:
query MyQuery {
_entities(representations: [{"__typename": "UsersData", "id": "1"}, {"__typename": "TwoPks", "id1": "1", "id2": "2"}]) {
... on TwoPks {
internalData
}
... on UsersData {
id
name
}
}
}
For evaluating this we need to do the following:
_entities query. In the above query, we will have two selection sets (one for
TwoPks and one for UsersData).TowPks), we will have the argument
(id1: "1", id2: "2").TwoPksByPk and
UsersDataByPk) to evaluate the Field (constructed using the selection set
and arguments in above steps).Note: The above method of evaluating the query may do multiple fetches from the database, which might be something that can be optimised in further iterations.
There are two main functions from the perspective of implementation:
generateSDL: This function generates the SDL of a given schema. It uses
the schema introspection inorder to build the SDL. The schema introspection is
generated while building the parsers. The type definition of the function is:
generateSDL :: G.SchemaIntrospection -> Text
generateSDL = undefined
mkEntityUnionFieldParser: This function will create a FieldParser for
the entities query. This uses the union selection set Parser and the
FieldParsers of fields having @key directive in order to evaluate the
query. The union selection set Parser can be generated using
selectionSetUnion and the FieldParsers can be collected based on the
schema introspection (for getting the types with @key directive) and query
FieldParsers. The type definition of the function is:
mkEntityUnionFieldParser ::
P.Parser 'P.Output (P.Parse) [[P.ParsedSelection a1]] ->
[(G.Name, [G.Directive Void], FieldParser (P.Parse) (NamespacedField (QueryRootField UnpreparedValue)))] ->
FieldParser (P.Parse) (NamespacedField (QueryRootField UnpreparedValue))
mkEntityUnionFieldParser bodyParser fieldParsers = undefined
Please note that the fieldParsers is a list of triple tuple, which have the
type name, directives associated with type name and the FieldParser
associated with the type.
There is a lot that can be improved in the v1 implementation. To list a few:
@key directives. This
will enable users to choose fields other than primary key.