I currently have two resolvers, Authors and Books, that return data from two separate API's. In most scenarios I only need to call one or the other, however, in this scenario, I need to attach the books to the author. It's not clear to me how I should do this.
Option 1 - Simple to do
I call the book API in the Author resolver and combine them here. This means I'd potentially make unnecessary calls to the Book API. It also means if the book API changes, I'd have to make updates to both the author and book resolvers instead of just updating the Book resolver.
Option 2 - Resolver
Is there a way to call the Book resolver from within the Author resolver?
Options 3 - Client
Is there a way to stitch the author and book together from within the client query?
I’m new to graphql and type-graphql so apologies if this is obvious.
Author
const author = {
name: 'James',
bookIds: [1, 2]
};
Book
const book = {
id: 1,
title: 'Book 1'
};
Desired outcome
const author = {
name: 'James',
books: [{
id: 1,
title: 'Book 1'
},
{
id: 2,
title: 'Book 2'
}]
}
Resolvers
@Service()
@Resolver(() => Author)
export class AuthorResolver {
constructor(private readonly authorService: authorService) { }
@Query(() => Author)
async author(
@Arg('authorId', () => ID, { nullable: false }) authorId: string,
@Ctx() { dataSources }: ResolverContext
): Promise<Author | undefined> {
const { authorService } = dataSources;
const author = await this.author.getAuthor(authorService, authorId);
return {
id: author.id,
name: author.name,
bookIds: author.bookIds
};
}
}
@Service()
@Resolver(() => Book)
export class BookResolver {
constructor(private readonly bookService: bookService) { }
@Query(() => Book)
async book(
@Arg('bookId', () => ID, { nullable: false }) bookId: string,
@Ctx() { dataSources }: ResolverContext
): Promise<Book | undefined> {
const { bookService } = dataSources;
const book = await this.book.getBook(bookService, bookId);
return {
id: book.id,
title: book.title
};
}
}
Client Side Query
query BookQuery($bookId: ID!) {
book(bookId: $bookId) {
id
title
}
}
query authorQuery($authorId: ID!) {
book(authorId: $authorId) {
id
name
books
}
}
You must implement a FieldResolver, called books
, in the Author
resolver.
If an API changes in the future, it will not affect the resolver since you use a service that talks to the API and acts as a middleware layer.
The service must be well implemented (abstraction) and the returned entity must be matched/mapped correctly to a GraphQL object type. i.e. there is no need to return {id: author.id, ...}
inside a resolver since it's done automatically by the service and class mappings.
Moreover, you inject a service instance inside the resolver, so there is no need to use @Ctx
and obtain the same service instance: simply use this.[SERVICE_NAME].[METHOD]
.
Keep you context as simple as possible (e.g. authenticated user id obtained by a JWT).
The final Author
resolver is much cleaner and more portable:
@Resolver(() => Author)
@Service()
export class AuthorResolver {
@Inject()
private readonly authorService!: AuthorService;
@Inject()
private readonly bookService!: BookService;
// 'nullable: false' is default behaviour
// No need for '@Ctx' here
// Returned value is inferred from service -> 'Promise<Author | undefined>'
@Query(() => Author)
async author(@Arg('id', () => ID) id: string) {
return this.authorService.findOne(id);
}
@FieldResolver(() => [Book])
async books(@Root() author: Author) {
return this.bookService.findManyByAuthorId(author.id);
// OR 'return this.bookService.findMany(author.bookIds);'
}
}
If you want an example project, see this.