GraphQL 2 — Mutation with Basic GraphQL
In the last entry, I went over the basics of GraphQL and how to create basic relational data and run a query (GraphQL 1 — My First Steps to Querying with GraphQL). In this entry, I will focus on how to work with the mutation of data; creating, updating, and deleting data.
In GraphQL, there are two types of interactions that we can have, query or mutation. Query is used to request the information from the database and read it, as described in more detail in the last entry. For example, query can request an existing blog entry from the database and display it on a blog page. In comparison, mutation allows writers to create a new blog entry, update an existing entry, or delete the entry.
Before moving on, clean up the file…
I will use the same example from the last entry. However, the last example code was getting too long for a single index.js file, so here are some brief steps to keep the code in separate files, before moving onto the mutation. Please skip to the next section, if you are just interested in mutations.
Cleanup step 1 — Type definitions
It is common practice to name the file schema.graphql. All types can be moved into this file from the index.js. These types are still accessible by declaring the directory to the schema.graphql in the server instance.
const server = new GraphQLServer({
typeDefs: ‘./src/schema.graphql’,
resolvers
})
Cleanup step 2 — The sample data
The separate db.js file now stores the sample data for this example, Movies and Actors arrays. In this file, these different arrays are combined into an exportable single object, db.
const db = {
movies,
actors
}export { db as default }
The db object would be imported into index.js, however, it needs to be accessed from resolvers, which would also be later located in separate files. The context (ctx), which had appeared in the previous entry as one of the parameters for resolvers, takes an important role. The context is “a value which is provided to every resolver and holds important contextual information like the currently logged in user, or access to a database” (Execution | GraphQL). The db would be declared as a context at the server instance of the index.js as below, so remote resolvers can access it later.
import db from ‘./db’const server = new GraphQLServer({
typeDefs: ‘./src/schema.graphql’,
resolvers,
context: {
db: db
}
})
Cleanup step 3 — Resolvers
The steps to break the resolver code into a separate file are similar to the previous steps. For instances that used to be accessed from the local arrays, the context is used here instead of access db, as shown in the Query resolver example below.
const Query = {
movies(parent, args, { db }, info) {
// If the search query does not exist, just return the list if(!args.query) {
return db.movies
} // It should filter out items that don’t align with the query definitions return db.movies.find((movie) => {
return movie.title.toLowerCase().includes(
args.query.toLowerCase()
)
}) }, actors(parent, args, { db }, info) {
return db.actors
}}export { Query as default }
In this case, each resolver was separated into a separate file. After importing these files and declaring them in the resolvers object at the server instance of index.js, the clean up steps are done, and we can move on to adding mutations.
import Query from ‘./resolvers/Query’
import Movie from ‘./resolvers/Movie’
import Actor from ‘./resolvers/Actor’const server = new GraphQLServer({
typeDefs: ‘./src/schema.graphql’,
resolvers: {
Query,
Movie,
Actor
}, context: {
db: db
}})
1. Create mutations
The goal of this entry is to add create, update and delete mutations for both movie and actor databases created from the last entry. Just like I prepared type Query in the schema for queries, the first step is to define type Mutation to create mutations in the schema, just as shown below with createMovie and createActor operations.
type Mutation { createMovie(
id: ID!,
name: String!,
agent: String,
movie: [Movie]
): Movie! createActor(
name: String!,
agent: String,
movie: ID
): Actor!}
As movies and actors are created, they need to pass information such as name, title, and others that the data comprise of. Some of these arguments are required and others are optional. However, these operators don’t need to pass all the information that the Movie and Actor schema comprises of. For example, a unique ID can be generated automatically in the resolvers, so these mutation operators don’t need to pass (In this example, uuid npm package was used to generate IDs).
The next step is to create a resolver. A new Mutation.js file in resolvers directory will house these operations shown below.
import uuidv4 from ‘uuid/v4’const Mutation = { createMovie(parent, args, { db }, info) { // Creating a new movie constant which comprises of arguments passed from the mutation operator. const movie = {
id: uuidv4(),
…args
} db.movies.push(movie) return movie }, createActor(parent, args, { db }, info) { // Creating a new actor constant which comprises of arguments passed from the mutation operator. const actor = {
id: uuidv4(),
…args.data
} db.actors.push(actor) return actor }}export { Mutation as default }
These are the basics that we all need for each mutation operation.
- It creates a new instance of a movie or actor that comprises of arguments that were passed.
- The instance was pushed into either the movie or actor arrays.
- It returns the instance back, as the mutation operation expects the return value.
For each operation, we can also check to avoid duplicates; check and throw an error if an actor with the same name has already existed, for example. In the final code, I will have some basic logic for validations, however, I will keep it simple in this section.
2. Delete mutations
Now, moving on to mutations to delete items from the database: deleting movies and actors. Adding a delete mutation is similar to adding create, however, the challenge in delete is that Movies are mapped to Actors- the Actor “Marlon Brando” has a property of the Movie ID 1, which is “The Godfather Part 2”, for example.
The first step is to define delete mutations in type Mutation as shown below. ID is the only parameter to pass to identify which movie or actor to delete. That’s pretty much it for the schema file.
type Mutation {
deleteMovie(id: ID!): Movie!
deleteActor(id: ID!): Actor!
}
The next step is to add new resolvers in the Mutation.js file. The basic steps are simple- remove Movie or Actor item with the passed ID from the corresponding array, which can be accessed from ctx, using the splice method.
deleteMovie(parent, args, { db }, info){ // Update the movie array. const deletedMovies = db.movies.splice(movieIndex, 1)
return deletedMovies[0]},deleteActor(parent, args, { db }, info){ // Update the movie array. const deletedActors = db.actors.splice(actorIndex, 1)
return deletedActors[0]}
Lastly, in deleteMovie mutation, Actors’ Movie IDs need to be removed if they had the deleted Movie ID. In this example, I used the simple one-to-one relationship between Actor and Movie, in other words, one Actor may have only one Movie ID, hence the simple forEach method was used to check each Actor’s Movie IDs.
db.actors.forEach((actor) => { if(actor.movie === args.id){
actor.movie = null
}})
3. Update mutations
Update is the last mutation to add in this entry. The process is straightforward based on previous mutations. The first step is to add these mutations into the type definitions. Similar to delete, ID needs to be passed to identify which movie or actor to update. Additionally, applicable fields also need to be passed just like in the create mutation. In this example, I wanted to make all fields optional so I removed the ! sign from each.
type Mutation { updateMovie(
id: ID!,
data: {
title: String
released: Boolean
}
): Movie! updateActor(
id: ID!,
data:{
name: String
agent: String
movie: ID
}
): Actor!}
Then, we need to add update resolvers. One notable difference from the past two examples in create and delete is that each field needs to be checked if the content is null or not, since they are all optional. Below are the final resolvers.
updateMovie(parent, { id, data }, { db }, info){ // Update the movie title, if the new title is not null if(typeof data.title === ‘string’){
movie.title = data.title
} // Update the movie release, if the new released is not null if(typeof data.released === ‘boolean’){
movie.released = data.released
} return movie},updateActor(parent, { id, data }, { db }, info){ // Update the actor name, if the new actor is not null if(typeof data.name === ‘string’){
actor.name = data.name
} // Update the actor’s agent, if the new agent is not null if(typeof data.agent === ‘string’){
actor.agent = data.agent
} // Update the actor’s movie, if the new movie is not null if(typeof data.movie === ‘string’){
actor.movie = data.movie
} return actor},
Source codes
Some of main files are listed below. The full project is hosted here
index.js
schema.graphql
Mutation.js
Original post: http://www.ta-kuma.com/programming/graphql-2-mutation-with-basic-graphql/
Reference
The Modern GraphQL Bootcamp (with Node.js and Apollo) | Udemy