Office Hours Demo: How To Perform Nested Writes with RedwoodJS and GraphQL

See: redwood-office-hours/2022-10-26-nested-writes-demo at main · redwoodjs/redwood-office-hours · GitHub

How To Perform Nested Writes with RedwoodJS and GraphQL

One of the common question asked in the RedwoodJS Discord community concerns what Prisma calls nested writes.

A nested write lets you perform a single Prisma Client API call with multiple operations that touch multiple related records. For example, creating a user together with a post or updating an order together with an invoice. Prisma Client ensures that all operations succeed or fail as a whole.

The questions are not so much about how do I perform the nested write using Prisma, but

  • Should I just reuse the existing “C” in the generated “CRUD” services and input types? (Spoiler: I say no)
  • What data do I send from the the web side/form in the GraphQL request
  • What does the SDL (Input and Operation/Mutation) look like?
  • What does the service/Prisma create look like?

Practical Example

Let’s look at a practical example where we have Courses and Students and Students can enroll in many Courses.

Entity Relationship Diagram

Note: Want to generate diagrams like this? See how the prisma-erd-generator is setup in the Prisma schema. You can generate mermaid, png and svg diagrams.

How do I create a new Course and assign existing students?

Given a number of existing Students, let’s launch a new Course and enroll several students in one create (mutation).

We wont use any existing generators or CRUD types or services; instead we will create a dedicated GraphQL mutation that is purposeful for the task of creating a Course and assigning students … and only that.

This way, we can also better control authorization (perhaps only certain roles can launch a new course with students, but other roles can create empty courses) as well as validate the information if needed (maybe a course can only be launched it 5 students signed up right away), etc.

SDL

In launchCourse.sdl.ts we define the input types and the mutation.

We want the StudentEnrollInput to be an object like: {id: 3} where id is the Student id.

Then LaunchCourseInput has a course which is the CreateCourseInput (meaning it has the fields to create a new Course like title and description) and studentIds which is an array of objects with a student id like: [{id: 2}, {id: 3}, {id: 5}].

And lastly, the mutation to launchCourse which needs the LaunchCourseInput data and will return the newly created Course. This is performed by the launchCourse service.

// 2022-10-26-nested-writes-demo/api/src/graphql/launchCourse.sdl.ts
export const schema = gql`
  input StudentEnrollInput {
    id: Int!
  }

  input LaunchCourseInput {
    course: CreateCourseInput!
    studentIds: [StudentEnrollInput!]!
  }

  type Mutation {
    launchCourse(input: LaunchCourseInput!): Course! @requireAuth
  }
`

Service

The launchCourse service receives the data needed to create a new Course and enroll several students in the GraphQL request having set the LaunchCourseInput information.

It used a “nested write” to:

  • create the Course by spreading the data in input.course (title, description, etc.)
  • connects this Course to several students using their primary key id
// 2022-10-26-nested-writes-demo/api/src/services/launchCourses/launchCourse.ts

import type { QueryResolvers, MutationResolvers } from 'types/graphql'

import { db } from 'src/lib/db'

export const launchCourse: MutationResolvers['launchCourse'] = ({ input }) => {
  return db.course.create({
    data: { ...input.course, students: { connect: input.studentIds } },
    include: { students: true },
  })
}

Make the Request

  1. Launch the dev server using yarn rw dev
  2. Visit the Redwood GraphQL Playground at http://localhost:8911/graphql
  3. Make the mutation and notice the input values
mutation {
  launchCourse(
    input: {
      course: {
        title: "The Works of Frank Lloyd Wright",
        description: "In this course we will explore the works of the American architect, designer, writer, and educator.",
      	subject: ARCHITECTURE
      },
      studentIds: [{id: 2}, {id: 3}, {id: 5}]
      }) {
    id
    title
    description
    subject
    students {
      id
      name
      major
    }
  }
}

And the response should return the newly created course with the enrolled students.

{
  "data": {
    "launchCourse": {
      "id": 17,
      "title": "The Works of Frank Lloyd Wright",
      "description": "In this course we will explore the works of the American architect, designer, writer, and educator.",
      "subject": "ARCHITECTURE",
      "students": [
        {
          "id": 2,
          "name": "Leland Brakus",
          "major": "LITERATURE"
        },
        {
          "id": 3,
          "name": "Paolo Kerluke",
          "major": "MUSIC"
        },
        {
          "id": 5,
          "name": "Kody Mitchell",
          "major": "BIOLOGY"
        }
      ]
    }
  }
}

Future Next Steps

  • Validate input (maybe 5 students needed to launch new course)
  • Handle Prisma errors if connected student does not exist and return a friendly error
  • Have a createOrConnect to also add a new Student
  • Optimize resolvers to avoid fetching multiple students
2 Likes