Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for A Deep Dive into Mutations with Absinthe
AppSignal profile imageSapan Diwakar
Sapan Diwakar forAppSignal

Posted on • Originally published atblog.appsignal.com

A Deep Dive into Mutations with Absinthe

In the last two posts of this series, we saw how easy it is to integrate Absinthe into your app to provide a GraphQL API for querying data. We also shared some useful tips for building and maintaining large schemas and optimizing queries.

This post will show how we can provide an API to create GraphQL mutations with Absinthe for Elixir.

Let's get started!

A Note on Queries Vs. Mutations in GraphQL

GraphQL places a clear distinction between queries and mutations. Technically, we can implement any query to write data to the server. ButGraphQL convention clearly separates them into different top-levelmutation types.

Mutations in GraphQL

First, let’s see what a typical mutation looks like:

mutationCreatePost($post:PostCreateInput!){createPost(post:$post){post{id}errors}}
Enter fullscreen modeExit fullscreen mode

This is a mutation to create a new post. It accepts a variable namedpost of typePostCreateInput (which is required because! follows the type name). This variable is passed into thecreatePost field inside the top-levelmutation type.

The result of that mutation is apost (which can have further selections) and anerrors array. Note that we have named the mutationCreatePost but that is just a client-side identifier. Here is what a sample response from this mutation looks like:

{"data":{"createPost":{"post":{"id":"1"},"errors":null}}}
Enter fullscreen modeExit fullscreen mode

Input Objects with Absinthe for Elixir

Let’s see how we can implement this with Absinthe. The first thing we need is aninput_object that can be used as a variable for the mutation. Let’s create one now:

defmoduleMyAppWeb.SchemadouseAbsinthe.Schemaenum:post_state_enumdovalue:activevalue:draftendinput_object:post_create_inputdofield:title,non_null(:string)field:author_id,non_null(:id)field:body,non_null(:string)field:state,:post_state_enum,default_value::draftendend
Enter fullscreen modeExit fullscreen mode

Here, we use theenum macro to create an enum namedpost_state_enum with two possible values —draft andactive.

We then use theinput_object macro to define an input object namedpost_create_input which has four fields. Note that the definition of fields changes slightly from what we would use in an output type. For example, for fields inside an input object, we can add adefault_value to a non-required field. Absinthe will fill the field in if the user doesn’t provide a value.

Defining Mutations in Absinthe

Next, let’s define our mutation type in the schema. All individual mutations (likecreatePost) will be fields inside this mutation:

defmoduleMyAppWeb.SchemadouseAbsinthe.Schemamutationdofield:create_post,non_null(:post_mutation_result)doarg:post,non_null(:post_create_input)endendend
Enter fullscreen modeExit fullscreen mode

Here, we use themutation..do..end block to define the base mutation type. And inside it, we define a field namedcreate_post. This takes a single argument namedpost of thepost_create_input type defined above. The result of this field is of typepost_mutation_result. We haven’t defined it yet, so let’s do that now.

object:post_mutation_resultdofield:post,:postfield:errors,list_of(non_null(:string))end
Enter fullscreen modeExit fullscreen mode

Now on to the interesting part — actually performing the mutation operation. This is similar to what we already did for queries. We will define a resolver to handle it.

If you remember fromour previous post, a 3-arity resolver receives the parent object, the attributes, and anAbsinthe.Resolution struct. Let’s define that first to create the post:

defmoduleMyAppWeb.Resolvers.PostResolverdodefcreate_post(_parent,%{post:attrs},_resolution)docaseMyAppWeb.Blog.create_post(attrs)do{:ok,post}->{:ok,%{post:post}}{:error,changeset}->{:ok,%{errors:translate_changeset_errors(changeset)}}endendend
Enter fullscreen modeExit fullscreen mode

In the resolver, we use thepost attributes to create a post. If that is successful, we respond with the post object (the result map should correspond to the mutation's result type —post_mutation_result, in our case). On an error, we respond with theerrors. Note thattranslate_changeset_errors is a custom helper function that translatesEcto.Changeset errors into an array of strings.

With the resolver in place, we can now plug it into our schema to create a post:

defmoduleMyAppWeb.SchemadouseAbsinthe.Schemamutationdofield:create_post,:post_mutation_resultdoarg:post,non_null(:post_create_input)resolve&MyAppWeb.Resolvers.PostResolver.create_post/3endendend
Enter fullscreen modeExit fullscreen mode

Now, when we execute the above mutation against our schema, Absinthe will call thecreate_post function. The return value from the resolver's{:ok, value} tuple will then be used as the mutation's result and returned to the user.

Authorization in Your Elixir App with GraphQL and Absinthe

Up until now, we have been discussing everything as if we're using an open public API. But in most apps, this is not how things work. Often, certain APIs are only available to logged-in users, and this is even more important in the case of mutations. Let’s see how we can add authorization to our API.

First, we will need to identify the users making requests. In thefirst post of this series, we usedAbsinthe.Plug to handle all the requests coming into the/api endpoint using theMyAppWeb.Schema schema. This doesn’t handle any user authorization for us.

Absinthe provides acontext concept containing shared information that might be useful for all queries and mutations. This is passed to all resolver functions that accept anAbsinthe.Resolution struct inside thecontext field.

Let’s fix that first to identify usersbefore a request is forwarded to Absinthe.

An Example Using Absinthe Context

Update the router in your app to execute an additional plug before forwarding a request:

defmoduleMyAppWeb.RouterdouseMyAppWeb,:routerpipeline:apidoplug:accepts,["json"]endpipeline:graphqldoplugMyAppWeb.Schema.Contextendscope"/"dopipe_through[:api,:graphql]forward"/api",Absinthe.Plug,schema:MyAppWeb.Schemaendend
Enter fullscreen modeExit fullscreen mode

We've added a newMyAppWeb.Schema.Context plug to the requests. Let’s implement it to put a user in the Absinthe context.

defmoduleMyAppWeb.Schema.Contextdo@behaviourPlugimportPlug.Conn#...defcall(conn,_default)doAbsinthe.Plug.put_options(conn,context:absinthe_context(conn))enddefabsinthe_context(conn)do%{conn:fetch_query_params(conn)}|>put_user()endend
Enter fullscreen modeExit fullscreen mode

Note: I have intentionally left out some app-specific parts of the plug. Here is thefull code of that module if you are interested.

An interesting part of the code in the above code block is the use ofAbsinthe.Plug.put_options/2 to set values thatAbsinthe.Plug will pass to Absinthe when executing the query/mutation. This is similar to how we usePlug.Conn.assign to assign something on the conn.

Internally,Absinthe.Plug puts a private assign inside theconn and passes it as the third parameter toAbsinthe.run/3 when executing the document. Thecontext option we use above is a special value that Absinthe will then pass to all resolvers inside theAbsinthe.Resolution struct.

Now let’s update ourcreate_post/3 resolver function to use this context and deny the request if the user isn’t present.

defmoduleMyAppWeb.Resolvers.PostResolverdodefcreate_post(_parent,%{post:attrs},%Absinthe.Resolution{context:%{user:nil}})do{:error,"unauthorized"}enddefcreate_post(_parent,%{post:attrs},%Absinthe.Resolution{})docaseMyAppWeb.Blog.create_post(attrs)do{:ok,post}->{:ok,%{post:post}}{:error,changeset}->{:ok,%{errors:translate_changeset_errors(changeset)}}endendend
Enter fullscreen modeExit fullscreen mode

If a resolver function returns an error tuple, Absinthe will not resolve that field and adds an error to the response.

This is just one example of how to use Absinthe context. There are many more advanced potential use cases. For example, you might want to automatically set a user’s id as the post author, instead of accepting anauthor_id in thepost_create_input.

Middleware in Absinthe for Elixir

We have used context to authorize a user during resolution. But if we have many fields to handle, this soon becomes too cumbersome. Absinthe provides middleware to handle such cases.

For example, let's assume that several fields require a user to be present when performing an operation. Instead of updating the resolver function for all those fields, wouldn’t it be better if we could just writemiddleware MyAppWeb.Schema.RequireUser in the field definition?

We can do that using themiddleware/2 macro:

defmoduleMyAppWeb.SchemadouseAbsinthe.Schemamutationdofield:create_post,:post_mutation_resultdoarg:post,non_null(:post_create_input)middlewareMyAppWeb.Schema.RequireUserresolve&MyAppWeb.Resolvers.PostResolver.create_post/3endendend
Enter fullscreen modeExit fullscreen mode

The argument tomiddleware is a module that implements theAbsinthe.Middleware behaviour. The module should start acall/2 function that receives anAbsinthe.Resolution struct as the first argument.

The second argument is whatever is passed to themiddleware/2 macro. We don’t pass anything above, so it's nil by default. Thecall/2 function should return an updatedAbsinthe.Resolution struct.

ImplementRequireUser Middleware

Let’s implement theRequireUser middleware now to stop requests if a user is not present:

defmoduleSpendraWeb.Schema.RequireUserdo@behaviourAbsinthe.Middlewaredefcall(%Absinthe.Resolution{state::resolved}=resolution,_config),do:resolutiondefcall(%Absinthe.Resolution{context:{user:nil}}=resolution,_config)doAbsinthe.Resolution.put_result(resolution,{:error,"You must be logged in to access this API"})enddefcall(%Absinthe.Resolution{}=resolution,_config),do:resolutionend
Enter fullscreen modeExit fullscreen mode

The middle clause is the most important. Here, we receive a context with anil value for the user. We useAbsinthe.Resolution.put_result to mark that the resolution is now complete. It does two things:

  1. Marks thestate of the resolution asresolved.
  2. Puts the specified value as the resolution result. Here we use an error tuple to signal that this is an erroneous response.

We also have a special clause at the beginning that checks for the resolution's existing state. If you are using multiple middlewares in your app, this is important. It avoids a middleware running on a resolution that has already been resolved.

Finally, if a resolution's state is not alreadyresolved and we have a user in the context, we just return the original resolution struct without any modifications. This allows execution to proceed.

Chaining Middleware

The great thing about middleware is that it can be chained. For example, we might have several user roles and only want authors to be able to create posts. We can use multiple middleware clauses that each perform a single task before the final resolution.

field:create_post,:post_mutation_resultdomiddlewareMyAppWeb.Schema.RequireUsermiddlewareMyAppWeb.Schema.RequireAuthorresolve&MyAppWeb.Resolvers.PostResolver.create_post/3end
Enter fullscreen modeExit fullscreen mode

In fact,resolve itself is just a middleware which roughly translates tomiddleware Absinthe.Resolution, unquote(function_ast).

Another important point about middlewares is that they are executed in the order they are defined in a field. So in the above example, firstRequireUser will be executed, followed byRequireAuthor and, finally, the resolver.

It is also possible to use middleware after a resolve call — for example, to handle resolution errors.

Wrap Up

In this post, we created GraphQL mutations with Absinthe. We also added a custom plug to put shared information in an Absinthe context and defined middleware to handle common tasks like authorization.

In the next and final part of this series, we will discuss advanced GraphQL use cases like subscriptions (including how to implement and deliver subscriptions over a WebSocket connection).

Happy coding!

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press,subscribe to our Elixir Alchemy newsletter and never miss a single post!

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

To get a steady dose of magic, subscribe to 🎩Ruby Magic.

Magicians never share their tricks. But we do. Subscribe and we’ll deliver our monthly edition straight to your inbox.

More fromAppSignal

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp