I am using Apollo Server 4 and Prisma 5 to build a simple ecomm project. At the moment I am trying to set up my resolvers to be able to bidirectionally query for Users + their comments...and also query Comments as the top level and return it's associated user. In the database Comment
table has an associated user_id
FK relationship to the User
table. User
table does NOT have a comments id, thus a one -> many and many -> one relationship.
When I try to query a user, comments is null
unless I write a separate resolver for it and then use Prisma orm to include
the relationship. I don't want to follow this as it seems like an antipattern when using gql. What I want to accomplish is to have a userByEmail()
resolver in my top level Query{}
from my SDL...and then have a Comment: {}
2nd level object in my resolvers to then have the corresponding User
be the parent
argument coming in the resolver...this is more gql dependant than db dependant.
Relevant Prisma Models:
model User {
id String @id @default(uuid())
username String @unique
email String @unique
password String
first_name String
last_name String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted Boolean @default(false)
comments Comment[]
ratings Rating[]
orders Order[]
UserAddress UserAddress[]
}
model Comment {
id String @id @default(uuid())
user User @relation(fields: [user_id], references: [id])
user_id String // Change type to String
product Product @relation(fields: [product_id], references: [id])
product_id String
content String
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted Boolean @default(false)
}
Relevant schema.graphql types:
type Query {
"Get a user by their email"
userByEmail(email: String): User!
}
"This type represents a user in the system"
type User {
"Unique identifier for the user"
id: ID!
"Username for login"
username: String!
"User's email address"
email: String!
"User's first name"
first_name: String!
"User's last name (optional)"
last_name: String!
"List of comments authored by the user (optional)"
comments: [Comment]
"List of ratings created by the user (optional)"
ratings: [Rating]
"List of orders placed by the user (optional)"
orders: [Order]
"List of user's addresses"
addresses: [Address]
}
type Comment {
"Unique identifier for the comment"
id: ID!
"Content of the comment"
content: String!
"User who authored the comment"
user: User
"Product this comment is associated with"
product: Product
}
resolvers.ts
import { getUserByEmail, getCommentsByUserId } from '../../dataAccessLayer/orm/user';
import { User } from '../../generated/types';
// Use intersection type to combine Resolvers and Query
export const resolvers = {
Query: {
userByEmail: async (_parent: unknown, { email }: { email: string }) => {
console.log('parent', email);
const user = await getUserByEmail(email); // Already includes comments
return user;
},
},
Comment: {
userComments: (parent: User) => {
console.log('parent', parent);
const userId = parent.id;
return getCommentsByUserId(userId);
},
},
};
ORM DB Queries
import prisma from '../prismaClient';
export async function getUserByEmail(email: string) {
return await prisma.user.findUnique({
where: {
email,
},
});
}
export async function getCommentsByUserId(id: string) {
return await prisma.comment.findMany({
where: {
user_id: id,
},
include: {
user: true, // Include user relation
},
});
}
When I run my project I get this error:
Error: Comment.userComments defined in resolvers, but not in schema
[0] at addResolversToSchema
How can I properly query for sub fields using the Apollo Recommended approach of using (parent, args, context, info)
arguments where parent
should be the related user initially queried for when trying to get comments? This way I don't stitch together db queries at the orm level.
Thanks!
Take a look at the examples in apollo server doc, on how to implement resolvers
In the schema you have already included comments
property to the User
type
type User {
id: ID!
username: String!
"List of comments authored by the user (optional)"
comments: [Comment]
# ... other fields
}
you also defined userByEmail
to fetch user in top-level type Query
type Query {
userByEmail(email: String): User!
}
Next, you need to implement resolvers for this schema. For the type Query
you need to implement resolver for the method userByEmail
. You've done this part
import { getUserByEmail } from '../../dataAccessLayer/orm/user';
import { User } from '../../generated/types';
// Use intersection type to combine Resolvers and Query
export const resolvers = {
Query: {
userByEmail: async (_, { email }: { email: string }) => {
console.log('parent', email);
const user = await getUserByEmail(email); // Already includes comments
return user;
},
},
};
The next thing you need to do, is to define resolver for comments
property of User
, which give you exactly what you want: user comments.
import { getUserByEmail, getCommentsByUserId } from '../../dataAccessLayer/orm/user';
import { User } from '../../generated/types';
// Use intersection type to combine Resolvers and Query
export const resolvers = {
Query: {
userByEmail: async (_, { email }: { email: string }) => {
const user = await getUserByEmail(email); // Already includes comments
return user;
},
},
// User. not Comment.
User: {
// The parent entity for comments field is user
comments: async (parent: User, _: any) {
if (parent.comments !== undefined) {
// you mentioned that user fetched with userByEmail already
// includes comments, so you might not even need to refetch it
return parent.comments
}
const comments = await getCommentsByUserId(user.id)
return comments;
},
}
};
And this is an example of the query compatible with our schema
query userWithComments($email: String!) {
userByEmail(email: $email) {
id
username
# some other fields
comments {
id
content
}
}
}