TutorialGraph Relations

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 Commentss are posted on Linkss.

Open prisma/schema.prisma and add the following code, making sure to also update your existing Link model accordingly:

prisma/schema.prisma
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

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:

src/schema.ts
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.

  1. 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, called ID. In general, it is the best practice to use it. The downside is that a GraphQL ID is always a string. Thus, within the resolver function, you must parse the actual integer value of the ID, as the SQLite database uses integer values for IDs.
  2. 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.

src/schema.ts
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.

src/schema.ts
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:

  1. comments: [Comment]
  2. comments: [Comment!]
  3. comments: [Comment]!
  4. 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:

src/schema.ts
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!