Build and Deploy a GraphQL API to the Edge with MySQL and PlanetScale — Part 5
productUpdate mutation
This section adds all the code needed to update product records inside the PlanetScale database.
Let's begin by updating the Grafbase Configuration to add a mutation that accepts the following arguments;
by
— something we can use to pass anid
orslug
to target the product we want to updateinput
— the actual input of the fields we want to update, fields should be optional
Inside grafbase/grafbase.config.ts
you will want to add the following:
const productUpdateInput = g.input('ProductUpdateInput', {
name: g.string().optional(),
slug: g.string().optional(),
price: g.int().optional(),
onSale: g.boolean().optional()
})
const productByInput = g.input('ProductByInput', {
id: g.id().optional(),
slug: g.string().optional()
})
g.mutation('productUpdate', {
args: {
by: g.inputRef(productByInput),
input: g.inputRef(productUpdateInput)
},
resolver: 'products/update',
returns: g.ref(product).optional()
})
Next, create the file grafbase/resolvers/products/update.ts
and add the following:
import { connect } from '@planetscale/database'
import { GraphQLError } from 'graphql'
import { config } from '../../lib'
const conn = connect(config)
export default async function ProductsUpdate(_, { by, input }) {
let updateClauses: string[] = []
let params: (string | number | boolean)[] = []
let selectStatement: string = ''
let selectParams: (string | number)[] = []
}
In the code above we assigned the arguments by
and input
to their own variables that we can use later on. We also set 4 new mutable variables:
updateClauses
— the field names we want to updateparams
— the updated value for the fieldsselectStatement
— theid
orslug
field passed to theby
argumentselectParams
— the value that is either the entryid
orslug
Next, we will fetch the input
and for each of them check that the value matches the allowed types — string
, number
and boolean
.
Object.entries(input).forEach(([field, value]) => {
if (
value !== undefined &&
value !== null &&
!(typeof value === 'object' && Object.keys(value).length === 0)
) {
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
updateClauses.push(`${field} = ?`)
params.push(value)
}
}
})
if (params.length === 0) {
throw new GraphQLError('At least one field to update must be provided.')
}
If there is value
we will push the field name and value to updateClases
and params
respectively. If there is no params
at all, we will throw an error.
Next, we will follow the same rules as above for conditionally adding to the SQL statement and values that will be used to target the specific entry we want to update using the WHERE
keyword.
let updateStatement = 'UPDATE Products SET ' + updateClauses.join(', ')
const byEntries = Object.entries(by)
if (byEntries.length > 1) {
throw new GraphQLError('Only one of ID or Slug should be provided')
}
const [field, value] = byEntries[0]
if (
value !== undefined &&
value !== null &&
(typeof value === 'string' || typeof value === 'number')
) {
updateStatement += ` WHERE ${field} = ?`
params.push(value)
selectStatement = `SELECT * FROM Products WHERE ${field} = ?`
selectParams = [value]
}
if (!selectStatement) {
throw new GraphQLError('ID or Slug must be provided')
}
Once we're done constructing the SQL statement we can now move to actually executing it. This time we will use a transaction to update the record and select it so we can return the updated document to the user.
We could just return the updated fields instead of fetching them from the database after updating but if the user has requested more fields than those you have passed to update then you will get an error from GraphQL that a field is null
when it shouldn't be.
Finally, if there's a row we will return it, otherwise, we'll return an empty list ([]
):
const [_, results] = await conn.transaction(async (tx) => {
const update = await tx.execute(updateStatement, params, options)
const select = await tx.execute(selectStatement, selectParams, options)
return [update, select]
})
return results?.rows[0] ?? null
Similar to creating records, we need to pass the custom options
function. Instead of copying/pasting this code to every file, you should update the file grafbase/lib.ts
and add the export there.
Don't forget to import cast
from @planetscale/database
:
import { cast } from '@planetscale/database'
export const options = {
cast(field, value) {
switch (field.name) {
case 'id': {
return String(value)
}
case 'onSale': {
return Boolean(value)
}
default: {
return cast(field, value)
}
}
}
}
Now all that's left to do is update the create.ts
and update.ts
files to contain the options
import:
import { config, options } from '../../lib'
You should now have an update resolver that looks something like this:
import { connect } from '@planetscale/database'
import { GraphQLError } from 'graphql'
import { config, options } from '../../lib'
const conn = connect(config)
export default async function ProductsUpdate(_: unknown, args: any) {
const { by, input } = args
let updateClauses: string[] = []
let params: (string | number | boolean)[] = []
let selectStatement: string = ''
let selectParams: (string | number)[] = []
Object.entries(input).forEach(([field, value]) => {
if (
value !== undefined &&
value !== null &&
!(typeof value === 'object' && Object.keys(value).length === 0)
) {
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
updateClauses.push(`${field} = ?`)
params.push(value)
}
}
})
if (params.length === 0) {
throw new GraphQLError('At least one field to update must be provided.')
}
let updateStatement = 'UPDATE Products SET ' + updateClauses.join(', ')
const byEntries = Object.entries(by)
if (byEntries.length > 1) {
throw new GraphQLError('Only one of ID or Slug should be provided')
}
const [field, value] = byEntries[0]
if (
value !== undefined &&
value !== null &&
(typeof value === 'string' || typeof value === 'number')
) {
updateStatement += ` WHERE ${field} = ?`
params.push(value)
selectStatement = `SELECT * FROM Products WHERE ${field} = ?`
selectParams = [value]
}
if (!selectStatement) {
throw new GraphQLError('ID or Slug must be provided')
}
try {
const [_, results] = await conn.transaction(async tx => {
const update = await tx.execute(updateStatement, params, options)
const select = await tx.execute(selectStatement, selectParams, options)
return [update, select]
})
return results?.rows[0] ?? null
} catch (error) {
return null
}
}
With the Grafbase development server running, open Pathfinder at http://localhost:4000
and execute the following mutation:
mutation {
productUpdate(by: { id: "1" }, input: { name: "New name" }) {
id
name
slug
price
onSale
}
}
Make sure to pass a valid id
or slug
to the by
argument.