@geodedb/client
v1.0.0-alpha.23
Published
Node.js client library for Geode graph database (QUIC/gRPC + TLS 1.3)
Maintainers
Readme
Geode Node.js Client
A high-performance Node.js client library for the Geode graph database, implementing the ISO/IEC 39075:2024 GQL standard over QUIC or gRPC with TLS 1.3.
Features
- Full GQL Support: Implements ISO/IEC 39075:2024 Graph Query Language standard
- Dual Transport: QUIC (default) or gRPC transport with TLS 1.3 encryption
- Protobuf Wire Protocol: Efficient binary serialization using Protocol Buffers
- Connection Pooling: Efficient connection management for high-throughput applications
- Type Safety: Full TypeScript support with comprehensive type definitions
- Query Builder: Fluent API for building GQL queries programmatically
- Transaction Support: Full transaction management with savepoints
- Async/Await: Modern async API with proper cancellation support
- ESM Native: Pure ESM package for modern Node.js applications
Installation
npm install @geodedb/clientRequirements:
- Node.js 20.0.0 or later
- A running Geode server (default port: 3141)
Quick Start
import { createClient } from '@geodedb/client';
// Connect to Geode
const client = await createClient('quic://localhost:3141');
// Execute a query
const rows = await client.queryAll('MATCH (n:Person) RETURN n.name, n.age');
console.log(rows);
// [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
// Close connection
await client.close();Configuration
DSN Format
Note: See
geode/docs/DSN.mdfor the complete DSN specification.
The client supports multiple transport schemes:
quic://[username:password@]host[:port][?options] # QUIC transport (default port: 3141)
grpc://[username:password@]host[:port][?options] # gRPC transport (default port: 50051)
host:port # Simple format (uses QUIC)Options
| Option | Description | Default |
| ----------------- | --------------------------------- | ------------ |
| page_size | Results per page | 1000 |
| hello_name | Client name | geode-nodejs |
| hello_ver | Client version | 1.0.0 |
| conformance | GQL conformance level | min |
| tls | Enable TLS (gRPC only) | true |
| insecure_tls_skip_verify | Skip TLS verification (dev only) | false |
| ca | Path to CA certificate | - |
| cert | Path to client certificate (mTLS) | - |
| key | Path to client key (mTLS) | - |
| server_name | SNI server name | - |
| connect_timeout | Connection timeout (ms) | 30000 |
| request_timeout | Request timeout (ms) | 120000 |
Environment Variables
| Variable | Description |
| ----------------- | ------------------------------ |
| GEODE_HOST | Default host |
| GEODE_PORT | Default port |
| GEODE_TRANSPORT | Default transport (quic/grpc) |
| GEODE_TLS_CA | Default CA certificate path |
| GEODE_USERNAME | Default username |
| GEODE_PASSWORD | Default password |
Example Configurations
// Simple QUIC connection (default)
const client = await createClient('localhost:3141');
// Explicit QUIC transport
const client = await createClient('quic://localhost:3141');
// gRPC transport
const client = await createClient('grpc://localhost:50051');
// gRPC without TLS (development only)
const client = await createClient('grpc://localhost:50051?tls=0');
// With authentication
const client = await createClient('quic://admin:secret@localhost:3141');
// With TLS CA certificate
const client = await createClient('quic://localhost:3141?ca=/path/to/ca.crt');
// With mTLS
const client = await createClient(
'quic://localhost:3141?ca=/path/to/ca.crt&cert=/path/to/client.crt&key=/path/to/client.key'
);
// With connection pool
const client = await createClient('quic://localhost:3141', {
pooling: true,
pool: {
min: 2,
max: 10,
acquireTimeout: 30000,
idleTimeout: 60000,
},
});Usage Examples
Basic Queries
// Query with iteration
const result = await client.query('MATCH (n:Person) RETURN n');
for await (const row of result) {
console.log(row.get('n')?.asNode);
}
// Query all rows at once
const rows = await client.queryAll('MATCH (n:Person) RETURN n.name AS name');
// Get first row
const first = await client.queryFirst('MATCH (n:Person) RETURN n.name LIMIT 1');
// Get scalar value
const count = await client.queryScalar<number>('MATCH (n:Person) RETURN count(n) AS cnt', 'cnt');Parameterized Queries
// Named parameters
const rows = await client.queryAll('MATCH (n:Person) WHERE n.age > $minAge RETURN n', {
params: { minAge: 21 },
});
// Positional parameters (converted to $p1, $p2, etc.)
const rows = await client.queryAll('MATCH (n:Person) WHERE n.age > ? AND n.name = ? RETURN n', {
params: [21, 'Alice'],
});Create, Update, Delete
// Create nodes
await client.exec("CREATE (n:Person {name: 'Alice', age: 30})");
// Create with helper method
const node = await client.createNode('Person', { name: 'Bob', age: 25 });
// Update
await client.exec("MATCH (n:Person {name: 'Alice'}) SET n.age = 31");
// Delete
await client.exec("MATCH (n:Person {name: 'Alice'}) DELETE n");
// Delete with relationships
await client.exec("MATCH (n:Person {name: 'Alice'}) DETACH DELETE n");Relationships
// Create relationship
await client.exec(`
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS {since: 2020}]->(b)
`);
// Query relationships
const rows = await client.queryAll(`
MATCH (a:Person)-[r:KNOWS]->(b:Person)
RETURN a.name AS from, b.name AS to, r.since AS since
`);
// Variable-length paths
const rows = await client.queryAll(`
MATCH (a:Person {name: 'Alice'})-[:KNOWS*1..3]->(friend)
RETURN friend.name
`);Transactions
// Basic transaction
await client.withTransaction(async (tx) => {
await tx.exec("CREATE (n:Person {name: 'Alice'})");
await tx.exec("CREATE (n:Person {name: 'Bob'})");
// Auto-commits on success
});
// Manual transaction control
const conn = await client.getConnection();
const tx = await conn.begin();
try {
await tx.exec('CREATE (n:Test {value: 1})');
await tx.savepoint('sp1');
await tx.exec('CREATE (n:Test {value: 2})');
await tx.rollbackTo('sp1'); // Undo second create
await tx.commit();
} catch (e) {
await tx.rollback();
throw e;
} finally {
await client.releaseConnection(conn);
}Query Builder
import { query, pattern, predicate } from '@geodedb/client';
// Build complex queries
const q = query()
.match(
pattern()
.node((n) => n.as('a').label('Person'))
.outgoing((e) => e.type('KNOWS'))
.node((n) => n.as('b').label('Person'))
)
.where(predicate().gt('a.age', 21).isNotNull('b.email'))
.return('a.name AS from', 'b.name AS to')
.orderBy('a.name')
.limit(10);
const { query: gql, params } = q.build();
const rows = await client.queryAll(gql, { params });Prepared Statements
import { PreparedStatement } from '@geodedb/client';
// Prepare a statement once
const stmt = await client.prepare('MATCH (n:Person {name: $name}) RETURN n');
// Execute multiple times with different parameters
const alice = await stmt.executeAll({ name: 'Alice' });
const bob = await stmt.executeAll({ name: 'Bob' });
// Validate parameters without executing
stmt.validate({ name: 'Charlie' });
// Check parameter info
console.log(stmt.parameterCount); // 1
console.log(stmt.namedParameters); // ['name']
// Close when done
stmt.close();Query Explain/Profile
import { explain, profile, formatPlan, formatProfile } from '@geodedb/client';
// Get query execution plan
const plan = await client.explain('MATCH (n:Person)-[:KNOWS]->(m) RETURN n, m');
console.log('Estimated cost:', plan.totalCost);
console.log('Estimated rows:', plan.totalRows);
// Format plan for display
console.log(formatPlan(plan));
// Profile actual execution
const prof = await client.profile('MATCH (n:Person) RETURN count(n)');
console.log('Execution time:', prof.totalTimeMs, 'ms');
console.log('Rows processed:', prof.totalRows);
console.log('Planning time:', prof.planningTimeMs, 'ms');
console.log('Memory used:', prof.memoryBytes, 'bytes');
// Format profile for display
console.log(formatProfile(prof));Batch Operations
import { batch, batchMap, batchParallel } from '@geodedb/client';
// Execute multiple queries in a batch
const summary = await client.batch([
{ query: "CREATE (n:Person {name: 'Alice'})" },
{ query: "CREATE (n:Person {name: 'Bob'})" },
{ query: 'MATCH (n:Person) RETURN count(n) AS cnt' },
]);
console.log(`${summary.successful}/${summary.total} queries succeeded`);
console.log('Total time:', summary.totalDurationMs, 'ms');
// Stop on first error
const summary2 = await client.batch(queries, { stopOnError: true });
// Execute in a transaction (all or nothing)
const summary3 = await client.batch(queries, { transaction: true });
// Map over data
const people = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];
await batchMap(conn, 'CREATE (n:Person {name: $name})', people);Authentication Client
import { AuthClient } from '@geodedb/client';
const auth = await client.auth();
// User management
await auth.createUser('alice', { password: 'securePass123', roles: ['analyst'] });
await auth.changePassword('alice', 'newPassword456');
await auth.deactivateUser('alice');
const users = await auth.listUsers();
// Role management
await auth.createRole('analyst', {
description: 'Data analyst role',
permissions: [{ resource: 'NODE', action: 'READ' }],
});
await auth.assignRole('alice', 'analyst');
await auth.grantPermission('analyst', { resource: 'NODE', action: 'READ', label: 'Person' });
// Row-level security policies
await auth.createRLSPolicy(
'tenant_isolation',
'Document',
'ALL',
'n.tenant_id = current_user().tenant_id',
{ roles: ['user', 'analyst'] }
);
await auth.enableRLSPolicy('tenant_isolation', 'Document');
// Session info
const currentUser = await auth.currentUser();
const roles = await auth.currentRoles();
const canRead = await auth.hasPermission('NODE', 'READ', 'Person');Abort/Cancellation
const controller = new AbortController();
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
const rows = await client.queryAll('MATCH (n) RETURN n', { signal: controller.signal });
} catch (e) {
if (e.message.includes('Aborted')) {
console.log('Query was cancelled');
}
}Type System
The client provides a comprehensive GQL type system:
import { GQLValue, fromJSON } from '@geodedb/client';
// Create typed values
const intVal = GQLValue.int(42);
const strVal = GQLValue.string('hello');
const arrVal = GQLValue.array([GQLValue.int(1), GQLValue.int(2)]);
const nodeVal = GQLValue.node({
id: '123',
labels: ['Person'],
properties: { name: 'Alice' },
});
// Type-safe access
intVal.asNumber; // 42
intVal.asInt; // 42n (bigint)
strVal.asString; // 'hello'
arrVal.asArray; // [GQLValue, GQLValue]
nodeVal.asNode; // { id, labels, properties }
// Convert to plain JS
intVal.toJS(); // 42
nodeVal.toJS(); // { id: '123', labels: ['Person'], ... }
// Parse from JSON
const value = fromJSON({ name: 'Alice', age: 30 });Supported Types
| GQL Type | Node.js Type | | ---------- | ------------- | | INT | number/bigint | | FLOAT | number | | DECIMAL | Decimal | | STRING | string | | BOOL | boolean | | NULL | null | | ARRAY/LIST | Array | | OBJECT/MAP | Object/Map | | NODE | GQLNode | | EDGE | GQLEdge | | PATH | GQLPath | | BYTEA | Uint8Array | | DATE | Date | | TIME | Date | | TIMESTAMP | Date | | UUID | string | | JSON/JSONB | any |
Error Handling
import {
DriverError,
TransportError,
ConfigError,
SecurityError,
isRetryableError,
} from '@geodedb/client';
try {
await client.queryAll('INVALID QUERY');
} catch (e) {
if (e instanceof DriverError) {
console.log('Server error:', e.code, e.message);
console.log('Status class:', e.statusClass); // ISO 39075 code
if (isRetryableError(e)) {
// Serialization or deadlock - can retry
}
} else if (e instanceof TransportError) {
console.log('Network error:', e.operation, e.message);
} else if (e instanceof ConfigError) {
console.log('Configuration error:', e.field, e.message);
} else if (e instanceof SecurityError) {
console.log('Security error:', e.type, e.message);
}
}ISO 39075 Status Classes
| Class | Meaning | Retryable | | ----- | ------------------------- | --------- | | 00000 | Success | - | | 01000 | Warning | No | | 02000 | No data | No | | 25000 | Invalid transaction state | No | | 28000 | Authorization error | No | | 40001 | Serialization failure | Yes | | 40502 | Transaction deadlock | Yes | | 42000 | Syntax error | No | | 23000 | Constraint violation | No | | 58000 | System error | No |
Connection Pool
const client = await createClient('quic://localhost:3141', {
pooling: true,
pool: {
min: 2, // Minimum connections to maintain
max: 10, // Maximum connections
acquireTimeout: 30000, // Max wait time to acquire
idleTimeout: 60000, // Close idle connections after
},
});
// Check pool stats
console.log(client.poolStats);
// { total: 5, available: 3, inUse: 2, waiting: 0 }Testing
# Run unit tests
npm test
# Run with coverage
npm run test:coverage
# Run integration tests (requires Docker)
npm run test:integrationAPI Reference
GeodeClient
| Method | Description |
| --------------------------------- | ------------------------------------ |
| query(gql, options?) | Execute query, return async iterator |
| queryAll(gql, options?) | Execute query, return all rows |
| queryFirst(gql, options?) | Get first row |
| queryScalar(gql, col, options?) | Get single value |
| exec(gql, options?) | Execute statement (no results) |
| withTransaction(fn) | Execute in transaction |
| prepare(gql) | Create prepared statement |
| explain(gql, options?) | Get query execution plan |
| profile(gql, options?) | Profile query execution |
| batch(queries, options?) | Execute multiple queries |
| auth() | Get authentication client |
| ping() | Check connection health |
| close() | Close client |
Connection
| Method | Description |
| -------------------------- | ------------------------- |
| query(gql, options?) | Execute query |
| exec(gql, options?) | Execute statement |
| begin() | Start transaction |
| prepare(gql) | Create prepared statement |
| explain(gql, options?) | Get query plan |
| profile(gql, options?) | Profile execution |
| batch(queries, options?) | Execute batch |
| ping() | Health check |
| reset() | Reset session |
| close() | Close connection |
Transaction
| Method | Description |
| ---------------------- | --------------------- |
| query(gql, options?) | Execute query |
| exec(gql, options?) | Execute statement |
| savepoint(name) | Create savepoint |
| rollbackTo(name) | Rollback to savepoint |
| commit() | Commit transaction |
| rollback() | Rollback transaction |
PreparedStatement
| Method | Description |
| ------------------------------- | ------------------------- |
| execute(params?, options?) | Execute, return iterator |
| executeAll(params?, options?) | Execute, return all rows |
| exec(params?, options?) | Execute (no results) |
| validate(params?) | Validate parameters |
| close() | Close statement |
| query | Get query text |
| parameterCount | Get parameter count |
| namedParameters | Get named parameter names |
AuthClient
| Method | Description |
| ------------------------------------ | --------------------- |
| createUser(name, options) | Create user |
| deleteUser(name) | Delete user |
| getUser(name) | Get user info |
| listUsers() | List all users |
| changePassword(name, password) | Change password |
| activateUser(name) | Activate user |
| deactivateUser(name) | Deactivate user |
| createRole(name, options?) | Create role |
| deleteRole(name) | Delete role |
| assignRole(user, role) | Assign role to user |
| revokeRole(user, role) | Revoke role from user |
| grantPermission(role, perm) | Grant permission |
| revokePermission(role, perm) | Revoke permission |
| createRLSPolicy(...) | Create RLS policy |
| deleteRLSPolicy(name, label) | Delete RLS policy |
| currentUser() | Get current user |
| currentRoles() | Get current roles |
| hasPermission(res, action, label?) | Check permission |
Contributing
See CLAUDE.md for development guidelines.
Development Setup
git clone https://gitlab.com/devnw/geode/geode-client-nodejs.git
cd geode-client-nodejs
npm install
npm test
npm run buildLicense
Apache License 2.0
Related Projects
- Geode Server - The Geode database server
- Go Client - Go client library
- Python Client - Python client library
- Rust Client - Rust client library
