mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
588 lines
14 KiB
Markdown
588 lines
14 KiB
Markdown
|
# Apollo federation v1 support
|
|||
|
|
|||
|
|
|||
|
Original issue [#3064](https://github.com/hasura/graphql-engine/issues/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](https://www.apollographql.com/docs/federation/federation-spec/).
|
|||
|
|
|||
|
## Product perspective
|
|||
|
|
|||
|
### Requirements
|
|||
|
|
|||
|
We want the following things:
|
|||
|
1. Hasura should mount on Apollo federated gateway
|
|||
|
2. Other subgraphs can use Hasura. At a minimum, this means that table types
|
|||
|
(generated by Hasura) should be available for other subgraphs.
|
|||
|
|
|||
|
### Setup experience
|
|||
|
|
|||
|
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:
|
|||
|
```json
|
|||
|
{
|
|||
|
"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:
|
|||
|
1. Extending the support to v2 directives in future such as adding `@sharable`
|
|||
|
to some columns.
|
|||
|
2. Changing `@key` directive `fields` (or primary key for apollo federation in
|
|||
|
simple terms).
|
|||
|
|
|||
|
### Behaviour
|
|||
|
|
|||
|
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`.
|
|||
|
|
|||
|
### Evaluating feature
|
|||
|
|
|||
|
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.
|
|||
|
|
|||
|
## Implementation perspective
|
|||
|
|
|||
|
### Spec
|
|||
|
|
|||
|
According to the apollo spec:
|
|||
|
> To make a GraphQL service subgraph-capable, it needs the following:
|
|||
|
>
|
|||
|
> 1. Implementation of the federation schema specification
|
|||
|
> 2. Support for fetching service capabilities
|
|||
|
> 3. Implementation of stub type generation for references
|
|||
|
> 4. Implementation of request resolving for entities.
|
|||
|
|
|||
|
### SDL generation
|
|||
|
|
|||
|
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:
|
|||
|
|
|||
|
```haskell
|
|||
|
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:
|
|||
|
|
|||
|
```haskell
|
|||
|
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:
|
|||
|
|
|||
|
#### HGE setup
|
|||
|
A tracked table, called users with fields `id` and `name` using the
|
|||
|
`graphql-default` naming convention.
|
|||
|
|
|||
|
#### Generated SDL
|
|||
|
|
|||
|
(Shortened version)
|
|||
|
```graphql
|
|||
|
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!
|
|||
|
}
|
|||
|
|
|||
|
.
|
|||
|
.
|
|||
|
.
|
|||
|
```
|
|||
|
|
|||
|
<details>
|
|||
|
|
|||
|
<summary>Click here for full SDL</summary>
|
|||
|
|
|||
|
|
|||
|
```graphql
|
|||
|
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!
|
|||
|
}
|
|||
|
```
|
|||
|
</details>
|
|||
|
|
|||
|
### Entities Union
|
|||
|
Apollo federation needs an `_entities` field as part of the specification,
|
|||
|
which is defined as follows:
|
|||
|
|
|||
|
```graphql
|
|||
|
# 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:
|
|||
|
```haskell
|
|||
|
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 `Parser`s
|
|||
|
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 `Parser`s.
|
|||
|
|
|||
|
Next, we need to evaluate the `_entities` query. Consider the following:
|
|||
|
```graphql
|
|||
|
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:
|
|||
|
|
|||
|
1. Get selection set for each of the union types in the selection set of the
|
|||
|
`_entities` query. In the above query, we will have two selection sets (one for
|
|||
|
`TwoPks` and one for `UsersData`).
|
|||
|
2. Generate the arguments using the arguments of the query. For the above
|
|||
|
query, for the 1st selection set (`TowPks`), we will have the argument
|
|||
|
`(id1: "1", id2: "2")`.
|
|||
|
3. Next we would have to use the parsers for the fields (`TwoPksByPk` and
|
|||
|
`UsersDataByPk`) to evaluate the `Field` (constructed using the selection set
|
|||
|
and arguments in above steps).
|
|||
|
4. Finally we would concatenate the results in a list.
|
|||
|
|
|||
|
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.
|
|||
|
|
|||
|
### Implementation
|
|||
|
|
|||
|
There are two main functions from the perspective of implementation:
|
|||
|
1. `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:
|
|||
|
```haskell
|
|||
|
generateSDL :: G.SchemaIntrospection -> Text
|
|||
|
generateSDL = undefined
|
|||
|
```
|
|||
|
|
|||
|
2. `mkEntityUnionFieldParser`: This function will create a `FieldParser` for
|
|||
|
the entities query. This uses the union selection set `Parser` and the
|
|||
|
`FieldParser`s 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:
|
|||
|
```haskell
|
|||
|
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.
|
|||
|
|
|||
|
### Future work
|
|||
|
|
|||
|
There is a lot that can be improved in the v1 implementation. To list a few:
|
|||
|
1. Include action types under the apollo federation as well. This will require
|
|||
|
a few internal representational changes (such as adding directives in action
|
|||
|
types).
|
|||
|
2. Let the user choose the fields that are used in `@key` directives. This
|
|||
|
will enable users to choose fields other than primary key.
|
|||
|
3. Look into supporting apollo federation v2.
|