Skip to main content

lib/terminus_db/database.ex

defmodule TerminusDB.Database do
  @moduledoc """
  Database management API for TerminusDB.

  A database is the top-level container holding a schema, instance data, and a full
  commit history. This module wraps the `/api/db` endpoints.

  All functions accept a `TerminusDB.Config` and return `{:ok, result}` or
  `{:error, TerminusDB.Error.t()}`. The `!/`-suffixed variants raise instead.
  The organization defaults to `config.organization` but can be overridden per call
  via the `:organization` option.

  ## Examples

      config = TerminusDB.Config.new(endpoint: "http://localhost:6363")

      {:ok, _} = TerminusDB.Database.create(config, "mydb",
        label: "My Database",
        comment: "A demo database",
        schema: true
      )

      {:ok, details} = TerminusDB.Database.info(config, "mydb")
      true = TerminusDB.Database.exists?(config, "mydb")
      {:ok, _} = TerminusDB.Database.delete(config, "mydb")

  """

  alias TerminusDB.{Client, Error}
  alias TerminusDB.Client.Params

  @type create_opt ::
          {:label, String.t()}
          | {:comment, String.t()}
          | {:schema, boolean()}
          | {:public, boolean()}
          | {:prefixes, map()}
          | {:organization, String.t()}

  @type info_opt :: {:organization, String.t()} | {:branches, boolean()} | {:verbose, boolean()}
  @type delete_opt :: {:organization, String.t()} | {:force, boolean()}

  @doc """
  Creates a new database `db_name` in the configured (or given) organization.

  ## Options

  - `:label` — human-readable name (defaults to `db_name`).
  - `:comment` — description (defaults to `""`).
  - `:schema` — whether to initialize a schema graph (default `true`).
  - `:public` — whether the database is accessible to all users.
  - `:prefixes` — custom `@base`/`@schema` IRI prefixes.
  - `:organization` — overrides `config.organization`.

  Returns `{:ok, response_body}` on success. The response is a map of the shape
  `%{"@type" => "api:DbCreateResponse", "api:status" => "api:success"}`.

  ## Examples

      iex> {:ok, _} =
      ...>   TerminusDB.Database.create(config, "mydb", label: "My DB", schema: true)

  """
  @spec create(TerminusDB.Config.t(), String.t(), [create_opt()]) ::
          {:ok, map()} | {:error, Error.t()}
  def create(config, db_name, opts \\ []) do
    org = opts[:organization] || config.organization

    body =
      %{
        "label" => opts[:label] || db_name,
        "comment" => opts[:comment] || "",
        "schema" => Keyword.get(opts, :schema, true)
      }
      |> Params.maybe_put("public", opts[:public])
      |> Params.maybe_put("prefixes", opts[:prefixes])

    Client.request(config, :post, "db/#{org}/#{db_name}", json: body, area: :database)
  end

  @doc """
  Creates a database, returning the response body or raising `TerminusDB.Error`.

  ## 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.Database.create!(config, "mydb", label: "My DB")
      %{"api:status" => "api:success"}

  """
  @spec create!(TerminusDB.Config.t(), String.t(), [create_opt()]) :: map()
  def create!(config, db_name, opts \\ []) do
    case create(config, db_name, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  @doc """
  Deletes the database `db_name`.

  ## Options

  - `:force` — force deletion of databases in inconsistent states (default `false`).
  - `:organization` — overrides `config.organization`.

  ## Examples

      iex> {:ok, _} = TerminusDB.Database.delete(config, "mydb")
      iex> {:ok, _} = TerminusDB.Database.delete(config, "mydb", force: true)

  """
  @spec delete(TerminusDB.Config.t(), String.t(), [delete_opt()]) ::
          {:ok, map() | nil} | {:error, Error.t()}
  def delete(config, db_name, opts \\ []) do
    org = opts[:organization] || config.organization
    params = if opts[:force], do: [force: true], else: []

    Client.request(config, :delete, "db/#{org}/#{db_name}", params: params, area: :database)
  end

  @doc """
  Deletes a database, returning the response body or raising `TerminusDB.Error`.

  ## 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.Database.delete!(config, "mydb")
      %{"api:status" => "api:success"}

  """
  @spec delete!(TerminusDB.Config.t(), String.t(), [delete_opt()]) :: map() | nil
  def delete!(config, db_name, opts \\ []) do
    case delete(config, db_name, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  @doc """
  Returns details for the database `db_name` (a list of database descriptors).

  ## Options

  - `:branches` — include branch information (default `false`).
  - `:verbose` — return all available information (default `false`).
  - `:organization` — overrides `config.organization`.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"name" => "mydb", "@type" => "UserDatabase"}])} end
      ...> )
      iex> {:ok, details} = TerminusDB.Database.info(config, "mydb")
      iex> hd(details)["name"]
      "mydb"

  """
  @spec info(TerminusDB.Config.t(), String.t(), [info_opt()]) ::
          {:ok, [map()]} | {:error, Error.t()}
  def info(config, db_name, opts \\ []) do
    org = opts[:organization] || config.organization

    params =
      Params.flag_param(:branches, opts[:branches]) ++
        Params.flag_param(:verbose, opts[:verbose])

    Client.request(config, :get, "db/#{org}/#{db_name}", params: params, area: :database)
  end

  @doc """
  Returns database details, or raises `TerminusDB.Error`.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"name" => "mydb"}])} end
      ...> )
      iex> TerminusDB.Database.info!(config, "mydb")
      [%{"name" => "mydb"}]

  """
  @spec info!(TerminusDB.Config.t(), String.t(), [info_opt()]) :: [map()]
  def info!(config, db_name, opts \\ []) do
    case info(config, db_name, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  @doc """
  Lists all databases available to the authenticated user.

  ## Options

  - `:branches` — include branch information (default `false`).
  - `:verbose` — return all available information (default `false`).

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"name" => "a"}, %{"name" => "b"}])} end
      ...> )
      iex> {:ok, dbs} = TerminusDB.Database.list(config)
      iex> Enum.map(dbs, & &1["name"])
      ["a", "b"]

  """
  @spec list(TerminusDB.Config.t(), [info_opt()]) :: {:ok, [map()]} | {:error, Error.t()}
  def list(config, opts \\ []) do
    params =
      Params.flag_param(:branches, opts[:branches]) ++
        Params.flag_param(:verbose, opts[:verbose])

    Client.request(config, :get, "db", params: params, area: :database)
  end

  @doc """
  Lists all databases, or raises `TerminusDB.Error`.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"name" => "mydb"}])} end
      ...> )
      iex> TerminusDB.Database.list!(config)
      [%{"name" => "mydb"}]

  """
  @spec list!(TerminusDB.Config.t(), [info_opt()]) :: [map()]
  def list!(config, opts \\ []) do
    case list(config, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  @doc """
  Returns `true` if the database `db_name` exists, `false` otherwise.

  Uses `HEAD /api/db/:org/:db`. A 404 is interpreted as "does not exist"; any other
  non-success response raises `TerminusDB.Error`.

  ## 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: "")} end
      ...> )
      iex> TerminusDB.Database.exists?(config, "mydb")
      true

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 404, body: "")} end
      ...> )
      iex> TerminusDB.Database.exists?(config, "missing")
      false

  """
  @spec exists?(TerminusDB.Config.t(), String.t(), [delete_opt()]) :: boolean()
  def exists?(config, db_name, opts \\ []) do
    org = opts[:organization] || config.organization

    case Client.request_response(config, :head, "db/#{org}/#{db_name}", area: :database) do
      {:ok, _resp} -> true
      {:error, %Error{status: 404}} -> false
      {:error, error} -> raise error
    end
  end

  @doc """
  Updates metadata (label, comment, etc.) for the database `db_name`.

  Accepts the same body options as `create/3` (`:label`, `:comment`, `:schema`,
  `:public`, `:prefixes`) plus `: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.Database.update(config, "mydb", label: "New Label")
      iex> resp["api:status"]
      "api:success"

  """
  @spec update(TerminusDB.Config.t(), String.t(), [create_opt()]) ::
          {:ok, map()} | {:error, Error.t()}
  def update(config, db_name, opts \\ []) do
    org = opts[:organization] || config.organization

    body =
      %{
        "label" => opts[:label] || db_name,
        "comment" => opts[:comment] || "",
        "schema" => Keyword.get(opts, :schema, true)
      }
      |> Params.maybe_put("public", opts[:public])
      |> Params.maybe_put("prefixes", opts[:prefixes])

    Client.request(config, :put, "db/#{org}/#{db_name}", json: body, area: :database)
  end

  @doc """
  Updates a database, returning the response body or raising `TerminusDB.Error`.

  ## 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.Database.update!(config, "mydb", label: "New Label")
      %{"api:status" => "api:success"}

  """
  @spec update!(TerminusDB.Config.t(), String.t(), [create_opt()]) :: map()
  def update!(config, db_name, opts \\ []) do
    case update(config, db_name, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  @doc """
  Optimizes a resource path (branch, `_meta`, or `_commits` graph).

  ## 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.Database.optimize(config, "admin/mydb/local/branch/main")
      iex> resp["api:status"]
      "api:success"

  """
  @spec optimize(TerminusDB.Config.t(), String.t()) ::
          {:ok, map()} | {:error, TerminusDB.Error.t()}
  def optimize(config, path) do
    TerminusDB.Client.request(config, :post, "optimize/#{path}", area: :database)
  end

  @doc """
  Optimizes a resource path, 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.Database.optimize!(config, "_system")
      %{"api:status" => "api:success"}

  """
  @spec optimize!(TerminusDB.Config.t(), String.t()) :: map()
  def optimize!(config, path) do
    case optimize(config, path) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  # Helpers -------------------------------------------------------------------
end