We talked before in this blog about federation, and its advanced possibilities. We also covered the topic of generating the typescript objects from your GraphQL files using codegen. So in this article we will be talking about both, generating typescript objects from Federated GraphQL!
It’s not as straightforward, because federation depending on your use case may add some circular references to your types. So the default generated type won’t cut it.
GraphQL Schema
GraphQL types
We will go with the Book
type that has an Editor
linked to it which is resolved externally via federation with
another API.
# Federation
directive @key(fields: String) on OBJECT
directive @external on FIELD_DEFINITION
directive @extends on OBJECT
type Book @key(fields: "id") {
id: ID!
title: String!
author: String!
category: BookCategory!
pages: [Page]!
editor: Editor!
}
type Editor @key(fields: "id") @extends {
id: ID! @external
}
type Page {
number: Int!
content: String!
book: Book!
}
enum BookCategory {
FICTION,
BIOGRAPHY,
POETRY,
HISTORY,
EDUCATION,
}
I am using some federation-specific directives that we seen in the graphql federation article.
We also have the Page
type with a back-link to its book and an enum for the BookCategory
for interesting use cases.
Mutations and Queries
To add onto that, we will have a couple of queries and mutation. The mutation’s input and response are pretty much
irrelevant in this example, they would take some book information and return the Book
type in the response:
type Query {
books: [Book!]!
pages(bookId: ID!): [Page!]!
}
type Mutation {
addBook(input: AddBookInput): AddBookResponse
}
This should be enough preparations to get started on the code generation.
GraphQL Codegen
Configuration
If you are using the typescript version, rather than with the yaml configuration, the configuration keys and values are the same. I leave the default example for federation, Find the full installation and configuration of the codegen tool in the previous article.
schema:
- "./src/graphql/schema.graphql"
generates:
./src/__generated__/resolvers-types.ts:
plugins:
- "typescript"
- "typescript-resolvers"
- "typescript-operations"
config:
federation: true
wrapFieldDefinitions: true
useIndexSignature: true
skipTypename: true
typesPrefix: GraphQL
defaultMapper: Partial<{T}>
mappers:
BookCategory: ../app/models#BookCategory
enumValues:
BookCategory: ../app/models#BookCategory
Let’s review the configuration we’re using:
federation
: To create theReferenceResolver
for the__resolveReference
used by federated objects.useIndexSignature
: To create the types in an index object, so you can doResolversTypes['Book']
to get the type.wrapFieldDefinitions
: Adds aFieldWrapper
type around the returned objects, for flexibility (not actually needed in the examples).skipTypename
: To remove the__typename
from the GraphQL type, since they’re not necessary and added by Apollo automatically.typesPrefix
: To prefixGraphQL
in front of all generated typesmappers
,enumValues
: So it doesn’t generate a new GraphQL enum when I have one already setup in the code (easier casting).defaultMapper
: UsingPartial
types since some fields can be resolved by different resolvers.
Let’s have a closer look at the generated objects.
Generated Objects
Circular dependency
This is how the type is being generated if page
as a reference to its book
in GraphQL or not. This kind of
circular dependency in GraphQL can be common, having the generated types makes it easier to deal with.
Usually the back link (page -> book) is handled via a dedicated resolver, so the resolving logic doesn’t spiral out:
export type GraphQLResolversTypes = ResolversObject<{
// With Circular dependency
Page: ResolverTypeWrapper<Omit<GraphQLPage, 'book'> & { book?: Maybe<GraphQLResolversTypes['Book']> }>;
// Without Circular dependency
Page: ResolverTypeWrapper<GraphQLPage>;
}>
The type uses Omit
to remove the mandatory book
field and replace it with a book?
attribute that is optional.
Enum
Since we mapped the GraphQL enum to the typescript enum we have in the project, we can see it being imported in the generated types file.
import { BookCategory } from '../app/models';
export interface GraphQLBook {
category: FieldWrapper<BookCategory>;
// ...other fields
}
And we can see it being used by the GraphQLBook
generated type for Book
.
Federation resolver
For the federation resolver, we are talking about the __resolveReference
which is used for to resolve a type by federation.
It is not meant to be used directly by a user only for the federation framework, but needs to be implemented in the service.
The type is being wrapped with ReferenceResolver
which is a generated help type for reference.
export type ReferenceResolver<TResult, TReference, TContext> = (
reference: TReference,
context: TContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;
export type GraphQLBookResolvers<ContextType = MyContext, ParentType = GraphQLResolversParentTypes['Book']> = ResolversObject<{
__resolveReference?: ReferenceResolver<
Maybe<GraphQLResolversTypes['Book']>, // TResult
{ __typename: 'Book' } & GraphQLRecursivePick<UnwrappedObject<ParentType>, {"id":true}>, // TReference
ContextType>; // TContext
pages?: Resolver<Array<Maybe<GraphQLResolversTypes['Page']>>, ParentType, ContextType>;
}>;
If we look closer at the type arguments, we have:
- The
TResult
: theGraphQLResolversTypes['Book']
which is the generated type that is returned for theBook
graphql type - The
TReference
: the reference to find the book by federation in the@key
directive, hereid
forBook
graphql type. - The
TContext
: Since we mapped the context to the one in-appMyContext
, it’s populated by default.
We can also find the pages
resolver type that we are going to use in-app with a separate resolver for the pages.
We have seen the GraphQLResolversTypes
before it holds the resolver types for all GraphQL objects.
Usage
Now let’s use our generated type with a simple mock app for each query, mutation, fields and federation resolving use case.
Query
We have the books query defined as field in the GraphQLQueryResolvers:
const Query: GraphQLQueryResolvers = {
books,
}
function books(
_parent: null,
_args: null,
context: MyContext,
): Array<GraphQLResolversTypes['Book']> {
return context.dataSources.books.getBooks();
}
Some information regarding the type:
- We can’t have
Array<GraphQLBook>
here because we don’t return thepages
with.getBooks()
since it’s handled by another resolver. - The type
Array<GraphQLResolversTypes['Book']>
is equivalent toGraphQLResolversTypes['Book'][]
.
Mutation
For the mutation, we have something similar as one without federated types:
const Mutation: GraphQLMutationResolvers = {
addBook: (
_: null,
{ input }: GraphQLMutationAddBookArgs,
context: MyContext,
): GraphQLResolversTypes['AddBookResponse'] => {
return context.datasources.book.addBook(input);
},
}
The only difference is regarding the return type, since it returns the created book.
In our case the book, doesn’t have the pages on it and so we have to use this type instead of GraphQLAddBookResponse
directly. It’s similar to the query they don’t reflect the same object:
// AddBookResponse generated type
GraphQLResolversTypes['AddBookResponse']
// Maps to this type
ResolverTypeWrapper<Partial<Omit<GraphQLAddBookResponse, 'book'> & { book?: Maybe<GraphQLResolversTypes['Book']> }>>;
As shown above, it’s better to use the type from GraphQLResolversTypes
than its actual form, which can be pretty verbose.
The downside is that mistakes or typo can easily happen which would mean losing the type check.
Entity resolution
For Book
For the federation resolver __resolveReference
, we don’t have a neat parent type that is generated.
If you look back at the resolver’s definition it looks like:
- The typename plus the id:
{ __typename: 'Book' } & GraphQLRecursivePick<UnwrappedObject<ParentType>, {"id":true}>
So instead I added a custom type for it that’s less complex.
const Book: GraphQLBookResolvers = {
__resolveReference: (
book: { id: string },
{ dataSources: { books } }: MyContext
): GraphQLResolversTypes['Book'] => books.getBookById(book.id),
pages, // pages resolver for book
}
I didn’t go into too many details for the pages
resolver, as like the __resolveReference
, the type used would be
from the GraphQLResolversTypes
.
We use the GraphQLBookResolvers
at the top of our fields resolvers for book to ensure that the created resolver stays
consistent with the GraphQL schema.
For Page
The other interesting part is resolving the book from the page. In this case we use an internal value on the page
to
match it to the book: bookId
. This is not on the GraphQL schema and so is not generated.
It is marked as optional, so it doesn’t break the GraphQLPageResolvers
type which doesn’t know about it.
const Page: GraphQLPageResolvers = {
book: (
page: GraphQLPage & { bookId?: string },
_args: null,
{ dataSources: { books } }: MyContext
): GraphQLResolversTypes['Book'] => {
return books.getBookById(page.bookId);
}
}
This is one gimmick to be able to return the book from the pages. Another one could have been to return the parent book in the pages resolver of the book directly, so it’s not fetched again!
const Book: GraphQLBookResolvers = {
pages: (
book: GraphQLBook, // The parent book
_args: null,
{ dataSources: { books } }: MyContext
): GraphQLResolversTypes['Page'][] =>
books.getPagesByBookId(book.id).map(page => ({ ...page, book })),
}
This sounds like an optimal solution, but can get out of hands with malicious circular queries where each time the pages
resolver gets called the full parent (and pages) get copied over.
A way to stop that is using rate limiter or query depth limiter on your GraphQL API as discussed in this article.
Conclusion
Using generated types with federation is not as good of an experience as you’d expect.
Particularly for resolvers outside mutations and queries that can be either reached from outside the application
(federated), or via within the application (i.e. for fields resolved through multiple resolvers in-app).
There are no special Federated
types,
but they are grouped in a ResolversParentTypes
or ResolversTypes
object which makes accessing the right one
obnoxious:
- Making a typo while accessing the type will create a situation where the type is silently converted to
any
- For example, between ✅
ResolversTypes['Book']
and ❌ResolversTypes['book']
- For example, between ✅
- The
__resolveReference
doesn’t even have a real type to use for the reference. - Duplicates types that may not work depending on the resolving object
- For example, ✅
GraphQLResolversTypes['AddBookResponse']
and ❌GraphQLAddBookResponse
- For example, ✅
- Missing flexibility for parent types with internal fields.
- For example, with the
bookId
on the page which is not in GraphQL but passed by the parent resolver. - In this case, I don’t think it can be automated, since it could be dependent on the framework.
- For example, with the
Using those generated types is not that simple, I would rather have aliases with maybe a prefix for federation use instead. However, it does help with typing, and in the end, that’s why it is being used. The maintainers are aware of the challenges and a re-work of the federation generation is on their backlog. All in all, it is a pretty cool tool for your graphql API and not with the new server GraphQL generation starter pack, it almost built the whole API shell for you!