Skip to main content

TypeScript

CamusDB ships a Node.js connector for TypeScript and JavaScript applications. The package name is camusdb.

It supports REST and gRPC transports, typed parameters, transactions, automatic prepared statements, authentication, learned routing, database branches, query result cache metadata, and online backup administration.

The connector requires Node.js 20.19 or later, or Node.js 22.12 or later on the 22 line. It ships as ECMAScript modules with TypeScript declarations.

Install​

npm install camusdb

The gRPC transport uses optional peer dependencies. Install them only when you set protocol: 'grpc':

npm install @grpc/grpc-js @grpc/proto-loader

Create a client​

Build a client from an options object:

import { CamusClient } from 'camusdb';

const client = new CamusClient({
endpoint: 'http://localhost:5095',
database: 'test',
});

You can also use the same connection string keys as the .NET driver:

const client = CamusClient.fromConnectionString(
'Endpoint=http://localhost:5095;Database=test;Timeout=30',
);

endpoint can be a single URL, an array of URLs, or a comma-separated connection-string value. With several endpoints, the connector rotates through them and sets aside an unreachable endpoint for 30 seconds.

Connection options​

OptionConnection-string keyDescription
endpointEndpointBase URL, or several URLs to rotate through.
databaseDatabaseDatabase sent with each request.
timeoutSecondsTimeoutRequest timeout in seconds. Default is 10.
protocolProtocolrest, the default, or grpc.
userUserUser to authenticate as. Aliases include UserId, Uid, and Username.
passwordPasswordPassword for user. Alias: Pwd.
accessTokenAccessTokenBearer token obtained elsewhere.
tokenLifetimeSecondsTokenLifetimeFallback token lifetime when the server reports no expiry. Default is 600.
maxAutoPrepareMaxAutoPrepareMaximum automatic prepared statements. Default is 128; 0 disables automatic preparation.
autoPrepareMinUsagesAutoPrepareMinUsagesExecutions before automatic preparation. Default is 2.
channelPoolSizeChannelPoolSizegRPC streams per endpoint. Default is 2.
coalescingThresholdCoalescingThresholdgRPC batch coalescing threshold. Default is 10; 1 disables coalescing.
coalescingDelayMsCoalescingDelaygRPC coalescing delay in milliseconds. Default is 0.
backupEndpointBackupEndpointHTTP endpoint for the backup admin API. Required when protocol is grpc.
backupTimeoutSecondsBackupTimeoutBackup admin timeout in seconds. Default is 300.
allowInsecureCredentialsAllowInsecureCredentialsAllows credentials over a remote plaintext endpoint. Use only behind a trusted TLS terminator or private hop.
routingModeRoutingModeauto, learned, or off. Default is auto.
routingNodesRoutingNodesOperator trust map from server node identities to endpoint pool members.
routingMaxHintAgeMsRoutingMaxHintAgeMaximum reuse age for a learned route. Default is 5000.
defaultTransactionOptionsIsolationLevel, TransactionMode, LockingDefaults for transactions and autocommit statements.
int64Int64auto, number, or bigint. Default is auto.

Connection string keys are case-insensitive and trimmed. Repeating a key is an error. Quote a value with single or double quotes when it contains a semicolon or surrounding spaces.

client.toString() masks secrets. Use redactConnectionString(...) when you need to log a raw connection string safely.

Queries and writes​

Use query<T>() for a SELECT that fits in memory:

interface Robot {
id: string;
name: string;
year: number;
}

const result = await client.query<Robot>(
'SELECT id, name, year FROM robots WHERE year = @year',
{ year: 1974 },
);

result.rows;
result.columns;
result.rowCount;

Use queryOne<T>() for the first row, and scalar<T>() for the first column of the first row.

Use execute() for INSERT, UPDATE, and DELETE:

const result = await client.execute(
'UPDATE robots SET year = @year WHERE id = @id',
{ year: 1984, id: '507f1f77bcf86cd799439011' },
);

result.affectedRows;

Use executeDdl() for schema statements when you want to make that route explicit. Use insert(table, values) for the row-level insert endpoint:

import { CamusObjectId, camus } from 'camusdb';

await client.insert('robots', {
id: camus.id(CamusObjectId.generateAsString()),
name: 'R2-D2',
year: camus.int64(1977),
});

Parameters and types​

Parameters are bound by name. The leading @ is optional in the parameter object:

await client.query('SELECT * FROM robots WHERE year = @year', {
year: 1977,
});

The connector infers common values: strings, booleans, numbers, bigints, Date, byte arrays, Float32Array vectors, arrays, null, and CamusObjectId. Use camus.* helpers when inference cannot express the type you need:

import { camus, ColumnType } from 'camusdb';

await client.query('SELECT * FROM events WHERE id = @id AND event_uuid = @uuid', {
id: camus.id('507f1f77bcf86cd799439011'),
uuid: camus.uuid('550e8400-e29b-41d4-a716-446655440000'),
tags: camus.array([], ColumnType.String),
});

INT64 values stay exact on both transports. With int64: 'auto', safe values return as number and larger values return as bigint. Set int64: 'bigint' when you want one uniform type.

DATE and DATETIME values return as UTC Date objects. JavaScript dates have millisecond precision; use ticksToDate and dateToTicks when you need to work with the raw .NET tick value.

Vectors are stored in BYTES columns as packed little-endian float32 values:

import { CamusVector, camus } from 'camusdb';

await client.execute('INSERT INTO docs (id, embedding) VALUES (GEN_ID(), @embedding)', {
embedding: camus.vector([0.1, 0.2, 0.3]),
});

const row = await client.queryOne<{ embedding: Uint8Array }>('SELECT embedding FROM docs LIMIT 1');
const values = CamusVector.toFloats(row!.embedding);

Streaming​

queryStream<T>() reads rows incrementally:

await using stream = await client.queryStream<Robot>('SELECT * FROM robots');

for await (const robot of stream) {
console.log(robot.name);
}

The REST transport streams rows from the server as the iterator advances. The gRPC transport uses the same API, but buffers the result internally before replaying it to the iterator.

Transactions and retries​

Use transaction() for the common case. It commits when the function returns and rolls back when the function throws. Serializable conflicts retry the whole function on a fresh transaction, so the function must be safe to run more than once.

await client.transaction(async (txn) => {
await client.execute(
'UPDATE accounts SET balance = balance - @amount WHERE id = @id',
{ amount, id: from },
{ transaction: txn },
);

await client.execute(
'UPDATE accounts SET balance = balance + @amount WHERE id = @id',
{ amount, id: to },
{ transaction: txn },
);
});

Use beginTransaction() when the unit of work does not fit in one function:

await using txn = await client.beginTransaction();

await client.execute(
'UPDATE robots SET year = @year WHERE id = @id',
{ year: 1984, id },
{ transaction: txn },
);

await txn.commit();

Transaction options include isolationLevel, mode, locking, and, with learned routing, affinity. The connector exports OPTIMISTIC_TRANSACTION_OPTIONS and SNAPSHOT_TRANSACTION_OPTIONS.

Use withRetry(...) around your own idempotent units of work. CADB0509 TransactionFinalizeUnresolved is handled by retrying the same commit on the same handle; it is not a signal to replay the whole transaction.

Prepared statements​

The connector prepares repeated statements automatically. The default policy prepares a statement on its second execution and keeps the 128 most recently used registrations. Set maxAutoPrepare: 0 to disable automatic preparation.

await client.prepare('SELECT * FROM robots WHERE year = @year');

client.isPrepared('SELECT * FROM robots WHERE year = @year');
client.preparedStatementCount;

Prepared statements are an optimization. If the server refuses a registration, the connector keeps running the statement inline.

Authentication​

With authentication disabled, the connector sends no Authorization header. To authenticate, pass user and password, or log in explicitly at runtime:

await client.login('app', await secrets.get('camus-password'));
await client.logout();

The password is exchanged for a bearer token. Tokens are shared across clients with the same identity and deployment, renewed before expiry, and refreshed once after a token rejection. Privilege errors are not retried.

Use https:// for credential-bearing connections outside loopback. The connector refuses credentials over remote plaintext unless allowInsecureCredentials is set.

Transport and routing​

REST is the default transport. Set protocol: 'grpc' and point endpoint at the gRPC port to use gRPC:

const client = new CamusClient({
endpoint: 'http://localhost:5096',
database: 'test',
protocol: 'grpc',
backupEndpoint: 'http://localhost:5095',
});

gRPC statements ride a small pool of long-lived BatchExecute streams and carry the session causal token between requests.

For multi-node clusters, learned routing can use server routing advice to send future executions of the same statement directly to the data leader:

const client = new CamusClient({
endpoint: ['http://a:5095', 'http://b:5095', 'http://c:5095'],
database: 'test',
routingMode: 'learned',
routingNodes: {
'camus-a:7070': 'http://a:5095',
'camus-b:7070': 'http://b:5095',
'camus-c:7070': 'http://c:5095',
},
});

The routingNodes map is the authority. A server response can name only an opaque node identity, and the connector routes only to endpoints that the operator mapped and also listed in the endpoint pool. See SQL Routing Advice.

Databases, branches, cache, and backups​

The connector includes helpers for database administration:

await client.createDatabase('analytics', { ifNotExists: true });
await client.dropDatabase('analytics');
await client.createBranchDatabase('feature-x', 'production');
await client.showBranches('production');
await client.showAncestors('feature-x');
client.changeDatabase('analytics');

Use cacheHint(...) to build query result cache hints and inspect result.cacheMetadata for the verdict:

import { cacheHint } from 'camusdb';

const hint = cacheHint('recent_orders', { ttlMs: 30_000, strict: true });
const result = await client.query(`SELECT id FROM orders ${hint}`);

The online backup administration API is available at client.backups. It is node-wide and uses REST, so a gRPC client must set backupEndpoint.

const full = await client.backups.takeFullBackup();
const catalog = await client.backups.listBackups();
const preview = await client.backups.previewGarbageCollection();

Process lifetime​

A CamusClient is cheap to create and safe to share. Endpoint rotation, bearer tokens, transports, prepared-statement registrations, and learned routes are shared process-wide by the deployment and configuration they depend on.

When a Node.js process is shutting down, close shared gRPC channels explicitly:

import { CamusTransportPool } from 'camusdb';

await CamusTransportPool.closeAll();

Sample application​

For a complete Next.js app built on this connector, see Advanced Samples.