gleamql/operation

Operation builders for constructing GraphQL queries and mutations.

This module provides functions for building complete GraphQL operations with variable definitions and root fields. It supports both single and multiple root field operations.

Basic Usage - Single Root Field

import gleamql/operation
import gleamql/field

let country_op = 
  operation.query("CountryQuery")
  |> operation.variable("code", "ID!")
  |> operation.field(country_field())

Multiple Root Fields

Query multiple fields at the root level while maintaining type safety:

operation.query("GetDashboard")
|> operation.variable("userId", "ID!")
|> operation.root(fn() {
  use user <- field.field(user_field())
  use posts <- field.field(posts_field())
  use stats <- field.field(stats_field())
  field.build(#(user, posts, stats))
})

This generates clean GraphQL without wrapper fields:

query GetDashboard($userId: ID!) {
  user { ... }
  posts { ... }
  stats { ... }
}

Types

A complete GraphQL operation (query or mutation) with its decoder.

pub opaque type Operation(a)

Builder for constructing operations.

pub opaque type OperationBuilder(a)

The type of GraphQL operation.

pub type OperationType {
  Query
  Mutation
}

Constructors

  • Query
  • Mutation

A variable definition for a GraphQL operation.

pub type VariableDef {
  VariableDef(name: String, type_def: String)
}

Constructors

  • VariableDef(name: String, type_def: String)

Values

pub fn anonymous_mutation() -> OperationBuilder(a)

Create an anonymous mutation operation.

Example

operation.anonymous_mutation()
|> operation.field(create_post_field())
|> operation.build()
pub fn anonymous_query() -> OperationBuilder(a)

Create an anonymous query operation.

Anonymous operations have no name and are useful for simple queries.

Example

operation.anonymous_query()
|> operation.field(countries_field())
|> operation.build()
pub fn build(operation: Operation(a)) -> Operation(a)

Alias for field that builds the operation.

pub fn build_variables(
  operation: Operation(a),
  values: List(#(String, json.Json)),
) -> json.Json

Build the variables JSON object for the GraphQL request.

Takes a list of variable name/value pairs and constructs the variables object to send with the request.

pub fn decoder(operation: Operation(a)) -> decode.Decoder(a)

Get the decoder from an operation.

This decoder will automatically unwrap the “data” field from the GraphQL response.

pub fn field(
  builder: OperationBuilder(a),
  root_field: field.Field(a),
) -> Operation(a)

Set the root field for the operation and build the final Operation.

This completes the operation builder and generates the GraphQL query string. Fragments used in the field tree are automatically collected and included in the operation.

Example

operation.query("GetCountry")
|> operation.variable("code", "ID!")
|> operation.field(country_field())
|> operation.build()
pub fn fragment(
  builder: OperationBuilder(a),
  frag: fragment.Fragment(b),
) -> OperationBuilder(a)

Add a fragment definition to the operation (optional).

Note: As of version 1.0.0, fragments are automatically collected when you use fragment.spread(), so this function is optional. You only need to use it if you want to explicitly register a fragment that isn’t used in the current operation’s fields.

For most use cases, simply use fragment.spread() in your field selections and the fragment will be automatically included.

Example (modern approach - auto-collection)

import gleamql/fragment

let user_fields = fragment.on("User", "UserFields", fn() {
  use id <- field.field(field.id("id"))
  use name <- field.field(field.string("name"))
  field.build(User(id:, name:))
})

// No need to call operation.fragment() - it's auto-collected!
operation.query("GetUser")
|> operation.variable("id", "ID!")
|> operation.field(
  field.object("user", fn() {
    use user_data <- field.field(fragment.spread(user_fields))
    field.build(user_data)
  })
)

Example (legacy approach - manual registration)

// You can still manually register if needed
operation.query("GetUser")
|> operation.fragment(user_fields)  // Optional - for backwards compatibility
|> operation.variable("id", "ID!")
|> operation.field(user_field())
pub fn mutation(name: String) -> OperationBuilder(a)

Create a named mutation operation.

Example

operation.mutation("CreatePost")
|> operation.variable("input", "CreatePostInput!")
|> operation.field(create_post_field())
|> operation.build()
pub fn query(name: String) -> OperationBuilder(a)

Create a named query operation.

Example

operation.query("GetCountry")
|> operation.variable("code", "ID!")
|> operation.field(country_field())
|> operation.build()
pub fn root(
  builder: OperationBuilder(b),
  root_builder: fn() -> field.ObjectBuilder(a),
) -> Operation(a)

Set multiple root fields for the operation using a builder pattern.

This allows you to query multiple root fields in a single operation while maintaining full type safety. The builder pattern is identical to field.object(), but the generated GraphQL will not wrap the fields in an additional object.

Example

pub type UserAndPosts {
  UserAndPosts(user: User, posts: List(Post))
}

operation.query("GetData")
|> operation.variable("userId", "ID!")
|> operation.root(fn() {
  use user <- field.field(
    field.object("user", user_builder)
    |> field.arg("id", "userId")
  )
  use posts <- field.field(
    field.list(field.object("posts", post_builder))
    |> field.arg_int("limit", 10)
  )
  field.build(UserAndPosts(user:, posts:))
})

Generates:

query GetData($userId: ID!) {
  user(id: $userId) { ... }
  posts(limit: 10) { ... }
}

Single Root Field

You can also use root() with a single field (though field() is simpler):

operation.root(fn() {
  use user <- field.field(user_field())
  field.build(user)
})

Backward Compatibility

The existing field() function continues to work for single-field operations. Use root() when you need multiple root fields or want a consistent API.

pub fn to_string(operation: Operation(a)) -> String

Get the GraphQL query string from an operation.

pub fn variable(
  builder: OperationBuilder(a),
  name: String,
  type_def: String,
) -> OperationBuilder(a)

Add a variable definition to the operation.

Variables allow you to parameterize your operations and reuse them with different values.

Example

operation.query("GetCountry")
|> operation.variable("code", "ID!")  // Non-null ID
|> operation.variable("lang", "String")  // Optional String

The type definition should be a valid GraphQL type:

  • Scalars: "String", "Int", "Float", "Boolean", "ID"
  • Non-null: "String!", "ID!"
  • Lists: "[String]", "[ID!]!"
  • Custom types: "CreatePostInput!", "[UserInput!]"
pub fn variable_names(operation: Operation(a)) -> List(String)

Get the list of variable names defined in the operation.

This is useful for knowing which variables need values at send time.

Search Document