GraphQL Query

CIPackageDocumentationHex.pmHex.pm

GraphQL Query provides a library for validating, parsing, and formatting GraphQL queries and schemas

⚠️ Disclaimer: This library is still in early development. APIs may change as it evolves.


Table of Contents


Why This Library?


Features


Quick Start

Installation

Add graphql_query to your dependencies in mix.exs:

def deps do
  [
    {:graphql_query, "~> 0.5"}
  ]
end

Fetch deps:

mix deps.get

No Rust installation required — precompiled binaries are used.

Examples

You can find more examples in the Cheatsheet, here is a little compilation.

Example: Compile-time Document Validation

import GraphqlQuery

# Valid query
~GQL"""
query GetUser($id: ID!) {
  user(id: $id) {
    name
  }
}
"""

# Invalid query → compile-time warning
~GQL"""
query GetUser($unused: String!) {
  user {
    name
  }
}
"""
# warning: GraphQL validation errors:
# Error: unused variable: `$unused` at file.ex:10:1 - variable is never used

Example: Schema Validation

defmodule MyApp.Schema do
  use GraphqlQuery.Schema, schema_path: "priv/graphql/schema.graphql"
end

defmodule MyApp.Queries do
  use GraphqlQuery, schema: MyApp.Schema

  def get_user_query do
    ~GQL"""
    query GetUser($id: ID!) {
      user(id: $id) {
        id
        name
        email
      }
    }
    """
  end
end

Example: Usage in requests

query = gql [fragments: [@user_fragment]], """
query { ... }
"""

user = "1"
user_query = GraphqlQuery.Document.add_variable(query, :id, user)
Req.post!("/api", json: user_query)

Usage

~GQL Sigil

import GraphqlQuery

# Valid query
~GQL"""
query GetUser($id: ID!) {
  user(id: $id) {
    name
  }
}
"""

# Ignore warnings
~GQL"""
query GetUser($id: ID!, $unused: String) {
  user(id: $id) { name }
}
"""i

# Parse schema
~GQL"""
type User {
  id: ID!
  name: String!
}
"""s

# Parse fragment
~GQL"""
fragment UserData on User {
  id
  name
}
"""f

# Delegate validation to runtime
# Try not to use it, but if you need it you have the option
~GQL"""
query GetUser($id: ID!) {
  user(id: $id) {
    ...UserData
  }
}
"""r |> GraphqlQuery.Document.add_fragment(user_data)

gql_from_file Macro

Example project structure:

priv/
├── graphql/
|   ├── schema.graphql
|   ├── get_user.graphql
|   └── create_user.gql
|   └── user_fragment.gql
defmodule MyApp.Schema do
  use GraphqlQuery.Schema, schema_path: "priv/graphql/schema.graphql"
end

defmodule MyApp.Queries do
  use GraphqlQuery, schema: MyApp.Schema

  @user_fragment gql_from_file "priv/graphql/user_fragment.gql", type: :fragment

  def get_user_query do
    gql_from_file "priv/graphql/get_user.graphql", fragments: [@user_fragment]
  end

  def create_user_mutation do
    gql_from_file "priv/graphql/create_user.gql", schema: MyApp.Schema
  end
end

gql Macro

defmodule Example do
  use GraphqlQuery

  @fields "name email"

  # Expand module attributes
  def query do
    gql """
    query {
      user { #{@fields} }
    }
    """
  end

  # Expand other module calls
  def query_with_eval do
    gql [evaluate: true], """
    query {
      ...#{OtherModule.fragment_name()}
      #{OtherModule.more_fields()}
    }
    #{OtherModule.fragment()}
    """
  end

  # Specify fragments for the query
  def query_with_fragments do
    gql [fragments: [OtherModule.fragment()]], """
    query {
      users {
        ...UserFragment
      }
    }
    """
  end

  # Runtime validation for local variables
  def query_runtime(user_id) do
    gql [runtime: true], """
    query {
      user(id: #{user_id}) { name }
    }
    """
  end

  # Automatic formatting when converting to string
  def formatted_query do
    gql [format: true], """
    query{user{id name}}
    """
    # When converted to string, will be properly formatted:
    # query {
    #   user {
    #     id
    #     name
    #   }
    # }
  end
end

document_with_options Macro

The main use case for the macro document_with_options is to use fragments and schema validation with sigils. The ~GQL sigil doesn't support schema or fragments options directly, but document_with_options enables it:

# This won't work - sigils don't support schema options
~GQL"""
query GetUser { user { id name } }
"""[schema: MySchema]  # ❌ Invalid syntax

# This works perfectly
document_with_options schema: MySchema do
  ~GQL"""
  query GetUser { user { ...UserFragment } }
  """  # ✅ Schema validation applied
end
defmodule MyApp.Schema do
  use GraphqlQuery.Schema, schema_path: "priv/schema.graphql"
end

defmodule MyApp.Queries do
  use GraphqlQuery, schema: MyApp.Schema

  @user_fragment ~GQL"""
  fragment UserFragment on User {
    id
    name
  }
  """f

  def user_query do
    document_with_options fragments: [@user_fragment] do
      ~GQL"""
      query GetUser($id: ID!) {
        user(id: $id) {
          ...UserFragment
        }
      }
      """
    end
  end
end

Use the documents in HTTP requests

At the end, we want to build GraphQL queries to do requests to the GraphQL server.

To make it easy, the GraphQL.Document struct returned by ~GQL, gql_from_file and gql implement the protocol for the standard library JSON and for Jason.

To use queries in requests, you can directly put the query document in the body if the library supports JSON encoding, or manually call JSON.encode!(query) or Jason.encode!(query) to get the request body as a string.

The encoding build a json such as {"query": "document", "variables": {}}. The document is the query or mutation with the fragments (if any) at the end.

Example with Req and GraphQLZero mock server:

base_query = ~GQL"""
query GetUser($id: ID!) {
  user(id: $id) {
    id
    username
    email
    address {
      geo {
        lat
        lng
      }
    }
  }
}
"""
# base_query is a %GraphqlQuery.Document{} struct

# We add variables to create a new document with that information
user_query = GraphqlQuery.Document.add_variable(base_query, :id, "1")

Req.post!("https://graphqlzero.almansi.me/api", json: user_query).body

# %{
#   "data" => %{
#     "user" => %{
#       "address" => %{"geo" => %{"lat" => -37.3159, "lng" => 81.1496}},
#       "email" => "Sincere@april.biz",
#       "id" => "1",
#       "username" => "Bret"
#     }
#   }
# }

Fragment support

You can define your fragments and use them with the macros.

Define fragments

# With sigil
fragment = ~GQL"""
fragment UserFragment on User { id name }
"""f

# With macro
fragment = gql [type: :fragment], """
fragment UserFragment on User { id name }
"""f

# From file
fragment = gql_from_file "fragment.graphql", type: :fragment

Use fragments

# With sigils you have to use the global module registration, or manually set them and validate on runtime:

defmodule UserQuery do
  use GraphqlQuery, fragments: [MyFragments.user_fragment()], schema: UserSchema

  # Use the fragments registered for the module.
  def query do
    ~GQL"""
    query GetUser($id: ID!) {
      user(id: $id) {
        ...UserFragment
      }
    }
    """
  end

  # Evaluate at runtime, and add the fragments later instead of using the global ones
  def runtime_query do
    query = ~GQL"""
    query GetUser($id: ID!) {
      user(id: $id) {
        ...UserFragment
      }
    }
    """r

    GraphqlQuery.Document.add_fragment(query, MyFragments.user_fragment())
  end
end


# With the gql and gql_from_file macros, you can use the module fragments, or per-query fragments:

gql [fragments: [MyFragments.user_fragment()]], """
    query GetUser($id: ID!) {
      user(id: $id) {
        ...UserFragment
      }
    }
"""

gql_from_file "query.graphql", fragments: [MyFragments.user_fragment()]

Schema Support

Parsing and Validating Schemas

With macros:

schema = gql [type: :schema], """
type User { id: ID! name: String! }
type Query { user(id: ID!): User }
"""

schema = gql_from_file "path/to/schema.graphql", type: :schema

Or with sigil:

~GQL"""
type User { id: ID! name: String! }
type Query { user(id: ID!): User }
"""s

Schema Modules

From GraphQL Files

Automatically implement the behaviour with a schema file:

defmodule MyApp.Schema do
  use GraphqlQuery.Schema, schema_path: "priv/graphql/schema.graphql"
end

From Absinthe Schemas

Automatically extract schema from existing Absinthe schema modules, really useful specially for testing:

defmodule MyApp.Schema do
  use GraphqlQuery.Schema, absinthe_schema: MyAppWeb.Graphql.Schema
end

Manual Implementation

Or manually implement the behaviour:

defmodule MyApp.Schema do
  use GraphqlQuery.Schema

  @impl GraphqlQuery.Schema
  def schema do
    ~GQL"""
    type User { id: ID! name: String! }
    type Query { user(id: ID!): User }
    """s
  end

  @impl GraphqlQuery.Schema
  def schema_path, do: nil
end

Document Validation Against Schema

You can validate against the schema any document (queries, mutations or fragments)

Per-document validation:

gql [schema: MyApp.Schema], """
query GetUser($id: ID!) {
  user(id: $id) { name email }
}
"""

gql_from_file "path.graphql", [schema: MyApp.Schema]

Module-level schema:

defmodule MyApp.Queries do
  use GraphqlQuery, schema: MyApp.Schema

  def get_users do
    ~GQL"""
    query { users { id name } }
    """
  end

  def get_user(user_id) do
    # It is recommended to use GraphQL variables, this is just an example to showcase runtime validation with schema
    gql [runtime: true], """
    query GetUserById { user(id: "#{user_id}") { name } }
    """
  end
end

Apollo Federation Support

GraphQL Query supports Apollo Federation v2 directives, allowing you to validate federated schemas that use directives like @key, @shareable, @external, and others.

Enabling Federation Support

Enable federation validation by setting the federation: true option. This makes all Apollo Federation v2 directives available in your schema.

With sigil (using F modifier):

~GQL"""
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@shareable"])

type User @key(fields: "id") {
  id: ID!
  name: String! @shareable
}
"""sF  # s = schema, F = federation

With gql macro:

gql [type: :schema, federation: true], """
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

type Product @key(fields: "id") {
  id: ID!
  price: Float
}
"""

With gql_from_file macro:

gql_from_file "priv/graphql/federated_schema.graphql", type: :schema, federation: true

With document_with_options:

document_with_options type: :schema, federation: true do
  ~GQL"""
  extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
  
  type Order @key(fields: "id") {
    id: ID!
    total: Float
  }
  """s
end

At module level:

defmodule MyApp.FederatedSchema do
  use GraphqlQuery, federation: true
  
  def schema do
    ~GQL"""
    extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])
    
    type User @key(fields: "id") {
      id: ID!
      name: String!
    }
    """s
  end
end

Supported Federation Versions

The library supports multiple Apollo Federation v2 versions. Specify the version in your @link directive:

Unknown versions automatically fall back to standard validation without federation directives.

Implementation Note

When federation: true is enabled, all Apollo Federation directives are available for use in your schema, regardless of what you specify in the @link import list. This simplified approach covers the vast majority of use cases and makes it easier to work with federated schemas.

The following directives are available:


Formatter Integration

Add to .formatter.exs:

[
  inputs: ["{lib,test,priv}/**/*.{ex,exs,graphql,gql}"],
  plugins: [GraphqlQuery.Formatter],
  import_deps: [:graphql_query]
]

Now mix format will:


Manual API

You shouldn't need to use the manual API, but if you need to, you can do everything yourself.

Check the documentation of these modules if you want to know more about the manual API:


Roadmap

Planned

Done


License

Beerware 🍺 — do whatever you want with it, but if we meet, buy me a beer. (This is essentially MIT-like. Use it freely, but if we meet, buy me a beer)


Links