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}
]
endPlease 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
new/1- Create a new GraphQL documentparse/1- Parse a GraphQL string into a documentparse_file/1- Parse a GraphQL file into a document
Operation Configuration
type/2- Set operation type (query, mutation, subscription)name/2- Set operation name
Variables
variable/3- Add a variable definitionremove_variable/2- Remove a variable definitioninline_variables/2- Replace variables with values
Fields
field/3- Add a fieldremove_field/3- Remove a fieldreplace_field/3- Replace a field
Arguments
argument/3- Add an argument to a fieldreplace_argument/3- Update an argument valueremove_argument/3- Remove an argument
Directives
directive/4- Add a directive to a field or operation (use empty path[]for operation)
Fragments
fragment/2- Define a named fragment (withname:andtype:options) or add an inline fragment (withpath:andtype:options)remove_fragment/2- Remove a fragment definitionspread_fragment/3- Spread a fragment into a selectioninline_fragments/1- Replace all fragment spreads with their content
Utilities
merge/2- Merge two documentsinject_typenames/1- Add __typename to all selectionsis_operation/1- Guard for operation types