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())
createdAt DateTime @default(now())
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.
You might ask, Links might not have Comments, so why don’t we annotate comments like this:
comments Comment[]?
The reason is that Prisma does not support optional on lists. This is by design as they say it is a better practice to return an empty array in that case (reference).
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-comment-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 Query {
info: String!
feed: [Link!]!
comment(id: ID!): Comment
}
type Mutation {
postLink(url: String!, description: String!): Link!
postCommentOnLink(linkId: ID!, body: String!): Comment!
}
type Link {
id: ID!
description: String!
url: String!
}
type Comment {
id: ID!
createdAt: String!
body: String!
}
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.
The createdAt field will be automatically created by Prisma
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
createdAt
body
}
}
The response will look identical to this:
{
"data": {
"postCommentOnLink": {
"id": "1",
"createdAt": "1733487844288",
"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!]!
}
On the Prisma API level, the method call for loading will always return an array. It does not distinguish between an empty array and an array with comments.
As we design our GraphQL schema we need to consider how we will model the concept of a Link without comments.
We have a couple of options:
First in terms of the array itself, we can define it as nullable or not. Then for the elements of the array, also there we can define them as nullable or not.
Here are the 4 options, first two are for nullable array and the last two for none nullable array:
comments: [Comment]
comments: [Comment!]
comments: [Comment]!
comments: [Comment!]!
Let’s try to explain the differences between the options. We’ll also describe the Typescript return value of each option (which will be automatically generated in the later Codegen chapter).
Option 1, [Comment]
, means that the elements both the array and its elements are nullable. The
Typescript return type of that resolver would be Array<Comment | null> | null
. Representing
there's no Comment related to the Link
in that option can be applied in many different ways - an
empty array, a null as array or an array with null elements. That means that the client who consumes
this will need to handle many cases.
Option 2, [Comment!]
, means that the elements of the array are none nullable, but the array can be
empty or null. The Typescript return type of that resolver would be Array<Comment> | null
.
Representing there's no Comment related to the Link
in that option can be applied in many
different ways - the array can be null or be empty, but at least the array can’t have null elements.
Here also the client needs to handle a couple of different possibilities.
Option 3, [Comment]!
, means that the array is none nullable, but the elements can be null.
Representing there's no Comment related to the Link
in that option can be applied in many
different ways - it can’t be a null as array, but it can be an empty array or an array with null
elements. Here also the client needs to handle a couple of the different possibilities.
Option 4, [Comment!]!
, means that the elements of the array are none nullable and the array itself
must return a value. The Typescript return type for of resolver would be Array<Comment>
.
With [Comment!]!
, the only way for the server to represent
there's no Comment related to the Link
is to return an empty array []
.
This means it’s easier for the client to handle because there’s only one way to represent the
there's no Comment related to the Link
concept.
For the reasons above we’ve chose option 4
for our implementation.
Now let’s implement the corresponding Link.comments
resolver. For this, you need to touch the
Link
object types resolver map.
As we also have the createdAt field on comments, let’s sort the comments by that field when we return them to the user.
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({
orderBy: { createdAt: 'desc' },
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!