defmodule TerminusDB.Remote do @moduledoc """ Remote collaboration API for TerminusDB. Wraps the `/api/clone`, `/api/fetch`, `/api/push`, and `/api/pull` endpoints for working with remote TerminusDB repositories. All functions require a `TerminusDB.Config` scoped to a database (except `clone/3` which creates a new database). ## Quick start config = TerminusDB.Config.new(endpoint: "http://localhost:6363") # Clone a remote database {:ok, _} = TerminusDB.Remote.clone(config, "https://data.terminusdb.org/public/star-wars", "star-wars", label: "Star Wars", comment: "Star Wars dataset") # Push current branch to remote {:ok, _} = TerminusDB.Remote.push(config, "origin", "main", author: "admin", message: "push changes") """ alias TerminusDB.{Client, Config, Error} @type remote_opt :: {:organization, String.t()} | {:repo, String.t()} | {:label, String.t()} | {:comment, String.t()} | {:author, String.t()} | {:message, String.t()} | {:remote_branch, String.t()} | {:push_prefixes, boolean()} @doc """ Clones a remote repository into a new local database. ## Options - `:label` — database label (defaults to `newid`). - `:comment` — database comment (defaults to `""`). - `:organization` — target organization (defaults to `config.organization`). ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end ...> ) iex> {:ok, resp} = TerminusDB.Remote.clone(config, "https://data.terminusdb.org/public/star-wars", "star-wars", label: "Star Wars") iex> resp["api:status"] "api:success" """ @spec clone(Config.t(), String.t(), String.t(), [remote_opt()]) :: {:ok, map()} | {:error, Error.t()} def clone(config, remote_url, newid, opts \\ []) do org = opts[:organization] || config.organization path = "clone/#{org}/#{newid}" body = %{ "remote_url" => remote_url, "label" => opts[:label] || newid, "comment" => opts[:comment] || "" } Client.request(config, :post, path, json: body, area: :remote) end @doc """ Clones a remote repository, or raises. ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end ...> ) iex> TerminusDB.Remote.clone!(config, "https://data.terminusdb.org/public/star-wars", "star-wars") %{"api:status" => "api:success"} """ @spec clone!(Config.t(), String.t(), String.t(), [remote_opt()]) :: map() def clone!(config, remote_url, newid, opts \\ []) do case clone(config, remote_url, newid, opts) do {:ok, body} -> body {:error, error} -> raise error end end @doc """ Fetches branches from a remote repository. ## Options - `:organization` — overrides `config.organization`. - `:repo` — overrides `config.repo`. - `:remote_id` — remote repository ID (default `"origin"`). ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success", "api:head_has_changed" => true})} end ...> ) |> TerminusDB.Config.with_database("mydb") iex> {:ok, resp} = TerminusDB.Remote.fetch(config) iex> resp["api:head_has_changed"] true """ @spec fetch(Config.t(), [remote_opt()]) :: {:ok, map()} | {:error, Error.t()} def fetch(config, 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 = config.branch remote_id = opts[:remote_id] || "origin" path = "fetch/#{org}/#{db}/#{repo}/branch/#{branch}/#{remote_id}/_commits" Client.request(config, :post, path, area: :remote) end @doc """ Fetches from remote, or raises. ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success", "api:head_has_changed" => false})} end ...> ) |> TerminusDB.Config.with_database("mydb") iex> TerminusDB.Remote.fetch!(config) %{"api:head_has_changed" => false} """ @spec fetch!(Config.t(), [remote_opt()]) :: map() def fetch!(config, opts \\ []) do case fetch(config, opts) do {:ok, body} -> body {:error, error} -> raise error end end @doc """ Pushes the current branch to a remote repository. ## Options - `:remote` — remote name (default `"origin"`). - `:remote_branch` — remote branch name (default `config.branch`). - `:author` — commit author. - `:message` — commit message. - `:organization` — overrides `config.organization`. - `:repo` — overrides `config.repo`. - `:push_prefixes` — also push prefixes (default `false`). ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success", "api:repo_head_updated" => true})} end ...> ) |> TerminusDB.Config.with_database("mydb") iex> {:ok, resp} = TerminusDB.Remote.push(config, "origin", "main", author: "admin", message: "push") iex> resp["api:repo_head_updated"] true """ @spec push(Config.t(), String.t(), String.t(), [remote_opt()]) :: {:ok, map()} | {:error, Error.t()} def push(config, remote, remote_branch, 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 = config.branch path = "push/#{org}/#{db}/#{repo}/branch/#{branch}" body = %{ "remote" => remote, "remote_branch" => remote_branch } body = body |> maybe_put_string("author", opts[:author]) |> maybe_put_string("message", opts[:message]) params = if opts[:push_prefixes] do [push_prefixes: true] else [] end Client.request(config, :post, path, json: body, params: params, area: :remote) end @doc """ Pushes to remote, or raises. ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success", "api:repo_head_updated" => true})} end ...> ) |> TerminusDB.Config.with_database("mydb") iex> TerminusDB.Remote.push!(config, "origin", "main") %{"api:repo_head_updated" => true} """ @spec push!(Config.t(), String.t(), String.t(), [remote_opt()]) :: map() def push!(config, remote, remote_branch, opts \\ []) do case push(config, remote, remote_branch, opts) do {:ok, body} -> body {:error, error} -> raise error end end @doc """ Pulls updates from a remote repository into the current branch. ## Options Same as `push/4`. ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end ...> ) |> TerminusDB.Config.with_database("mydb") iex> {:ok, resp} = TerminusDB.Remote.pull(config, "origin", "main", author: "admin", message: "pull") iex> resp["api:status"] "api:success" """ @spec pull(Config.t(), String.t(), String.t(), [remote_opt()]) :: {:ok, map()} | {:error, Error.t()} def pull(config, remote, remote_branch, 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 = config.branch path = "pull/#{org}/#{db}/#{repo}/branch/#{branch}" body = %{ "remote" => remote, "remote_branch" => remote_branch } body = body |> maybe_put_string("author", opts[:author]) |> maybe_put_string("message", opts[:message]) Client.request(config, :post, path, json: body, area: :remote) end @doc """ Pulls from remote, or raises. ## Examples iex> config = TerminusDB.Config.new( ...> endpoint: "http://localhost:6363", ...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end ...> ) |> TerminusDB.Config.with_database("mydb") iex> TerminusDB.Remote.pull!(config, "origin", "main") %{"api:status" => "api:success"} """ @spec pull!(Config.t(), String.t(), String.t(), [remote_opt()]) :: map() def pull!(config, remote, remote_branch, opts \\ []) do case pull(config, remote, remote_branch, opts) do {:ok, body} -> body {:error, error} -> raise error end end defp maybe_put_string(map, _key, nil), do: map defp maybe_put_string(map, key, value), do: Map.put(map, key, value) end