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
- Launch the dev server using
yarn rw dev
- Visit the Redwood GraphQL Playground at http://localhost:8911/graphql
- 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