I'm new to GraphQL but the way I understand is, if I got a User
type like:
type User {
email: String
userId: String
firstName: String
lastName: String
}
and a query such as this:
type Query {
currentUser: User
}
implemeting the resolver like this:
Query: {
currentUser: {
email: async (_: any, __: any, ctx: any, ___: any) => {
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
const { email } = await UserService.getUserByFirebaseId(userId)
return email;
},
firstName: async (_: any, __: any, ctx: any, ___: any) => {
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
const { firstName } = await UserService.getUserByFirebaseId(userId)
return firstName;
}
}
// same for other fields
},
It's clear that something's wrong, since I'm duplicating the code and also the database's being queried once per field requested. Is there a way to prevent code-duplication and/or caching the database call?
How about the case where I need to populate a MongoDB field? Thanks!
CodePudding user response:
I would rewrite your resolver like this:
// import ...;
type User {
email: String
userId: String
firstName: String
lastName: String
}
type Query {
currentUser: User
}
const resolvers = {
Query: {
currentUser: async (parent, args, ctx, info) {
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
return UserService.getUserByFirebaseId(userId);
}
}
};
This should work, but... With more information the code could be better as well (see my comment).
More about resolvers you can read here: https://www.apollographql.com/docs/apollo-server/data/resolvers/
CodePudding user response:
A few things:
1a. Parent Resolvers
As a general rule, any given resolver should return enough information to either resolve the value of the child or enough information for the child to resolve it on their own. Take the answer by Ruslan Zhomir. This does the database lookup once and returns those values for the children. The upside is that you don't have to replicate any code. The downside is that the database has to fetch all of the fields and return those. There's a balance act there with trade-offs. Most of the time, you're better off using one resolver per object. If you start having to massage data or pull fields from other locations, that's when I generally start adding field-level resolvers like you have.
1b. Field Level Resolvers
The pattern you're showing of ONLY field-level resolvers (no parent object resolvers) can be awkward. Take your example. What "is expected" to happen if the user isn't logged in?
I would expect a result of:
{
currentUser: null
}
However, if you build ONLY field-level resolvers (no parent resolver that actually looks in the database), your response will look like this:
{
currentUser: {
email: null,
userId: null,
firstName: null,
lastName: null
}
}
If on the other hand, you actually look in the database far enough to verify that the user exists, why not return that object? It's another reason why I recommend a single parent resolver. Again, once you start dealing with OTHER datasources or expensive actions for other properties, that's where you want to start adding child resolvers:
const resolvers = {
Query: {
currentUser: async (parent, args, ctx, info) {
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
return UserService.getUserByFirebaseId(userId);
}
},
User: {
avatarUrl(parent) {
const hash = md5(parent.email)
return `https://www.gravatar.com/avatar/${hash}`;
},
friends(parent, args, ctx) {
return UsersService.findFriends(parent.id);
}
}
}
2a. DataLoaders
If you really like the child property resolvers pattern (there's a director at PayPal who EATS IT UP, the DataLoader pattern (and library) uses memoization with cache keys to do a lookup to the database once and cache that result. Each resolver asks the service to fetch the user ("here's the firebaseId"), and that service caches the response. The resolver code you have would be the same, but the functionality on the backend that does the database lookup would only happen once, while the others returned from cache. The pattern you're showing here is one that I've seen people do, and while it's often a premature optimization, it may be what you want. If so, DataLoaders are an answer. If you don't want to go the route of duplicated code or "magic resolver objects", you're probably better off using just a single resolver.
Also, make sure you're not falling victim to the "object of nulls" problem described above. If the parent doesn't exist, the parent should be null, not just all of the children.
2b. DataLoaders and Context
Be careful with DataLoaders. That cache might live too long or return values for people who didn't have access. It is generally, therefore, recommended that the dataLoaders get created for every request. If you look at DataSources (Apollo), it follows this same pattern. The class is instantiated on each request and the object is added to the Context (ctx
in your example). There are other dataLoaders that you would create outside of the scope of the request, but you have to solve Least-Used and Expiration and all of that if you go that route. That's also an optimization you need much further down the road.