A walkthrough of every feature in terminusdb_ex v0.2. Each section demonstrates
a module with runnable examples (against a live TerminusDB at
http://localhost:6363).
Setup
Start a TerminusDB server:
docker compose up -d
until curl -sf http://localhost:6363/api/ok >/dev/null 2>&1; do sleep 1; done
Then in an Elixir shell (iex -S mix):
alias TerminusDB.{Config, Database, Document, Schema, Branch, Client, Error}1. Configuration (immutable context)
TerminusDB.Config is an immutable struct carrying the server endpoint,
credentials, and resource scope. All API functions take a config and return
derived configs via with_* helpers, never mutating.
# Basic config with default admin:root auth
config = Config.new(endpoint: "http://localhost:6363")
# With a bearer token
config = Config.new(endpoint: "http://localhost:6363", token: "tok_abc")
# Inspect the auth tuple
Config.auth(config)
# => {:bearer, "tok_abc"}
# Redact secrets for logging / telemetry
Config.redact(config).token
# => "[redacted]"2. Database management
Create, inspect, update, and delete databases.
# Create
{:ok, _} = Database.create(config, "demo", label: "Demo DB", schema: true)
# Check existence
Database.exists?(config, "demo") # => true
Database.exists?(config, "nope") # => false
# Get info
{:ok, details} = Database.info(config, "demo")
# List all databases
{:ok, all} = Database.list(config)
# Update metadata
{:ok, _} = Database.update(config, "demo", label: "Renamed DB")
# Delete
{:ok, _} = Database.delete(config, "demo")3. Document CRUD
Insert, retrieve, query, replace, and delete documents. Requires a config scoped to a database.
config = Config.with_database(config, "demo")
# Insert a schema (Class document in the schema graph)
{:ok, _} =
Document.insert(config,
%{"@type" => "Class", "@id" => "Person", "name" => "xsd:string", "age" => "xsd:integer"},
author: "admin", message: "add schema",
graph_type: :schema
)
# Insert a document
{:ok, _} =
Document.insert(config,
%{"@type" => "Person", "name" => "Alice", "age" => 30},
author: "admin", message: "add Alice"
)
# Insert multiple documents at once
{:ok, _} =
Document.insert(config, [
%{"@type" => "Person", "name" => "Bob", "age" => 25},
%{"@type" => "Person", "name" => "Carol", "age" => 28}
], author: "admin", message: "add more people")
# Get all documents of a type
{:ok, docs} = Document.get(config, type: "Person", as_list: true)
# Get a specific document by ID
{:ok, person} = Document.get(config, id: "Person/Alice")
# Query by template
{:ok, matches} = Document.query(config, %{"@type" => "Person", "age" => 30})
# Replace (update) a document
{:ok, _} =
Document.replace(config,
%{"@id" => "Person/Alice", "name" => "Alicia", "age" => 31},
author: "admin", message: "rename Alice"
)
# Delete a document
{:ok, _} = Document.delete(config, id: "Person/Alice", author: "admin", message: "remove")4. Schema frames
Retrieve the schema frame (JSON-LD class description) for a class or all classes.
# Frame for a specific class
{:ok, frame} = Schema.frame(config, "Person")
# => %{"@type" => "Class", "name" => "xsd:string", "age" => "xsd:integer"}
# All class frames
{:ok, all} = Schema.all(config)
# => %{"Person" => %{"@type" => "Class", ...}}
# With options
{:ok, frame} = Schema.frame(config, "Person", compress_ids: false, expand_abstract: false)5. Branches
Create, check, and delete branches (git-like version control for data).
# Create a branch (forks from main by default)
{:ok, _} = Branch.create(config, "feature")
# Fork from a specific branch
{:ok, _} = Branch.create(config, "dev", from: "main")
# Check existence
Branch.exists?(config, "feature") # => true
# Work on the branch
feature_config = Config.with_branch(config, "feature")
{:ok, _} = Document.insert(feature_config,
%{"@type" => "Person", "name" => "Dave"},
author: "admin", message: "add Dave on feature branch"
)
# Delete the branch
{:ok, _} = Branch.delete(config, "feature")6. Streaming
Stream large document result sets with constant memory using Req's async response and the concatenated-JSON decoder.
# Stream all Person documents
Document.stream(config, type: "Person")
|> Stream.each(&IO.inspect/1)
|> Stream.run()
# Count without loading all into memory
count = Document.stream(config, type: "Person") |> Enum.count()7. Telemetry
Every operation emits [:terminusdb, <area>, :start] and [:stop] events.
# Attach a handler
:telemetry.attach_many(
"my-handler",
[[:terminusdb, :document, :stop], [:terminusdb, :database, :stop]],
fn _event, measurements, meta, _ctx ->
IO.puts("[#{meta.area}] #{meta.method} #{meta.path} -> #{meta.status} (#{measurements[:duration]} ns)")
end,
nil
)
# Now operations emit events:
{:ok, _} = Database.create(config, "telemetry_test", label: "Test")
# [database] :post db/admin/telemetry_test -> 200 (1234567 ns)
:telemetry.detach("my-handler")8. Error handling
All public functions return {:ok, result} or {:error, %TerminusDB.Error{}}.
The !/ variants raise instead.
# Tuple-returning (non-raising)
case Database.create(config, "demo", label: "Demo") do
{:ok, _} -> :ok
{:error, %Error{reason: :api, api_type: "api:DatabaseAlreadyExists"} = e} ->
IO.puts("Database already exists: #{Exception.message(e)}")
{:error, %Error{reason: :transport} = e} ->
IO.puts("Network error: #{Exception.message(e)}")
end
# Raising variant
Database.create!(config, "demo", label: "Demo")
# raises TerminusDB.Error if it fails9. Raw client access
For endpoints not yet wrapped by a higher-level module, use Client.request/4
directly.
# Raw GET
{:ok, body} = Client.request(config, :get, "ok")
# Raw POST with JSON body
{:ok, resp} = Client.request(config, :post, "db/admin/mydb",
json: %{label: "My DB", schema: true},
area: :database
)
# Get the full Req.Response (headers, status, body)
{:ok, resp} = Client.request_response(config, :get, "info")
resp.status # => 200
resp.headers["content-type"] # => ["application/json"]Cleanup
{:ok, _} = Database.delete(config, "demo", force: true)