grafio-mongo 3.0.0 → 3.1.0

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 CHANGED
@@ -1,18 +1,15 @@
1
1
  # grafio-mongo
2
2
 
3
- MongoDB storage backend for [grafio](https://github.com/witspry/grafio) — a graph database with pluggable storage architecture.
3
+ MongoDB storage backend for **grafio** — a graph database with pluggable storage architecture.
4
4
 
5
- ## Overview
6
-
7
- This package provides the MongoDB storage provider for grafio, extracted from the core project. It enables **persistent storage** for grafio graphs using MongoDB (>= 5.0.0), with optimized indexes for nodes and edges and native transaction support.
5
+ **Full documentation**: [https://satya-jugran.github.io/grafio](https://satya-jugran.github.io/grafio)
8
6
 
9
7
  ## Features
10
8
 
11
- - **MongoDB Backend** — Optional MongoDB backend (>= 5.0.0) with optimized indexes for nodes and edges
12
- - **Multiple Graph Support** — via `graphId` partitioning (isolated graphs in one MongoDB instance)
13
- - **Pluggable Storage** — implements the `IStorageProvider` interface from grafio
14
- - **Native Transactions** — MongoDB sessions for atomic multi-operation updates
15
- - **Graph Factories** — `MongoGraphFactory` for controlled instance creation
9
+ - **MongoDB Backend** — Persistent storage with MongoDB (>= 5.0.0)
10
+ - **Multiple Graphs** — via `graphId` partitioning
11
+ - **Native Transactions** — Atomic multi-operation updates
12
+ - **Optimized Indexes** — For nodes and edges
16
13
 
17
14
  ## Installation
18
15
 
@@ -26,399 +23,20 @@ npm install grafio-mongo
26
23
  import { MongoClient } from 'mongodb';
27
24
  import { MongoGraphFactory } from 'grafio-mongo';
28
25
 
29
- // Connect to MongoDB
30
26
  const client = new MongoClient('mongodb://localhost:27017');
31
27
  await client.connect();
32
28
 
33
29
  const factory = new MongoGraphFactory(client.db('mydb'));
34
-
35
- // Create indexes once at startup (idempotent — safe to call every time)
36
30
  await factory.ensureIndexes();
37
31
 
38
- // Get a graph scoped to a named partition
39
32
  const graph = factory.forGraph('my-graph');
33
+ graph.addNode('Person', { name: 'Alice', age: 30 });
34
+ graph.addNode('City', { name: 'New York' });
35
+ graph.addEdge('LIVES_IN', 'alice', 'nyc', { since: 2020 });
40
36
 
41
- // Add nodes and edges
42
- const alice = await graph.addNode('Person', { name: 'Alice' });
43
- const bob = await graph.addNode('Person', { name: 'Bob' });
44
- await graph.addEdge(alice.id, bob.id, 'KNOWS');
45
-
46
- // Navigate the graph
47
- const path = await graph.traverse(alice.id, bob.id, { edgeTypes: ['KNOWS'] });
48
-
49
- // Caller manages the MongoClient lifecycle
50
37
  await client.close();
51
38
  ```
52
39
 
53
- ## MongoGraphFactory
54
-
55
- Factory class for creating MongoDB-backed Graph instances:
56
-
57
- ```typescript
58
- import { MongoGraphFactory } from 'grafio-mongo';
59
-
60
- const factory = new MongoGraphFactory(db);
61
- await factory.ensureIndexes();
62
-
63
- const graph = factory.forGraph('my-graph');
64
- // or with custom options
65
- const graph2 = factory.forGraph('custom-graph', {
66
- nodesCollection: 'my_nodes', // default: 'sgdb_nodes'
67
- edgesCollection: 'my_edges', // default: 'sgdb_edges'
68
- });
69
- ```
70
-
71
- ### Factory Options
72
-
73
- | Option | Type | Default | Description |
74
- |--------|------|---------|-------------|
75
- | `nodesCollection` | `string` | `'sgdb_nodes'` | Collection name for nodes |
76
- | `edgesCollection` | `string` | `'sgdb_edges'` | Collection name for edges |
77
-
78
- ## Direct MongoStorageProvider Usage
79
-
80
- For fine-grained control over collection names and graph partitioning:
81
-
82
- ```typescript
83
- import { MongoClient } from 'mongodb';
84
- import { Graph, MongoStorageProvider } from 'grafio-mongo';
85
-
86
- const client = new MongoClient('mongodb://localhost:27017');
87
- await client.connect();
88
-
89
- const provider = new MongoStorageProvider(client.db('mydb'), {
90
- graphId: 'my-graph', // default: 'default' — partitions data by graph id
91
- nodesCollection: 'my_nodes', // default: 'sgdb_nodes'
92
- edgesCollection: 'my_edges', // default: 'sgdb_edges'
93
- });
94
-
95
- await provider.ensureIndexes();
96
-
97
- const graph = new Graph(provider);
98
- ```
99
-
100
- ## Indexes
101
-
102
- The `ensureIndexes()` method creates the following indexes for optimized queries:
103
-
104
- ### Nodes Collection
105
-
106
- | Index | Purpose |
107
- |-------|---------|
108
- | `{ graphId: 1, id: 1 }` unique | Fast node id lookups within a graph partition |
109
- | `{ graphId: 1, type: 1 }` | `getNodesByType()` within a graph partition |
110
- | `{ graphId: 1, properties: 1 }` | Property value lookups within a graph partition |
111
-
112
- ### Edges Collection
113
-
114
- | Index | Purpose |
115
- |-------|---------|
116
- | `{ graphId: 1, id: 1 }` unique | Fast edge id lookups within a graph partition |
117
- | `{ graphId: 1, type: 1 }` | `getEdgesByType()` within a graph partition |
118
- | `{ graphId: 1, sourceId: 1, type: 1 }` | Outgoing adjacency queries |
119
- | `{ graphId: 1, targetId: 1, type: 1 }` | Incoming adjacency queries |
120
-
121
- ## Transactions
122
-
123
- MongoDB storage provider supports native transactions via MongoDB sessions:
124
-
125
- ```typescript
126
- import { Graph, GraphTransaction } from 'grafio';
127
-
128
- const graph = factory.forGraph('my-graph');
129
- const txn = graph.createTransaction();
130
- await txn.begin();
131
-
132
- try {
133
- const alice = await graph.addNode('Person', { name: 'Alice' }, txn);
134
- const bob = await graph.addNode('Person', { name: 'Bob' }, txn);
135
- await graph.addEdge(alice.id, bob.id, 'KNOWS', {}, txn);
136
- await txn.commit();
137
- } catch (error) {
138
- if (txn.isActive()) {
139
- await txn.rollback();
140
- }
141
- throw error;
142
- }
143
- ```
144
-
145
- **Note:** MongoDB storage provider requires a replica set for transaction support.
146
-
147
- ### Transaction Lifecycle
148
-
149
- - `begin()` — starts a new transaction
150
- - `commit()` — applies all changes atomically (throws if transaction failed)
151
- - `rollback()` — discards all changes
152
- - `isFailed()` — returns true if a storage operation failed within the transaction
153
- - `isActive()` — returns true if transaction is active and not failed
154
-
155
- ## Cypher Query Language
156
-
157
- MongoDB-backed graphs support read-only openCypher-compatible queries via the `CypherEngine`:
158
-
159
- ```typescript
160
- import { Graph } from 'grafio';
161
- import { CypherEngine } from 'grafio/cypher';
162
-
163
- const graph = factory.forGraph('my-graph');
164
-
165
- // Build your graph
166
- const alice = await graph.addNode('Person', { name: 'Alice', age: 30 });
167
- const bob = await graph.addNode('Person', { name: 'Bob', age: 25 });
168
- await graph.addEdge(alice.id, bob.id, 'KNOWS', { since: 2020 });
169
-
170
- const engine = new CypherEngine(graph);
171
-
172
- // Scan nodes by type
173
- const result = await engine.query('MATCH (p:Person) RETURN p.name, p.age');
174
-
175
- // Filter with WHERE
176
- const adults = await engine.query(
177
- 'MATCH (p:Person) WHERE p.age > 25 RETURN p.name'
178
- );
179
-
180
- // Follow relationships
181
- const friends = await engine.query(
182
- 'MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name'
183
- );
184
-
185
- // Multi-hop traversal
186
- const network = await engine.query(
187
- 'MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) RETURN DISTINCT a.name, b.name'
188
- );
189
-
190
- // Parameterized queries
191
- const byName = await engine.query(
192
- 'MATCH (p:Person {name: $name}) RETURN p',
193
- { name: 'Alice' }
194
- );
195
-
196
- // Pagination
197
- const page = await engine.query(
198
- 'MATCH (p:Person) RETURN p ORDER BY p.age DESC SKIP 0 LIMIT 10'
199
- );
200
- ```
201
-
202
- ### Supported Clauses
203
-
204
- | Clause | Support | Notes |
205
- |--------|---------|-------|
206
- | `MATCH` | ✅ Read-only patterns | Typed/untyped nodes, directed edges, multi-label `(n:A\|B)`, inline property maps |
207
- | `WHERE` | ✅ Full expressions | `AND`/`OR`/`NOT`, comparisons, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL` |
208
- | `RETURN` | ✅ With `DISTINCT` | Property access, aliases with `AS` |
209
- | `ORDER BY` | ✅ ASC/DESC | Default ASC when omitted |
210
- | `SKIP` | ✅ Literal + `$param` | Evaluated at runtime |
211
- | `LIMIT` | ✅ Literal + `$param` | Evaluated at runtime |
212
- | `CREATE` / `DELETE` / `SET` / `REMOVE` / `MERGE` | ❌ Rejected | Validation gate prevents execution |
213
-
214
- #### Aggregation Functions
215
-
216
- ```typescript
217
- // Basic count
218
- const total = await engine.query('MATCH (p:Person) RETURN COUNT(p) AS total');
219
-
220
- // Group by with aggregation
221
- const byCity = await engine.query(
222
- 'MATCH (p:Person) RETURN p.city, COUNT(*) AS cnt ORDER BY cnt DESC'
223
- );
224
-
225
- // HAVING clause
226
- const popular = await engine.query(
227
- 'MATCH (p:Person) RETURN p.city, COUNT(*) AS cnt HAVING cnt > 1'
228
- );
229
-
230
- // Multiple aggregates
231
- const stats = await engine.query(
232
- 'MATCH (p:Person) RETURN MIN(p.age), MAX(p.age), AVG(p.age)'
233
- );
234
-
235
- // Named path variable
236
- const paths = await engine.query(
237
- 'MATCH p = (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) RETURN p'
238
- );
239
- ```
240
-
241
- #### Query Plan — Inspect Execution Steps
242
-
243
- Use `getQueryPlan()` to inspect the logical execution plan for a query without running it:
244
-
245
- ```typescript
246
- // Get query plan in JSON format (default)
247
- const planJson = await engine.getQueryPlan('MATCH (p:Person) RETURN p.name');
248
- // Returns: { plan: { steps: [...] } }
249
-
250
- // Get in Text tree format
251
- const planText = await engine.getQueryPlan('MATCH (p:Person)-[:KNOWS]->(b) RETURN p.name, b.name', undefined, 'text');
252
- /*
253
- NodeScanStep (Person)
254
- EdgeExpandStep (KNOWS, outgoing)
255
- ProjectStep [p.name, b.name]
256
- */
257
-
258
- // Get in Mermaid flowchart format
259
- const planMermaid = await engine.getQueryPlan('MATCH (p:Person)-[:KNOWS*1..2]->(b) RETURN p.name', undefined, 'mermaid');
260
- /*
261
- flowchart TD
262
- Step1[NodeScanStep Person]
263
- Step2[EdgeExpandStep KNOWS, 1..2 hops, outgoing]
264
- Step3[ProjectStep [p.name]]
265
- Step1 --> Step2
266
- Step2 --> Step3
267
- */
268
- ```
269
-
270
- #### Execution Plan — Query Plan with Runtime Statistics
271
-
272
- Use `execute()` with the `executionPlan` option to get the query plan enriched with per-step timing and row counts:
273
-
274
- ```typescript
275
- const result = await engine.execute(
276
- 'MATCH (p:Person)-[:KNOWS]->(b:Person) RETURN p.name, b.name',
277
- {},
278
- { executionPlan: { format: 'json' } }
279
- );
280
-
281
- // result.executionPlan contains the formatted plan with stats
282
- // result.summary contains timing metadata
283
- // result.summary.planExecutionStats contains per-step timing data
284
-
285
- // Get execution plan in Text format with timing
286
- const execText = await engine.execute(
287
- 'MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) RETURN a.name',
288
- {},
289
- { executionPlan: { format: 'text' } }
290
- );
291
- console.log(execText.executionPlan);
292
- /*
293
- NodeScanStep Person (1ms, 33.3%, 2 rows)
294
- EdgeExpandStep KNOWS, outgoing (1ms, 33.3%, 5 rows)
295
- EdgeExpandStep KNOWS, outgoing (1ms, 33.3%, 3 rows)
296
- ProjectStep [a.name]
297
- */
298
- ```
299
-
300
- Supported formats: `'json'`, `'text'`, `'mermaid'`
301
-
302
- The execution plan includes:
303
- - **timeMs**: Time spent in each step
304
- - **percentageOfTotal**: Percentage of total query time
305
- - **rowsOut**: Number of rows output by each step
306
-
307
- ### Variable-length Edge Syntax
308
-
309
- | Syntax | Meaning |
310
- |--------|---------|
311
- | `[*]` | Unbounded (up to 100 hops) |
312
- | `[*1..3]` | 1 to 3 hops (BFS by default) |
313
- | `[*2]` | Exactly 2 hops |
314
- | `[*..5]` | Up to 5 hops |
315
-
316
- > **Strategy selection**: BFS is used by default for multi-hop expansion. When `LIMIT` is present, DFS is selected automatically for better early-result performance.
317
-
318
- #### Property Filter Operators
319
-
320
- The `filter.properties` option supports various comparison operators:
321
-
322
- | Operator | Syntax | Description |
323
- |----------|--------|-------------|
324
- | `=` | `{ key: 'age', value: 30 }` | Equality (default) |
325
- | `<>` | `{ key: 'age', value: 30, op: '<>' }` | Not equal |
326
- | `>` | `{ key: 'age', value: 25, op: '>' }` | Greater than |
327
- | `<` | `{ key: 'age', value: 25, op: '<' }` | Less than |
328
- | `>=` | `{ key: 'age', value: 25, op: '>=' }` | Greater than or equal |
329
- | `<=` | `{ key: 'age', value: 25, op: '<=' }` | Less than or equal |
330
- | `CONTAINS` | `{ key: 'name', value: 'John', op: 'CONTAINS' }` | String contains |
331
- | `STARTS_WITH` | `{ key: 'name', value: 'J', op: 'STARTS_WITH' }` | String prefix |
332
- | `ENDS_WITH` | `{ key: 'name', value: 'n', op: 'ENDS_WITH' }` | String suffix |
333
- | `IN` | `{ key: 'city', value: ['NYC', 'LA'], op: 'IN' }` | In array |
334
- | `NOT_IN` | `{ key: 'city', value: ['SF', 'CHI'], op: 'NOT_IN' }` | Not in array |
335
- | `IS_NULL` | `{ key: 'age', op: 'IS_NULL' }` | Is null |
336
- | `IS_NOT_NULL` | `{ key: 'age', op: 'IS_NOT_NULL' }` | Is not null |
337
-
338
- **AND/OR Chaining:**
339
-
340
- ```typescript
341
- // AND chaining
342
- const result = await graph.getNodes({
343
- filter: {
344
- AND: [
345
- { key: 'age', value: 25, op: '>' },
346
- { key: 'city', value: 'NYC' }
347
- ]
348
- }
349
- });
350
-
351
- // OR chaining
352
- const result = await graph.getNodes({
353
- filter: {
354
- OR: [
355
- { key: 'city', value: 'NYC' },
356
- { key: 'city', value: 'LA' }
357
- ]
358
- }
359
- });
360
- ```
361
-
362
- ## Graph Operations
363
-
364
- All graph operations from grafio are available when using MongoDB storage. See the [grafio documentation](https://github.com/satyajugran/grafio) for the complete API reference.
365
-
366
- ### Example Operations
367
-
368
- ```typescript
369
- const graph = factory.forGraph('my-graph');
370
-
371
- // Node operations
372
- const alice = await graph.addNode('Person', { name: 'Alice', age: 30 });
373
- const bob = await graph.addNode('Person', { name: 'Bob' });
374
- await graph.addEdge(alice.id, bob.id, 'KNOWS');
375
-
376
- // Navigation
377
- const parents = await graph.getParents(bob.id);
378
- const children = await graph.getChildren(alice.id);
379
-
380
- // Traversal
381
- const path = await graph.traverse(alice.id, bob.id, { method: 'bfs' });
382
-
383
- // Type filtering
384
- const allPersons = await graph.getNodesByType('Person');
385
-
386
- // Property queries with operators
387
- const adults = await graph.getNodes({ filter: { properties: [{ key: 'age', value: 25, op: '>' }] } });
388
-
389
- // DAG check and topological sort
390
- const isDag = await graph.isDAG();
391
- const order = await graph.topologicalSort();
392
-
393
- // Export/Import
394
- const data = await graph.exportJSON();
395
- await Graph.importJSON(data, new MongoStorageProvider(db, { graphId: 'restored' }));
396
- ```
397
-
398
- ## Development
399
-
400
- ```bash
401
- # Install dependencies
402
- npm install
403
-
404
- # Build TypeScript
405
- npm run build
406
-
407
- # Run tests
408
- npm test
409
-
410
- # Run tests with coverage
411
- npm run test:coverage
412
- ```
413
-
414
- ## Testing
415
-
416
- The test suite runs against the MongoDB backend using `mongodb-memory-server` for integration testing.
417
-
418
- ### Test Structure
40
+ ## License
419
41
 
420
- - `tests/graph/*.mongo.test.ts` — Graph operations via MongoDB provider
421
- - `tests/EducationGraph.mongo.test.ts` — Education domain graph via MongoDB
422
- - `tests/SocialGraph.mongo.test.ts` — Social network graph via MongoDB
423
- - `tests/storage/MongoGraphFactory.test.ts` — Factory lifecycle tests
424
- - `tests/storage/MongoStorageProvider.test.ts` — Provider unit tests
42
+ GPL 3.0
@@ -26,6 +26,7 @@ export declare class MongoStorageProvider implements IStorageProvider {
26
26
  deleteNode(id: string, transaction?: ITransactionHandle): Promise<void>;
27
27
  hasNode(id: string, transaction?: ITransactionHandle): Promise<boolean>;
28
28
  getNode(id: string, transaction?: ITransactionHandle): Promise<NodeData | undefined>;
29
+ getNodesByIds(ids: string[], transaction?: ITransactionHandle): Promise<Map<string, NodeData>>;
29
30
  getNodeCount(options?: StorageQueryOptions): Promise<number>;
30
31
  aggregateNodeProperty(key: string, options?: StorageQueryOptions): Promise<{
31
32
  count: number;
@@ -79,6 +79,15 @@ class MongoStorageProvider {
79
79
  const doc = await this._nodes.findOne({ graphId: this._graphId, id }, { session });
80
80
  return doc ? this._docToNode(doc) : undefined;
81
81
  }
82
+ async getNodesByIds(ids, transaction) {
83
+ const session = transaction?.context;
84
+ const cursor = this._nodes.find({ graphId: this._graphId, id: { $in: ids } }, { session });
85
+ const nodes = new Map();
86
+ for await (const doc of cursor) {
87
+ nodes.set(doc.id, this._docToNode(doc));
88
+ }
89
+ return nodes;
90
+ }
82
91
  async getNodeCount(options) {
83
92
  const session = options?.transaction?.context;
84
93
  const filter = this._buildNodeFilter(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grafio-mongo",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "MongoDB storage backend for grafio",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "peerDependencies": {},
18
18
  "dependencies": {
19
- "grafio": ">=7.0.0",
19
+ "grafio": ">=7.1.0",
20
20
  "mongodb": ">=5.0.0"
21
21
  },
22
22
  "devDependencies": {