Building an Action

The first thing we’ll need is a simple way to list all the menu items that we have in our system so that we can then take further action upon them. If you aren’t super familiar with doing server-side rendering with Phoenix, don’t worry; we’ll cover everything you need to know here. In order to avoid writing a ton of HTML boilerplate, we’ll use one of the Phoenix generators and then we’ll replace the generated controller contents as needed. Run this in your shell:

 $ ​​mix​​ ​​phx.gen.html​​ ​​--no-context​​ ​​--no-schema​​ ​​Menu​​ ​​Item​​ ​​items
 $ ​​rm​​ ​​test/plate_slate_web/controllers/item_controller_test.exs

The first command generates some boilerplate HTML for us, and the second removes a test case we won’t be needing. Up next is our router. We’ll be making use of the :browser pipeline that’s been sitting around unused this whole time by creating an "/admin" scope inside of which we’ll be setting up our controller:

 scope ​"​​/admin"​, PlateSlateWeb ​do
  pipe_through ​:browser
 
  resources ​"​​/items"​, ItemController
 end

We can confirm that that our router is properly set up by using the handy mix phx.routes command:

 $ ​​mix​​ ​​phx.routes
 * /api Absinthe.Plug [schema: PlateSlateWeb.Schema]
 * /graphiql Absinthe.Plug.GraphiQL [...]
 item_path GET /admin/items PlateSlateWeb.ItemController :index
 item_path GET /admin/items/:id/edit PlateSlateWeb.ItemController :edit
 item_path GET /admin/items/new PlateSlateWeb.ItemController :new
 item_path GET /admin/items/:id PlateSlateWeb.ItemController :show
 item_path POST /admin/items PlateSlateWeb.ItemController :create
 item_path PATCH /admin/items/:id PlateSlateWeb.ItemController :update
 PUT /admin/items/:id PlateSlateWeb.ItemController :update
 item_path DELETE /admin/items/:id PlateSlateWeb.ItemController :delete

With that out of our way, we can turn our attention to the controller, where we should replace its existing contents entirely with the following:

 defmodule​ PlateSlateWeb.ItemController ​do
 use​ PlateSlateWeb, ​:controller
 use​ Absinthe.Phoenix.Controller,
 schema:​ PlateSlateWeb.Schema
 
 end

Now for the fun part. The way that Absinthe.Phoenix.Controller works is that it gives you a way to associate a GraphQL query with a controller action, and use the data looked up from that query in your controller. We won’t be replacing the controller actions but rather augmenting them by utilizing all the lookup ability we’ve already written, letting our controller focus on just managing the HTTP connection. Start with something basic:

 @graphql ​"""
 {
  menu_items {
  name
  }
 }
 """
 def​ index(conn, result) ​do
» result |> IO.inspect
  render(conn, ​"​​index.html"​, ​items:​ result.data[​"​​menu_items"​] || [])
 end

Let’s break this down. At the top of this snippet is a @graphql module attribute on which we’re putting a string with a GraphQL query. Beneath that there’s a relatively ordinary-looking Phoenix controller callback index/2, which gets the HTTP conn and some params, and then renders an index.html.

At a high level, the controller action is acting as a GraphQL client. Instead of looking up menu items by directly hitting the database or PlateSlate.Menu context, it submits a GraphQL query to Absinthe, and then it receives the results of that query as the second argument to the index/2 function. The controller then can go about whatever it would normally do with data; in this case, we’re using it to render an HTML template, and we’re providing that template with some :assigns.

By way of Phoenix review, :assigns is a key on the connection struct that holds a map where you can just “assign” values. Those values propagate along with the connection itself and are made available to you inside of templates.

Head over to the template briefly so that you can make sure it will actually show you something interesting:

 <h2>Listing Items</h2>
 
 <table class=​"table"​>
  <thead>
  <tr>
  <th>Menu Item</th>
  </tr>
  </thead>
  <tbody>
 <%=​ for item <- @items ​do​ ​%>
  <tr>
  <td>​<%=​ item[​"​​name"​] ​%>​</td>
  </tr>
 <%​ ​end​ ​%>
  </tbody>
 </table>

This is all 100% totally normal EEx (Embedded Elixir[31]) template code. As with other templates in Phoenix, we can access values that we place on the connection assigns (from within our controller) as usual via @. We put all of the items that we got back from GraphQL under the items: assign in the controller, so we can access it in the template as @items. Then we can just loop over each item and build out the HTML table.

Before we go deeper, let’s get this running a bit so we can get some hands-on familiarity. Start your server:

 $ ​​iex​​ ​​-S​​ ​​mix​​ ​​phx.server

Then browse to http://localhost:4000/admin/items.

images/chp.serverui/simple_index.png

It isn’t much, but we’ve got items!

Now let’s take a look at the logs. The IO.inspect line from our controller will show us the result of our GraphQL query:

 [info] GET /admin/items
 [debug] Processing ​with​ PlateSlateWeb.ItemController.index/2
 Parameters:​ %{}
 Pipelines:​ [​:browser​]
 [debug] QUERY OK source=​"​​items"​ db=2.​4​ms decode=0.​1​ms
 SELECT ... FROM ​"​​items"​ AS i0 ORDER BY i0.​"​​name"​ []
 %{​data:​ %{​"​​menu_items"​ => [%{​"​​name"​ => ​"​​Bánh mì"​},
  %{​"​​name"​ => ​"​​Chocolate Milkshake"​}, %{​"​​name"​ => ​"​​Croque Monsieur"​},
  %{​"​​name"​ => ​"​​French Fries"​}, %{​"​​name"​ => ​"​​Lemonade"​},
  %{​"​​name"​ => ​"​​Masala Chai"​}, %{​"​​name"​ => ​"​​Muffuletta"​},
  %{​"​​name"​ => ​"​​Papadum"​}, %{​"​​name"​ => ​"​​Pasta Salad"​}, %{​"​​name"​ => ​"​​Reuben"​},
  %{​"​​name"​ => ​"​​Soft Drink"​}, %{​"​​name"​ => ​"​​Thai Salad"​},
  %{​"​​name"​ => ​"​​Vada Pav"​}, %{​"​​name"​ => ​"​​Vanilla Milkshake"​},
  %{​"​​name"​ => ​"​​Water"​}]}}
 ]}}

No surprises here. The result that we get in the second argument to our index/2 function is essentially the output you’d get from using Absinthe.run manually. The only difference is that here we’re doing menu_items instead of menuItems, so we get keys that are a bit more idiomatic to Elixir.

The response could be easier to use. If we were getting this data directly from a Menu.list_items/1 function call, we’d have nice atom keys to work with, and what we want is a way to get the same kind of result from GraphQL too. In fact, if we want to use Phoenix path or form helpers, we really need to be able to get the entire MenuItem struct back, because Phoenix uses some internal information in Ecto structs to make certain markup decisions.

In other words, if we wanted to link to each menu item in our template, we’d do something like <%= link "Show", to: item_path(@conn, :show, item) %>. Right now, however, item is just %{"name" => "Reuben"} and, understandably, Phoenix doesn’t know how to build a link from that. It needs a fully fleshed out %Menu.Item{} struct and all of its data.

Here’s where directives come into play.

A directive is a type that’s defined in our schema, just like an object or a scalar, and we can use these types to annotate parts of our GraphQL documents for special handling.

Absinthe.Phoenix ships with a couple of directives, which we can get access to by importing them into our own schema:

 import_types __MODULE__.MenuTypes
 import_types __MODULE__.OrderingTypes
 import_types __MODULE__.AccountsTypes
»import_types Absinthe.Phoenix.Types

With this in place, we have access to the :action directive, which we can use to annotate GraphQL queries in our controller. Directives are placed in GraphQL documents prefixed with a @ sigil. Let’s see it at work:

»@graphql ​"​​"​​"
»query Index @action(mode: INTERNAL) {

Here you can see the :action directive placed in the GraphQL document via @action, marking the query operation. Much like a field, directives can take arguments. The mode: INTERNAL bit isn’t a strange new syntax; this is a totally ordinary argument with an enum value that tells Absinthe.Phoenix that we want to have it adjust the results of executing the query to suit internal Elixir usage.

If we do just this one change and reload our controller, we won’t get the list anymore, since our code still expects string keys, but we do get some interesting IO.inspect output:

 %{​data:​ %{​menu_items:​ [%{​name:​ ​"​​Bánh mì"​}, %{​name:​ ​"​​Chocolate Milkshake"​},
  %{​name:​ ​"​​Croque Monsieur"​}, %{​name:​ ​"​​French Fries"​}, %{​name:​ ​"​​Lemonade"​},
  %{​name:​ ​"​​Masala Chai"​}, %{​name:​ ​"​​Muffuletta"​}, %{​name:​ ​"​​Papadum"​},
  %{​name:​ ​"​​Pasta Salad"​}, %{​name:​ ​"​​Reuben"​}, %{​name:​ ​"​​Soft Drink"​},
  %{​name:​ ​"​​Thai Salad"​}, %{​name:​ ​"​​Vada Pav"​}, %{​name:​ ​"​​Vanilla Milkshake"​},
  %{​name:​ ​"​​Water"​}]}}

Because we placed the @action directive on our query, flagging the query as something we want to run in the INTERNAL mode, we get atom keys. When Absinthe.Phoenix ran the query, it used special phases that looked for these flags to adjust the output for us.

While atom keys are nice, they aren’t enough to give us a first-class, server-side experience. We need to be able to get the full structs of each menu item so that we can have the full Phoenix.HTML experience. Thankfully, @action has our back.

»@graphql ​"""
»query Index @action(mode: INTERNAL) {
» menu_items
»}
»"""
 def​ index(conn, result) ​do
  result |> IO.inspect
» render(conn, ​"​​index.html"​, ​items:​ result.data.menu_items)
 end

The most important thing to notice here is that our GraphQL query now just has a bare menu_items field instead of the previous menu_items { name }. When using @action, this bears special significance: it will return the bare data from field resolvers. With this and the change we make to the render line (to use the atom keys), a controller reload will give us back our list.

Take a look at the debug output:

 %{​data:​ %{​menu_items:​ [
  %PlateSlate.Menu.Item{​name:​ ​"​​Bánh mì"​, ...},
  %PlateSlate.Menu.Item{​name:​ ​"​​Chocolate Milkshake"​, ...],
  ...

The GraphQL document on the controller is returning regular Elixir structs! This is the power of directives. By giving you a flexible way to annotate your document, GraphQL clients and servers have the ability to make deep customizations to their APIs.

Let’s put this new struct data to use:

 <%=​ for item <- @items ​do​ ​%>
  <tr>
  <td>​<%=​ item.name ​%>​</td>
» <td class=​"text-right"​>
» <span>​<%=​ link ​"​​Show"​, ​to:​ item_path(@conn, ​:show​, item),
»class:​ ​"​​btn btn-default btn-xs"​ ​%>
» </span>
» <span>​<%=​ link ​"​​Edit"​, ​to:​ item_path(@conn, ​:edit​, item),
»class:​ ​"​​btn btn-default btn-xs"​ ​%>
» </span>
» <span>​<%=​ link ​"​​Delete"​, ​to:​ item_path(@conn, ​:delete​, item),
»method:​ ​:delete​,
»data:​ [​confirm:​ ​"​​Are you sure?"​],
»class:​ ​"​​btn btn-danger btn-xs"​ ​%>
» </span>
» </td>
  </tr>
 <%​ ​end​ ​%>

The index page here is a jumping-off point to other pages that will let us view more details or edit particular items on the menu. The chunk of code we’ve added to the table contains various links to those pages that make use of Phoenix path helpers.

Phoenix path helpers are pretty cool. At compile time, the routes that you set up in your router are compiled into a bunch of functions within the PlateSlate- Web.Router.Helpers module, which get imported for us inside of templates. Together with the Phoenix.Param protocol, which builds a URL parameter out of a struct, we can use handy functions like item_path(@conn, :show, item).

The real value of the @action directive is that we can treat our GraphQL API as a first-class Elixir data source. With that kind of support, it frees us to go back to writing completely ordinary server-side UI code. The effort you expend to build your GraphQL API for a mobile or JavaScript application can be immediately reused to power backend UIs, and vice versa.

Let’s wrap up this first pass at our index by including the name of the category that each menu item belongs to. Here we face a small challenge. If we do the ordinary { menu_items { category { name }}}, we would no longer have a bare menu_items field, so the result would no longer include the full structs. What we want to have happen here is for the contents of the category resolver to simply get placed into the results we were getting before. To accomplish this, we’ll use another directive, :put:

 @graphql ​"""
 query Index @action(mode: INTERNAL) {
  menu_items @put {
  category
  }
 }
 """
 def​ index(conn, result) ​do
  render(conn, ​"​​index.html"​, ​items:​ result.data.menu_items)
 end

The use of @put in our document indicates to Absinthe.Phoenix that instead of narrowing down the results of the menu_items field to only the fields in the selection set, we want to put those values into the previous result.

Now that we’re loading our category, we can update our template:

 <table class=​"table"​>
  <thead>
  <tr>
  <th>Menu Item</th>
» <th>Category</th>
» <th></th>
  </tr>
  </thead>
  <tbody>
 
 <%=​ for item <- @items ​do​ ​%>
  <tr>
  <td>​<%=​ item.name ​%>​</td>
» <td>​<%=​ item.category.name ​%>​</td>

Reload your server to see it all come together:

images/chp.serverui/index_category.png

At the end of the day, our controller is hardly doing anything at all, which is how it should be. The controller can focus on managing the HTTP connection itself, which, in the case of an index page, is essentially just a matter of sending a rendered template over the wire. Our Absinthe schema already knows how to do all the heavy lifting to get our data, so we just tell it what the controller needs and pass it along to the template.

Directives Are Safe

images/aside-icons/info.png

When using normal GraphQL documents, all values that the client will receive are noted explicitly in the schema. This means that clients to the API will only get exactly the data that we’re willing to give them; they can’t get data that might be sensitive. When using @action, however, extra Elixir values are making their way to our templates, even if they aren’t in the schema.

Fortunately, this isn’t unsafe at all. Directives can’t force the server to do anything; they just ask nicely. The :action and :put directives we’re using here are ignored completely unless run through Absinthe.Phoenix.Controller. This means that if someone uses them in a normal API, they are ignored completely, and any sensitive values remain safely on the server.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.232.189