fireproofsocks

fireproofsocks

Best inputs for searching methods (e.g. list_items)

This is more of a general question, but it really hit home as I read through the “Craft GraphQL APIs” Absinthe book. I noticed this pattern:

defmodule MyContext do
    def list_items(args) do 
      args
      |> Enum.reduce(Item, fn 
        {:order, order}, query ->
          query |> order_by({^order, :name}) 
        {:filter, filter}, query ->
          query |> filter_with(filter) 
      end)
      |> Repo.all
    end

    defp filter_with(query, filter) do
      Enum.reduce(filter, query, fn
        {:name, name}, query ->
          from q in query, where: ilike(q.name, ^"%#{name}%")
        {:priced_above, price}, query ->
          from q in query, where: q.price >= ^price
        {:priced_below, price}, query ->
          from q in query, where: q.price <= ^price
        {:category, category_name}, query ->
          from q in query,
            join: c in assoc(q, :category),
            where: ilike(c.name, ^"%#{category_name}%")
        {:tag, tag_name}, query ->
          from q in query,
            join: t in assoc(q, :tags),
            where: ilike(t.name, ^"%#{tag_name}%")
      end)
    end
end

That seems like a brilliant and expressive way of structuring dynamic search criteria, and it has the benefit of tying in well to the Absinthe resolvers, but my questions are:

  1. What exactly IS that function expecting? Is it something like this: args = [{name: "Some name"}, {order: :asc}]
  2. How could you “feed” into this structure with a standard REST endpoint? I’m having a hard time wrapping my head around how you could send a JSON body (let alone standard form-encoded inputs) that would properly represent linked lists.
  3. I have noticed that for standard Phoenix controllers, the submitted params arrives as a map using binary keys, but here within the GraphQL resolves, the inputs use atom keys. Would it be a good idea to normalize both forms of input?

Thanks – sorry if this is rambling.

First Post!

axelson

axelson

Scenic Core Team

Yes, that’s exactly what it’s expecting, which is one of Elixir’s built-in data structures, a keyword list. You might not recognize it because it has a few different representations which are all equivalent:

list_items([{name: "Some name"}, {order: :asc}])
list_items([name: "Some name", order: :asc])
list_items(name: "Some name", order: :asc)

You could do something like this:

def list(conn, params) do
  # params = %{"order" => "asc", "name" => "Some Name"}

  args =
    params
    |> Enum.map(fn
      {"order", order} -> {:order, order}
      {"name", name} -> {:name, name}
    end)
    |> Enum.into([])

  MyContext.list_items(args)
end

What the code above is doing is expecting very specific keys for this endpoint and converting the data from an external form (map with string keys) to an internal form (keyword list). Yes, in a sense this is “duplicating” a specification of the expected keys, but this validation/conversion from external data to internal data is very important for a robust and secure application. And if you find yourself doing this often then you can use some tools to help you like Ecto (using embedded schemas), or a library like vex or optimal.

Yes it is good to normalize the input, which is what I was getting at above where you are converting untrusted external input, into trusted (or semi-trusted) internal input. Absinthe is able to do the initial validation step for you because you initially provided it a GraphQL Schema which states exactly which arguments, and their types that you expect, and if a required argument is missing, or have the incorrect type, then your resolver function is never even called.

Also in the case of a Phoenix Controller, it would be unwise to directly convert an incoming params map (with string keys) into atom keys because you will end up calling a function like String.to_atom which (as the docs state) should not be called with untrusted external input because created atoms are not garbage collected which means that eventually an attacker could bring your system down.

Most Liked

fireproofsocks

fireproofsocks

I checked out Optimal and I really like it (I did submit a PR to add some more examples and fix some of the broken code in the existing examples), but I feel it would be even better if I could define my validation rules in one place and have that handle the normalization between string and atom keys. It feels smelly to have to do that in my controllers…

axelson

axelson

Scenic Core Team

If you pass a keyword list as order_by it would work (for example with order_by == [asc: :name]):

{:order_by, order_by}, query ->
          query |> order_by(order_by)

Or just separate out the clauses:

{:order_asc, field}, query ->
  query |> order_by([asc: field])

{:order_desc, field}, query ->
  query |> order_by([desc: field])

Does that accomplish what you’re trying to do? If not a larger code snippet would probably be helpful.

OvermindDL1

OvermindDL1

As per the docs you can pass in a list of things to order by:

query |> order_by([c], asc: c.name, desc: c.population)

It can even be dynamic:

values = [asc: :name, desc_nulls_first: :population]
query |> order_by([c], ^values)

Last Post!

OvermindDL1

OvermindDL1

You can even do it by multiple order_by clauses as I recall, I think they get combined in order. It all depends on how you are giving the information to it. ^.^

Where Next?

Popular in Questions Top

vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31586 112
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49266 226
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement