Accept JSON Object in Ash Graphql


Here's how to send json as an input variable to a graphql endpoint in ash_graphql.

Endpoint

Ash is all about the declarations. I hereby declare that we'll write an endpoint that takes a JSON object. For our purpose, json just means an object, or a non-scalar.

defmodule MyEntity do
  # ...
  
  actions do
    create :take_things do
      argument :things {:array, :map}
      manual TakeThings
    end
  end

  graphql do
    # ...
    mutations do
      create :take_things, :take_things
      transaction? true
      manual TakeThings
    end
  end
end

defmodule TakeThings do
  def create(changeset, _opts, _context) do
    things = changeset.arguments.things

    things |> Enum.map(fn thing -> 
      %{
        id: thing["id"],
        subthings: thing["subthings"] |> Enum.map(fn subthing -> 
          %{
            id: subthing["id"]
          }
        end)
      }
    end)

    # write... not shown
  end
end

What are we looking at here? We have an entity that is set up (much not shown, see ash_graphql docs for more) to be exposed in a graphql endpoint. We've added a new mutation, takeThings. It takes an argument as input, things. This is typed as an :array of :map, or an array of objects.

I wasn't able to get the maps with graphql option working. But it would have been nicer to have a struct there, as a more specific type, compared to a map. Seems like it should be possible. Let me know if you figure it out.

My use case (and a likely one for a blob of schtuff), is that you'll want some sort of manual handling of the data. It's going to be something more complicated that the usual single entity CRUD. That's what's shown here. In the TakeThings module, we've implemented a create function to match the action type. The things argument is an array of maps when access here. The keys are strings and are thus accessed that way.

Client

That should about do it for the server. What about the client? To make a request to such an endpoint, let's set up the input and fetch:

async function saveThings(theThings) {
  return fetch(MY_SERVER_HOST + '/graphql', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ 
      query: `
        mutation takeThings($input: TakeThingsInput!) {
          takeThings(input: $input) {
            result {
              id
              subthings {
                id
              }
            }
            errors {
              code
              fields
              message
            }
          }
        }
      `,
      variables: {
        input: {
          things: JSON.stringify(theThings)
        }
      }
    })
  })
}

What's interesting here? We are set up to call the mutation in our query string. The $input variable is of a type that's generated by Ash, TakeThingsInput. When we set the input, it has a key of things inside of it. That matches the things argument in our action. The value of things is stringified. This happens before and in addition to the stringification of the entire post request body.

Those are the essential pieces needed to set up Ash Graphql to take a JSON object in a POST request.