gleamql/field
Field builders for constructing GraphQL field selections with synchronized decoders.
This module provides the core building blocks for constructing GraphQL queries and mutations while ensuring the query structure and response decoder stay in sync.
Basic Usage
import gleamql/field
// Simple scalar field
let name_field = field.string("name")
// List of strings
let tags_field = field.list(field.string("tags"))
// Optional field
let nickname_field = field.optional(field.string("nickname"))
Inline Fragments
Inline fragments allow you to conditionally select fields based on type or to group fields together with directives.
// Querying a union type
field.object("search", fn() {
use user <- field.field(field.inline_on("User", fn() {
use name <- field.field(field.string("name"))
field.build(name)
}))
field.build(user)
})
// Generates: search { ... on User { name } }
// Grouping fields with directives
field.inline(builder)
|> field.with_directive(directive.include("var"))
Types
Arguments that can be passed to GraphQL fields.
Supports both variables (defined in the operation) and inline literal values.
pub type Argument {
Variable(name: String)
InlineString(value: String)
InlineInt(value: Int)
InlineFloat(value: Float)
InlineBool(value: Bool)
InlineNull
InlineObject(fields: List(#(String, Argument)))
InlineList(items: List(Argument))
}
Constructors
-
Variable(name: String)A reference to a variable: $variableName
-
InlineString(value: String)An inline string literal: “value”
-
InlineInt(value: Int)An inline integer literal: 42
-
InlineFloat(value: Float)An inline float literal: 3.14
-
InlineBool(value: Bool)An inline boolean literal: true or false
-
InlineNullAn inline null value
-
InlineObject(fields: List(#(String, Argument)))An inline object: { key: value, … }
-
InlineList(items: List(Argument))An inline list: [item1, item2, …]
A Field represents a GraphQL field with its selection set and decoder.
The Field type keeps the GraphQL selection string and the response decoder synchronized, ensuring they can never get out of sync.
pub opaque type Field(a)
Internal type for building object field selections.
pub opaque type ObjectBuilder(a)
The selection set for a field - either a scalar (leaf) or object (nested fields).
pub type SelectionSet {
Scalar
Object(fields: String)
FragmentSpread(name: String)
InlineFragment(
type_condition: option.Option(String),
fields: String,
)
PhantomRoot(fields: String)
}
Constructors
-
ScalarA scalar field with no nested selection (e.g., name, id, count)
-
Object(fields: String)An object field with nested field selections
-
FragmentSpread(name: String)A fragment spread: …FragmentName
-
InlineFragment( type_condition: option.Option(String), fields: String, )An inline fragment: … on TypeName { fields } or … { fields }
-
PhantomRoot(fields: String)A phantom root for multiple root-level fields. This selection type exists only in the builder - it renders its children directly without wrapping them in a named field.
Values
pub fn arg(
fld: Field(a),
arg_name: String,
var_name: String,
) -> Field(a)
Add a single variable argument to a field.
This is a helper for the common case of passing a variable to a field.
Example
field.object("country", country_builder)
|> field.arg("code", "code")
// Generates: country(code: $code) { ... }
pub fn arg_bool(
fld: Field(a),
arg_name: String,
value: Bool,
) -> Field(a)
Add an inline bool argument to a field.
Example
field.object("posts", posts_builder)
|> field.arg_bool("published", True)
// Generates: posts(published: true) { ... }
pub fn arg_float(
fld: Field(a),
arg_name: String,
value: Float,
) -> Field(a)
Add an inline float argument to a field.
Example
field.object("products", products_builder)
|> field.arg_float("minPrice", 9.99)
// Generates: products(minPrice: 9.99) { ... }
pub fn arg_int(
fld: Field(a),
arg_name: String,
value: Int,
) -> Field(a)
Add an inline int argument to a field.
Example
field.object("posts", posts_builder)
|> field.arg_int("first", 10)
// Generates: posts(first: 10) { ... }
pub fn arg_list(
fld: Field(a),
arg_name: String,
items: List(Argument),
) -> Field(a)
Add an inline list argument to a field.
Example
field.object("users", users_builder)
|> field.arg_list("ids", [InlineString("1"), InlineString("2")])
// Generates: users(ids: ["1", "2"]) { ... }
pub fn arg_object(
fld: Field(a),
arg_name: String,
fields: List(#(String, Argument)),
) -> Field(a)
Add an inline object argument to a field.
This is commonly used for mutation input objects.
Example
field.object("createPost", create_post_builder)
|> field.arg_object("input", [
#("title", InlineString("My Post")),
#("body", InlineString("Content here")),
])
// Generates: createPost(input: { title: "My Post", body: "Content here" }) { ... }
pub fn arg_string(
fld: Field(a),
arg_name: String,
value: String,
) -> Field(a)
Add an inline string argument to a field.
Example
field.object("country", country_builder)
|> field.arg_string("code", "GB")
// Generates: country(code: "GB") { ... }
pub fn argument_to_string(arg: Argument) -> String
Convert an Argument to its GraphQL string representation.
This is used internally to generate GraphQL query strings and is also exposed for use by the directive module.
pub fn bool(name: String) -> Field(Bool)
Create a Bool field.
Example
let active_field = field.bool("isActive")
// Generates: isActive
// Decodes: Bool
pub fn build(value: a) -> ObjectBuilder(a)
Complete the object with a constructor.
This is the final step in building an object field. It takes the constructed value and wraps it in a field.
Example
field.object("country", fn() {
use name <- field.field(field.string("name"))
field.build(Country(name:)) // <- Complete with constructor
})
pub fn field(
fld: Field(b),
next: fn(b) -> ObjectBuilder(a),
) -> ObjectBuilder(a)
Add a field to the object being built.
This function is designed to be used with the use keyword to chain
multiple fields together in a codec-style builder.
Example
field.object("person", fn() {
use name <- field.field(field.string("name"))
use age <- field.field(field.int("age"))
field.build(Person(name:, age:))
})
pub fn field_as(
alias: String,
fld: Field(b),
next: fn(b) -> ObjectBuilder(a),
) -> ObjectBuilder(a)
Add a field with an alias to the object being built.
This function is similar to field() but allows you to specify an alias
for the field. The alias will be used as the key in the response object.
Example
field.object("user", fn() {
use small_pic <- field.field_as("smallPic",
field.string("profilePic")
|> field.arg_int("size", 64))
use large_pic <- field.field_as("largePic",
field.string("profilePic")
|> field.arg_int("size", 1024))
field.build(#(small_pic, large_pic))
})
// Generates: user { smallPic: profilePic(size: 64) largePic: profilePic(size: 1024) }
pub fn float(name: String) -> Field(Float)
Create a Float field.
Example
let price_field = field.float("price")
// Generates: price
// Decodes: Float
pub fn fragments(field: Field(a)) -> List(String)
Get the fragments used by this field (internal use).
pub fn from_fragment_spread(
fragment_name: String,
fragment_decoder: decode.Decoder(a),
) -> Field(a)
Create a field from a fragment spread (internal use by fragment module).
This creates a field that represents a fragment spread (…FragmentName) in the GraphQL query. The field has an empty name since fragment spreads don’t have field names.
pub fn from_fragment_spread_with_definition(
fragment_name: String,
fragment_decoder: decode.Decoder(a),
fragment_definition: String,
) -> Field(a)
Create a field from a fragment spread with the fragment definition (internal use by fragment module).
pub fn from_fragment_spread_with_directives(
fragment_name: String,
fragment_decoder: decode.Decoder(a),
fragment_definition: String,
fragment_directives: List(directive.Directive),
) -> Field(a)
Create a field from a fragment spread with directives (internal use by fragment module).
pub fn id(name: String) -> Field(String)
Create an ID field (decoded as String).
GraphQL IDs are always decoded as strings, even if they look like numbers.
Example
let id_field = field.id("id")
// Generates: id
// Decodes: String
pub fn inline(builder: fn() -> ObjectBuilder(a)) -> Field(a)
Create an inline fragment without a type condition.
Inline fragments without type conditions are used to group fields together, typically to apply directives to multiple fields at once without affecting the parent type condition.
Example - Grouping fields with directives
field.object("user", fn() {
use name <- field.field(field.string("name"))
use private_data <- field.field(
field.inline(fn() {
use email <- field.field(field.string("email"))
use phone <- field.field(field.string("phone"))
field.build(#(email, phone))
})
|> field.with_directive(directive.include("showPrivate"))
)
field.build(User(name:, private: private_data))
})
// Generates: user { name ... @include(if: $showPrivate) { email phone } }
pub fn inline_on(
type_condition: String,
builder: fn() -> ObjectBuilder(a),
) -> Field(a)
Create an inline fragment with a type condition.
Inline fragments with type conditions are used to select fields based on the runtime type of an interface or union field. This is essential for querying polymorphic types in GraphQL.
Example - Querying a union type
// GraphQL schema:
// union SearchResult = User | Post | Comment
field.object("search", fn() {
use user_result <- field.field(
field.inline_on("User", fn() {
use name <- field.field(field.string("name"))
use email <- field.field(field.string("email"))
field.build(UserResult(name:, email:))
})
)
use post_result <- field.field(
field.inline_on("Post", fn() {
use title <- field.field(field.string("title"))
field.build(PostResult(title:))
})
)
field.build(SearchResults(user: user_result, post: post_result))
})
// Generates: search { ... on User { name email } ... on Post { title } }
Example - Querying an interface type
field.object("node", fn() {
use common_id <- field.field(field.id("id"))
use user_fields <- field.field(
field.inline_on("User", fn() {
use name <- field.field(field.string("name"))
field.build(name)
})
)
field.build(#(common_id, user_fields))
})
// Generates: node { id ... on User { name } }
pub fn int(name: String) -> Field(Int)
Create an Int field.
Example
let age_field = field.int("age")
// Generates: age
// Decodes: Int
pub fn is_phantom_root(field: Field(a)) -> Bool
Check if a field is a phantom root.
Phantom roots are used internally by operation.root() to support
multiple root fields while maintaining type safety.
Example
let phantom = field.phantom_root(builder)
field.is_phantom_root(phantom) // -> True
let regular = field.object("user", builder)
field.is_phantom_root(regular) // -> False
pub fn list(field: Field(a)) -> Field(List(a))
Wrap a field as a list.
GraphQL lists are decoded as Gleam lists. The inner field’s decoder is applied to each item in the list.
Example
let tags_field = field.list(field.string("tags"))
// Generates: tags
// Decodes: List(String)
You can also combine with optional:
// List of optional strings
let items_field = field.list(field.optional(field.string("items")))
// Decodes: List(Option(String))
// Optional list of strings
let maybe_tags_field = field.optional(field.list(field.string("tags")))
// Decodes: Option(List(String))
pub fn object(
name: String,
builder: fn() -> ObjectBuilder(a),
) -> Field(a)
Build an object field with multiple nested fields using a codec-style builder.
This is the core function for building GraphQL object selections. It uses
a continuation-passing style with use expressions to build up both the
field selection string and the decoder simultaneously.
Example
pub type Country {
Country(name: String, code: String)
}
fn country_field() {
field.object("country", fn() {
use name <- field.field(field.string("name"))
use code <- field.field(field.string("code"))
field.build(Country(name:, code:))
})
}
// Generates: country { name code }
// Decodes: Country
For nested objects:
pub type Data {
Data(country: Country)
}
fn data_field() {
field.object("data", fn() {
use country <- field.field(country_field())
field.build(Data(country:))
})
}
// Generates: data { country { name code } }
pub fn object_builder_decoder(
builder: ObjectBuilder(a),
) -> decode.Decoder(a)
Get the decoder from an ObjectBuilder (internal use).
pub fn object_builder_to_selection(
builder: ObjectBuilder(a),
) -> String
Extract the selection string from an ObjectBuilder (internal use).
This is used by the fragment module to get the field selection string from an ObjectBuilder.
pub fn optional(field: Field(a)) -> Field(option.Option(a))
Wrap a field as optional (nullable in GraphQL).
GraphQL fields can be nullable. This function wraps a field’s decoder
to handle null values, returning None for null and Some(value) for present values.
Example
let nickname_field = field.optional(field.string("nickname"))
// Generates: nickname
// Decodes: Option(String)
pub fn phantom_root(
builder: fn() -> ObjectBuilder(a),
) -> Field(a)
Create a phantom root field for multiple root-level selections.
Internal use only - Users should use operation.root() instead.
A phantom root exists only in the builder API. When rendered to GraphQL, its child fields are output directly at the operation level without any wrapper field. This enables type-safe multiple root field operations.
Example
// Internal usage (via operation.root):
let phantom = field.phantom_root(fn() {
use user <- field.field(user_field())
use posts <- field.field(posts_field())
field.build(#(user, posts))
})
Generates: user { ... } posts { ... } (no wrapper)
pub fn string(name: String) -> Field(String)
Create a String field.
Example
let name_field = field.string("name")
// Generates: name
// Decodes: String
pub fn to_selection(field: Field(a)) -> String
Build the GraphQL selection string for a field.
This is an internal function used to generate the actual GraphQL query text.
pub fn with_args(
fld: Field(a),
args: List(#(String, Argument)),
) -> Field(a)
Add multiple arguments to a field.
Example
field.object("posts", posts_builder)
|> field.with_args([
#("first", Variable("limit")),
#("after", InlineString("cursor123")),
])
// Generates: posts(first: $limit, after: "cursor123") { ... }
pub fn with_directive(
fld: Field(a),
dir: directive.Directive,
) -> Field(a)
Add a directive to a field.
Directives modify the behavior of fields at execution time. Common directives include @skip and @include for conditional field inclusion.
Example
import gleamql/directive
field.string("name")
|> field.with_directive(directive.skip("shouldSkipName"))
// Generates: name @skip(if: $shouldSkipName)
Multiple directives can be chained:
field.string("email")
|> field.with_directive(directive.include("showEmail"))
|> field.with_directive(directive.deprecated(Some("Use emailAddress instead")))
// Generates: email @include(if: $showEmail) @deprecated(reason: "Use emailAddress instead")
pub fn with_directives(
fld: Field(a),
dirs: List(directive.Directive),
) -> Field(a)
Add multiple directives to a field at once.
This is a convenience function for adding multiple directives in one call.
Example
import gleamql/directive
field.string("profile")
|> field.with_directives([
directive.include("showProfile"),
directive.deprecated(Some("Use profileV2")),
])