GraphQLDocument - Build GraphQL document strings from Elixir primitives

Does your Elixir application call into a GraphQL API?

If so,

  • Do you write GraphQL queries in raw strings?
  • Are you ever uneasy about whether you’re interpolating arguments securely?
  • What if it were dead simple to modify GraphQL queries programmatically?

Enter GraphQLDocument:

GraphQLDocument.to_string(query: [
  human: {
    [id: "1000"],
    [:name, :height]
  }
])

# Result:
"""
query {
  human(id: "1000") {
    name
    height
  }
}
"""

GraphQLDocument allows you to generate valid GraphQL code using Elixir primitives, to unlock a new set of possibilities when working with GraphQL APIs in Elixir.

A Quick Primer on the Syntax

GraphQL syntax involves specifying lists of field names:

name
age
height

Naturally, to emit a list of field names, you provide a list of atoms (or strings):

[:name, :age, :height]

GraphQL queries can get pretty nested, to request multiple layers of related data.

me {
  notification_count
  friends {
    name
    email
    posts {
      topic
      body
    }
  }
}

This mixture of “primitive” fields (e.g. string, integer) and “object” fields (those with sub-fields) maps nicely to Elixir’s keyword list syntax. It’s valid in Elixir to create a regular list, but to end it with keyword pairs. To emit the above GraphQL snippet, you’d input:

[me: [
  :notification_count,
  friends: [
    :name,
    :email,
    posts: [
      :topic,
      :body
    ]
  ]
]]

Lastly, in GraphQL you often want to pass arguments (“args”) into a field:

library(type: "hex", name: "graphql_document") {
  downloads
  dependencies {
    name
    url
  }
}

To do that in GraphQLDocument, you wrap the args and “sub-fields” in an {args, fields} tuple, like this:

[library:
  {
    [type: "hex", name: "graphql_document"],
    [
      :downloads,
      dependencies: [:name, :url]
    ]
  }
]

The documentation goes into more detail.

This syntax is a bit “heavier” than plain GraphQL syntax. But it was designed to “flow” similarly to GraphQL. Hopefully that makes it easy to follow. (Constructive criticism is welcome!)

Also, using this slightly “heavier” syntax opens up new possibilities, such as the Elixir compiler catching syntax errors, or using this library to create even more sophisticated tooling. On my team, we’ve done just that. I hope to release some of that tooling in the near future.

5 Likes

Hey @paulstatezny, always great to see more GraphQL things. Couple of questions:

  • How does the library handle field aliases? Eg: { foo: bar() }
  • How does it handle directives? { bar() @include(if: true) }
  • Fragment support and abstract types?
2 Likes

Thanks for chiming in, Ben!

Alias support expands the field: {args, subfields} syntax to alias: {field, args, subfields}, like this:

me: {
  :user,
  [id: 100],
  [:name]
}

Which would emit

me: user(id: 100) {
  name
}

Directives and fragments aren’t currently supported, but I’m always open to feature requests, or contributions. :smile: I’ll have to look into abstract types.

1 Like

@benwilson512 Assuming these features all get supported, how do you feel about the syntax/API so far?

Version 0.2.0 of GraphQLDocument has been released. :slight_smile:

You can now use it to express:

  • Directives
  • Variables
  • Fragments (including both Fragment Spreads and Inline Fragments)

There are some breaking changes from 0.1.0, but there is now much more documentation and code structure.

I’ve also focused some of the messaging to emphasize the ability to create higher-level DSLs for making GraphQL requests. (I conjecture this is the primary value proposition of this library.)

As a clarification, it does not support generating GraphQL Type System strings. Only “execution” strings. (Queries, mutations, and subscriptions.)

I’d encourage anyone interested to read the docs:
https://hexdocs.pm/graphql_document/0.2.0/GraphQLDocument.html

An example of some of the new features:

iex> query(
...>   [
...>     customer: {[id: var(:customerId)], [
...>       :name,
...>       :email,
...>       phoneNumbers: field(args: [type: MOBILE]),
...>       cartItems: [
...>         :costPerItem,
...>         ...: :cartDetails
...>       ]
...>     ]}
...>   ],
...>   variables: [customerId: Int],
...>   fragments: [cartDetails: {
...>     on(CartItem),
...>     [:sku, :description, :count]
...>   }]
...> )
"""
query ($customerId: Int) {
  customer(id: $customerId) {
    name
    email
    phoneNumbers(type: MOBILE)
    cartItems {
      costPerItem
      ...cartDetails
    }
  }
}

fragment cartDetails on CartItem {
  sku
  description
  count
}
"""

That’s a great idea for the language, nice library!

I have a few questions and suggestions:

  1. Why do you use tuple for blocks?
    I get that it is the curly braces like in GraphQL, but it is a bad structure in Elixir because it’s not easy to iterate over it, and it is not easy to unquote into it (because unquote_splicing always looks scary)

  2. As far as I can see, the structure is not easy to transform at runtime. How do you suggest to approach generation of queries at runtime?

My suggestion:

Take a look at lisps, and at Clojure. They have this awesome idea of representing the DSL as a structure called “code is data”. For example, for SQL queries they just write AST. It is a nice approach, though it has some drawbacks: it doesn’t have any syntax (and syntax makes writing AST easier), and they have almost no compile-time checks for the AST.

But in Elixir, we can have both syntax and structures at the same time. So I think that good DSL lives in two forms. In form of a structure like

{:query, ["$customerId": :Int], [
  {:customer, [id: :"$customerId"], [
    :name,
    :email,
    {:phoneNumbers, [type: :MOBILE]},
    ...]}]}

And in a form of a language which is translated in a structure at compile time

query("$customerId": :Int) do
  customer(id: :"$customerId") do
    name
    email
    phoneNumbers(type: :MOBILE)
    cartItems do
       costPerItem
       ~~~cartDetails
    end
  end
end

Using this approach you can acheive

  1. Compile time checks, because no-one needs to write an AST in a plain structures
  2. More straightforward manipulation of the queries in runtime. Because it is just a structure and it is encoded in a pretty obvious way, it is easy to manipulate and share and merge.
  3. Translating structure into a string will be very easy to do

For example, you can take a look at my tiny playground projects where I write this “DSL → Structure → String” approach for NASM assembly language.

@hissssst Thanks for your message! I like your idea for using do/end blocks. The dual approach of “code as data” AST paired with a lightweight DSL is more “Elixir-ey”. :smile:

The query function/macro in your example makes sense to me. But, question: How would the customer and phoneNumber ones work? Since, of course, there are an infinite number of possibilities of field names, we can’t write a function/macro for every one.

I’m guessing query must be a macro which sees those function calls in the quoted Elixir and transforms them into data. Is that right?


To answer your questions it may help to explain our original goal that led to building this.

We were building a GraphQL layer with Absinthe to help us easily compose calls to multiple related data sources. But we were calling from LiveView, and we wanted to be able to receive Ecto schema structs from our GraphQL calls instead of plain maps. That way we could do useful things like building changesets from them to integrate with Phoenix forms.

So we have a separate library which pulls a list of fields from an Ecto schema, feeds it to GraphQLDocument to build the GraphQL query, and then takes the GraphQL response and deserializes it back into the Ecto schema struct.

The abstractions make all of this a 1-line call within a LiveView or Controller to dispatch an entire GraphQL query and receive back a response composted of nested structs.

So you can see, our goal was more about generating chunks of GraphQL from structs rather than more abstract AST transformations. The tuples were just the most succinct structure that made sense to me.

I’m very interested to learn from your approach though. I’ll take a look at your library!

1 Like

Actually, you shouldn’t write a function/macro for every one. You’ve got my idea correclty, that query is a macro. If you don’t like an idea of having multiple macros for graphql, you can just use one super-macro like

graphql do
  query(...) do
    ...
  end
  fragment(...)
end

So you can see, our goal was more about generating chunks of GraphQL from structs rather than more abstract AST transformations

Yeah, I totally understand that. I was just sharing my thoughts I got with morning coffee, hehe

Oh, I just said that as a response to the “this isn’t easy to transform” and “why the tuples?” questions.

Honestly, your approach seems like a much stronger option to me. Curious if anyone else feels the same. :smile:

1 Like