GQL

A composable query builder for dynamically constructing and manipulating GraphQL queries, mutations, and subscriptions in Elixir.

Overview

GQL builds upon Absinthe to allow users to dynamically build GraphQL queries. While Absinthe provides Blueprint-type structs to build schema-specific documents, GQL focuses on schemaless building of queries based on the Absinthe.Language.Document struct.

This library provides a programmatic way to build GraphQL documents as data structures, similar to how Ecto.Query makes SQL queries composable. Instead of working with static query strings, you can dynamically create, merge, and transform GraphQL operations using a functional API.

Installation

Add gql to your list of dependencies in mix.exs:

def deps do
  [
    {:gql, "~> 0.1.0", hex: :gql_builder}
  ]
end

Please notice that the Hex package name and the project name are different, because the package name gql was already taken.

Quick Start

Creating a Basic Query

Start with a new document and add fields:

GQL.new()
|> GQL.name("contact")
|> GQL.field(:user)
|> GQL.field(:name, path: [:user])
|> GQL.field(:email, path: [:user])

This generates:

query contact {
  user {
    name
    email
  }
}

Parsing Existing Queries

Use the ~GQL sigil to parse an inline GraphQL documents from the Elixir source:

import GQL

~GQL[query { user(id: 19) { id } }]
|> GQL.field(:mailbox_size, path: [:user])
|> GQL.type(:subscription)

This generates:

subscription {
  user(id: 19) {
    mailbox_size
    id
  }
}

The ~GQL sigil validates syntax at compile time while still allowing runtime manipulation of the document structure.

Core Concepts

Creating Documents

Create a new empty GraphQL document:

GQL.new()

You can also initialize with options:

GQL.new(name: "test", field: "__typename")

This generates:

query test {
  __typename
}

Or build more complex queries inline:

GQL.new(
  field: {"posts", alias: "p", args: %{id: 42}},
  field: {:title, alias: "t", path: ["p"]},
  field: {:author, path: "p", alias: "a"}
)

This generates:

query {
  p: posts(id: 42) {
    t: title
    a: author
  }
}

You can take a GraphQL query represented by a binary as the base of the GQL funcitons:

"query { temperature }" |> GQL.argument(:unit, path: "temperature", value: :CELSIUS)

This generates:

query {
  temperature(unit: CELSIUS)
}

Operation Types

Set the operation type to query, mutation, or subscription:

GQL.new(field: :field)
|> GQL.type(:subscription)

This generates:

subscription {
  field
}

Naming Operations

Assign a name to your operation for better logging and observability:

GQL.new(field: :field)
|> GQL.name(:hello)

This generates:

query hello {
  field
}

Working with Fields

Add fields to your document:

GQL.new()
|> GQL.field(:id)
|> GQL.field(:name)
|> GQL.field(:email)

This generates:

query {
  email
  name
  id
}

Use paths to add nested fields:

GQL.new()
|> GQL.field(:id, path: ["blogs", "posts"])

This creates:

query {
  blogs {
    posts {
      id
    }
  }
}

Nested Field Definitions

Use the fields option to define nested fields more concisely:

GQL.new()
|> GQL.field(:user, fields: [:id, :name, :email])

This generates:

query {
  user {
    id
    name
    email
  }
}

Subfields can also have options like alias and args:

GQL.new()
|> GQL.field(:user, fields: [
  :id,
  {:name, alias: "fullName"},
  {:posts, args: %{limit: 5}, fields: [:title, :content]}
])

This generates:

query {
  user {
    id
    fullName: name
    posts(limit: 5) {
      title
      content
    }
  }
}

Field Aliases

Add aliases to fields:

GQL.new(field: {:posts, alias: "p", args: %{id: 42}})

This generates:

query {
  p: posts(id: 42)
}

Removing and Replacing Fields

Remove fields from your document:

"query { apple { foo bar baz } banana }"
|> GQL.remove_field(:banana)
|> GQL.remove_field(:baz, path: ["apple"])

This generates:

query {
  apple {
    foo
    bar
  }
}

Replace a field with a new definition:

"query { user { id name email } }"
|> GQL.replace_field(:user, args: %{id: 42})

This generates:

query {
  user(id: 42) {
    id
    name
    email
  }
}

Composable Queries

Build queries from reusable components:

def base_user_fields(query, path) do
  query
  |> GQL.field(:id, path: path)
  |> GQL.field(:name, path: path)
end

def with_posts(query, path) do
  query
  |> GQL.field(:posts, path: path)
  |> GQL.field(:title, path: List.wrap(path) ++ [:posts])
end

GQL.new(field: :user)
|> base_user_fields("user")
|> with_posts("user")

This generates:

query {
  user {
    id
    name
    posts {
      title
    }
  }
}

This composable approach allows you to build complex queries from simple, testable building blocks, making it easier to maintain and reuse query logic throughout your application.

Variables

Defining Variables

Add variable definitions to your operation:

GQL.new()
|> GQL.variable(:id, type: "ID")
|> GQL.field(:user, args: %{id: "$id"})
|> GQL.field(:name, path: [:user])
|> GQL.name(:GetUser)

This generates:

query GetUser($id: ID!) {
  user(id: $id) {
    name
  }
}

Variable Options

Specify types, defaults, and optionality:

GQL.new()
|> GQL.type(:mutation)
|> GQL.variable(:id, type: ID, optional: true)
|> GQL.variable(:key, type: Integer)
|> GQL.variable(:name, default: "Joe")
|> GQL.variable(:age, default: 42)
|> GQL.field(:add_user, args: %{name: "$name", age: "$age", id: "$id"})

This generates:

mutation Mutation($id: ID, $key: Integer!, $name: String! = "Joe", $age: Integer! = 42) {
  add_user(name: $name, age: $age, id: $id)
}

Removing Variables

Remove variable definitions:

"query hello($id: ID!, $semver: Boolean! = true) { serverVersion(semver: $semver) }"
|> GQL.remove_variable(:id)

This generates:

query hello($semver: Boolean! = true) {
  serverVersion(semver: $semver)
}

Inlining Variables

Replace variable references with concrete values:

"query Q($id: ID!) { get(id: $id) { name } }"
|> GQL.inline_variables(%{id: 42})

This generates:

query Q {
  get(id: 42) {
    name
  }
}

Arguments

Adding Arguments

Attach arguments to fields:

"query { hello }"
|> GQL.argument(:who, path: ["hello"], value: "World!")

This generates:

query {
  hello(who: "World!")
}

Replacing Arguments

Update existing argument values:

"query { user(id: 42) { name } }"
|> GQL.replace_argument(:id, path: ["user"], value: 99)

This generates:

query {
  user(id: 99) {
    name
  }
}

Removing Arguments

Delete specific arguments:

"query { user(id: 42, name: \"John\") { email } }"
|> GQL.remove_argument(:name, ["user"])

This generates:

query {
  user(id: 42) {
    email
  }
}

Directives

Adding Directives to Fields

Add directives like @include or @skip to fields:

"query { user { name email } }"
|> GQL.directive("include", ["user"], %{if: "$showUser"})

This generates:

query {
  user @include(if: $showUser) {
    name
    email
  }
}

Add directives to nested fields:

"query { user { name email } }"
|> GQL.directive("skip", ["user", "email"], %{if: "$hideEmail"})

This generates:

query {
  user {
    name
    email @skip(if: $hideEmail)
  }
}

Adding Directives to Operations

Add directives to the operation itself (query/mutation/subscription) by using an empty path:

GQL.new(field: :user)
|> GQL.field(:name, path: [:user])
|> GQL.directive("cached", [])

This generates:

query @cached {
  user {
    name
  }
}

With arguments:

GQL.new(field: :user)
|> GQL.directive("rateLimit", [], %{max: 100, window: 60})

This generates:

query @rateLimit(max: 100, window: 60) {
  user
}

Fragments

Named Fragments

Define reusable named fragments:

GQL.new()
|> GQL.fragment(name: :UserFields, type: :User)
|> GQL.field(:name, path: [:UserFields])
|> GQL.field(:email, path: [:UserFields])

This generates:

query {
}
fragment UserFields on User {
  email
  name
}

Spreading Fragments

Use fragment spreads to include fragments in queries:

GQL.new()
|> GQL.fragment(name: :UserFields, type: :User)
|> GQL.field(:name, path: [:UserFields])
|> GQL.field(:email, path: [:UserFields])
|> GQL.field(:user)
|> GQL.spread_fragment(:UserFields, path: [:user])

This generates:

query {
  user {
    ...UserFields
  }
}
fragment UserFields on User {
  email
  name
}

Nesting fields and named fragments into each other

Use the fields option to define nested fields more concisely.

Use the spread field option to inlcude a named fragment more concisely:

GQL.new()
|> GQL.fragment(name: :UserFields, type: :User, fields: [:name, :email])
|> GQL.field(:user, spread: [:UserFields])

This generates:

query {
  user {
    ...UserFields
  }
}
fragment UserFields on User {
  email
  name
}

Inlining Named Fragments

Replace fragment spreads with their actual content:

GQL.new()
|> GQL.fragment(name: :UserFields, type: :User)
|> GQL.field(:name, path: [:UserFields])
|> GQL.field(:email, path: [:UserFields])
|> GQL.field(:user)
|> GQL.spread_fragment(:UserFields, path: [:user])
|> GQL.inline_fragments()

This generates:

query {
  user {
    name
    email
  }
}

The inline_fragments/1 function replaces all fragment spreads with the fields from the fragment definition and removes the fragment definitions. This is useful when you want to convert a document with named fragments into a single inline query.

Removing Fragments

Delete fragment definitions:

"query { user { id } }"
|> GQL.fragment(name: :UserFields, type: :User)
|> GQL.remove_fragment(:UserFields)

This generates:

query {
  user {
    id
  }
}

Inline Fragments

Add inline fragments for handling union or interface types:

GQL.new()
|> GQL.field(:search, args: %{term: "elixir"})
|> GQL.fragment(type: :User, path: [:search])
|> GQL.field(:name, path: [:search, {nil, type: :User}])
|> GQL.field(:email, path: [:search, {nil, type: :User}])
|> GQL.fragment(type: :Post, path: [:search])
|> GQL.field(:title, path: [:search, {nil, type: :Post}])
|> GQL.field(:content, path: [:search, {nil, type: :Post}])

This generates:

query {
  search(term: "elixir") {
    ... on Post {
      content
      title
    }
    ... on User {
      email
      name
    }
  }
}

You can also use the fields option to add subfields directly:

GQL.new()
|> GQL.field(:search, args: %{term: "elixir"})
|> GQL.fragment(type: :User, path: [:search], fields: [:name, :email])
|> GQL.fragment(type: :Post, path: [:search], fields: [:title, :content])

This generates the same output as above but more concisely.

You can also use the spread_on field option to add inline fragments to a field:

GQL.new()
|> GQL.field(:user, args: %{id: 42}, fields: [:id, :name], spread_on: [{:Admin, fields: [:permissions]}])

This generates:

query {
  user(id: 42) {
    id
    name
    ... on Admin {
      permissions
    }
  }
}

Utilities

Merging Documents

Combine two GraphQL documents:

doc1 = "query { user { id } }"
doc2 = "query { posts { title } }"
GQL.merge(doc1, doc2)

This generates:

query {
  user {
    id
  }
  posts {
    title
  }
}

When merging documents with the same fields, they are intelligently deduplicated and their subfields are merged:

doc1 = "query { user { id } }"
doc2 = "query { user { name } }"
GQL.merge(doc1, doc2)

This generates:

query {
  user {
    id
    name
  }
}

Parsing from Files

Load and parse GraphQL documents from files:

GQL.parse_file("/path/to/query.graphql")

Injecting Typenames

Automatically add __typename to all object selections:

"query { apple { foo bar { baz } } }"
|> GQL.inject_typenames()

This generates:

query {
  __typename
  apple {
    __typename
    foo
    bar {
      __typename
      baz
    }
  }
}

This is particularly useful when working with GraphQL clients that require typename information for caching and normalization.

Type Guards

GQL provides a guard to check for valid operation types:

import GQL

def my_function(doc, type) when is_operation(type) do
  doc
  |> type(type)
  |> name("My#{type |> to_string() |> String.capitalize()}")
end

The is_operation/1 guard checks if the argument is one of :query, :mutation, or :subscription.

Converting to Strings

Documents automatically convert to GraphQL strings:

doc = GQL.new(field: :user)
to_string(doc)

# or

"#{doc}"

API Reference

Document Creation

Operation Configuration

Variables

Fields

Arguments

Directives

Fragments

Utilities