JavaScript / TypeScript — HTTP Driver

@arcadedb/driver is ArcadeDB’s TypeScript/JavaScript HTTP client, generated from the server’s OpenAPI contract.

Node.js 20+, ESM only.

The package requires Node.js 20 or later, and it ships ESM only — there is no CommonJS build and no require() entry point. Import it with import, not require.

Install

npm install @arcadedb/driver

Connect and query

import { ArcadeDBError, basicAuth, createClient } from "@arcadedb/driver";

const server = createClient({
  baseUrl: "http://localhost:2480",
  auth: basicAuth("root", "playwithdata"),
});

const db = server.db("mydb");

const envelope = await db.query({
  language: "sql",
  command: "SELECT FROM Person WHERE age > ?",
  params: { 0: 21 },
});
console.log(envelope.result, envelope.returned, envelope.truncated);
[
  {
    '@rid': '#1:0',
    '@type': 'Person',
    '@cat': 'v',
    name: 'Alice',
    age: 30
  },
  {
    '@rid': '#1:1',
    '@type': 'Person',
    '@cat': 'v',
    name: 'Bob',
    age: 25
  }
] 2 false

Positional parameters are zero-indexed — { 1: 21 } silently returns nothing.

Positional parameters are keyed by their zero-based index. For the single placeholder above, the key is 0, not 1. Passing params: { 1: 21 } does not raise an error and does not match the wrong rows — it runs the query with no bound value for ? at all and comes back with an empty result (returned: 0, truncated: false). The driver’s own README shows { 1: 21 }; it looks plausible and returns quietly, which is exactly what makes it easy to ship. Always start positional parameter keys at 0.

There is no timeout option, and fetch has no default timeout.

createClient accepts exactly four options — baseUrl, auth, fetch and WebSocket (used only by the insert session described below). There is no timeout, and the client calls the runtime’s global fetch, which waits forever by default. A request against a stalled connection therefore hangs indefinitely unless you bound it yourself. The fetch option is where to do that: pass a wrapper that attaches an AbortSignal.

import { basicAuth, createClient } from "@arcadedb/driver";

const server = createClient({
  baseUrl: "http://localhost:2480",
  auth: basicAuth("root", "playwithdata"),
  fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(5000) }),
});

const envelope = await server.db("mydb").query({ language: "sql", command: "SELECT FROM Person" });
console.log(envelope.returned);
2

The Python driver takes an explicit timeout argument instead; see the Python HTTP driver page, where the same "no timeout unless you ask" default applies.

basicAuth("root", "playwithdata") builds the Authorization header for HTTP basic auth; bearerAuth("AU-…​") substitutes directly for it wherever you already hold a session token or other bearer credential; see Sessions and API tokens for getting one.

The result envelope

query and command do not return bare rows. They return the whole response envelope:

interface QueryEnvelope<T> {
  result: T[];
  limit: number;
  returned: number;
  truncated: boolean;
  explain?: string;                       // EXPLAIN / PROFILE only
  explainPlan?: Record<string, unknown>;  // present exactly when `explain` is
}

See Native Drivers for why truncated matters and what to do when it is true.

An EXPLAIN or PROFILE statement answers with its execution plan instead of rows: explain holds it as indented text and explainPlan as the same plan in structured form, while result is empty and returned is 0. An empty result there is not "no rows matched".

Transactions

const total = await db.transaction(async (tx) => {
  await tx.command({ language: "sql", command: "INSERT INTO Account SET balance = 100" });
  const { result } = await tx.query({ language: "sql", command: "SELECT sum(balance) AS total FROM Account" });
  return result[0].total;
});
console.log("total:", total);
total: 200

(The total above reflects balances already present in this database from earlier examples on this page’s server; a fresh Account type would show 100.)

Every call inside the callback must go through the tx handle it receives, not the outer db — calls made through db while a transaction is open auto-commit individually, outside it. The contract:

  • transaction commits when the callback resolves, and returns the callback’s own return value.

  • It rolls back and re-throws when the callback throws or rejects.

  • The caller always sees the callback’s own error. If the rollback itself also fails, that failure is attached to the error as err.cause rather than replacing it.

  • If the commit itself fails, transaction issues a best-effort rollback first, to release the server-side session, before re-throwing the commit’s error.

Two error models

Facade methods — query, command, transaction, and others — throw ArcadeDBError on any non-2xx response:

try {
  await db.query({ language: "sql", command: "SELECT FROM NoSuchType" });
} catch (err) {
  if (err instanceof ArcadeDBError) console.error(err.status, err.error, err.detail, err.requestId);
}
500 Internal error Type with name 'NoSuchType' was not found 1a9b928039f2a6cd-30

requestId is the server’s request identifier for that call and differs on every request; use it to correlate a failure with the server log. Two more fields come from the response’s headers: retryAfter, the seconds the server asked you to wait before retrying (on a 503, or a 409 for an identical request still in flight), and commitIndex, the Raft index to send back as X-ArcadeDB-Read-After on a later read. Both are undefined when the server sent none, and 0 is a valid retryAfter, so compare against undefined.

A 409 has two meanings. It is either "an identical request is still in flight", which is retryable after retryAfter, or "query terminated", because an operator stopped the statement and its writes were rolled back. Retrying the second undoes the operator’s decision. Tell them apart with isQueryTerminated(err), never by status === 409.

A rejected time-series write throws TimeSeriesWriteError, a subclass of ArcadeDBError that says how many samples were written and dropped and why. A write is not atomic: the written samples landed, so re-sending the whole batch writes them twice.

server.raw — the underlying openapi-fetch client beneath the facade — takes the opposite approach: it never throws. It returns { data, error } and leaves handling the error to the caller. Use the facade for the ergonomics of try/catch; use raw when you would rather branch on { data, error } without exceptions.

Beyond query, command and transaction, a database handle also exposes db.ts, db.grafana and db.promql for time-series ingestion, Grafana panel queries and a PromQL-compatible query surface (including db.promql.labelValues), and db.vector for vector, hybrid and full-text search — see the package README linked below for those.

Labelling work with a query tag

query, command, queryStream, commandStream, batchLoad and batchLoadStream take an optional queryTag, and transaction takes one as its second argument. The tag travels as the X-ArcadeDB-Query-Tag header, so you can list and terminate the work by it:

await db.query({ language: "sql", command: "SELECT FROM Orders", queryTag: "nightly-report" });

await db.transaction(
  async (tx) => {
    await tx.command({ language: "sql", command: "UPDATE Orders SET archived = true" });
  },
  { queryTag: "archive-run" },
);

A transaction’s tag is sent on the begin request only; the server applies it to every statement in the transaction that carries no tag of its own.

Duplex insert session

db.insertSession() opens an insert session on the server’s /ws endpoint. Unlike batchLoad, whose commit policy is fixed before the load starts, a session lets you read each chunk’s acknowledgement before deciding what to send next, and whether to commit at all:

const session = await db.insertSession({ targetType: "Person" });
try {
  const ack = await session.sendChunk([{ name: "a" }, { name: "b" }]);
  console.log(ack.inserted, ack.failed, ack.errors); // nothing is durable yet
  const done = await session.commit();                // or: await session.rollback()
  console.log(done.outcome, done.summary.inserted);
} finally {
  await session.close();                              // rolls back if still open
}

It uses the runtime’s global WebSocket (Node 22+); on Node 20, pass the ws package’s class as createClient’s `WebSocket option.

Administration

createDatabase, dropDatabase, createUser, updateUser, dropUser, listUsers, saveGroup, deleteGroup and listGroups are on the server handle, and serverCommand runs any other server command. All of them are root-only:

await server.createDatabase("tenant_a");
await server.createUser("alice", "a-long-password", { databases: { tenant_a: ["admin"] } });
const users = await server.listUsers();
await server.dropUser("alice");
await server.dropDatabase("tenant_a");

A user or database that already exists is a 409 on ArcadeDB 26.10.2 and later. Older servers answer 403 for a duplicate user and 400 for a duplicate database.

Sessions and API tokens

// Log in as each user once, then keep a client that holds only that user's token.
const login = await server.loginAs("alice", "a-long-password"); // or server.login()
const alice = server.withToken(login.token);                    // server is unchanged
await alice.listDatabases();                                    // only what alice may see
await alice.logout();

// A long-lived machine credential (root only).
const created = await server.createAPIToken("ci", { database: "tenant_a" });
store(created.token);                          // the only time the plaintext exists
await server.deleteAPIToken(created.tokenHash); // by hash, never by token

listSessions and listAPITokens list what exists. Each user holds at most 100 sessions, and a further login silently evicts the oldest, so log in once per user and reuse the token rather than logging in per request. Wrong Basic credentials are answered 403, not 401; 401 is what an invalid, expired or logged-out token gets.

Running work

import { isQueryTerminated } from "@arcadedb/driver";

const running = await server.listQueries({ tag: "nightly-report" }); // or { database }
const results = await server.terminateQueriesByTag("nightly-report", { waitMs: 3000 });
await server.terminateQuery(running[0].id);
const open = await server.listTransactions();
await server.terminateTransaction(open[0].id);
const progress = await db.progress(); // long-running maintenance operations

Any authenticated user may call these: root sees and stops every statement, another user only their own. Each terminate answer carries a status, and which one you get decides whether you have anything to undo:

status Meaning

"terminated"

It stopped, and what it wrote was rolled back.

"completed"

It finished before it noticed, so what it did stands.

"terminating"

It had not stopped when the wait ended; the request’s own outcome is the answer.

"not found"

No such statement on this server, or another user’s.

  • Each server sees only its own work. These commands are not forwarded to the cluster leader, so behind a load balancer the server you reach may not be the one running the statement.

  • A commit after terminateTransaction still reports success. The transaction’s writes are rolled back, but the server answers its owner’s commit with 204, as if it had committed (ArcadeData/arcadedb#10164).

Next steps

  • Native Drivers — the result envelope and why truncated matters, and how this driver compares to the others.

  • JavaScript / TypeScript — gRPC Driver — the gRPC counterpart for throughput-sensitive server-to-server work.

  • The package README for the full method list and every failure mode of the methods above, including db.ts, db.grafana, db.promql and db.vector.