Exadb

Exadb is a pragmatic Elixir client for ArangoDB’s HTTP API.

It is built for the way teams actually work with ArangoDB in real applications:

That workflow has been used in production for years. The code was stable and useful long before it was packaged for public release.

Exadb stays intentionally small and direct:

If you want a thin, dependable layer over ArangoDB instead of a large abstraction that hides the database, Exadb is the right shape.

Installation

Add :exadb to your dependencies:

defp deps do
  [
    {:exadb, "~> 0.1.0"}
  ]
end

Then fetch dependencies:

mix deps.get

Configuration

The client reads these environment variables by default:

You can also pass explicit options to every function:

opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "example"]

This makes it easy to use Exadb in both styles:

Why Exadb

Feature List

Exadb currently covers these ArangoDB workflows:

The Main Workflow

The most natural Exadb workflow is:

  1. fetch a document
  2. edit the map
  3. persist it back

Exadb.Doc.persist/2 decides what to do from the content you give it.

That means the common edit cycle stays clean:

opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "app", col: "users"]

user = Exadb.Doc.fetch("users/123", opts)

updated_user =
  user
  |> Map.put("display_name", "Jane Doe")
  |> Map.put("active", true)
  |> Exadb.Doc.persist(opts)

No separate create-vs-update branch in your application code.

Query And Cursor Workflow

Not every job is a single-document workflow.

When the work is better expressed in AQL, Exadb gives you a second natural path:

  1. write the AQL you actually want
  2. pass bind variables as a plain map
  3. read the decoded result
  4. move to cursor paging or streaming when the result set grows

That means you can use Exadb comfortably in both modes:

For straightforward AQL execution:

opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "app"]

Exadb.Query.run(
  "FOR user IN users FILTER user.active == @active SORT user.email RETURN user",
  %{active: true},
  opts
)

For cursor-based reads, start a cursor with a map:

first_page =
  Exadb.Query.cursor(
    %{
      query: "FOR user IN users SORT user.email RETURN user",
      batchSize: 100
    },
    opts
  )

If ArangoDB returns more results, pass the cursor response back into Exadb.Query.cursor/2 to fetch the next page.

If you want a cleaner streaming model, use Exadb.Query.cursor_stream/2:

Exadb.Query.cursor_stream(
  %{
    query: "FOR user IN users SORT user.email RETURN user",
    batchSize: 100
  },
  opts
)
|> Enum.each(fn page ->
  Enum.each(page["result"], fn user ->
    IO.inspect(user["email"])
  end)
end)

This is especially useful for:

Create New Documents

For new data, just pass a plain map:

opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "app", col: "users"]

created =
  Exadb.Doc.persist(%{
    "email" => "jane@example.com",
    "display_name" => "Jane",
    "active" => true,
    "roles" => ["admin"]
  }, opts)

created["_id"]
#=> "users/123"

Clone Or Reinsert Without Metadata

If you fetched a document and want to store it as a fresh record instead of updating the existing one, use persist_new/2.

copy =
  created
  |> Map.put("email", "copy@example.com")
  |> Exadb.Doc.persist_new(opts)

Usage

Documents

Exadb is at its best when your application logic is document-centric.

opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "app", col: "users"]

created = Exadb.Doc.persist(%{"email" => "jane@example.com", "tags" => ["new"]}, opts)
found = Exadb.Doc.fetch(created["_id"], opts)

retagged =
  found
  |> Map.put("tags", ["new", "customer"])
  |> Exadb.Doc.persist(opts)

Exadb.Doc.push(retagged["_id"], "tags", "beta", opts)
Exadb.Doc.pop(retagged["_id"], "tags", "new", opts)
Exadb.Doc.switch_on(retagged["_id"], "active", opts)

Other useful document helpers:

Queries

For AQL-heavy parts of an application, use Exadb.Query.

opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "app"]

Exadb.Query.run(
  "FOR user IN users FILTER user.email == @email RETURN user",
  %{email: "jane@example.com"},
  opts
)

If you need cursor-based processing, Exadb.Query.cursor/2 and Exadb.Query.cursor_stream/2 let you work through larger result sets without changing mental models.

Collections, Edges, And Indexes

Schema-level operations stay just as direct.

db_opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "app"]
dblink = Exadb.Api.db(nil, db_opts)

Exadb.Collection.new_collection("users", %{}, db_opts)
Exadb.Collection.new_edge("follows", %{}, db_opts)

Exadb.Index.new(
  "users",
  %{"type" => "persistent", "fields" => ["email"], "unique" => true},
  dblink
)

This is enough to keep bootstrapping, migration scripts, and admin tooling readable.

Graphs

When you need named graph setup, Exadb keeps that API small too:

db_opts = [url: "localhost:8529", user: "root", pwd: "secret", db: "app"]
dblink = Exadb.Api.db(nil, db_opts)

Exadb.Graph.new(
  "social",
  [
    %{
      collection: "follows",
      from: ["users"],
      to: ["users"]
    }
  ],
  [],
  dblink
)

Users And Databases

Exadb also covers the practical admin operations needed by real systems.

admin_opts = [url: "localhost:8529", user: "root", pwd: "secret"]

Exadb.Database.new_db_and_user("tenant_a", "tenant-secret", admin_opts)
Exadb.User.give_access("tenant_a", "tenant_a", Keyword.merge(admin_opts, level: "rw"))

For operational data movement, Exadb.Manager also includes higher-level copy helpers. That covers cases such as copying a database schema into a new tenant database, cloning collection data, or moving records selected by a custom AQL filter.

That makes it useful not just in application code, but also in:

URL Helpers

If you want to build URLs once and pass them through explicitly, the API helpers are available too:

Exadb.Api.url(url: "localhost:8529", user: "root", pwd: "secret")
#=> "http://root:secret@localhost:8529"

Exadb.Api.root(url: "localhost:8529", user: "root", pwd: "secret")
#=> "http://root:secret@localhost:8529/_api"

Exadb.Api.db("app", url: "localhost:8529", user: "root", pwd: "secret")
#=> "http://root:secret@localhost:8529/_db/app/_api"

Testing And Documentation

Run the normal test suite:

mix test

Run integration tests against a real ArangoDB instance:

mix test --include integration
EXADB_RUN_INTEGRATION=1 mix test

The integration suite supports these environment variables:

By default, the integration suite recreates the mix_test database and matching user before the run and removes them afterwards.

Generate docs locally with:

mix docs

Additional guides:

Exadb is production-proven code that stayed private for a long time. Its design is direct because it grew out of repeated real-world use, not from trying to model every possible abstraction up front.

If you want a client that makes ArangoDB pleasant to use from Elixir without forcing you into a heavy framework, Exadb is ready for that job.