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!
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/
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.
Is there a way to prevent code-duplication and/or caching the database call?
So first, this
const provider = getAuthenticationProvider()
should actually be injected into the graphql server request's context, such that you would use it in resolver for example as:
ctx.authProvider
The rest follows Dan Crews' answer. Parent resolvers, preferably with dataloaders. In that case you actually won't need authProvider and will use only dataloaders, depending on the entity type & by passing extra variables from context (like user Id)
Related
I have an interface created for my model, where I only want to return specific data from the record
// code.interface.ts
import { Document } from 'mongoose';
export interface CodeI extends Document {
readonly _id: string;
readonly logs: any;
}
But when I get the result from mongo, it completely ignores what is in my interface. (I am using NestJs as framework)
//constructor
constructor(#InjectModel(Coupon.name) private couponModel: Model<CouponDocument>) {}
// function
async findOne(codeId: string): Promise<CodeI> {
const coupon = await this.couponModel.findOne({ _id: codeId }).exec();
if (!coupon) {
throw new NotFoundException([`#${codeId} not found`]);
}
return coupon;
}
TypeScript interfaces don't work this way. They can't limit the fields of an object because they don't exist at runtime, so, we can't use them to guide any runtime behavior. TypeScript interfaces are useful for compile-time type check only.
However, in your case, there are two ways you can achieve the expected behavior.
The first one is to select only the required fields which you need to return (recommended).
In your findOne, you can do something like this
async findOne(codeId: string): Promise<CodeI> {
const coupon = await this.couponModel.findOne({ _id: codeId }, '_id logs').exec();
if (!coupon) {
throw new NotFoundException([`#${codeId} not found`]);
}
return coupon;
}
Here, as you can see, I have passed an additional string type parameter to findOne function which is projection and it will select only the specified fields from the object. This will not only solve your problem but also save query time and have increase query performance. Read more about findOne here.
The other way is to create a DTO where you can define the fields you want to return from the function.
Something like this:
// CouponDto.ts
class CouponDto {
public readonly _id: string;
public readonly logs: any;
constructor(data: CodeI) {
this._id = data._id;
this.logs = data.logs;
}
}
Then, in your service file, you can do something like
return new CouponDto(coupon);
(make sure to change the return type of the function to CouponDto as well)
You can use any of these two ways. While I would recommend going with the first one, it's up to you and how you wanna structure your project.
External Links:
Mongoose FindOne Docs
I am using prisma ORM with nestjs and it is awesome. Can you please help me understand how can I separate my database layer from my service methods since results produced by prisma client queries are of types generated by prisma client itself ( so i wont be having those types when i shift to lets say typeorm ). how can i prevent such coupling of my service methods returning results of types generated by prisma client and not my custom entities. Hope it makes sense.
The generated #prisma/client library is responsible for generating both the types as well as the custom entity classes. As a result, if you replace Prisma you end up losing both.
Here are two possible workarounds that can decouple the types of your service methods from the Prisma ORM.
Workaround 1: Generate types indepedently of Prisma
With this approach you can get rid of Prisma altogether in the future by manually defining the types for your functions. You can use the types generated by Prisma as reference (or just copy paste them directly). Let me show you an example.
Imagine this is your Prisma Schema.
model Post {
id Int #default(autoincrement()) #id
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
title String #db.VarChar(255)
author User #relation(fields: [authorId], references: [id])
authorId Int
}
model User {
id Int #default(autoincrement()) #id
name String?
posts Post[]
}
You could define a getUserWithPosts function as follows:
// Copied over from '#prisma/client'. Modify as necessary.
type User = {
id: number
name: string | null
}
// Copied over from '#prisma/client'. Modify as necessary.
type Post = {
id: number
createdAt: Date
updatedAt: Date
title: string
authorId: number
}
type UserWithPosts = User & {posts: Post[]}
const prisma = new PrismaClient()
async function getUserWithPosts(userId: number) : Promise<UserWithPosts> {
let user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
posts: true
}
})
return user;
}
This way, you should be able to get rid of Prisma altogether and replace it with an ORM of your choice. One notable drawback is this approach increases the maintenance burden upon changes to the Prisma schema as you need to manually maintain the types.
Workaround 2: Generate types using Prisma
You could keep Prisma in your codebase simply to generate the #prisma/client and use it for your types. This is possible with the Prisma.validator type that is exposed by the #prisma/client. Code snippet to demonstrate this for the exact same function:
// 1: Define the validator
const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
include: { posts: true },
})
// 2: This type will include a user and all their posts
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
// function is same as before
async function getUserWithPosts(userId: number): Promise<UserWithPosts> {
let user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
posts: true
}
})
return user;
}
Additionally, you can always keep the Prisma types updated to your current database state using the Introspect feature. This will work even for changes you have made with other ORMS/Query Builders/SQL.
If you want more details, a lot of what I've mentioned here is touched opon in the Operating against partial structures of your model types concept guide in the Prisma Docs.
Finally, if this dosen't solve your problem, I would request that you open a new issue with the problem and your use case. This really helps us to track and prioritize problems that people are facing.
I have a restaurant query that returns info on my restaurant. The general information that most of my consumers want comes back from restaurant-general-info.com. There are additional fields though that my consumer might want to know, restaurant-isopen.com, which provides whether or not the restaurant is currently open and the hours that it is open.
I have written two property specific resolvers to handle isOpen and hours as show below:
type Query {
restaurant(name: String): Restaurant
}
type Restaurant {
name: String!,
address: String!,
ownerName: String!,
isOpen: Boolean!,
hours: String!
}
Query: {
restaurant: async(parent, {name}) => {
const response = await axios.get(`https://restaurant-general-info.com/${name}`);
return {
address: response.address
ownerName: response.owner
};
}
}
Restaurant: {
isOpen: async(parent) => {
const response = await axios.get(`https://restaurant-isopen.com/${name}`);
return response.openNow;
},
hours: async(parent) => {
const response = await axios.get(`https://restaurant-isopen.com/${name}`);
return response.hoursOfOperation;
}
}
The problem is that isOpen and hours share the same data source. So I don't want to make the same call twice. I know I could make a property like "open-info" that contains two properties, isOpen and hours, but I don't want the consumers of my graph to need to know / think about how that info is separated differently.
Is there anyway I can have a resolver that could handle multiple fields?
ex.
isOpen && hours: async(parent) => {
const response = await axios.get(`https://restaurant-isopen.com/${name}`);
return {
isOpen: response.openNow,
hours: response.hoursOfOperation
}
},
or is there some smarter way of handling this?
Note: The APIs are not real
This is a classic situation where using DataLoader is going to help you out a lot.
Here is the JS library, along with some explanations: https://github.com/graphql/dataloader
In short, implementing the DataLoader pattern allows you to batch requests, helping you to avoid the classic N+1 problem in GraphQL and mitigate overfetching, whether that involves your database, querying other services/APIs (as you are here), etc.
Setting up DataLoader and batching the keys you're requesting (in this case, openNow and hoursOfOperation) will ensure that the axios GET request will only fire once.
Here is a great Medium article to help you visualize how this works behind the scenes: https://medium.com/#__xuorig__/the-graphql-dataloader-pattern-visualized-3064a00f319f
I'm using typescript and graphql and every time I want to send request to my graphql I need to write a query.
My question is there is anyway to use type/interface to create the query?
for example take a look at this interface:
interface Document {
id: string;
name: string;
author: {
name: string;
}
}
The graphql query for this is
query document {
id
name
author {
name
}
}
and I use axois to get the data:
const data = await axios.get("/graphql", { query });
Is there easy way to get the data using strongly typed? something like:
const data = await axois.get('/graphql', { fields: ['id', 'name', 'author.name'] })
And typescript will throw an error if some string from fields doesn't include in the interface.
axios.get can take a generic which will provide the types on the return type. An example should make it clear. Using your code above
// vvvvvvvv==> your expected return data
const result = await axios.get<Document>("/graphql", { query });
// data will be strongly typed (but NOT checked)
result.data.id; // => string
result.data.author.name; // => string
For more info check axios' index.d.ts file.
I have a GraphQl resolvers that resolves nested data.
for eg. this is my type definitions
type Users {
_id: String
company: Company
}
For the post I have my resolver which resolves post._id as
Users: {
company: (instance, arguments, context, info) => {
return instance.company && Company.find({_id: instance.company});
}
}
The above example works perfectly fine when I query for
Query {
Users {
_id
name
username
company {
_id
PAN
address
}
}
}
But the problem is sometime I don't have to use the company resolver inside Users, because it is coming along with the user so I just need to pass what's in the user object (no need of database call here)
I can achieve this just by checking if instance.company is and _id or Object, if _id get from database otherwise resolve whatever coming in.
But the problem is I have these type of resolvers in many places so I don't think it's a good idea to have this check in all places wherever I have resolver.
Is there a better way where I can define a configuration just to skip this resolver check.
Any feedback or suggestions would be highly appreciated.
Thanks