defmodule TerminusDB.Commit do
@moduledoc """
Commit history and inspection API for TerminusDB.
Every write to TerminusDB creates an immutable commit. Commits form a chain
(per branch) that gives the full history of the database. This module wraps
the history/log endpoints to traverse and inspect commits.
All functions require a `TerminusDB.Config` scoped to a database (via
`TerminusDB.Config.with_database/2`). The branch defaults to `config.branch`
but can be overridden per call via the `:branch` option.
## Quick start
config =
TerminusDB.Config.new(endpoint: "http://localhost:6363")
|> TerminusDB.Config.with_database("mydb")
# List recent commits on the current branch
{:ok, log} = TerminusDB.Commit.log(config)
# Full history with commit metadata
{:ok, history} = TerminusDB.Commit.history(config)
# Inspect a specific commit
{:ok, commit} = TerminusDB.Commit.get(config, "commit/abc123")
"""
alias TerminusDB.{Client, Config, Error}
alias TerminusDB.Client.Params
@type commit_opt ::
{:branch, String.t()}
| {:repo, String.t()}
| {:organization, String.t()}
| {:start, String.t()}
| {:limit, pos_integer()}
| {:count, pos_integer()}
@type history_opt ::
{:id, String.t()}
| {:start, non_neg_integer()}
| {:count, pos_integer()}
| {:created, boolean()}
| {:updated, boolean()}
| {:organization, String.t()}
defp commit_path(config, opts, resource) do
org = opts[:organization] || config.organization
db = config.database || raise Error, reason: :http, message: "no database scoped in config"
repo = opts[:repo] || config.repo
branch = opts[:branch] || config.branch
"#{resource}/#{org}/#{db}/#{repo}/branch/#{branch}"
end
@doc """
Returns a concise log of recent commits on the current (or given) branch.
Each entry includes the commit ID, author, message, and timestamp.
## Options
- `:branch` - overrides `config.branch`.
- `:repo` - overrides `config.repo`.
- `:organization` - overrides `config.organization`.
- `:start` - a commit ID to start listing from (for pagination).
- `:limit` - max number of commits to return.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req ->
...> {req, Req.Response.new(status: 200, body: [
...> %{"commit" => "commit/abc", "author" => "admin", "message" => "add Alice", "timestamp" => "2026-06-24T10:00:00Z"}
...> ])}
...> end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, log} = TerminusDB.Commit.log(config)
iex> hd(log)["author"]
"admin"
"""
@spec log(Config.t(), [commit_opt()]) :: {:ok, [map()]} | {:error, Error.t()}
def log(config, opts \\ []) do
path = commit_path(config, opts, "log")
params =
Params.flag_param(:start, opts[:start]) ++
Params.flag_param(:limit, opts[:limit]) ++
Params.flag_param(:count, opts[:count])
Client.request(config, :get, path, params: params, area: :commit)
end
@doc """
Returns a concise log of commits, or raises `TerminusDB.Error`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"commit" => "c1"}])} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> TerminusDB.Commit.log!(config)
[%{"commit" => "c1"}]
"""
@spec log!(Config.t(), [commit_opt()]) :: [map()]
def log!(config, opts \\ []) do
case log(config, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
@doc """
Returns the full commit history for the current (or given) branch.
This is an alias for `log/2` - both use the same `/api/log` endpoint with
the same parameters. Provided as a semantically distinct name for callers
building history viewers or audit trails.
## Options
Same as `log/2`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req ->
...> {req, Req.Response.new(status: 200, body: [
...> %{"@id" => "commit/abc", "author" => "admin", "message" => "init", "timestamp" => 1782350430.12}
...> ])}
...> end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, history} = TerminusDB.Commit.history(config)
iex> is_list(history)
true
"""
@spec history(Config.t(), [commit_opt()]) :: {:ok, [map()]} | {:error, Error.t()}
def history(config, opts \\ []) do
log(config, opts)
end
@doc """
Returns the full commit history, or raises `TerminusDB.Error`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"commit" => "c1"}])} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> TerminusDB.Commit.history!(config)
[%{"commit" => "c1"}]
"""
@spec history!(Config.t(), [commit_opt()]) :: [map()]
def history!(config, opts \\ []) do
case history(config, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
@doc """
Retrieves a single commit by its descriptor ID (e.g. `"commit/abc123"`).
Returns the commit object with its metadata: author, message, timestamp,
parent reference, and the schema/data references.
## Options
- `:branch` - overrides `config.branch`.
- `:repo` - overrides `config.repo`.
- `:organization` - overrides `config.organization`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req ->
...> {req, Req.Response.new(status: 200, body: %{"@id" => "commit/abc", "author" => "admin", "message" => "add Alice"}}
...> end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, commit} = TerminusDB.Commit.get(config, "commit/abc")
iex> commit["author"]
"admin"
"""
@spec get(Config.t(), String.t(), [commit_opt()]) :: {:ok, map()} | {:error, Error.t()}
def get(config, commit_id, opts \\ []) do
org = opts[:organization] || config.organization
db =
config.database ||
raise Error, reason: :http, message: "no database scoped in config"
repo = opts[:repo] || config.repo
branch = opts[:branch] || config.branch
path = "history/#{org}/#{db}/#{repo}/branch/#{branch}/#{commit_id}"
Client.request(config, :get, path, area: :commit)
end
@doc """
Retrieves a single commit, or raises `TerminusDB.Error`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"@id" => "commit/abc", "author" => "admin"}} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> TerminusDB.Commit.get!(config, "commit/abc")
%{"@id" => "commit/abc", "author" => "admin"}
"""
@spec get!(Config.t(), String.t(), [commit_opt()]) :: map()
def get!(config, commit_id, opts \\ []) do
case get(config, commit_id, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
@doc """
Retrieves the commit history for a specific document.
Returns the history of changes made to a document, ordered backwards in
time from the most recent change.
## Options
- `:id` (required) - the document ID (IRI) to retrieve history for.
- `:start` - starting index for pagination (default `0`).
- `:count` - maximum number of entries (default `10`).
- `:created` - if `true`, return only the creation time.
- `:updated` - if `true`, return only the last update time.
- `:organization` - overrides `config.organization`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"author" => "admin", "identifier" => "abc", "message" => "Created"}])} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, history} = TerminusDB.Commit.document_history(config, id: "Person/Alice", count: 5)
iex> length(history)
1
"""
@spec document_history(Config.t(), [history_opt()]) ::
{:ok, [map()]} | {:error, Error.t()}
def document_history(config, opts \\ []) do
org = opts[:organization] || config.organization
db = config.database || raise Error, reason: :http, message: "no database scoped in config"
params =
[
id: Keyword.fetch!(opts, :id),
start: opts[:start] || 0,
count: opts[:count] || 10
]
|> maybe_add_flag(:created, opts[:created])
|> maybe_add_flag(:updated, opts[:updated])
Client.request(config, :get, "history/#{org}/#{db}", params: params, area: :commit)
end
@doc """
Retrieves document commit history, or raises.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"author" => "admin"}])} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> TerminusDB.Commit.document_history!(config, id: "Person/Alice")
[%{"author" => "admin"}]
"""
@spec document_history!(Config.t(), [history_opt()]) :: [map()]
def document_history!(config, opts \\ []) do
case document_history(config, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
defp maybe_add_flag(params, _key, nil), do: params
defp maybe_add_flag(params, key, value), do: Keyword.put(params, key, value)
end