JavaScript / TypeScript — gRPC Driver
@arcadedb/driver-grpc is ArcadeDB’s TypeScript/JavaScript gRPC client, generated from the
server’s Protobuf contract. It wraps the generated Connect client in a
small facade for streaming queries, streaming inserts and transactions.
Install
npm install @arcadedb/driver-grpc
|
The gRPC server is a plugin, and it is not started by default. Register it in the server’s plugin list before connecting:
Once registered, it listens on port |
Runtime targets: Node, Bun, Deno — not browsers
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.
Bun and Deno are intended targets for this driver, but only Node is what the arcadedb-drivers repository’s continuous integration actually exercises against a live server. Treat Bun and Deno as likely to work rather than as verified.
There is no browser build, and there cannot be one until the server changes. ArcadeDB’s
GrpcServerPlugin is plain grpc-java over HTTP/2, with no gRPC-Web handler and no Connect
protocol in front of it, so a browser cannot speak this driver’s wire format at all. Use
the HTTP driver there.
Connect and query
import { createClient, passwordAuth } from "@arcadedb/driver-grpc";
const grpc = createClient({
baseUrl: "http://localhost:50051",
auth: passwordAuth("root", "playwithdata", "mydb"),
insecure: true,
});
const response = await grpc.raw.executeQuery({
database: "mydb",
query: "SELECT FROM Person WHERE age > 21",
language: "sql",
});
console.log(response.results.flatMap((r) => r.records.map((rec) => [rec.rid, rec.properties])));
[
[ '#1:0', { name: [Object], age: [Object], '@rid': [Object], '@type': [Object], '@cat': [Object] } ],
[ '#1:1', { name: [Object], age: [Object], '@rid': [Object], '@type': [Object], '@cat': [Object] } ]
]
Node’s default console.log collapses nested objects two levels deep, and each property value
here is itself a nested message — that is the summarizing behavior of util.inspect, not
evidence of anything wrong. See below for what those objects actually contain and how to unwrap
them.
|
The unary
Streaming is the answer for result sets that large: |
raw is the generated Connect client for ArcadeDbService, the same one you get from
createClient(ArcadeDbService, transport) if you built the transport yourself — every RPC in
the data-plane contract is callable through it exactly as shown above with executeQuery.
createClient from this package adds streamQuery, insertStream and transaction on top,
covering the RPCs the generated client alone handles awkwardly: consuming a server-side stream,
feeding a client-side stream, and pairing begin/commit/rollback into one call.
record.properties and the GrpcValue wrapper
Every value in a record’s properties map comes back as a GrpcValue object, not a plain
JavaScript scalar. Printed at full depth, the name and age fields above look like this:
name: {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'stringValue', value: 'Alice' },
logicalType: ''
}
age: {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'int32Value', value: 30 },
logicalType: ''
}
'@rid': {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: {
case: 'linkValue',
value: { '$typeName': 'com.arcadedb.grpc.GrpcLink', rid: '#1:0', type: '' }
},
logicalType: 'rid'
}
GrpcValue.kind is this package’s Connect-ES rendering of the contract’s oneof field: a
discriminated union with case naming which alternative is set and value holding it directly — there is no per-type field name to guess, unlike the generated Python client’s
int32_value/string_value/link_value fields for the same oneof. Unwrap it by branching on
kind.case:
function unwrap(value) {
if (value.kind.case === "linkValue") return value.kind.value.rid;
return value.kind.value;
}
for (const result of response.results) {
for (const rec of result.records) {
const props = Object.fromEntries(Object.entries(rec.properties).map(([k, v]) => [k, unwrap(v)]));
console.log(rec.rid, props);
}
}
#1:0 { name: 'Alice', age: 30, '@rid': '#1:0', '@type': 'Person', '@cat': 'v' }
#1:1 { name: 'Bob', age: 25, '@rid': '#1:1', '@type': 'Person', '@cat': 'v' }
linkValue is the one case that does not unwrap to a scalar directly — its value is itself a
GrpcLink message, so reach into .rid for the record ID string, as unwrap does above. There
is no library function that performs this unwrapping; it is a plain discriminated-union read,
shown here for reference.
Authentication and the insecure guard
bearerAuth(token) sets authorization: Bearer <token> on every outgoing call’s metadata.
passwordAuth(user, password, database?) sets x-arcade-user, x-arcade-password and (when
given) x-arcade-database instead. Both are passed once, as the auth option to createClient — there is no per-call credential to attach separately.
|
The insecure-channel guard, and its limit.
This is why the connect example above needs The check has a real limit, stated plainly: it recognizes only the exact interceptor value
|
Streaming queries and retrievalMode
import { createClient, passwordAuth, StreamQueryRequest_RetrievalMode } from "@arcadedb/driver-grpc";
const grpc = createClient({
baseUrl: "http://localhost:50051",
auth: passwordAuth("root", "playwithdata", "mydb"),
insecure: true,
});
for await (const row of grpc.streamQuery({
database: "mydb",
query: "SELECT FROM Person",
language: "sql",
retrievalMode: StreamQueryRequest_RetrievalMode.CURSOR,
batchSize: 500,
})) {
console.log(row.rid, row.properties);
}
#1:0 {
name: {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'stringValue', value: 'Alice' },
logicalType: ''
},
age: {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'int32Value', value: 30 },
logicalType: ''
},
'@cat': {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'stringValue', value: 'v' },
logicalType: ''
}
}
#1:1 {
name: {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'stringValue', value: 'Bob' },
logicalType: ''
},
age: {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'int32Value', value: 25 },
logicalType: ''
},
'@cat': {
'$typeName': 'com.arcadedb.grpc.GrpcValue',
kind: { case: 'stringValue', value: 'v' },
logicalType: ''
}
}
streamQuery flattens the server’s stream of row batches into one record at a time and does
nothing else — each properties entry is still a GrpcValue needing the same unwrap shown
above. Both retrievalMode and batchSize are deliberately the caller’s own choice; the wrapper
picks no default for either. There are three retrieval modes:
-
CURSOR— the proto default (value0). The server runs the query once and streams rows as you iterate. -
MATERIALIZE_ALL— the server runs the query to completion first, then batches the already-materialized result set back to you. -
PAGED— the client re-issues the query withLIMIT/SKIPper batch.
Leaving both fields out does not pick CURSOR and a sensible batch size as a considered choice — it sends whatever protobuf’s zero values happen to be for those fields (0, which is
CURSOR, and 0 rows per batch). Set both explicitly, as above.
Streaming inserts
insertStream owns the envelope bookkeeping the client-streaming InsertStream RPC needs — one
session id for the stream, an incrementing chunk sequence, database on the first chunk, last
on the final one — and leaves how rows are batched to you.
|
The field exists because the |
Transactions
import { createClient, passwordAuth } from "@arcadedb/driver-grpc";
const grpc = createClient({
baseUrl: "http://localhost:50051",
auth: passwordAuth("root", "playwithdata", "mydb"),
insecure: true,
});
const total = await grpc.transaction("mydb", async (tx) => {
await tx.executeCommand({
command: "INSERT INTO Account SET balance = 100, owner = 'grpc-demo-js'",
language: "sql",
});
const response = await tx.executeQuery({
query: "SELECT sum(balance) AS total FROM Account WHERE owner = 'grpc-demo-js'",
language: "sql",
});
return response.results[0].records[0].properties.total.kind.value;
});
console.log("total:", total);
total: 100
transaction(database, fn) begins a server-side transaction, hands fn a TransactionHandle,
and ends the transaction on both paths: it commits when fn resolves and returns the value fn
returned, and rolls back and re-throws the original error when fn throws or rejects. A rollback
that fails on that path is attached as cause on the original error rather than replacing it,
and a failing commit issues a best-effort rollback first so the server-side session is released.
A commitTransaction that resolves with committed: false — what the server answers for a
transaction it already reaped — throws rather than reporting a success whose writes were lost.
The handle exposes executeQuery, executeCommand, createRecord, updateRecord,
deleteRecord, lookupByRid and streamQuery, each with the transaction’s database and
transaction context bound in automatically, so you never set either yourself. Note the
omissions: insertStream and bulkInsert are not on the handle, because the server does not run
them inside your transaction at all — binding them there would silently lie.
The trap is the same one the HTTP driver has: calls made through the outer client while a transaction is open do not join it. Only calls through the handle take part.
Errors
Every RPC rejects with a Connect ConnectError on failure — this driver adds no error type of
its own. err.code is a Connect Code and err.rawMessage is the server’s message. Both come
from @connectrpc/connect, which the driver depends on; add it to your own dependencies to
import it directly.
import { Code, ConnectError } from "@connectrpc/connect";
import { createClient, passwordAuth } from "@arcadedb/driver-grpc";
const grpc = createClient({
baseUrl: "http://localhost:50051",
auth: passwordAuth("root", "playwithdata", "mydb"),
insecure: true,
});
try {
await grpc.raw.executeQuery({ database: "mydb", query: "SELECT FROM NoSuchType", language: "sql" });
} catch (err) {
if (err instanceof ConnectError) console.log(Code[err.code], err.rawMessage);
}
Internal Query execution failed: Type with name 'NoSuchType' was not found
The code you are most likely to meet first is Unauthenticated. A stock ArcadeDB gRPC server
refuses anonymous calls, so a client built without auth is created happily and then fails on
the first RPC:
import { Code, ConnectError } from "@connectrpc/connect";
import { createClient } from "@arcadedb/driver-grpc";
const anonymous = createClient({ baseUrl: "http://localhost:50051" });
try {
await anonymous.raw.executeQuery({ database: "mydb", query: "SELECT FROM Person", language: "sql" });
} catch (err) {
if (err instanceof ConnectError) console.log(err.code === Code.Unauthenticated, err.rawMessage);
}
true Authentication required
ResourceExhausted is the other code worth branching on: it is what a result set larger than the
server’s row cap returns, as described above.
Next steps
-
Native Drivers — how this driver compares to the other three, and when to reach for gRPC instead of HTTP.
-
JavaScript / TypeScript — HTTP Driver — the HTTP counterpart, and the only option for browsers.
-
The package README for the full method list, including
insertStreamandtransaction.