# 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! } . . . ```
Click here for full SDL ```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! } ```
### 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.