Updated at: Jul 23, 2023
In the last article about GraphQL, we learned how to create a Query and how to avoid N + 1. The systems need to fetch data but the most of time we need to create these data too and GraphQL has a mechanism called Mutation to do this job.
Goal
Create a Mutation to save a book using the code of the last article
Mutation
The Mutation works very similar to Query, it queries the data in the same way, but after creating the record, and as expected we receive the input data as a parameter, like in Query, but instead it be used as a filter it’s used as a parameter to be inserted into the database.
We need to import the mutations in the Schema:
# lib/app/graphql/schema.ex
import_types(GraphQL.Mutations)
mutation do
import_fields(:book_mutations)
end
The mutations files will contain the mutations:
# lib/app/graphql/mutations.ex
defmodule App.GraphQL.Mutations do
use Absinthe.Schema.Notation
alias App.GraphQL.Mutations
import_types(Mutations.Book)
end
The mutation is called create_book
and it receives name
and position
as arguments, returning a Book:
# lib/app/graphql/mutations/book.ex
defmodule App.GraphQL.Mutations.Book do
use Absinthe.Schema.Notation
alias App.GraphQL.Resolvers
object :book_mutations do
field :create_book, :book do
arg(:name, :string)
arg(:position, :integer)
resolve(&Resolvers.Book.create_book/2)
end
end
end
And the resolver will create the book and return it in case of success, but in case of error an message
is added along with a details
key with a better explanation:
# lib/app/resolvers/books.ex
alias App.GraphQL
def create_book(args, _context) do
case Documents.create_book(args) do
{:ok, book} ->
{:ok, book}
{:error, changeset} ->
{:error, message: "Book creation failed!", details: GraphQL.Errors.extract(changeset)}
end
end
Already exists a method to extract the errors from a changeset called traverse_errors/2:
# lib/app/graphql/errors.ex
defmodule App.GraphQL.Errors do
def extract(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
end
Now let’s create a book:
mutation {
createBook(name: "Book Name", position: 4) {
id
name
position
}
}
Pay attention here because now we need to use the root key called mutation
, when we just do queries we use query
or just omit it that defaults to query
. Another detail is that we should use Lower Camel Case when we have a composed name. Inside GraphQL it’ll be converted to Snake Case.
{
"data": {
"createBook": {
"id": "4",
"name": "Book Name",
"position": 4
}
}
}
Nested Fields
Sometimes we need to create a record and some children of this record at the same time. We call these fields as nested and Absinthe treats it like an input_object
that is used as an argument of your Mutation:
# lib/app/graphql/mutations/book.ex
field :create_book, :book do
...
arg(:verses, list_of(:verse_create_inputs))
end
Now we have one extra argument called verses
that is a list of verse_create_inputs
that represents the allowed fields to be used in nested parameters:
# lib/app/graphql/types/verse.ex
input_object :verse_create_inputs do
field :body, non_null(:string)
field :chapter, non_null(:integer)
field :number, non_null(:integer)
end
You can’t referer to a type in the args, it accepts only input_object
. Finally, we need to enable the association of verses in the method that creates the book:
# lib/app/documents.ex
def create_book(attrs \\ %{}) do
%Book{}
|> Book.changeset(attrs)
|> Ecto.Changeset.cast_assoc(:verses, with: &Verse.changeset/2)
|> Repo.insert()
end
Here we ask Ecto to cast the association verses and use the Verse changeset to do that, like a normal insertion.
Let’s try it:
mutation {
createBook(name: "NĂºmeros", position: 4, verses: [{chapter: 1, number: 1, body: "No segundo ano..."}, {chapter: 1, number: 2, body: "Levantai o censo..."}]) {
id
name
position
verses {
body
chapter
id
number
}
}
}
{
"data": {
"createBook": {
"id": "4",
"name": "NĂºmeros",
"position": 4,
"verses": [
{
"body": "No segundo ano...",
"chapter": 1,
"id": "7",
"number": 1
},
{
"body": "Levantai o censo...",
"chapter": 1,
"id": "8",
"number": 2
}
]
}
}
}
Conclusion
Mutations are very similar to Queries, you can expose the allowed fields and even create nested records. In the next article, we’ll see how to protect our API with authentication.
Repository link: https://github.com/wbotelhos/graphql-with-absinthe-on-phoenix
Any suggestion? Please, send me an email here.