Arangox v0.2.0 Arangox View Source

Build Status

An implementation of 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.

Tested on:

  • ArangoDB 3.3.9 - 3.5
  • Elixir 1.6 - 1.9
  • OTP 20 - 22

Supports active failover.

Peer Dependencies

Arangox requires a json library and http client to work, the defaults are :jason and :gun:

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:

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:

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:

Arangox.start_link(client: Arangox.Client.Mint)

NOTE: Mint does not accept paths to unix sockets at the moment.

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

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 assumes a default :pool_size of 1 so the following:

Arangox.start_link()

Is equivalent to:

options = [
  pool_size: 1,
  endpoints: ["http://localhost:8529"],
  username: "root",
  password: ""
]
Arangox.start_link(options)

Endpoints

See the arangosh or arangojs documentation for examples of supported endpoint formats.

As is common amongst ArangoDB drivers, arangox takes a list of endpoints as binaries:

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:

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"

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.

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 transfomed requests returned by arangox, for obvious reasons:

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).

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:

[{"header", "value"}, {"another-header", "another-value"}]

When given to the start option they are merged with every request.

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:

iex> {:ok, conn} = Arangox.start_link()
iex> {:ok, request, _response} = Arangox.get(conn, "/_admin/time", [{"header", "value"}])
iex> request.headers
[{"header", "value"}, {"authorization", "..."}]

Headers given to the start option will not override any of the headers set by Arangox, but headers passed to requests will.

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.

See :gen_tcp.connect_option() for more information on :tcp_opts, or :ssl.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 values set by arangox. Some options cannot be overridden (i.e. Mint's :mode option). 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 or 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

If anyone would like to collaborate, find me on the elixir-lang or arangodb-community slack.

Link to this section Summary

Functions

Returns a supervisor child specification for a DBConnection pool.

Runs a DELETE request against a connection pool.

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

Runs a GET request against a connection pool.

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

Runs a HEAD request against a connection pool.

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

Returns the configured JSON library.

Runs a OPTIONS request against a connection pool.

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

Runs a PATCH request against a connection pool.

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

Runs a POST request against a connection pool.

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

Runs a PUT request against a connection pool.

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

Runs a request against a connection pool.

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

Starts a connection pool.

Acquire a connection from a pool and run a series of requests with it. If the connection disconnects, all future calls using that connection reference will fail.

Link to this section Types

Link to this type

body()

View Source
body() :: binary() | map() | list() | nil
Link to this type

endpoint()

View Source
endpoint() :: binary()
Link to this type

method()

View Source
method() :: :get | :head | :delete | :post | :put | :patch | :options
Link to this type

start_option()

View Source
start_option() ::
  {:client, module()}
  | {:endpoints, [endpoint()]}
  | {:auth?, boolean()}
  | {:database, binary()}
  | {:username, binary()}
  | {:password, binary()}
  | {:headers, [header()]}
  | {:read_only?, boolean()}
  | {:connect_timeout, timeout()}
  | {:failover_callback,
     (Arangox.Error.t() -> any()) | {module(), atom(), [any()]}}
  | {:tcp_opts, [:gen_tcp.connect_option()]}
  | {:ssl_opts, [:ssl.tls_client_option()]}
  | {:client_opts, :gun.opts() | keyword()}
  | DBConnection.start_option()

Link to this section Functions

Returns a supervisor child specification for a DBConnection pool.

Link to this function

delete(conn, path, headers \\ [], opts \\ [])

View Source
delete(conn(), path(), [header()], [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a DELETE request against a connection pool.

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

Link to this function

delete!(conn, path, headers \\ [], opts \\ [])

View Source
delete!(conn(), path(), [header()], [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.

Link to this function

get(conn, path, headers \\ [], opts \\ [])

View Source
get(conn(), path(), [header()], [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a GET request against a connection pool.

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

Link to this function

get!(conn, path, headers \\ [], opts \\ [])

View Source

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.

Link to this function

head(conn, path, headers \\ [], opts \\ [])

View Source
head(conn(), path(), [header()], [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a HEAD request against a connection pool.

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

Link to this function

head!(conn, path, headers \\ [], opts \\ [])

View Source

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.

Link to this function

json_library()

View Source
json_library() :: module()

Returns the configured JSON library.

To customize the JSON library, include the following in your config/config.exs:

config :arangox, :json_library, Module

Defaults to Jason.

Link to this function

options(conn, opts \\ [])

View Source
options(conn(), [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a OPTIONS request against a connection pool.

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

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.

Link to this function

patch(conn, path, body \\ "", headers \\ [], opts \\ [])

View Source
patch(conn(), path(), body(), [header()], [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a PATCH request against a connection pool.

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

Link to this function

patch!(conn, path, body \\ "", headers \\ [], opts \\ [])

View Source
patch!(conn(), path(), body(), [header()], [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.

Link to this function

post(conn, path, body \\ "", headers \\ [], opts \\ [])

View Source
post(conn(), path(), body(), [header()], [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a POST request against a connection pool.

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

Link to this function

post!(conn, path, body \\ "", headers \\ [], opts \\ [])

View Source
post!(conn(), path(), body(), [header()], [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.

Link to this function

put(conn, path, body \\ "", headers \\ [], opts \\ [])

View Source
put(conn(), path(), body(), [header()], [DBConnection.option()]) ::
  {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}

Runs a PUT request against a connection pool.

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

Link to this function

put!(conn, path, body \\ "", headers \\ [], opts \\ [])

View Source

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.

Link to this function

request(conn, method, path, body \\ "", headers \\ [], opts \\ [])

View Source
request(conn(), method(), path(), body(), [header()], [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.

Link to this function

request!(conn, method, path, body \\ "", headers \\ [], opts \\ [])

View Source
request!(conn(), method(), path(), body(), [header()], [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.

Link to this function

start_link(opts \\ [])

View Source
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 - A list of ArangoDB 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 one 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 headers to merge with every request.
  • :auth? - Configure whether or not to add an authorization header to every request with the provided username and password. Defaults to true.
  • :username - Defaults to "root".
  • :password - Defaults to "".
  • :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.
  • :connect_timeout - Sets the timeout for establishing a connection with a server.
  • :transport_opts - Transport options for the socket interface (which is :gen_tcp or :ssl for both gun and mint, depending on whether or not they are connecting via ssl).
  • :tcp_opts - Transport options for the tcp socket interface (:gen_tcp in the case of gun or mint).
  • :ssl_opts - Transport options for the ssl socket interface (:ssl in the case of gun or mint).
  • :client - A module that implements the Arangox.Client behaviour. Defaults to Arangox.Client.Gun.
  • :client_opts - Options for the client library being used. WARNING: If :transport_opts is set here it will override the options given to :tcp_opts and :ssl_opts.
  • :failover_callback - A function to call every time arangox fails to establish a connection. This is called 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).
Link to this function

transaction(conn, fun, opts \\ [])

View Source
transaction(conn(), (DBConnection.t() -> result), [DBConnection.option()]) ::
  {:ok, result} | {:error, any()}
when result: var

Acquire a connection from a pool and run a series of requests with it. If the connection disconnects, all future calls using that connection reference will fail.

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

Delegates to DBConnection.transaction/3.

Example

{:ok, result} =
  Arangox.transaction(conn, fn c  ->
    Arangox.request!(c, ...)
  end)