Graph Relations
So far you built a simple GraphQL API for creating and retrieving links.
Another popular feature of Hackernews is to comment on links for attacking and criticizing the original poster. We definetly don't want to miss out on that comment feature!
In this chapter, you will introduce a new object type Comment
(and the corresponding Prisma model) in your code and also write a Link.comments
field for retrieving all the comments that belong to a Link
.
Adding the Comment
Model
The first thing you need is a way to represent the comment data in the database. To do so, you can add a Comment
type to your Prisma data model.
You'll also want to add a relation between the Comment
and the existing Link
type to express that Comments
s are posted on Links
s.
Open prisma/schema.prisma
and add the following code, making sure to also update your existing Link
model accordingly:
model Link {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
description String
url String
comments Comment[]
}
model Comment {
id Int @id @default(autoincrement())
body String
link Link? @relation(fields: [linkId], references: [id])
linkId Int?
}
Notice how you're adding a new relation field called comments
to the Link
model that points to a list of Comment
instances. The Comment
model then has a link
field that point's to the associated Link
instance.
To hint this relation to Prisma, the link
field on the Comment
model must be annotated with the @relation
attribute. This is a requirement for every relation field in your Prisma schema, and by doing so you define the foreign keys of the affected table.
In this specific case, we have a one-to-many relationship.
A Link
has many comments. A Comment
belongs to one Link
.
If this is quite new to you, don't worry! We're going to be adding a few of these relational fields, and you'll get the hang of it as you go! For a deeper dive on relations with Prisma, check out these docs.
Updating Prisma Client
This is a great time to refresh your memory on the workflow we described for your project at the end of chapter 4!
After every change you make to the data model, you need to migrate your database and then re-generate Prisma Client.
In the root directory of the project, run the following command:
npx prisma migrate dev --name "add-user-model"
This command has now generated your second migration inside of prisma/migrations
, and you can start to see how this becomes a historical record of how your database evolves over
time. This script also runs the Prisma migration, so your new models and types are ready-to-use.
That might feel like a lot of steps, but the workflow will become automatic by the end of this tutorial!
Your database is ready and Prisma Client is now updated to expose all the CRUD queries for the newly added Comment
model – woohoo! 🎉
Extending the GraphQL Schema
Remember back when we were setting up your GraphQL server and discussed the process of schema-driven development? It all starts with extending your schema definition with the new types and fields that you want to add to the API.
In this case, you first want a way of creating a comment using a mutation field (Mutation.postCommentOnLink
) and also fetching a comment via its id using a query field (Query.comment
).
type Link {
id: ID!
description: String!
url: String!
}
type Comment {
id: ID!
body: String!
}
type Query {
info: String!
feed: [Link!]!
comment(id: ID!): Comment
}
type Mutation {
postLink(url: String!, description: String!): Link!
postCommentOnLink(linkId: ID!, body: String!): Comment!
}
Implementing the GraphQL Field Resolvers
Mutation.postCommentOnLink
Resolver
Let's start with the Mutation.postCommentOnLink
resolver, so you can post comments on a link.
Within the Mutation
object type resolver maps create a new postCommentOnLink
field resolver function.
const resolvers = {
// ... other resolver maps ...
Mutation: {
// ... other field resolver functions
async postCommentOnLink(
parent: unknown,
args: { linkId: string; body: string },
context: GraphQLContext
) {}
}
}
This is the skeleton of your new field resolver. As you might notice, it looks a lot familiar to the Mutation.postLink
field resolver function. Since it will write to the database, the implementation will also look pretty familiar!
Add the following business logic within the resolver, that uses the newly generated Prisma functions:
const resolvers = {
// ... other resolver maps ...
Mutation: {
// ... other field resolver functions
async postCommentOnLink(
parent: unknown,
args: { linkId: string; body: string },
context: GraphQLContext
) {
const newComment = await context.prisma.comment.create({
data: {
linkId: parseInt(args.linkId),
body: args.body
}
})
return newComment
}
}
}
You pass two arguments for creating the new comment to the comment.create
call.
linkId
- this is the id of the link to the comment belongs to. GraphQL has a special scalar for describing ids that you used before, calledID
. In general, it is the best practice to use it. The downside is that a GraphQLID
is always a string. Thus, within the resolver function, you must parse the actual integer value of theID
, as the SQLite database uses integer values for IDs.body
- this is the comment body, there is nothing special about it.
You probably can't wait to try sending a mutation for creating your first comment.
So let's do it!
You already created some links before, so there should be a link with the id 1
within your database.
Use that id for creating a new comment that references the link with the id 1
.
Execute the following mutation operation:
mutation postCommentOnLink {
postCommentOnLink(linkId: "1", body: "This is my first comment!") {
id
body
}
}
The response will look identical to this:
{
"data": {
"postCommentOnLink": {
"id": "1",
"body": "This is my first comment!"
}
}
}
Up next let's also try to create a comment with a linkId
with no corresponding link in the database.
Executing the following mutation operation:
mutation postCommentOnLink {
postCommentOnLink(linkId: "99999999999", body: "This is my second comment!") {
id
body
}
}
Assuming that you did not already create 99999999999 link elements before, the response will now look a bit different:
{
"errors": [
{
"message": "Unexpected error.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["postCommentOnLink"],
"extensions": {
"originalError": {
"message": "\nInvalid `context.prisma.comment.create()` invocation in\nhackernews/src/schema.ts:69:52\n\n 66 args: { linkId: string; body: string },\n 67 context: GraphQLContext,\n 68 ) => {\n→ 69 const comment = await context.prisma.comment.create(\n Foreign key constraint failed on the field: `foreign key`",
"stack": "Error: \nInvalid `context.prisma.comment.create()` invocation in\nhackernews/src/schema.ts:69:52\n\n 66 args: { linkId: string; body: string },\n 67 context: GraphQLContext,\n 68 ) => {\n→ 69 const comment = await context.prisma.comment.create(\n Foreign key constraint failed on the field: `foreign key`\n at cb (hackernews/node_modules/@prisma/client/runtime/index.js:38703:17)\n at PrismaClient._request (hackernews/node_modules/@prisma/client/runtime/index.js:40859:18)"
}
}
}
],
"data": null
}
Let's try to analyze this error:
Invalid `context.prisma.comment.create()` invocation
Foreign key constraint failed on the field: `foreign key`
As you might notice, Prisma is not that great at giving us a clear description of what is happening.
But, since we know that the linkId
field on the Comment
model is a foreign key reference to the id
column on the Link
model, we can conclude that the origin of this error is a missing Link
entity with the linkId
99999999999
.
Let's ignore this ugly error message for now. We will improve upon that within the next chapter.
Instead, you will first create a resolver for retrieving a comment via its id.
Query.comment
resolver
Within the Query
object type resolver map create a new comment
field resolver function.
const resolvers = {
// ... other resolver maps ...
Query: {
// ... other field resolver functions
async comment(
parent: unknown,
args: { id: string },
context: GraphQLContext
) {}
}
// ... other resolver maps ...
}
Within the resolver, you want to fetch the comment by its id.
Add the corresponding logic for retrieving a comment by its id.
const resolvers = {
// ... other resolver maps ...
Query: {
// ... other field resolver functions
async comment(
parent: unknown,
args: { id: string },
context: GraphQLContext
) {
return context.prisma.comment.findUnique({
where: { id: parseInt(args.id) }
})
}
}
// ... other resolver maps ...
}
Similar to our mutation resolver, we first need to parse the integer value of the id
variable value before using the comment.findUnique
function for finding one single record with that specific id.
Awesome, let's fetch our comment using a query operation!
Execute the following operation on GraphiQL:
query comment {
comment(id: 1) {
id
body
}
}
Awesome! 🎉 Now you can fetch the newly created comment!
{
"data": {
"comment": {
"id": "1",
"body": "This is my first comment!"
}
}
}
Let's also try to fetch a comment by an id
that does not exist.
Execute the following operation on GraphiQL:
query comment {
comment(id: 999999999) {
id
body
}
}
As expected, the comment does not exist, and we receive the following response:
{
"data": {
"comment": null
}
}
So now you implemented fetching a comment by its id.
Up next, let's utilize the graph part of GraphQL for connecting Link
object type with the Comment
object type.
Link.comments
resolver
So far you only fetched single entities within your GraphQL operations.
Let's also add a one-to-many connection from the Link to Comment object types within the GraphQL schema.
Add the Link.comments
field to the GraphQL schema definition.
type Link {
id: ID!
description: String!
url: String!
comments: [Comment!]!
}
Up next, you implement the corresponding Link.comments
resolver. For this, you need to touch the Link
object types resolver map.
Add the following Link.resolvers
implementation:
const resolvers = {
// ... other resolver maps ...
Link: {
// ... other field resolver functions
comments(parent: Link, args: {}, context: GraphQLContext) {
return context.prisma.comment.findMany({
where: {
linkId: parent.id
}
})
}
}
// ... other resolver maps ...
}
As explained before, the first resolver function argument is always the parent argument. In the case of the context of a Link
object type resolver, it is the type of the Link
model as exported from the @prisma/client
package.
Within the resolver logic, we fetch all the comments that belong to the parent Link
element by adding a where filter on the linkId
for the value parent.id
.
Let's see that in action!
Execute the following operation on GraphiQL:
query feed {
feed {
id
comments {
id
body
}
}
}
Congratulations! You just fetched the feed of Link elements and the comments that belong to those Link elements within a single query operation!
{
"data": {
"feed": [
{
"id": "1",
"comments": [
{
"id": "1",
"body": "This is my first comment!"
}
]
}
]
}
}
Being able to fetch multiple resources within a single request is one of the main advantages of using GraphQL!
Optional Exercise
As an additional exercise, you can gain more hands-on experience by implementing the resolvers for Comment.link
and Query.link
.
type Comment {
link: Link
}
type Query {
link(id: ID): Link
}
Start by adding the fields to the schema definitions and then implement the actual field resolvers.
If you are struggling check out the implementations of the previous resolvers! There is nothing new happening here. You can do it!