Build and Deploy a GraphQL API to the Edge with MySQL and PlanetScale — Part 4

Build and Deploy a GraphQL API to the Edge with MySQL and PlanetScale — Part 4

productCreate mutation

A GraphQL mutation is a type of operation in GraphQL that modifies data on the server and typically look something like this:

mutation {
  doSomething(a: String, b: Int) {
    someField
  }
}
  • doSomething is the name of the mutation

  • a and b are names of the arguments passed to mutations

  • String and Int are the data types of the arguments

  • someField is the name of a field returned by the mutation

Let's begin by creating a mutation to create new products in the database.

If you selected TypeScript as the configuration type when using grafbase init you will have the file grafbase/grafbase.config.ts. Inside here we will add the type for Product with the following fields:

  • id

  • name

  • slug

  • price

  • onSale

import { config, g } from '@grafbase/sdk'

const product = g.type('Product', {
  id: g.id(),
  name: g.string(),
  slug: g.string(),
  price: g.int(),
  onSale: g.boolean()
})

export default config({
  schema: g
})

Below the Product definition, we can now add the createProduct mutation. We'll need to create an input type used by the mutation and configure the mutation itself, which points to the resolver file:

const productCreateInput = g.input('ProductCreateInput', {
  name: g.string(),
  slug: g.string(),
  price: g.int(),
  onSale: g.boolean().optional()
})

g.mutation('productCreate', {
  args: { input: g.inputRef(productCreateInput) },
  resolver: 'products/create',
  returns: g.ref(product).optional()
})

Now let's create the code that runs when the GraphQL mutation productCreate is executed. This code is known as a GraphQL resolver.

Create the file grafbase/resolvers/products/create.ts and begin by exporting a default async function, it can be named whatever you like but we'll call it ProductsCreate.

import { connect } from '@planetscale/database'
import { config } from '../../lib'

const conn = connect(config)

export default async function ProductsCreate(_, { input }) {
  const fields: string[] = []
  const placeholders: string[] = []
  const values: (string | number | boolean)[] = []
}

In the code above we will destructure input from the second arguments passed to the resolver function. This input argument will be populated by the fields passed to the GraphQL mutation productCreate.

Also in this code we create three new variables — fields, placeholders and values that we will use to collect the necessary data to pass onto the database request.

Next for each of the entries in the input object we will check to see if the value is one of the expected types — string, number or boolean. These values match the field types of the Product type we created previously.

If the value is present and one of those types then we will push the field name onto the fields array, add a new placeholder into the placeholders array and add the value to the values array.

Object.entries(input).forEach(([field, value]) => {
  if (
    value !== undefined &&
    value !== null &&
    (typeof value === 'string' ||
      typeof value === 'number' ||
      typeof value === 'boolean')
  ) {
    fields.push(`\`${field}\``)
    placeholders.push('?')
    values.push(value)
  }
})

Next, we use the fields and placeholders inside of the SQL statement. We'll begin the statement by using INSERT INTO products followed by the fields joined using , and an empty space then add the placeholders (e.g. ?, ?, ?, ?).

const statement = `INSERT INTO products (${fields.join(
  ', '
)}) VALUES (${placeholders.join(', ')})`

Now we can pass this statement to conn.execute and pass along the values:

const { insertId } = await conn.execute(statement, values)

One thing to note is that the id and onSale values will both be of the an integer type. What we will need to do is cast these values into the correct type and we can do that by passing a third argument to conn.execute():

const { insertId } = await conn.execute(statement, values, {
  cast(field, value) {
    switch (field.name) {
      case 'id': {
        return String(value)
      }
      case 'onSale': {
        return Boolean(value)
      }
      default: {
        return cast(field, value)
      }
    }
  }
})

Once you added a basic try/catch block to wrap the request and surface any database errors, you should have something that looks like this:

import { cast, connect } from '@planetscale/database'
import { config } from '../../lib'

const conn = connect(config)

export default async function ProductsCreate(_, { input }) {
  const fields: string[] = []
  const placeholders: string[] = []
  const values: (string | number | boolean)[] = []

  Object.entries(input).forEach(([field, value]) => {
    if (
      value !== undefined &&
      value !== null &&
      (typeof value === 'string' ||
        typeof value === 'number' ||
        typeof value === 'boolean')
    ) {
      fields.push(`\`${field}\``)
      placeholders.push('?')
      values.push(value)
    }
  })

  const statement = `INSERT INTO products (${fields.join(
    ', '
  )}) VALUES (${placeholders.join(', ')})`

  try {
    const { insertId } = await conn.execute(statement, values, {
      cast(field, value) {
        switch (field.name) {
          case 'id': {
            return String(value)
          }
          case 'onSale': {
            return Boolean(value)
          }
          default: {
            return cast(field, value)
          }
        }
      }
    })

    return {
      id: insertId,
      ...input
    }
  } catch (error) {
    console.log(error)

    return null
  }
}

That's all we need to successfully create a product in the database using a GraphQL mutation.

Now run the Grafbase local development server using the command below:

npx grafbase dev

Next open Pathfinder at http://localhost:4000 and execute the following mutation:

mutation {
  productCreate(
    input: { name: "Shoes", slug: "shoes", price: 1000, onSale: true }
  ) {
    id
    name
    slug
    onSale
    price
  }
}

You can repeat this mutation as many times as you like with unique content. Make sure you don't use an slug.

👉 Continue to Part 5