# Arangox
[![Build Status](https://travis-ci.com/suazithustra/arangox.svg?branch=master)](https://travis-ci.org/suazithustra/arangox)
An implementation of [`db_connection`](https://hex.pm/packages/db_connection)
for _ArangoDB_, which is silly because _Arangodb_ is not a transactional database (i.e.
no prepare, commit, rollback, etc.), but whatever, it's a solid connection pooler.
Arangox supports [active failover](https://www.arangodb.com/docs/stable/architecture-deployment-modes-active-failover-architecture.html).
### Peer Dependencies
Arangox requires a json library and http client to work, the defaults are `:jason` and
`:gun`:
```elixir
def deps do
[
...
{:arangox, "~> 0.1.0"},
{:jason, "~> 1.1"},
{:gun, "~> 1.3"}
]
end
```
You _might_ need to add `:gun` as an extra application in `mix.exs`:
```elixir
def application() do
[
extra_applications: [:logger, :gun])
]
end
```
To use a different json library, set the `:json_library` config to the module of your
choice:
```elixir
config :arangox, :json_library, Poison
```
Arangox already has a `Mint` client. To use it, add `:mint` to your deps instead of
`:gun` and set the `:client` start option to `Arangox.Client.Mint`:
```elixir
Arangox.start_link(client: Arangox.Client.Mint)
```
To use something else, you'd have to implement the `Arangox.Client` behaviour in a
module somewhere and set that instead. The `Arangox.Endpoint` module has utilities
for parsing _ArangoDB_ endpoints.
### Examples
```elixir
iex> {:ok, conn} = Arangox.start_link(pool_size: 10)
iex> Arangox.request(conn, :options, "/")
{:ok,
%Arangox.Request{
body: "",
headers: [{"authorization", "..."}],
method: :options,
path: "/"
},
%Arangox.Response{
body: nil,
headers: [
{"x-content-type-options", "nosniff"},
{"allow", "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT"},
{"server", "ArangoDB"},
{"connection", "Keep-Alive"},
{"content-type", "text/plain; charset=utf-8"},
{"content-length", "0"}
],
status: 200
}}
iex> Arangox.options!(conn)
%Arangox.Response{
body: nil,
headers: [
{"x-content-type-options", "nosniff"},
{"allow", "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT"},
{"server", "ArangoDB"},
{"connection", "Keep-Alive"},
{"content-type", "text/plain; charset=utf-8"},
{"content-length", "0"}
],
status: 200
}
```
## Options
Arangox assumes defaults for the `:endpoints`, `:username` and `:password` options,
and [`db_connection`](https://hex.pm/packages/db_connection) assumes a default
`:pool_size` of `1` so the following:
```elixir
Arangox.start_link()
```
Is equivalent to:
```elixir
options = [
pool_size: 1,
endpoints: ["http://localhost:8529"],
username: "root",
password: ""
]
Arangox.start_link(options)
```
### Endpoints
As is common amongst _ArangoDB_ drivers, arangox takes a list of endpoints as binaries:
```elixir
endpoints = [
"http://localhost:8529",
"http://localhost:8530",
"http://localhost:8531"
]
Arangox.start_link(endpoints: endpoints)
```
Arangox will try to establish a connection with the first endpoint it can and
check it's availability (via the _ArangoDB_ api). If an endpoint is in maintenance mode
or is a follower in an _active failover_ setup, it will be skipped.
With the `read_only?` option set to `true`, arangox will try to find a server in
_readonly_ mode instead and add the _x-arango-allow-dirty-read_ header to every request:
```elixir
iex> endpoints = ["http://localhost:8003", "http://localhost:8004", "http://localhost:8005"]
iex> {:ok, conn} = Arangox.start_link(endpoints: endpoints, read_only?: true)
iex> %Arangox.Response{body: body} = Arangox.get!(conn, "/_admin/server/mode")
iex> body["mode"]
"readonly"
iex> {:error, exception} = Arangox.post(conn, "/_api/database", %{name: "newDatabase"})
iex> exception.message
"forbidden"
```
See the
[arangosh](https://www.arangodb.com/docs/stable/programs-arangosh-examples.html) or
[arangojs](https://www.arangodb.com/docs/stable/drivers/js-reference-database.html)
documentation for examples of supported endpoint formats.
### Authentication
Arangox will generate an authorization header with the `:username` and `:password`
options and add it to every request. To prevent this behavior, set the `:auth?`
option to `false`.
```elixir
iex> {:ok, conn} = Arangox.start_link(auth?: false)
iex> {:error, exception} = Arangox.get(conn, "/_admin/server/mode")
iex> exception.message
"not authorized to execute this request"
```
The header value is obfuscated in the transfomed requests returned by arangox, for
obvious reasons:
```elixir
iex> {:ok, conn} = Arangox.start_link()
iex> {:ok, request, _response} = Arangox.options(conn)
iex> request.headers
[{"authorization", "..."}]
```
### Databases
If a value is given to the `:database` option, 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).
```elixir
iex> {:ok, conn} = Arangox.start_link()
iex> {:ok, request, _response} = Arangox.get(conn, "/_admin/time")
iex> request.path
"/_admin/time"
iex> {:ok, conn} = Arangox.start_link(database: "myDatabase")
iex> {:ok, request, _response} = Arangox.get(conn, "/_admin/time")
iex> request.path
"/_db/myDatabase/_admin/time"
iex> {:ok, request, _response} = Arangox.get(conn, "/_db/anotherDatabase/_admin/time")
iex> request.path
"/_db/anotherDatabase/_admin/time"
```
### Headers
Headers are given as lists of two-element tuples:
```elixir
[{"header", "value"}, {"another-header", "another-value"}]
```
When given to the `:headers` start option, they are merged with every request.
```elixir
iex> {:ok, conn} = Arangox.start_link(headers: [{"header", "value"}])
iex> {:ok, request, _response} = Arangox.options(conn)
iex> request.headers
[{"authorization", "..."}, {"header", "value"}]
```
Headers can also be passed as an argument to any request:
```elixir
iex> {:ok, conn} = Arangox.start_link()
iex> {:ok, request, _response} = Arangox.get(conn, "/_admin/time", [{"header", "value"}])
iex> request.headers
[{"header", "value"}, {"authorization", "..."}]
```
### Transport
Transport options can be specified via `:tcp_opts` and `:ssl_opts`, for non-encrypted and
encrypted connections respectively. These options are passed directly to the `:transport_opts`
option of `:gun` or `Mint`. Some transport options are set by arangox and cannot be
overridden with these options (i.e. `Mint`'s `:mode` option).
See [`:gen_tcp.connect_option()`](http://erlang.org/doc/man/gen_tcp.html#type-connect_option)
for more information on `:tcp_opts`, or [`:ssl.tls_client_option()`](http://erlang.org/doc/man/ssl.html#type-tls_client_option) for `:ssl_opts`.
The `:client_opts` option can be used to pass client-specific options to `:gun` or `Mint`.
These options are merged with and may override any of the values set by arangox. If
`:transport_opts` is set here it will override everything given to `:tcp_opts` or `:ssl_opts`,
regardless of whether or not a connection is encrypted.
See the `gun:opts()` type in the [gun docs](https://ninenines.eu/docs/en/gun/1.3/manual/gun/)
or [`connect/4`](https://hexdocs.pm/mint/Mint.HTTP.html#connect/4) in the mint docs for more
information.
## Contributing
```
mix do format, credo
docker-compose up -d
mix test
```
## Roadmap
- A VelocyStream client
- An Ecto adapter
- More descriptive logs