GraphQL combining two Resolvers - apollo-client

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.

Related

Apollo Rover CLI returns non-wrapped schema when using NestJS driver

I have two subgraphs hosting their own respective schemas. Each of these schemas have input types that are named the same. For example, both schemas have an entity called Product and therefore both have inputs relevant to this entity called ProductCreateInput. Due to the way Apollo Federation works, because Input types are merged using the intersection strategy, I have to rename the inputs to different names to avoid composition errors when composing a supergraph.
So I rename the ProductCreateInput to something like Product_ProductCreateInput and Review_ProductCreateInput. I do this for every input type by using a regex and wrapSchema from #graphql-tools/wrap to rename the input types to precede with their subgraph name.
The driver code :
#Injectable()
export class GqlConfigService implements GqlOptionsFactory {
constructor(private readonly config: ConfigService, private readonly prisma: PrismaService) {}
createGqlOptions(): ApolloFederationDriverConfig {
const plugins: PluginDefinition[] = [];
if (this.config.graphql.sandbox) plugins.push(ApolloServerPluginLandingPageLocalDefault);
if (!this.config.graphql.trace) plugins.push(ApolloServerPluginInlineTraceDisabled());
return {
typeDefs: print(ALL_TYPE_DEFS),
resolvers: { Upload: GraphQLUpload },
transformSchema: async (schema: GraphQLSchema) => {
return renamedInputTypesSchema(schema);
},
debug: !this.config.production,
playground: false,
plugins,
introspection: this.config.graphql.introspection,
cors: this.config.cors,
csrfPrevention: this.config.graphql.csrfPrevention,
cache: 'bounded',
installSubscriptionHandlers: this.config.graphql.subscriptions,
subscriptions: this.config.graphql.subscriptions
? {
'graphql-ws': {
onConnect: (context: Context<any>) => {
const { connectionParams, extra } = context;
extra.token = connectionParams.token;
},
},
}
: undefined,
context: async (ctx): Promise<IContext> => {
// Subscriptions pass through JWT token for authentication
if (ctx.extra) return { req: ctx.extra, prisma: this.prisma };
// Queries, Mutations
else return { ...ctx, prisma: this.prisma };
},
};
}
}
The schemaWrapper code :
import { RenameTypes, wrapSchema } from '#graphql-tools/wrap';
import { GraphQLSchema } from 'graphql';
export const modelNames = ['User', 'Product'];
//This subgraph's name is set to "Product".
//Input type args is modified to be preceded by "Product_" because inputs are merged using the intersection strategy in the current version of Apollo Federation and directives are not supported with input types.
export const renamedInputTypesSchema = async (schema: GraphQLSchema) => {
const typeMap = schema.getTypeMap();
const models: string = modelNames.join('|');
const inputTypes = Object.keys(typeMap).filter(type => {
const inputTypesRegex = new RegExp(
`(${models})(WhereInput|OrderByWithRelationInput|WhereUniqueInput|OrderByWithAggregationInput|ScalarWhereWithAggregatesInput|CreateInput|UncheckedCreateInput|UpdateInput|UncheckedUpdateInput|CreateManyInput|UpdateManyMutationInput|UncheckedUpdateManyInput|CountOrderByAggregateInput|AvgOrderByAggregateInput|MaxOrderByAggregateInput|MinOrderByAggregateInput|SumOrderByAggregateInput|Create.*?Input|Update.*?Input)`
);
return type.match(inputTypesRegex)?.input;
});
return wrapSchema({
schema: schema,
transforms: [new RenameTypes(name => (inputTypes.includes(name) ? `Product_${name}` : name))],
});
};
This works. When I go into Apollo Sandbox and look at the schema, all the inputs are successfully preceded with Product_ like I expect :
However, when I use rover subgraph introspect in order to pipe the output to publish to my managed federation, I get the unwrapped schema (the relevant rover subgraph introspect output):
input ProductCountOrderByAggregateInput {
id: SortOrder
sku: SortOrder
description: SortOrder
}
input ProductAvgOrderByAggregateInput {
id: SortOrder
}
input ProductMaxOrderByAggregateInput {
id: SortOrder
sku: SortOrder
description: SortOrder
}
input ProductMinOrderByAggregateInput {
id: SortOrder
sku: SortOrder
description: SortOrder
}
What is going on here? Apollo sandbox shows the correct wrapped schema, yet rover introspect doesn't.

Understanding React-Relay Connections in the Context of Type-GraphQL

The below code excerpt comes from the React-Relay docs on Rendering Connections. I am wondering if someone could provide me with an example of what the underlying schema definition (using `type-graphql for annotations/decorations) would look like.
const {graphql} = require('RelayModern');
const userFragment = graphql`
fragment UserFragment on User {
name
friends(after: $cursor, first: $count)
#connection(key: "UserFragment_friends") {
edges {
node {
...FriendComponent
}
}
}
}
`;
Would it look something like the following? With attention paid to the UserType type definition, and especial attention to the friends field. I am also hoping if anyone could turn my attention to a more elaborated upon example/boilerplate to help me understand what is compliant with the Relay specification. Some examples I am after:
How to type the return type of a Query if I intend one of the Query's resolved fields to be a Connection type? And what would this look when written as a fragment.
How to type the same scenario as above, except now the return type is an iterable of the original return type?
#ObjectType({ implements: [Node] })
export class UserType extends Node {
#Field()
name: string
#Field()
friends: UserConnection
}
const User = createUnionType({
name: 'User',
types: () => [UserType] as const,
resolveType: value => {
// if ('code' in value) {
// return Error
// }
return UserType
}
})
#ObjectType()
export class UserEdge extends EdgeType('report', User) {}
#ObjectType()
export class UserConnection extends ConnectionType<UserEdge>(
'user',
UserEdge
) {
}

What is a correct return type of a GraphQL resolve function?

I faced with an issue that can't resolve on my own. Let's go through it step by step to point out the problem.
I have a mutation bookAppointment which returns an Appointment object
GraphQL schema says that this object should return 4 properties: id, date, specialist, client.
To follow the GraphQL-style the specialist and client properties should be a field level resolvers
To fetch this objects I need pass specialistId to the specialist field level resolver, as well as clientId to the client field level resolver.
At this point a problem arises.
The field level resolvers of client, specialist expects that root mutation returns fields like clientId and specialistId. But GraphQL syntax and types that were generated by that syntax doesn't include this props (make sense).
How to "extend" the return type of the resolver and its interface BookAppointmentPayload to make me and TypeScript happy?
This is my GraphQL schema
type Client {
id: ID!
name: String!
}
type Specialist {
id: ID!
name: String!
}
type Appointment {
id: ID!
date: Date!
client: Client!
specialist: Specialist!
}
input BookAppointmentInput {
date: Date!
userId: ID!
specialistId: ID!
}
type BookAppointmentPayload {
appointment: Appointment!
}
type Mutation {
bookAppointment(input: BookAppointmentInput!): BookAppointmentPayload!
}
This is TypeScript representation of GraphQL schema
interface Client {
id: string
name: string
}
interface Specialist {
id: string
name: string
}
interface Appointment {
id: string
date: Date
client: Client
specialist: Specialist
}
interface BookAppointmentPayload {
appointment: Appointment
}
Here I define my resolvers objects
const resolvers = {
...
Mutation: {
bookAppointment: (parent, args, context, info): BookAppointmentPayload => {
return {
appointment: {
id: '1',
date: new Date(),
clientId: '1', // This prop doesn't exist in the TypeScript interface of Appointment, but is required for the field-level resolver of a `client` prop
specialistId: '1' // This prop doesn't exist int he TypeScript interface of Appointment, but is required for the field-level resolver of a `specialist` prop
}
}
}
},
Appointment: {
client: (parent, args, context, info) => {
// I need a clientId (e.g. args.clientId) to fetch the client object from the database
return {
id: '1',
name: 'Jhon'
}
},
specialist: (parent, args, context, info) => {
// I need a specialistId (e.g. args.specialistId) to fetch the specialist object from the database
return {
id: '1',
name: 'Jane'
}
}
}
}
Solution that come to my mind:
Create an interface which represent "actual" return type of the resolver
...
interface Apppointment {
id: string
date: Date
clientId: string // instead of `client: Client`
specialistId: string // instead of `specialist: Specialist`
}
interface BookAppointmentPayload {
appointment: Appointment
}
...
But this doesn't reflect the GraphQL type. Also tools like graphql-generator generates the type with actual objects that should be included in the response, not the fields that are going to be used by field-level resolvers. (Am I wrong?)
I would like to know how you're solving such issue?
I've been investigating this problem quite a lot and have come to the following conclusion.
Create an interface which represent "actual" return type of the resolver
Most of the time the return type of the resolver function (in JavaScript) doesn't match the type that was declared in the GraphQL SDL
For instance,
# GraphQL SDL
type Appointment {
id: String!
client: User!
specialist: Specialist!
}
type BookAppointmentInput { ... }
type BookAppointmentPayload {
appointment: Appointment!
}
type Mutation {
bookAppointment: (input: BookAppointmentInput!): BookAppointmentPayload!
}
interface AppointmentDatabaseEntity {
id: string
clientId: string // In GraphQL-world this prop is an object, but not in JS. Use this prop in field-level resolver to fetch entire object
specialistId: string // In GraphQL-world this prop is an object, but not in JS. Use this prop in field-level resolver to fetch entire object
}
interface BookAppointmentPayload {
appointment: AppointmentDatabaseEntity // The return type SHOULDN'T be equal to the GraphQL type (Appointment)
}
const resolvers = {
Mutatiuon: {
bookAppointment: (parent, args, context, info) => {
const appointment = { id: '1', specialistId: '1', clientId: '1' }
return {
id: appointment.id,
specialistId: appointment.specialistId, // Pass this prop to the child resolvers to fetch entire object
clientId: appointment.clientId // Pass this prop to the child resolvers to fetch entire object
}
}
},
Appointment: {
client: (parent: AppointmentDatabaseEntity, args, context, info) => {
const client = database.getClient(parent.clientId) // Fetching entire object by the property from the parent object
return {
id: client.id,
name: client.name,
email: client.email
}
},
specialist: (parent: AppointmentDatabaseEntity, args, context, info) => {
const specialist = database.getSpecialist(parent.specialistId) // Fetching entire object by the property from the parent object
return {
id: specialist.id,
name: specialist.name,
email: specialist.email
}
}
}
}
But this doesn't reflect the GraphQL type
As far as I understand it is okay
Also tools like graphql-generator generates the type with actual objects that should be included in the response, not the fields that are going to be used by field-level resolvers. (Am I wrong?)
Yes. I was wrong. The graphql-generator has a configuration file that can be used to replace default generated types with the types that you expect your resolvers to return. This option is called mappers.
plugins
config:
mappers:
User: ./my-models#UserDbObject # User is GraphQL object, which will be replaced with UserDbObject
Book: ./my-modelsBook # Same rule goes here
I don't want to go into details of how to configure it and use, but you can check the links that helped me to understand this
Documentation (check the mappers chapter)
Great explanation by
Jamie Barton (YouTube)
If you disagree with my conclusions or you have a better understanding of how to handle it feel free to leave a comment

NestJS | GraphQL - How to resolve a list of nested objects from multiple datasets

I'm setting up a NestJS service using GraphQL to be the middleman between the UI and multiple other services. For the sake of this example, I need to display a list of books with their relevant information from a single publisher.
The UI will hit the nest service with an id of a publisher. I need to then call a service and get a list of book Ids. When I have the list of Ids I need to hit two separate services, each return a list of objects for each book id. I then need to build a list of book objects from both datasets.
An example of the book models is:
export class BookModel {
#Field(type => BookInformationModel)
bookInfo: BookInformationModel;
#Field(type => BookSalesModel)
bookSales: BookSalesModel;
}
An example of the flow is:
UI hits Nest service with a publisher Id "pub1"
Nest service goes to the publisher-service which returns a list of books linked to the publisher ['book1', 'book2']
Nest service then hits the book-info-service that returns [{id: 'book1' title: ...}, {id: 'book2' title: ...}]
Nest service then hits the book-sales-service that returns [{price: 123 currency: ...}, {price: 456 currency: ...}]
Map both data sets to a list of BookModel
[{
bookInfo: {id: 'book1' title: ...}
bookSales: {price: 123 currency: ...}
}, {
bookInfo: {id: 'book2' title: ...}
bookSales: {price: 456 currency: ...}
}]
This is a simplified version of the resolver where I'm having trouble:
#Query(returns => [BookModel])
async getBooksByPublisherId(
#Args('id', { type: () => String }) id: string
) {
this.publisherService.getBookIds(id)
.then(response => {
return response.data;
})
}
#ResolveField('bookInfo', returns => BookInformationModel)
async getBookInfo() {
return this.bookInfoService.getBookInfo(bookIds)
}
#ResolveField('bookSales', returns => BookSalesModel)
async getBookSalesInfo() {
return this.bookSalesService.getSalesInfo(bookIds)
}
There are two issues I'm having:
How do I share the list of book Ids with the field resolvers?
I'm not sure how I write the field resolvers for bookInfo, bookSales as the services return a list of objects.
Figured it out. I needed to have the bookId on the BookModel. That way the field resolvers can access it via #Parent.
export class BookModel {
#Field()
bookId: string;
#Field(type => BookInformationModel)
bookInfo: BookInformationModel;
}
#Query(returns => [BookModel])
async getBooksByPublisherId(
#Args('id', { type: () => String }) id: string
) {
this.publisherService.getBookIds(id)
.then(response => {
return response.data.map(bookID => ({ bookId }));
})
}
#ResolveField('bookInfo', returns => BookInformationModel)
async getBookInfo(
#Parent() bookModel: BookModel
) {
return this.bookInfoService.getBookInfo(bookModel.bookId)
}

Graphql Object that implements an Interface is not not inheriting resolver from Interface

I am using graphql-tools to build a GraphQL Schema, esentially I have this structure
const typeDefs = `
type Query {
author(name: String): Author
}
interface Person {
id: Int
name: String
citizenship: String
type Author implements Person {
id: Int
name: String
citizenship: String
`
and I have the following resolvers
const resolvers = {
Query: {
author(_,args) {
return Author.find({where: args});
}
}
Person: {
citizenship() {
return "Example Citizenship";
}
}
}
I make the schema executable
const schema = makeExecutableSchema({
typeDefs,
resolvers,
inheritResolversFromInterfaces: true
});
and the optional argument inheritResolversFromInterfaces: true its supposed to allow me to inherit the citizenship resolver from Person to Author, based on the apollo graphql-tools documentation (link). That way when an Author is queried, the "Example Citizenship" string will appear.
However it does not, the query returns with
"author": {
"id": 1,
"name": "Peter",
"citizenship": null
}
Resolved, the feature inheritResolversFromInterfaces: true was added in version v2.24.0, need to have that version in order to use that feature.

Resources