Hex.pm Documentation CI

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.

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:

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:

{: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:

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

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

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:

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:

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:

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

Useful starting points include:

See the HexDocs API reference 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:

{: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:

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:

{: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.

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:

{: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:

{: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:

OptionPurposeDefault
:endpointsOne endpoint or an ordered failover list"http://localhost:8529"
:databaseDatabase prepended to database-scoped requestsServer default
:auth{:basic, user, password} or {:bearer, token}No authentication
:pool_sizeNumber of concurrently usable connections10
:headersHeaders sent with every request[]
:connect_timeoutBudget for each connection attempt5_000 ms
:request_timeoutPer-request socket-wait ceiling15_000 ms
:max_body_sizeLargest response body accepted for decoding128 MiB
:content_typeHTTP request-body codec:json
:clientTransport implementationArangox.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:

OptionGoes toRead when
:tcp_optsErlang's :gen_tcpThe endpoint is cleartext — http://, tcp://, or a Unix socket
:ssl_optsErlang's :sslThe endpoint is encrypted — https://, ssl://, or tls://
:client_optsThe transport library itselfAlways

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:

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:

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.

ClientProtocolsServer supportAdditional dependency
Arangox.MintClientHTTP/1.1 by default; optional HTTP/2ArangoDB 3.11 and 3.12{:mint, "~> 1.9"}
Arangox.GunClientHTTP/1.1 and HTTP/2ArangoDB 3.11 and 3.12{:gun, "~> 2.0"}
Arangox.VelocyClientVelocyStreamArangoDB 3.11 only{:velocy, "~> 0.2"}

Select another client per pool:

Arangox.start_link(client: Arangox.GunClient)

Mint's HTTP/2 mode is explicit:

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:

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:

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.
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 — the "Migrating from v0.7" section — before upgrading an existing application.

Development

Unit and protocol tests run without Docker:

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

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

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

Documentation

License

Arangox is released under the MIT License.