# `Arangox`
[🔗](https://github.com/ArangoDB-Community/arangox/blob/v0.8.0/lib/arangox.ex#L1)

[![Hex.pm](https://img.shields.io/hexpm/v/arangox.svg)](https://hex.pm/packages/arangox)
[![Documentation](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/arangox)
[![CI](https://github.com/ArangoDB-Community/arangox/actions/workflows/elixir.yml/badge.svg?branch=main&event=push)](https://github.com/ArangoDB-Community/arangox/actions/workflows/elixir.yml)

Arangox is a pooled Elixir driver for ArangoDB. It gives you a conventional
resource API for everyday work, first-class AQL queries and transactions, and
raw access to the HTTP API when you need it.

```elixir
alias Arangox.API.{Collections, Documents}

{:ok, _collection} = Collections.create(db, %{name: "products"})
{:ok, created} = Documents.create(db, "products", %{name: "Keyboard"})
{:ok, product} = Documents.get(db, "products", created["_key"])
```

Arangox 0.8 supports:

- ArangoDB 3.11 and 3.12
- Elixir 1.15 and later
- OTP 26 and later
- HTTP through Mint by default
- HTTP through Gun as an alternative
- VelocyStream for backward compatibility with ArangoDB 3.11 deployments

## Getting Started

Add Arangox, Mint, and a JSON library to `mix.exs`:

```elixir
def deps do
  [
    {:arangox, "~> 0.8.0"},
    {:jason, "~> 1.4"},
    {:mint, "~> 1.9"}
  ]
end
```

Mint and Jason are optional dependencies so applications can
choose another transport or codec. Add them explicitly when using the default
setup shown here.

For a local server on ArangoDB's default port:

```elixir
{:ok, db} =
  Arangox.start_link(
    endpoints: "http://localhost:8529",
    database: "store",
    auth: {:basic, "root", ""}
  )
```

`Arangox.start_link/1` starts a `DBConnection` pool. The defaults are
`http://localhost:8529` and a pool size of 10, so a local server without
authentication needs only:

```elixir
{:ok, db} = Arangox.start_link()
```

In an application, put the pool under your supervision tree and give it a
stable name:

```elixir
children = [
  {Arangox,
   name: MyApp.Arango,
   endpoints: System.fetch_env!("ARANGODB_URL"),
   database: System.fetch_env!("ARANGODB_DATABASE"),
   auth:
     {:basic, System.fetch_env!("ARANGODB_USER"),
      System.fetch_env!("ARANGODB_PASSWORD")}}
]

Supervisor.start_link(children, strategy: :one_for_one)
```

Calls can now use `MyApp.Arango` anywhere they would use the `db` value from
`start_link/1`.

## Resrouce API

The modules under `Arangox.API` cover ArangoDB's HTTP surface: 230 operations
across 22 resource modules. The pool is always the first argument, followed by
path parameters, a request body when the operation needs one, and options.

Here is a complete document lifecycle:

```elixir
alias Arangox.API.{Collections, Documents}

{:ok, _collection} =
  Collections.create(MyApp.Arango, %{
    name: "products",
    type: 2
  })

{:ok, created} =
  Documents.create(
    MyApp.Arango,
    "products",
    %{name: "Mechanical keyboard", price_cents: 12_900, in_stock: true},
    return_new: true
  )

key = created["_key"]

{:ok, product} = Documents.get(MyApp.Arango, "products", key)

{:ok, updated} =
  Documents.update(
    MyApp.Arango,
    "products",
    key,
    %{in_stock: false},
    return_new: true
  )

{:ok, _deleted} = Documents.delete(MyApp.Arango, "products", key)
```

Operation options use Elixir names. For example, `return_new: true` is sent as
ArangoDB's `returnNew=true`. Every operation also accepts:

- `:database` to override the pool's database for that call
- `:headers` as a list of `{name, value}` tuples
- `:transaction` with an `%Arangox.Transaction{}` handle
- `:timeout` for the caller's complete time budget
- `:request_timeout` for the maximum time spent waiting on the socket

Resource operations return the decoded response body:

```elixir
case Documents.get(MyApp.Arango, "products", key) do
  {:ok, product} ->
    {:found, product}

  {:error, %Arangox.Error{reason: :arango_document_not_found}} ->
    :not_found

  {:error, %Arangox.Error{} = error} ->
    {:failed, error}
end
```

Every operation has a bang form when raising is the clearer control flow:

```elixir
product = Documents.get!(MyApp.Arango, "products", key)
```

Useful starting points include:

- `Arangox.API.Collections` and `Arangox.API.Documents`
- `Arangox.API.Graphs`, `Arangox.API.Views`, and `Arangox.API.Indexes`
- `Arangox.API.Queries` and `Arangox.API.Transactions`
- `Arangox.API.Databases`, `Arangox.API.Users`, and `Arangox.API.Administration`

See the [HexDocs API reference](https://hexdocs.pm/arangox/api-reference.html)
for the complete surface.

## AQL Queries

Use `Arangox.query/4` when AQL is the natural interface. Bind variables stay
separate from the statement, and query options use snake_case names:

```elixir
{:ok, %Arangox.Response{body: %{"result" => products}}} =
  Arangox.query(
    MyApp.Arango,
    """
    FOR product IN products
      FILTER product.price_cents <= @maximum
      SORT product.price_cents ASC
      RETURN product
    """,
    %{maximum: 15_000},
    batch_size: 100
  )
```

ArangoDB has no prepared statements. On ArangoDB 3.12.4 and later, opt into
its server-side AQL plan cache when repeatedly executing eligible query text:

```elixir
Arangox.query(
  MyApp.Arango,
  "FOR product IN products FILTER product._key == @key RETURN product",
  %{key: key},
  use_plan_cache: true
)
```

`Arangox.plan_cache/1` lists the current database's cached plans and
`Arangox.clear_plan_cache/1` clears all of them. Both operations affect the
whole database, not only this pool.

For large results, stream cursor batches inside `Arangox.run/3` or
`Arangox.transaction/3`:

```elixir
{:ok, names} =
  Arangox.transaction(MyApp.Arango, fn conn ->
    conn
    |> Arangox.cursor(
      "FOR product IN products SORT product.name RETURN product.name",
      %{},
      batch_size: 100
    )
    |> Stream.flat_map(fn response -> response.body["result"] end)
    |> Enum.to_list()
  end, read: "products")
```

## Transactions

For work that fits naturally in one function, use the block form. Returning
commits the transaction; `Arangox.abort/2`, an exception, or an exit rolls it
back.

```elixir
alias Arangox.API.Documents

Arangox.transaction(MyApp.Arango, fn conn ->
    case Documents.update(conn, "accounts", from_key, %{active: false}) do
      {:ok, source} -> source
      {:error, error} -> Arangox.abort(conn, error)
    end
  end, write: "accounts"
)
```

For a transaction that must span separate calls, use a handle:

```elixir
{:ok, transaction} =
  Arangox.begin_transaction(MyApp.Arango, write: "products")

{:ok, _product} =
  Arangox.API.Documents.update(
    MyApp.Arango,
    "products",
    key,
    %{in_stock: true},
    transaction: transaction
  )

{:ok, %Arangox.Response{}} =
  Arangox.commit_transaction(MyApp.Arango, transaction)

# or

{:ok, %Arangox.Response{}} =
  Arangox.abort_transaction(MyApp.Arango, transaction)
```

## HTTP Primitives

Arangox exposes low-level HTTP primitives for
unsupported or custom endpoints, Foxx services, etc:

```elixir
{:ok, %Arangox.Response{status: 200, body: version}} =
  Arangox.get(MyApp.Arango, "/_api/version")

{:ok, %Arangox.Response{}} =
  Arangox.post(
    MyApp.Arango,
    "/_db/mydb/myservice/reindex",
    %{full: true}
  )
```

## Configuration

The options most applications need are:

| Option | Purpose | Default |
| --- | --- | --- |
| `:endpoints` | One endpoint or an ordered failover list | `"http://localhost:8529"` |
| `:database` | Database prepended to database-scoped requests | Server default |
| `:auth` | `{:basic, user, password}` or `{:bearer, token}` | No authentication |
| `:pool_size` | Number of concurrently usable connections | `10` |
| `:headers` | Headers sent with every request | `[]` |
| `:connect_timeout` | Budget for each connection attempt | `5_000` ms |
| `:request_timeout` | Per-request socket-wait ceiling | `15_000` ms |
| `:max_body_size` | Largest response body accepted for decoding | 128 MiB |
| `:content_type` | HTTP request-body codec | `:json` |
| `:client` | Transport implementation | `Arangox.MintClient` |

The pool also accepts standard `DBConnection` start options.

### Socket and transport options

Three options carry settings through to the socket and to the transport
library. Which one you want depends on who reads it:

| Option | Goes to | Read when |
| --- | --- | --- |
| `:tcp_opts` | Erlang's `:gen_tcp` | The endpoint is cleartext — `http://`, `tcp://`, or a Unix socket |
| `:ssl_opts` | Erlang's `:ssl` | The endpoint is encrypted — `https://`, `ssl://`, or `tls://` |
| `:client_opts` | The transport library itself | Always |

An option is read only when an endpoint's scheme matches, so `:ssl_opts`
behind an `http://` endpoint never reaches a socket. Arangox warns at pool
start when you pass an option no configured endpoint can read. An endpoint
list may mix schemes, in which case both socket options are live and each
applies to the endpoints it matches.

`:ssl_opts` also accepts plain socket options. Erlang's `:ssl` forwards what
it does not recognise to `:gen_tcp`, so one list can carry both:

```elixir
Arangox.start_link(
  endpoints: "https://db.example.com:8529",
  ssl_opts: [cacertfile: "/etc/my_app/ca.pem", nodelay: true]
)
```

`:client_opts` takes the shape the library itself takes — a keyword list for
Mint, a map for Gun. Each client's documentation says what it accepts and how
it combines with the two socket options. Both HTTP clients merge it per key,
so setting one transport option does not discard the rest.

### Deadlines and retries

`:timeout` is the caller's total budget, beginning before it queues for a pool
connection. `:request_timeout` is a ceiling on the socket wait. Arangox uses
whichever limit expires first.

A request that times out after being written retires its connection and is not
automatically retried. Retrying a write is an application decision because the
server may already have applied it.

### Multiple endpoints

Pass an ordered endpoint list to let each pooled connection find an available
server:

```elixir
Arangox.start_link(
  endpoints: [
    "https://coordinator-1.example.com:8529",
    "https://coordinator-2.example.com:8529"
  ],
  auth: {:bearer, token}
)
```

Arangox checks availability before admitting a connection. In ArangoDB 3.11
active-failover deployments, leader redirects are accepted only when the
target is already configured or explicitly admitted by `:endpoint_mapper`.

## Choose a transport

Most applications should keep the default Mint client.

| Client | Protocols | Server support | Additional dependency |
| --- | --- | --- | --- |
| `Arangox.MintClient` | HTTP/1.1 by default; optional HTTP/2 | ArangoDB 3.11 and 3.12 | `{:mint, "~> 1.9"}` |
| `Arangox.GunClient` | HTTP/1.1 and HTTP/2 | ArangoDB 3.11 and 3.12 | `{:gun, "~> 2.0"}` |
| `Arangox.VelocyClient` | VelocyStream | ArangoDB 3.11 only | `{:velocy, "~> 0.2"}` |

Select another client per pool:

```elixir
Arangox.start_link(client: Arangox.GunClient)
```

Mint's HTTP/2 mode is explicit:

```elixir
Arangox.start_link(client_opts: [protocols: [:http2]])
```

The Mint client sends request bodies whole. Under HTTP/2, a body larger than
the peer's flow-control window is rejected instead of streamed; ArangoDB
typically advertises roughly 64 KiB. Keep Mint on its default HTTP/1.1 for
large documents and bulk writes, or choose Gun. Because Arangox 0.8 is built
on `DBConnection`, HTTP/2 does not yet multiplex concurrent requests on one
connection; concurrency still comes from `:pool_size`.

HTTP pools can encode bodies as VelocyPack independently of the transport:

```elixir
Arangox.start_link(content_type: :velocypack)
```

This requires the optional `:velocy` dependency. Responses are decoded from
their own content type, so JSON responses still work.

## Security defaults

- TLS verifies certificates and hostnames using the system trust store.
- Credentials on a non-loopback cleartext endpoint are refused unless you
  explicitly pass `allow_cleartext_auth: true`.
- Authorization and transaction header values are redacted from returned
  requests, inspected state, and errors.
- Header names and values containing carriage returns, line feeds, or nulls
  are rejected before reaching a transport.
- Response bodies larger than `:max_body_size` are rejected before decoding.

If a private certificate authority signs your ArangoDB certificate, provide
its CA file rather than disabling verification:

```elixir
Arangox.start_link(
  endpoints: "https://db.example.com:8529",
  ssl_opts: [cacertfile: "/etc/my_app/arangodb-ca.pem"]
)
```

## Errors

Arangox reports driver, transport, and server failures with one structure:
`%Arangox.Error{}`.

- `:reason` is the stable atom to pattern-match on.
- `:status` is the HTTP status when the server responded.
- `:error_num` is ArangoDB's numeric error code when present.
- `:endpoint` is redacted.
- `:message` is for people and logs, not branching.

```elixir
case Arangox.API.Documents.get(MyApp.Arango, "products", key) do
  {:ok, document} -> document
  {:error, %Arangox.Error{reason: :arango_conflict}} -> retry_update()
  {:error, %Arangox.Error{} = error} -> raise error
end
```

## Upgrading from 0.7

Version 0.8 modernizes the transport and public contracts. In particular,
Mint is now the default client, TLS verification is enabled, headers are
ordered `{name, value}` lists, and stream transactions use
`%Arangox.Transaction{}` handles.

Read the complete [0.8 migration guide](CHANGELOG.md) — the "Migrating from
v0.7" section — before upgrading an existing application.

## Development

Unit and protocol tests run without Docker:

```console
mix deps.get
mix format --check-formatted
mix test
```

The integration suite uses the services in `docker-compose.yml`:

```console
docker compose up --detach --wait
mix test.integration
```

## Documentation

- [HexDocs](https://hexdocs.pm/arangox)
- [Changelog](CHANGELOG.md)
- [ArangoDB documentation](https://docs.arangodb.com/)
- [`DBConnection` documentation](https://hexdocs.pm/db_connection/)

## License

Arangox is released under the [MIT License](LICENSE).

# `bindvars`

```elixir
@type bindvars() :: keyword() | map()
```

# `body`

```elixir
@type body() :: binary() | map() | list() | nil
```

# `client`

```elixir
@type client() :: module()
```

# `conn`

```elixir
@type conn() :: DBConnection.conn()
```

# `endpoint`

```elixir
@type endpoint() :: binary()
```

# `endpoint_mapper`

```elixir
@type endpoint_mapper() ::
  %{optional(endpoint()) =&gt; endpoint()}
  | (endpoint() -&gt; endpoint() | nil)
  | {module(), atom(), [any()]}
```

Remaps a server-advertised redirect endpoint to one this client can reach.

A map is looked up by the advertised endpoint; a missing entry refuses the
redirect. A function (or `{module, function, args}`, which is applied with the
advertised endpoint prepended to `args`) returns the endpoint to use, and
anything other than an endpoint binary refuses the redirect.

# `headers`

```elixir
@type headers() :: [{binary(), binary()}]
```

# `method`

```elixir
@type method() :: :get | :head | :delete | :post | :put | :patch | :options
```

# `path`

```elixir
@type path() :: binary()
```

# `query`

```elixir
@type query() :: binary() | Arangox.Query.t()
```

# `start_option`

```elixir
@type start_option() ::
  {:client, module()}
  | {:endpoints, [endpoint()]}
  | {:endpoint_mapper, endpoint_mapper()}
  | {:auth, Arangox.Auth.t()}
  | {:database, binary()}
  | {:headers, headers()}
  | {:read_only?, boolean()}
  | {:connect_timeout, timeout()}
  | {:request_timeout, pos_integer()}
  | {:failover_callback,
     (Arangox.Error.t() -&gt; any()) | {module(), atom(), [any()]}}
  | {:tcp_opts, [:gen_tcp.connect_option()]}
  | {:ssl_opts, [:ssl.tls_client_option()]}
  | {:client_opts, keyword() | map()}
  | {:json_library, module()}
  | {:content_type, :json | :velocypack}
  | {:max_body_size, pos_integer()}
  | {:vst_maxsize, pos_integer()}
  | DBConnection.start_option()
```

# `transaction_option`

```elixir
@type transaction_option() ::
  {:read, binary() | [binary()]}
  | {:write, binary() | [binary()]}
  | {:exclusive, binary() | [binary()]}
  | {:properties, list() | map()}
  | DBConnection.option()
```

# `abort`

```elixir
@spec abort(conn(), reason :: any()) :: no_return()
```

Aborts a transaction for the given reason.

Delegates to `DBConnection.rollback/2`.

## Example

    iex> {:ok, conn} = Arangox.start_link()
    iex> Arangox.transaction(conn, fn c ->
    iex>   Arangox.abort(c, :reason)
    iex> end)
    {:error, :reason}

# `abort_transaction`

```elixir
@spec abort_transaction(conn(), Arangox.Transaction.t(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Aborts the stream transaction the handle names, discarding its writes.

This is the handle form's counterpart to the closure form's `abort/2`, and
deliberately not the same function: `abort/2` raises to roll the *current
closure* back through `DBConnection.rollback/2`, while this sends an abort
request for the named transaction and returns. It runs against any pooled
connection to the deployment; accepts only a `t:Arangox.Transaction.t/0`.

Accepts any of the options accepted by `DBConnection.execute/4`, plus
`:database` (pass the one the transaction was begun in).

# `abort_transaction!`

```elixir
@spec abort_transaction!(conn(), Arangox.Transaction.t(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Aborts the stream transaction the handle names. Raises in the case of an
error.

See `abort_transaction/3`.

# `begin_transaction`

```elixir
@spec begin_transaction(conn(), [transaction_option()]) ::
  {:ok, Arangox.Transaction.t()} | {:error, any()}
```

Begins a stream transaction and returns a handle to it.

This is the handle form of transactions. Where `transaction/3` binds the
transaction to one checked-out connection for the span of a closure, the
`t:Arangox.Transaction.t/0` returned here carries the transaction's identity
as a value: pass it to any request through the `:transaction` option and
that request joins the transaction, whichever pooled connection serves it —
then finish with `commit_transaction/3` or `abort_transaction/3`, again on
any connection reaching the same deployment. Verified against an ArangoDB
3.12 cluster, this includes connections to *different coordinators* of that
deployment; see `Arangox.Transaction` for the measured scope.

The handle is a bearer capability — treat it like authentication material
and do not pass it across a trust boundary. See `Arangox.Transaction`.

Accepts the same options as `transaction/3`: `:read`, `:write`,
`:exclusive`, `:properties`, `:database`, and any of the options accepted by
`DBConnection.execute/4`. A transaction lives in the database it was begun
in, so pass the same `:database` option to the requests and to
commit/abort/status that you pass here.

## Example

    {:ok, trx} = Arangox.begin_transaction(conn, write: "coll")
    Arangox.post!(conn, "/_api/document/coll", %{a: 1}, [], transaction: trx)
    {:ok, :running} = Arangox.transaction_status(conn, trx)
    {:ok, %Arangox.Response{}} = Arangox.commit_transaction(conn, trx)

# `begin_transaction!`

```elixir
@spec begin_transaction!(conn(), [transaction_option()]) :: Arangox.Transaction.t()
```

Begins a stream transaction and returns a handle to it. Raises in the case
of an error.

See `begin_transaction/2`.

# `child_spec`

```elixir
@spec child_spec([start_option()]) :: Supervisor.child_spec()
```

Returns a supervisor child specification for a DBConnection pool.

# `clear_plan_cache`

```elixir
@spec clear_plan_cache(conn(), [DBConnection.option()]) :: :ok | {:error, any()}
```

Discards every cached query plan in the current database.

A **database-wide administrative operation**, and the blunt kind: the server
has no per-entry release, so this affects every caller of the database, whose
next execution of an affected statement is planned afresh. Requires write
privileges on the database; read alone is refused with a `403`.

Clearing is the escape hatch for a plan that has gone stale — a plan is
optimised against the indexes and statistics that existed when it was built,
and nothing invalidates it when those change.

Accepts `:database` and any of the options accepted by
`DBConnection.execute/4`.

# `clear_plan_cache!`

```elixir
@spec clear_plan_cache!(conn(), [DBConnection.option()]) :: :ok
```

Discards every cached query plan in the current database. Raises in the case of
an error.

See `clear_plan_cache/2`.

# `commit_transaction`

```elixir
@spec commit_transaction(conn(), Arangox.Transaction.t(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Commits the stream transaction the handle names.

Runs against any pooled connection to the deployment the transaction was
begun on — the handle, not the connection, names the transaction. Accepts
only a `t:Arangox.Transaction.t/0`, never a bare identifier.

A failed commit leaves the handle usable: the transaction is still addressed
by the identifier, so a follow-up `abort_transaction/3` reaches it.
Committing an already-committed transaction gets a 200 from the server
(idempotent); aborting or using a committed one is the server's error.

Accepts any of the options accepted by `DBConnection.execute/4`, plus
`:database` (pass the one the transaction was begun in).

# `commit_transaction!`

```elixir
@spec commit_transaction!(conn(), Arangox.Transaction.t(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Commits the stream transaction the handle names. Raises in the case of an
error.

See `commit_transaction/3`.

# `cursor`

```elixir
@spec cursor(conn(), query(), bindvars(), [DBConnection.option()]) ::
  DBConnection.Stream.t()
```

Creates a cursor and returns a `DBConnection.Stream` struct. Results are fetched
upon enumeration.

The cursor is created, results fetched, then deleted from the database upon each
enumeration (not to be confused with iteration). When a cursor is created, an initial
result set is fetched from the database. The initial result is returned with the first
iteration, subsequent iterations are fetched lazily.

Can only be used within a `transaction/3` or `run/3` call.

Accepts any of the options accepted by `DBConnection.stream/4`, as well as any of the
following:

  * `:database` - Sets what database to run the cursor query on
  * `:properties` - A list or map of additional body attributes to append to the
  request body when creating the cursor.
  * `:transaction` - An `t:Arangox.Transaction.t/0` handle from
  `begin_transaction/2`; the cursor's requests join that stream transaction.

Delegates to `DBConnection.stream/4`.

## Example

    iex> {:ok, conn} = Arangox.start_link()
    iex> Arangox.transaction(conn, fn c ->
    iex>   stream =
    iex>     Arangox.cursor(
    iex>       c,
    iex>       "FOR i IN [1, 2, 3] FILTER i == 1 || i == @num RETURN i",
    iex>       %{num: 2},
    iex>       properties: [batchSize: 1]
    iex>     )
    iex>
    iex>   first_batch = Enum.at(stream, 0).body["result"]
    iex>
    iex>   exhaust_cursor =
    iex>     Enum.reduce(stream, [], fn resp, acc ->
    iex>       acc ++ resp.body["result"]
    iex>     end)
    iex>
    iex>   {first_batch, exhaust_cursor}
    iex> end)
    {:ok, {[1], [1, 2]}}

# `delete`

```elixir
@spec delete(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs a DELETE request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`.

# `delete!`

```elixir
@spec delete!(conn(), path(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs a DELETE request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `get`

```elixir
@spec get(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs a GET request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`.

# `get!`

```elixir
@spec get!(conn(), path(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
```

Runs a GET request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `head`

```elixir
@spec head(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs a HEAD request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`.

# `head!`

```elixir
@spec head!(conn(), path(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs a HEAD request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `json_library`

> This function is deprecated. Pass :json_library to Arangox.start_link/1 instead.

```elixir
@spec json_library() :: module()
```

Returns the JSON library from the deprecated application config, or `Jason`.

Deprecated in v0.8 and to be removed in the next release, along with the
application-config read it reports on. It returns the *fallback*, not what any particular pool uses:
since v0.8 the JSON library is a per-pool start option, so two pools can disagree
and neither has to agree with this. Pass `:json_library` to `start_link/1` instead.

Emits a deprecation warning on every call, because there is no correct number of
times to call it.

# `options`

```elixir
@spec options(conn(), path(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs a OPTIONS request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`.

# `options!`

```elixir
@spec options!(conn(), path(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs a OPTIONS request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `patch`

```elixir
@spec patch(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs a PATCH request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`.

# `patch!`

```elixir
@spec patch!(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs a PATCH request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `plan_cache`

```elixir
@spec plan_cache(conn(), [DBConnection.option()]) :: {:ok, [map()]} | {:error, any()}
```

Lists the current database's cached query plans.

A **database-wide administrative operation**. The plan cache is shared by
every caller of the database, so this exposes the query text of statements
other callers ran, and `clear_plan_cache/2` discards plans they are relying
on. Requires read privileges on the database.

Each entry is the server's own map, including `"query"`, `"hash"` — the same
value `Arangox.Response.plan_cache_key/1` returns — `"hits"`, `"memoryUsage"`
and `"created"`.

> #### An empty list is ambiguous {: .warning}
>
> The server returns only the plans whose collections the caller may read, and
> it filters rather than refuses. A caller permitted on no collections is
> given `[]`, which is indistinguishable from an empty cache. A caller with
> no privileges on the *database* is refused outright, which does surface as
> an error — note that the refusal is a `401`, and `401` is in the default
> `:disconnect_on_error_codes`, so it also retires the connection. ArangoDB
> uses the same status and error number for bad credentials and for
> insufficient rights, so the driver has nothing to tell them apart by.

Accepts `:database` and any of the options accepted by
`DBConnection.execute/4`.

# `plan_cache!`

```elixir
@spec plan_cache!(conn(), [DBConnection.option()]) :: [map()]
```

Lists the current database's cached query plans. Raises in the case of an error.

See `plan_cache/2`, including its warning about empty results.

# `post`

```elixir
@spec post(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs a POST request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`.

# `post!`

```elixir
@spec post!(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs a POST request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `put`

```elixir
@spec put(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs a PUT request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`.

# `put!`

```elixir
@spec put!(conn(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs a PUT request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `query`

```elixir
@spec query(conn(), query(), bindvars(), [DBConnection.option()]) ::
  {:ok, Arangox.Response.t()} | {:error, any()}
```

Runs an AQL query and returns every row of the result.

_ArangoDB_ executes AQL through a server-side cursor whichever of these two
functions you use — they send the same request. The difference is who drives
the cursor afterwards: this one drains it and hands back a single response
holding the complete result, while `cursor/4` hands you the cursor to read a
batch at a time inside a `run/3` or `transaction/3` block.

A result that fits one batch costs exactly one request either way, because the
server keeps no cursor when it has nothing more to send. The extra requests
happen only when there is more, and then only here — a caller who stops
reading a `cursor/4` stream never asks for the rest.

Takes an AQL binary or an `t:Arangox.Query.t/0`. AQL options may be given
here or carried by the query; see `Arangox.Query` for the names.

Does **not** ask the server to reuse a cached query plan unless told to. Pass
`use_plan_cache: true` when the same statement runs repeatedly; it is opt-in
because it has a server-version floor and the server refuses to cache some
statements outright. There is no prepare step to do it for you — see
`Arangox.Query`.

## Example

    iex> {:ok, conn} = Arangox.start_link()
    iex> {:ok, response} =
    iex>   Arangox.query(conn, "FOR i IN [1, 2, 3] FILTER i != @skip RETURN i", %{skip: 2})
    iex> response.body["result"]
    [1, 3]

# `query!`

```elixir
@spec query!(conn(), query(), bindvars(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs an AQL query and returns every row of the result. Raises on error.

See `query/4`.

# `request`

```elixir
@spec request(conn(), method(), path(), body(), headers(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}
```

Runs a request against a connection pool.

Accepts any of the options accepted by `DBConnection.execute/4`, as well as:

  * `:transaction` - An `t:Arangox.Transaction.t/0` handle from
  `begin_transaction/2`. The request joins that stream transaction: its
  identifier is sent in the _x-arango-trx-id_ header **for this request
  only** — connection state is never written, so the handle applies on
  whichever pooled connection serves the request. Accepts only the handle
  struct, never a bare identifier binary. Every request-running function in
  this module accepts this option.

`headers` is a list of `{name, value}` string tuples, appended after the
pool's `:headers` and sent as given — order kept, duplicates kept, nothing
renamed or re-cased. See the README's "Headers" section for the exact wire
behavior per client. Maps are no longer accepted (0.8).

The request struct echoed back on success carries `[redacted]` in place of
the _authorization_ and _x-arango-trx-id_ header values; what went on the
wire is the real thing.

# `request!`

```elixir
@spec request!(conn(), method(), path(), body(), headers(), [DBConnection.option()]) ::
  Arangox.Response.t()
```

Runs a request against a connection pool. Raises in the case of an error.

Accepts any of the options accepted by `DBConnection.execute!/4`.

# `run`

```elixir
@spec run(conn(), (DBConnection.t() -&gt; result), [DBConnection.option()]) :: result
when result: var
```

Acquires a connection from a pool and runs a series of requests or cursors with it.
If the connection disconnects, all future calls using that connection reference will
fail.

Runs can be nested multiple times if the connection reference is used to start a
nested run (i.e. calling another function that calls this one). The top level run
function will represent the actual run.

Delegates to `DBConnection.run/3`.

## Example

    result =
      Arangox.run(conn, fn c  ->
        Arangox.request!(c, ...)
      end)

# `start_link`

```elixir
@spec start_link([start_option()]) :: GenServer.on_start()
```

Starts a connection pool.

## Options

Accepts any of the options accepted by `DBConnection.start_link/2`, as well as any of the
following:

  * `:endpoints` - Either a single _ArangoDB_ endpoint binary, or a list of endpoints in
  order of presedence. Each process in a pool will individually attempt to establish a connection
  with and check the availablility of each endpoint in the order given until an available endpoint
  is found. Defaults to `"http://localhost:8529"`.
  * `:database` - Arangox will prepend `/_db/:value` to the path of every request that
  isn't already prepended. If a value is not given, nothing is prepended (_ArangoDB_ will
  assume the __system_ database).
  * `:headers` - A list of `{name, value}` header tuples sent with every request,
  in the order given, before the request's own headers. Nothing is merged,
  deduplicated, renamed or re-cased; see the "Headers" section of the README
  for the exact wire behavior per client.
  * `:disconnect_on_error_codes` - A list of status codes that will trigger a forced disconnect.
  Only integers within the range `400..599` are affected. Defaults to
  `[401, 405, 503, 505]`.
  * `:allow_cleartext_auth` - Permit `:auth` credentials to be sent to a cleartext
  (`http://`) endpoint that is not on this machine. Defaults to `false`, and a pool
  configured that way refuses to start: the credentials would be readable by anything
  on the network path. Loopback endpoints are exempt and need no opt-in.
  * `:auth` - Configure whether to resolve authorization.
  Options are: `{:basic, username, password}`, `{:bearer, token}`.
  * `:endpoint_mapper` - Remaps the endpoint a server advertises in the _x-arango-endpoint_
  header of a `503` to an address this client can reach. Either a map of
  `advertised endpoint => target endpoint`, a one-argument function returning the target
  endpoint, or an `{module, function, args}` tuple (the advertised endpoint is prepended to
  `args`). A map with no entry for the advertised endpoint, or a function returning anything
  that is not an endpoint binary, refuses the redirect. Defaults to no mapper.
  __This is a trust boundary__: see _Leader redirects_ below before setting it.
  * `:read_only?` - Read-only pools will only connect to _followers_ in an active failover
  setup and add an _x-arango-allow-dirty-read_ header to every request. Defaults to `false`.
  Read-only pools never follow a leader redirect — not landing on the leader is what the
  option is for.
  * `:connect_timeout` - Sets the timeout for establishing connections with a database.
  It also bounds the probes the connect pipeline makes on the open socket (the
  authentication, availability and version stages) — all of them together, not one budget
  each — so a server that completes the handshake and then goes quiet cannot wedge a
  connection attempt, and adding a stage cannot extend the worst case. The budget is per
  endpoint attempt: each endpoint the failover walk reaches gets its own.
  * `:request_timeout` - The longest arangox will wait on the socket for one request's
  response, in milliseconds. Must be a positive integer; `0`, negatives and `:infinity`
  are rejected here and per request. Defaults to `15_000`, the same as `DBConnection`'s
  `:timeout`. It is a **ceiling**, not the whole story — see _Request timeouts_ below.
  Can be overridden per request.
  * `:tcp_opts` - Transport options for the tcp socket interface (`:gen_tcp` in the case
  of mint).
  * `:ssl_opts` - Transport options for the ssl socket interface (`:ssl` in the case of
  mint).
  * `:client` - A module that implements the `Arangox.Client` behaviour. Defaults to
  `Arangox.MintClient`. `Arangox.VelocyClient` speaks VelocyStream, which ArangoDB
  removed in server 3.12 — it remains supported as an explicit opt-in for 3.11
  deployments.
  * `:client_opts` - Options passed to the client library itself, for settings that
  belong to the library rather than to the socket. Its shape is the library's own:
  a keyword list for mint, a map for gun. See each client's moduledoc for what it
  accepts and how it combines with `:tcp_opts` and `:ssl_opts`.
  * `:failover_callback` - A function to call every time arangox fails to establish a
  connection. This is only called if a list of endpoints is given, regardless of whether or not
  it's connecting to an endpoint in an _active failover_ setup. Can be either an anonymous function
  that takes one argument (which is an `%Arangox.Error{}` struct), or a three-element tuple
  containing arguments to pass to `apply/3` (in which case an `%Arangox.Error{}` struct is always
  prepended to the arguments).
  * `:json_library` - The module used to encode and decode JSON request and response
  bodies. Only the HTTP clients use it; `Arangox.VelocyClient` speaks _VelocyPack_.
  Must export `encode!/1`, `decode/1` and `decode!/1`. Defaults to `Jason`.
  * `:vst_maxsize` - The maximum size, in bytes, of a single _VelocyStream_ chunk
  written by `Arangox.VelocyClient`. Must be an integer greater than 24, the size of a
  chunk header. Defaults to `30_720`.
  * `:content_type` - `:json` (the default) or `:velocypack`, selecting how an
  HTTP client encodes request bodies. `:velocypack` requires the `:velocy`
  library, sends `content-type: application/x-velocypack`, and asks for the
  same in return with an `accept` header. Responses are decoded by *their own*
  content type, so a server that declines _VelocyPack_ and responds in JSON is
  still read correctly. A `content-type` or `accept` header set among a
  request's *own* headers wins over the pool's setting — the codec is chosen
  from the first `content-type` entry there, any casing, and never from the
  pool's `:headers` list — and one naming neither codec — `text/plain` for
  `/_api/import`, `application/octet-stream` — sends the request body raw:
  it must already be a binary in its wire form. Ignored by
  `Arangox.VelocyClient`, which carries its own body encoding.
  * `:max_body_size` - The largest response body, in bytes, handed to a codec.
  A larger body is refused with a structured error (`reason: :body_too_large`)
  before any decoding runs, bounding what a hostile or broken server can make
  the pool parse. Defaults to `134_217_728` (128 MiB). Ignored by
  `Arangox.VelocyClient`.

## Leader redirects

In an _active failover_ setup a follower responds `503` and names the current leader in an
_x-arango-endpoint_ response header. Arangox follows that header during connect, but only
when the target passes an admission policy. The pool holds your credentials and would send
them to whatever host the server named, so:

  1. The advertised value must parse as an endpoint. One that does not is refused, not
  raised.
  2. Its normalized origin — encryption class, host **and port** — must already appear in
  `:endpoints`, or `:endpoint_mapper` must admit it. Normalized means `tcp://` compares
  equal to `http://` and `ssl://`/`tls://` to `https://`, because the server advertises in
  its own scheme vocabulary rather than yours. The comparison uses the full origin and not
  just the host: a configured `http://db.internal:8529` does not authorize a redirect to
  `http://db.internal:9999`, which is a different service on the same machine.
  3. An encrypted connection is never redirected to a cleartext one, even to an endpoint
  that is configured.
  4. At most three redirects are followed per connect attempt, counted across the whole
  endpoint walk, so a pair of servers redirecting to each other terminates.

A refused redirect is treated as what it is — an endpoint that responded and is unusable. The
walk moves on to the next endpoint, `:failover_callback` fires, and the error says which
redirect was refused and why. A redirect that *is* followed is normal operation rather than a
failure and does not fire `:failover_callback`.

### `:endpoint_mapper` is a trust boundary

`:endpoint_mapper` is the only way to follow a redirect to an address that is not in
`:endpoints`. Its output is validated the same way — it must parse, and it may not downgrade
encryption — but it is **not** re-checked against `:endpoints`, because remapping to an
address you never configured is the whole point. A mapper that returns its input unchanged
therefore switches the policy off entirely and lets any server that can satisfy a connect
probe send your credentials anywhere. Map the advertised endpoints you recognise and refuse
everything else; the map form does exactly that.

It exists for the containerized case. Running `docker-compose.yml`'s `resilient_single`
service, the followers on host ports 8003 and 8005 advertise the leader as
`http://localhost:8539` — 8539 is the leader's port *inside* the container, and from the host
the leader is on 8004. No host-side `:endpoints` list can contain `localhost:8539`, so the
default policy refuses that redirect even though it is legitimate. The mapper is how you say
where the leader really is:

    Arangox.start_link(
      endpoints: ["http://localhost:8003"],
      endpoint_mapper: %{"http://localhost:8539" => "http://localhost:8004"}
    )

Map keys are matched on their normalized origin too, so the key above also matches an
advertised `tcp://localhost:8539`.

A topology whose members sit on their real published addresses — the usual bare-metal
active-failover deployment, where `:endpoints` lists the same addresses the servers
advertise — needs no mapper at all.

## Request timeouts

Every request is bounded. Two numbers decide by how much, and they are not the same
kind of number:

  * your `:timeout` (`DBConnection`'s, per call, 15 seconds by default) is a **budget**
    that starts counting the moment you ask for a connection — including the time you
    spend queueing for one;
  * `:request_timeout` is a **ceiling** on how long any single request may sit on the
    socket.

Arangox waits for `min(what is left of your budget, :request_timeout)`, less a small
margin. So a caller that waited 14 seconds for a free connection gets the last second,
not a fresh 15 — and `Arangox.get(conn, path, [], timeout: 500)` really does come back
within 500ms, with an `Arangox.Error` whose `:reason` is `:timeout`, rather than with
`DBConnection`'s "queued and checked out for longer than" error some time later.

Raise the ceiling for one slow request without loosening the pool:

    Arangox.post(conn, "/_api/cursor", body, [], request_timeout: 60_000, timeout: 65_000)

Both numbers matter there: `:request_timeout` alone cannot buy more time than the
caller's budget allows.

If the budget is already gone by the time a connection frees up, the request is not
sent at all and the error's `:reason` is `:deadline_exceeded`. That connection is
healthy and stays in the pool.

`timeout: :infinity` waives the budget, not the ceiling: such a request still gets
`:request_timeout` on the socket. There is no way to configure an unbounded wait.

**A request that times out on the socket takes its connection down with it.** Arangox
cannot tell where an abandoned response ended, so reusing the connection would serve
the leftover bytes to whoever draws it next. The pool reconnects; the request is *not*
retried, because it may well have been executed.

Inside `run/3` and `transaction/3` the block's own `:timeout` bounds every request made
**on the block's connection**, since that is the deadline `DBConnection` armed when the
block checked it out. Such a request never gets a budget longer than what is left of the
block's — including a cursor's per-batch fetches.

A request made from inside a block to a *different* pool is a different checkout with a
deadline of its own, and keeps the budget it asked for:

    Arangox.transaction(primary, fn conn ->
      Arangox.get(conn, path)               # bounded by the block
      Arangox.get(read_replica, other_path) # bounded by its own :timeout
    end)

## Deprecated application config

`:json_library` and `:vst_maxsize` used to be global application config:

    config :arangox, :json_library, Poison
    config :arangox, :vst_maxsize, 12_345

Both reads still work in v0.8 and will be removed in the next release. A pool that
does not pass the start option falls back to the application config, and the fallback
logs a deprecation warning. A value read from the fallback is validated exactly like the start option and
raises here on the same inputs — `:vst_maxsize` used to be checked by
`Application.compile_env/3` at compile time, and losing that check would only move the
failure to a `MatchError` in the middle of a request.

That warning is emitted **once per pool**: precisely, once per `start_link/1` or
`child_spec/1` call, in the calling process, and nowhere else. It is deliberately not
emitted from the `DBConnection` connect callback, which is not a once-per-pool function —
`DBConnection` re-enters it in the same process on every backoff cycle for as long as
the pool lives, in each of the pool's `:pool_size` processes. Warning there would log
several times a second, forever, against an unreachable server. Starting a pool through
`DBConnection.start_link/2` directly bypasses this module and therefore warns not at
all, while still honouring the fallback.

# `status`

```elixir
@spec status(conn()) :: DBConnection.status()
```

Fetches the current status of a transaction from the database and returns its
corresponding `DBconnection` status.

Delegates to `DBConnection.status/1`.

# `transaction`

```elixir
@spec transaction(conn(), (DBConnection.t() -&gt; result), [transaction_option()]) ::
  {:ok, result} | {:error, any()}
when result: var
```

Acquires a connection from a pool, begins a transaction in the database and runs a
series of requests or cursors with it. If the connection disconnects, all future calls
using that connection reference will fail.

Transactions can be nested multiple times if the connection reference is used to start a
nested transactions (i.e. calling another function that calls this one). The top level
transaction function will represent the actual transaction and nested transactions will
be interpreted as a `run/3`, erego, any collections declared in nested transactions will
have no effect.

Accepts any of the options accepted by `DBConnection.transaction/3`, as well as any of the
following:

  * `:read` - An array of collection names or a single collection name as a binary.
  * `:write` - An array of collection names or a single collection name as a binary.
  * `:exclusive` - An array of collection names or a single collection name as a binary.
  * `:database` - Sets what database to run the transaction on
  * `:properties` - A list or map of additional body attributes to append to the request
  body when beginning a transaction.

Delegates to `DBConnection.transaction/3`.

## Example

    Arangox.transaction(conn, fn c ->
      Arangox.status(c) #=> :transaction

      # do stuff
    end, [
      write: "something",
      properties: [waitForSync: true]
    ])

# `transaction_status`

```elixir
@spec transaction_status(conn(), Arangox.Transaction.t(), [DBConnection.option()]) ::
  {:ok, :running | :committed | :aborted | binary()} | {:error, any()}
```

Fetches the status of the stream transaction the handle names from the
database.

Answers from the body of the server's reply, not from the HTTP status: the
server keeps responding 200 for a finished transaction within a retention
window, with the actual state in the body. Returns `{:ok, :running}`,
`{:ok, :committed}` or `{:ok, :aborted}`; a status this driver does not
recognize is returned as its raw binary rather than turned into an atom.

This is the handle form's counterpart to `status/1`, which reports on the
closure transaction bound to a connection. It runs against any pooled
connection to the deployment; accepts only a `t:Arangox.Transaction.t/0`.

Accepts any of the options accepted by `DBConnection.execute/4`, plus
`:database` (pass the one the transaction was begun in).

# `transaction_status!`

```elixir
@spec transaction_status!(conn(), Arangox.Transaction.t(), [DBConnection.option()]) ::
  :running | :committed | :aborted | binary()
```

Fetches the status of the stream transaction the handle names from the
database. Raises in the case of an error.

See `transaction_status/3`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
