@geodedb/client 1.0.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,594 @@
1
+ # Geode Node.js Client
2
+
3
+ A high-performance Node.js client library for the [Geode](https://gitlab.com/devnw/geode) graph database, implementing the ISO/IEC 39075:2024 GQL standard over QUIC + TLS 1.3.
4
+
5
+ ## Features
6
+
7
+ - **Full GQL Support**: Implements ISO/IEC 39075:2024 Graph Query Language standard
8
+ - **QUIC Transport**: High-performance transport with TLS 1.3 encryption
9
+ - **Connection Pooling**: Efficient connection management for high-throughput applications
10
+ - **Type Safety**: Full TypeScript support with comprehensive type definitions
11
+ - **Query Builder**: Fluent API for building GQL queries programmatically
12
+ - **Transaction Support**: Full transaction management with savepoints
13
+ - **Async/Await**: Modern async API with proper cancellation support
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @geodedb/client
19
+ ```
20
+
21
+ **Requirements:**
22
+
23
+ - Node.js 20.0.0 or later
24
+ - A running Geode server (default port: 3141)
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { createClient } from '@geodedb/client';
30
+
31
+ // Connect to Geode
32
+ const client = await createClient('geode://localhost:3141');
33
+
34
+ // Execute a query
35
+ const rows = await client.queryAll('MATCH (n:Person) RETURN n.name, n.age');
36
+ console.log(rows);
37
+ // [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
38
+
39
+ // Close connection
40
+ await client.close();
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ ### DSN Format
46
+
47
+ ```
48
+ geode://[username:password@]host[:port][?options]
49
+ ```
50
+
51
+ ### Options
52
+
53
+ | Option | Description | Default |
54
+ | ----------------- | --------------------------------- | ------------ |
55
+ | `page_size` | Results per page | 1000 |
56
+ | `hello_name` | Client name | geode-nodejs |
57
+ | `hello_ver` | Client version | 1.0.0 |
58
+ | `conformance` | GQL conformance level | min |
59
+ | `insecure` | Skip TLS verification (dev only) | false |
60
+ | `ca` | Path to CA certificate | - |
61
+ | `cert` | Path to client certificate (mTLS) | - |
62
+ | `key` | Path to client key (mTLS) | - |
63
+ | `server_name` | SNI server name | - |
64
+ | `connect_timeout` | Connection timeout (ms) | 30000 |
65
+ | `request_timeout` | Request timeout (ms) | 120000 |
66
+
67
+ ### Environment Variables
68
+
69
+ | Variable | Description |
70
+ | ---------------- | --------------------------- |
71
+ | `GEODE_HOST` | Default host |
72
+ | `GEODE_PORT` | Default port |
73
+ | `GEODE_TLS_CA` | Default CA certificate path |
74
+ | `GEODE_USERNAME` | Default username |
75
+ | `GEODE_PASSWORD` | Default password |
76
+
77
+ ### Example Configurations
78
+
79
+ ```typescript
80
+ // Simple connection
81
+ const client = await createClient('localhost:3141');
82
+
83
+ // With authentication
84
+ const client = await createClient('geode://admin:secret@localhost:3141');
85
+
86
+ // With TLS
87
+ const client = await createClient('geode://localhost:3141?ca=/path/to/ca.crt');
88
+
89
+ // With mTLS
90
+ const client = await createClient(
91
+ 'geode://localhost:3141?ca=/path/to/ca.crt&cert=/path/to/client.crt&key=/path/to/client.key'
92
+ );
93
+
94
+ // With connection pool
95
+ const client = await createClient('geode://localhost:3141', {
96
+ pooling: true,
97
+ pool: {
98
+ min: 2,
99
+ max: 10,
100
+ acquireTimeout: 30000,
101
+ idleTimeout: 60000,
102
+ },
103
+ });
104
+ ```
105
+
106
+ ## Usage Examples
107
+
108
+ ### Basic Queries
109
+
110
+ ```typescript
111
+ // Query with iteration
112
+ const result = await client.query('MATCH (n:Person) RETURN n');
113
+ for await (const row of result) {
114
+ console.log(row.get('n')?.asNode);
115
+ }
116
+
117
+ // Query all rows at once
118
+ const rows = await client.queryAll('MATCH (n:Person) RETURN n.name AS name');
119
+
120
+ // Get first row
121
+ const first = await client.queryFirst('MATCH (n:Person) RETURN n.name LIMIT 1');
122
+
123
+ // Get scalar value
124
+ const count = await client.queryScalar<number>('MATCH (n:Person) RETURN count(n) AS cnt', 'cnt');
125
+ ```
126
+
127
+ ### Parameterized Queries
128
+
129
+ ```typescript
130
+ // Named parameters
131
+ const rows = await client.queryAll('MATCH (n:Person) WHERE n.age > $minAge RETURN n', {
132
+ params: { minAge: 21 },
133
+ });
134
+
135
+ // Positional parameters (converted to $p1, $p2, etc.)
136
+ const rows = await client.queryAll('MATCH (n:Person) WHERE n.age > ? AND n.name = ? RETURN n', {
137
+ params: [21, 'Alice'],
138
+ });
139
+ ```
140
+
141
+ ### Create, Update, Delete
142
+
143
+ ```typescript
144
+ // Create nodes
145
+ await client.exec("CREATE (n:Person {name: 'Alice', age: 30})");
146
+
147
+ // Create with helper method
148
+ const node = await client.createNode('Person', { name: 'Bob', age: 25 });
149
+
150
+ // Update
151
+ await client.exec("MATCH (n:Person {name: 'Alice'}) SET n.age = 31");
152
+
153
+ // Delete
154
+ await client.exec("MATCH (n:Person {name: 'Alice'}) DELETE n");
155
+
156
+ // Delete with relationships
157
+ await client.exec("MATCH (n:Person {name: 'Alice'}) DETACH DELETE n");
158
+ ```
159
+
160
+ ### Relationships
161
+
162
+ ```typescript
163
+ // Create relationship
164
+ await client.exec(`
165
+ MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
166
+ CREATE (a)-[:KNOWS {since: 2020}]->(b)
167
+ `);
168
+
169
+ // Query relationships
170
+ const rows = await client.queryAll(`
171
+ MATCH (a:Person)-[r:KNOWS]->(b:Person)
172
+ RETURN a.name AS from, b.name AS to, r.since AS since
173
+ `);
174
+
175
+ // Variable-length paths
176
+ const rows = await client.queryAll(`
177
+ MATCH (a:Person {name: 'Alice'})-[:KNOWS*1..3]->(friend)
178
+ RETURN friend.name
179
+ `);
180
+ ```
181
+
182
+ ### Transactions
183
+
184
+ ```typescript
185
+ // Basic transaction
186
+ await client.withTransaction(async (tx) => {
187
+ await tx.exec("CREATE (n:Person {name: 'Alice'})");
188
+ await tx.exec("CREATE (n:Person {name: 'Bob'})");
189
+ // Auto-commits on success
190
+ });
191
+
192
+ // Manual transaction control
193
+ const conn = await client.getConnection();
194
+ const tx = await conn.begin();
195
+ try {
196
+ await tx.exec('CREATE (n:Test {value: 1})');
197
+ await tx.savepoint('sp1');
198
+ await tx.exec('CREATE (n:Test {value: 2})');
199
+ await tx.rollbackTo('sp1'); // Undo second create
200
+ await tx.commit();
201
+ } catch (e) {
202
+ await tx.rollback();
203
+ throw e;
204
+ } finally {
205
+ await client.releaseConnection(conn);
206
+ }
207
+ ```
208
+
209
+ ### Query Builder
210
+
211
+ ```typescript
212
+ import { query, pattern, predicate } from '@geodedb/client';
213
+
214
+ // Build complex queries
215
+ const q = query()
216
+ .match(
217
+ pattern()
218
+ .node((n) => n.as('a').label('Person'))
219
+ .outgoing((e) => e.type('KNOWS'))
220
+ .node((n) => n.as('b').label('Person'))
221
+ )
222
+ .where(predicate().gt('a.age', 21).isNotNull('b.email'))
223
+ .return('a.name AS from', 'b.name AS to')
224
+ .orderBy('a.name')
225
+ .limit(10);
226
+
227
+ const { query: gql, params } = q.build();
228
+ const rows = await client.queryAll(gql, { params });
229
+ ```
230
+
231
+ ### Prepared Statements
232
+
233
+ ```typescript
234
+ import { PreparedStatement } from '@geodedb/client';
235
+
236
+ // Prepare a statement once
237
+ const stmt = await client.prepare('MATCH (n:Person {name: $name}) RETURN n');
238
+
239
+ // Execute multiple times with different parameters
240
+ const alice = await stmt.executeAll({ name: 'Alice' });
241
+ const bob = await stmt.executeAll({ name: 'Bob' });
242
+
243
+ // Validate parameters without executing
244
+ stmt.validate({ name: 'Charlie' });
245
+
246
+ // Check parameter info
247
+ console.log(stmt.parameterCount); // 1
248
+ console.log(stmt.namedParameters); // ['name']
249
+
250
+ // Close when done
251
+ stmt.close();
252
+ ```
253
+
254
+ ### Query Explain/Profile
255
+
256
+ ```typescript
257
+ import { explain, profile, formatPlan, formatProfile } from '@geodedb/client';
258
+
259
+ // Get query execution plan
260
+ const plan = await client.explain('MATCH (n:Person)-[:KNOWS]->(m) RETURN n, m');
261
+ console.log('Estimated cost:', plan.totalCost);
262
+ console.log('Estimated rows:', plan.totalRows);
263
+
264
+ // Format plan for display
265
+ console.log(formatPlan(plan));
266
+
267
+ // Profile actual execution
268
+ const prof = await client.profile('MATCH (n:Person) RETURN count(n)');
269
+ console.log('Execution time:', prof.totalTimeMs, 'ms');
270
+ console.log('Rows processed:', prof.totalRows);
271
+ console.log('Planning time:', prof.planningTimeMs, 'ms');
272
+ console.log('Memory used:', prof.memoryBytes, 'bytes');
273
+
274
+ // Format profile for display
275
+ console.log(formatProfile(prof));
276
+ ```
277
+
278
+ ### Batch Operations
279
+
280
+ ```typescript
281
+ import { batch, batchMap, batchParallel } from '@geodedb/client';
282
+
283
+ // Execute multiple queries in a batch
284
+ const summary = await client.batch([
285
+ { query: "CREATE (n:Person {name: 'Alice'})" },
286
+ { query: "CREATE (n:Person {name: 'Bob'})" },
287
+ { query: 'MATCH (n:Person) RETURN count(n) AS cnt' },
288
+ ]);
289
+
290
+ console.log(`${summary.successful}/${summary.total} queries succeeded`);
291
+ console.log('Total time:', summary.totalDurationMs, 'ms');
292
+
293
+ // Stop on first error
294
+ const summary2 = await client.batch(queries, { stopOnError: true });
295
+
296
+ // Execute in a transaction (all or nothing)
297
+ const summary3 = await client.batch(queries, { transaction: true });
298
+
299
+ // Map over data
300
+ const people = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];
301
+ await batchMap(conn, 'CREATE (n:Person {name: $name})', people);
302
+ ```
303
+
304
+ ### Authentication Client
305
+
306
+ ```typescript
307
+ import { AuthClient } from '@geodedb/client';
308
+
309
+ const auth = await client.auth();
310
+
311
+ // User management
312
+ await auth.createUser('alice', { password: 'securePass123', roles: ['analyst'] });
313
+ await auth.changePassword('alice', 'newPassword456');
314
+ await auth.deactivateUser('alice');
315
+ const users = await auth.listUsers();
316
+
317
+ // Role management
318
+ await auth.createRole('analyst', {
319
+ description: 'Data analyst role',
320
+ permissions: [{ resource: 'NODE', action: 'READ' }],
321
+ });
322
+ await auth.assignRole('alice', 'analyst');
323
+ await auth.grantPermission('analyst', { resource: 'NODE', action: 'READ', label: 'Person' });
324
+
325
+ // Row-level security policies
326
+ await auth.createRLSPolicy(
327
+ 'tenant_isolation',
328
+ 'Document',
329
+ 'ALL',
330
+ 'n.tenant_id = current_user().tenant_id',
331
+ { roles: ['user', 'analyst'] }
332
+ );
333
+ await auth.enableRLSPolicy('tenant_isolation', 'Document');
334
+
335
+ // Session info
336
+ const currentUser = await auth.currentUser();
337
+ const roles = await auth.currentRoles();
338
+ const canRead = await auth.hasPermission('NODE', 'READ', 'Person');
339
+ ```
340
+
341
+ ### Abort/Cancellation
342
+
343
+ ```typescript
344
+ const controller = new AbortController();
345
+
346
+ // Cancel after 5 seconds
347
+ setTimeout(() => controller.abort(), 5000);
348
+
349
+ try {
350
+ const rows = await client.queryAll('MATCH (n) RETURN n', { signal: controller.signal });
351
+ } catch (e) {
352
+ if (e.message.includes('Aborted')) {
353
+ console.log('Query was cancelled');
354
+ }
355
+ }
356
+ ```
357
+
358
+ ## Type System
359
+
360
+ The client provides a comprehensive GQL type system:
361
+
362
+ ```typescript
363
+ import { GQLValue, fromJSON } from '@geodedb/client';
364
+
365
+ // Create typed values
366
+ const intVal = GQLValue.int(42);
367
+ const strVal = GQLValue.string('hello');
368
+ const arrVal = GQLValue.array([GQLValue.int(1), GQLValue.int(2)]);
369
+ const nodeVal = GQLValue.node({
370
+ id: '123',
371
+ labels: ['Person'],
372
+ properties: { name: 'Alice' },
373
+ });
374
+
375
+ // Type-safe access
376
+ intVal.asNumber; // 42
377
+ intVal.asInt; // 42n (bigint)
378
+ strVal.asString; // 'hello'
379
+ arrVal.asArray; // [GQLValue, GQLValue]
380
+ nodeVal.asNode; // { id, labels, properties }
381
+
382
+ // Convert to plain JS
383
+ intVal.toJS(); // 42
384
+ nodeVal.toJS(); // { id: '123', labels: ['Person'], ... }
385
+
386
+ // Parse from JSON
387
+ const value = fromJSON({ name: 'Alice', age: 30 });
388
+ ```
389
+
390
+ ### Supported Types
391
+
392
+ | GQL Type | Node.js Type |
393
+ | ---------- | ------------- |
394
+ | INT | number/bigint |
395
+ | FLOAT | number |
396
+ | DECIMAL | Decimal |
397
+ | STRING | string |
398
+ | BOOL | boolean |
399
+ | NULL | null |
400
+ | ARRAY/LIST | Array |
401
+ | OBJECT/MAP | Object/Map |
402
+ | NODE | GQLNode |
403
+ | EDGE | GQLEdge |
404
+ | PATH | GQLPath |
405
+ | BYTEA | Uint8Array |
406
+ | DATE | Date |
407
+ | TIME | Date |
408
+ | TIMESTAMP | Date |
409
+ | UUID | string |
410
+ | JSON/JSONB | any |
411
+
412
+ ## Error Handling
413
+
414
+ ```typescript
415
+ import {
416
+ DriverError,
417
+ TransportError,
418
+ ConfigError,
419
+ SecurityError,
420
+ isRetryableError,
421
+ } from '@geodedb/client';
422
+
423
+ try {
424
+ await client.queryAll('INVALID QUERY');
425
+ } catch (e) {
426
+ if (e instanceof DriverError) {
427
+ console.log('Server error:', e.code, e.message);
428
+ console.log('Status class:', e.statusClass); // ISO 39075 code
429
+
430
+ if (isRetryableError(e)) {
431
+ // Serialization or deadlock - can retry
432
+ }
433
+ } else if (e instanceof TransportError) {
434
+ console.log('Network error:', e.operation, e.message);
435
+ } else if (e instanceof ConfigError) {
436
+ console.log('Configuration error:', e.field, e.message);
437
+ } else if (e instanceof SecurityError) {
438
+ console.log('Security error:', e.type, e.message);
439
+ }
440
+ }
441
+ ```
442
+
443
+ ### ISO 39075 Status Classes
444
+
445
+ | Class | Meaning | Retryable |
446
+ | ----- | ------------------------- | --------- |
447
+ | 00000 | Success | - |
448
+ | 01000 | Warning | No |
449
+ | 02000 | No data | No |
450
+ | 25000 | Invalid transaction state | No |
451
+ | 28000 | Authorization error | No |
452
+ | 40001 | Serialization failure | Yes |
453
+ | 40502 | Transaction deadlock | Yes |
454
+ | 42000 | Syntax error | No |
455
+ | 23000 | Constraint violation | No |
456
+ | 58000 | System error | No |
457
+
458
+ ## Connection Pool
459
+
460
+ ```typescript
461
+ const client = await createClient('geode://localhost:3141', {
462
+ pooling: true,
463
+ pool: {
464
+ min: 2, // Minimum connections to maintain
465
+ max: 10, // Maximum connections
466
+ acquireTimeout: 30000, // Max wait time to acquire
467
+ idleTimeout: 60000, // Close idle connections after
468
+ },
469
+ });
470
+
471
+ // Check pool stats
472
+ console.log(client.poolStats);
473
+ // { total: 5, available: 3, inUse: 2, waiting: 0 }
474
+ ```
475
+
476
+ ## Testing
477
+
478
+ ```bash
479
+ # Run unit tests
480
+ npm test
481
+
482
+ # Run with coverage
483
+ npm run test:coverage
484
+
485
+ # Run integration tests (requires Docker)
486
+ npm run test:integration
487
+ ```
488
+
489
+ ## API Reference
490
+
491
+ ### GeodeClient
492
+
493
+ | Method | Description |
494
+ | --------------------------------- | ------------------------------------ |
495
+ | `query(gql, options?)` | Execute query, return async iterator |
496
+ | `queryAll(gql, options?)` | Execute query, return all rows |
497
+ | `queryFirst(gql, options?)` | Get first row |
498
+ | `queryScalar(gql, col, options?)` | Get single value |
499
+ | `exec(gql, options?)` | Execute statement (no results) |
500
+ | `withTransaction(fn)` | Execute in transaction |
501
+ | `prepare(gql)` | Create prepared statement |
502
+ | `explain(gql, options?)` | Get query execution plan |
503
+ | `profile(gql, options?)` | Profile query execution |
504
+ | `batch(queries, options?)` | Execute multiple queries |
505
+ | `auth()` | Get authentication client |
506
+ | `ping()` | Check connection health |
507
+ | `close()` | Close client |
508
+
509
+ ### Connection
510
+
511
+ | Method | Description |
512
+ | -------------------------- | ------------------------- |
513
+ | `query(gql, options?)` | Execute query |
514
+ | `exec(gql, options?)` | Execute statement |
515
+ | `begin()` | Start transaction |
516
+ | `prepare(gql)` | Create prepared statement |
517
+ | `explain(gql, options?)` | Get query plan |
518
+ | `profile(gql, options?)` | Profile execution |
519
+ | `batch(queries, options?)` | Execute batch |
520
+ | `ping()` | Health check |
521
+ | `reset()` | Reset session |
522
+ | `close()` | Close connection |
523
+
524
+ ### Transaction
525
+
526
+ | Method | Description |
527
+ | ---------------------- | --------------------- |
528
+ | `query(gql, options?)` | Execute query |
529
+ | `exec(gql, options?)` | Execute statement |
530
+ | `savepoint(name)` | Create savepoint |
531
+ | `rollbackTo(name)` | Rollback to savepoint |
532
+ | `commit()` | Commit transaction |
533
+ | `rollback()` | Rollback transaction |
534
+
535
+ ### PreparedStatement
536
+
537
+ | Method | Description |
538
+ | ------------------------------- | ------------------------- |
539
+ | `execute(params?, options?)` | Execute, return iterator |
540
+ | `executeAll(params?, options?)` | Execute, return all rows |
541
+ | `exec(params?, options?)` | Execute (no results) |
542
+ | `validate(params?)` | Validate parameters |
543
+ | `close()` | Close statement |
544
+ | `query` | Get query text |
545
+ | `parameterCount` | Get parameter count |
546
+ | `namedParameters` | Get named parameter names |
547
+
548
+ ### AuthClient
549
+
550
+ | Method | Description |
551
+ | ------------------------------------ | --------------------- |
552
+ | `createUser(name, options)` | Create user |
553
+ | `deleteUser(name)` | Delete user |
554
+ | `getUser(name)` | Get user info |
555
+ | `listUsers()` | List all users |
556
+ | `changePassword(name, password)` | Change password |
557
+ | `activateUser(name)` | Activate user |
558
+ | `deactivateUser(name)` | Deactivate user |
559
+ | `createRole(name, options?)` | Create role |
560
+ | `deleteRole(name)` | Delete role |
561
+ | `assignRole(user, role)` | Assign role to user |
562
+ | `revokeRole(user, role)` | Revoke role from user |
563
+ | `grantPermission(role, perm)` | Grant permission |
564
+ | `revokePermission(role, perm)` | Revoke permission |
565
+ | `createRLSPolicy(...)` | Create RLS policy |
566
+ | `deleteRLSPolicy(name, label)` | Delete RLS policy |
567
+ | `currentUser()` | Get current user |
568
+ | `currentRoles()` | Get current roles |
569
+ | `hasPermission(res, action, label?)` | Check permission |
570
+
571
+ ## Contributing
572
+
573
+ See [CLAUDE.md](./CLAUDE.md) for development guidelines.
574
+
575
+ ### Development Setup
576
+
577
+ ```bash
578
+ git clone https://gitlab.com/devnw/geode/geode-client-nodejs.git
579
+ cd geode-client-nodejs
580
+ npm install
581
+ npm test
582
+ npm run build
583
+ ```
584
+
585
+ ## License
586
+
587
+ Apache License 2.0
588
+
589
+ ## Related Projects
590
+
591
+ - [Geode Server](https://gitlab.com/devnw/geode/geode) - The Geode database server
592
+ - [Go Client](https://gitlab.com/devnw/geode/geode-client-go) - Go client library
593
+ - [Python Client](https://gitlab.com/devnw/geode/geode-client-python) - Python client library
594
+ - [Rust Client](https://gitlab.com/devnw/geode/geode-client-rust) - Rust client library