defmodule TerminusDB.GraphQL do
@moduledoc """
GraphQL API for TerminusDB.
Wraps the `/api/graphql/{org}/{db}` endpoint. TerminusDB auto-generates a
GraphQL schema from the database's document schema, supporting queries
(filter, limit, offset, orderBy, backlinks, path queries, count) and
mutations (insert/replace/delete documents).
This is a **thin HTTP wrapper** — users write raw GraphQL query strings.
A programmatic query builder DSL is planned for a future release.
## Quick start
config =
TerminusDB.Config.new(endpoint: "http://localhost:6363")
|> TerminusDB.Config.with_database("mydb")
# Query
{:ok, result} = TerminusDB.GraphQL.query(config, "{ Person { name age } }")
# Mutation
{:ok, result} = TerminusDB.GraphQL.mutate(config, ~s'''
mutation {
_insertDocuments(json: "{\\"@type\\":\\"Person\\",\\"name\\":\\"Alice\\"}")
}
''')
# Introspect schema
{:ok, schema} = TerminusDB.GraphQL.introspect(config)
## Response format
- `{:ok, %{data: data, errors: nil}}` — successful query with no errors.
- `{:ok, %{data: data, errors: errors}}` — partial success with errors.
- `{:ok, %{data: nil, errors: errors}}` — query failed.
- `{:error, %Error{}}` — HTTP/transport error.
"""
alias TerminusDB.{Client, Config, Error}
alias TerminusDB.Client.Params
@type graphql_opt :: {:organization, String.t()}
@type result :: {:ok, %{data: term(), errors: [map()] | nil}} | {:error, Error.t()}
@doc """
Executes a GraphQL query against the database.
`query_string` is a raw GraphQL query string. `variables` is an optional
map of variable values for parameterized queries.
## Options
- `:organization` — overrides `config.organization`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"data" => %{"Person" => [%{"name" => "Alice"}]})})} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, result} = TerminusDB.GraphQL.query(config, "{ Person { name } }")
iex> result.data["Person"]
[%{"name" => "Alice"}]
"""
@spec query(Config.t(), String.t(), [graphql_opt()] | map()) :: result()
def query(config, query_string, opts \\ [])
def query(config, query_string, opts) when is_list(opts) do
variables = Keyword.get(opts, :variables)
do_query(config, query_string, variables, opts)
end
def query(config, query_string, variables) when is_map(variables) do
do_query(config, query_string, variables, [])
end
defp do_query(config, query_string, variables, opts) do
org = opts[:organization] || config.organization
case config.database do
nil ->
{:error, %Error{reason: :config, message: "no database scoped in config"}}
db ->
path = "graphql/#{org}/#{db}"
body = Params.maybe_put(%{"query" => query_string}, "variables", variables)
case Client.request(config, :post, path, json: body, area: :graphql) do
{:ok, resp} ->
{:ok, %{data: resp["data"], errors: resp["errors"]}}
{:error, _} = error ->
error
end
end
end
@doc """
Executes a GraphQL mutation against the database.
Same as `query/3` but conventionally used for mutations.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"data" => %{"_insertDocuments" => ["Person/Alice"]})})} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, result} = TerminusDB.GraphQL.mutate(config, ~s(mutation { _insertDocuments(json: "{\\"@type\\":\\"Person\\",\\"name\\":\\"Alice\\"}") }))
iex> result.data["_insertDocuments"]
["Person/Alice"]
"""
@spec mutate(Config.t(), String.t(), [graphql_opt()] | map()) :: result()
def mutate(config, query_string, opts \\ [])
def mutate(config, query_string, opts) when is_list(opts) do
variables = Keyword.get(opts, :variables)
do_query(config, query_string, variables, opts)
end
def mutate(config, query_string, variables) when is_map(variables) do
do_query(config, query_string, variables, [])
end
@doc """
Introspects the GraphQL schema of the database.
Sends a `__schema` introspection query and returns the raw schema map.
## Options
- `:organization` — overrides `config.organization`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"data" => %{"__schema" => %{"types" => [%{"name" => "Person"}]})})} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, schema} = TerminusDB.GraphQL.introspect(config)
iex> schema["__schema"]["types"]
[%{"name" => "Person"}]
"""
@spec introspect(Config.t(), [graphql_opt()]) ::
{:ok, map()} | {:error, Error.t()}
def introspect(config, opts \\ []) do
introspection_query = """
{
__schema {
types {
name
kind
description
fields {
name
type {
name
kind
ofType { name kind }
}
}
inputFields {
name
type {
name
kind
ofType { name kind }
}
}
}
}
}
"""
case do_query(config, introspection_query, nil, opts) do
{:ok, %{data: data}} -> {:ok, data}
{:error, _} = error -> error
end
end
end