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