grafio-mongo 2.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,262 +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
- ### Variable-length Edge Syntax
215
-
216
- | Syntax | Meaning |
217
- |--------|---------|
218
- | `[*]` | Unbounded (up to 100 hops) |
219
- | `[*1..3]` | 1 to 3 hops (BFS by default) |
220
- | `[*2]` | Exactly 2 hops |
221
- | `[*..5]` | Up to 5 hops |
222
-
223
- > **Strategy selection**: BFS is used by default for multi-hop expansion. When `LIMIT` is present, DFS is selected automatically for better early-result performance.
224
-
225
- ## Graph Operations
226
-
227
- 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.
228
-
229
- ### Example Operations
230
-
231
- ```typescript
232
- const graph = factory.forGraph('my-graph');
233
-
234
- // Node operations
235
- const alice = await graph.addNode('Person', { name: 'Alice', age: 30 });
236
- const bob = await graph.addNode('Person', { name: 'Bob' });
237
- await graph.addEdge(alice.id, bob.id, 'KNOWS');
238
-
239
- // Navigation
240
- const parents = await graph.getParents(bob.id);
241
- const children = await graph.getChildren(alice.id);
242
-
243
- // Traversal
244
- const path = await graph.traverse(alice.id, bob.id, { method: 'bfs' });
245
-
246
- // Type filtering
247
- const allPersons = await graph.getNodesByType('Person');
248
-
249
- // Property queries
250
- const adults = await graph.getNodesByProperty('age', 30);
251
-
252
- // DAG check and topological sort
253
- const isDag = await graph.isDAG();
254
- const order = await graph.topologicalSort();
255
-
256
- // Export/Import
257
- const data = await graph.exportJSON();
258
- await Graph.importJSON(data, new MongoStorageProvider(db, { graphId: 'restored' }));
259
- ```
260
-
261
- ## Development
262
-
263
- ```bash
264
- # Install dependencies
265
- npm install
266
-
267
- # Build TypeScript
268
- npm run build
269
-
270
- # Run tests
271
- npm test
272
-
273
- # Run tests with coverage
274
- npm run test:coverage
275
- ```
276
-
277
- ## Testing
278
-
279
- The test suite runs against the MongoDB backend using `mongodb-memory-server` for integration testing.
280
-
281
- ### Test Structure
40
+ ## License
282
41
 
283
- - `tests/graph/*.mongo.test.ts` — Graph operations via MongoDB provider
284
- - `tests/EducationGraph.mongo.test.ts` — Education domain graph via MongoDB
285
- - `tests/SocialGraph.mongo.test.ts` — Social network graph via MongoDB
286
- - `tests/storage/MongoGraphFactory.test.ts` — Factory lifecycle tests
287
- - `tests/storage/MongoStorageProvider.test.ts` — Provider unit tests
42
+ GPL 3.0
@@ -1,5 +1,12 @@
1
1
  import type { Db } from 'mongodb';
2
- import { IStorageProvider, IOrderBy, NodeData, ITransactionHandle, EdgeData, GraphData } from 'grafio';
2
+ import { IStorageProvider, NodeData, ITransactionHandle, EdgeData, GraphData, StorageQueryOptions } from 'grafio';
3
+ export interface QueryOptionsFilterProperty {
4
+ key?: string;
5
+ value?: unknown;
6
+ op?: '=' | '<>' | '>' | '<' | '>=' | '<=' | 'CONTAINS' | 'STARTS_WITH' | 'ENDS_WITH' | 'IN' | 'NOT_IN' | 'IS_NULL' | 'IS_NOT_NULL';
7
+ AND?: QueryOptionsFilterProperty[];
8
+ OR?: QueryOptionsFilterProperty[];
9
+ }
3
10
  export interface MongoStorageProviderOptions {
4
11
  graphId?: string;
5
12
  nodesCollection?: string;
@@ -19,23 +26,35 @@ export declare class MongoStorageProvider implements IStorageProvider {
19
26
  deleteNode(id: string, transaction?: ITransactionHandle): Promise<void>;
20
27
  hasNode(id: string, transaction?: ITransactionHandle): Promise<boolean>;
21
28
  getNode(id: string, transaction?: ITransactionHandle): Promise<NodeData | undefined>;
22
- getAllNodes(limit?: number, orderBy?: IOrderBy, transaction?: ITransactionHandle): Promise<NodeData[]>;
23
- getNodesByType(type: string, transaction?: ITransactionHandle): Promise<NodeData[]>;
24
- getNodesByProperty(key: string, value: unknown, nodeType?: string, transaction?: ITransactionHandle): Promise<NodeData[]>;
25
- getTotalNodeCount(transaction?: ITransactionHandle): Promise<number>;
26
- getEdgesByProperty(key: string, value: unknown, edgeType?: string, transaction?: ITransactionHandle): Promise<EdgeData[]>;
27
- getTotalEdgeCount(transaction?: ITransactionHandle): Promise<number>;
28
- insertEdge(edge: EdgeData, transaction?: ITransactionHandle): Promise<void>;
29
- deleteEdge(id: string, transaction?: ITransactionHandle): Promise<void>;
29
+ getNodesByIds(ids: string[], transaction?: ITransactionHandle): Promise<Map<string, NodeData>>;
30
+ getNodeCount(options?: StorageQueryOptions): Promise<number>;
31
+ aggregateNodeProperty(key: string, options?: StorageQueryOptions): Promise<{
32
+ count: number;
33
+ sum?: number;
34
+ avg?: number;
35
+ min?: number;
36
+ max?: number;
37
+ }>;
38
+ getNodes(options?: StorageQueryOptions): Promise<NodeData[]>;
30
39
  hasEdge(id: string, transaction?: ITransactionHandle): Promise<boolean>;
31
40
  getEdge(id: string, transaction?: ITransactionHandle): Promise<EdgeData | undefined>;
32
- getAllEdges(limit?: number, orderBy?: IOrderBy, transaction?: ITransactionHandle): Promise<EdgeData[]>;
33
- getEdgesByType(type: string, transaction?: ITransactionHandle): Promise<EdgeData[]>;
34
- getEdgesBySource(nodeId: string, type?: string, transaction?: ITransactionHandle): Promise<EdgeData[]>;
35
- getEdgesByTarget(nodeId: string, type?: string, transaction?: ITransactionHandle): Promise<EdgeData[]>;
41
+ getEdgeCount(options?: StorageQueryOptions): Promise<number>;
42
+ aggregateEdgeProperty(key: string, options?: StorageQueryOptions): Promise<{
43
+ count: number;
44
+ sum?: number;
45
+ avg?: number;
46
+ min?: number;
47
+ max?: number;
48
+ }>;
49
+ getEdges(options?: StorageQueryOptions): Promise<EdgeData[]>;
50
+ getEdgesBySource(nodeId: string, options?: StorageQueryOptions): Promise<EdgeData[]>;
51
+ getEdgesByTarget(nodeId: string, options?: StorageQueryOptions): Promise<EdgeData[]>;
52
+ insertEdge(edge: EdgeData, transaction?: ITransactionHandle): Promise<void>;
53
+ deleteEdge(id: string, transaction?: ITransactionHandle): Promise<void>;
36
54
  exportJSON(): Promise<GraphData>;
37
55
  importJSON(data: GraphData): Promise<void>;
38
- createIndex(target: 'node' | 'edge', propertyKey: string, type?: string): Promise<void>;
56
+ createIndex(target: 'node' | 'edge', propertyKey: string): Promise<void>;
57
+ hasIndex(target: 'node' | 'edge', propertyKey: string): Promise<boolean>;
39
58
  addProperty(target: 'node' | 'edge', id: string, key: string, value: unknown, transaction?: ITransactionHandle): Promise<void>;
40
59
  updateProperty(target: 'node' | 'edge', id: string, key: string, value: unknown, transaction?: ITransactionHandle): Promise<void>;
41
60
  deleteProperty(target: 'node' | 'edge', id: string, key: string, transaction?: ITransactionHandle): Promise<void>;
@@ -44,6 +63,11 @@ export declare class MongoStorageProvider implements IStorageProvider {
44
63
  beginTransaction(): Promise<ITransactionHandle>;
45
64
  commitTransaction(handle: ITransactionHandle): Promise<void>;
46
65
  rollbackTransaction(handle: ITransactionHandle): Promise<void>;
66
+ private _buildNodeFilter;
67
+ private _buildEdgeFilter;
68
+ private _buildPropertyFilter;
69
+ private _buildSimplePropertyFilter;
70
+ private _escapeRegex;
47
71
  private _docToNode;
48
72
  private _docToEdge;
49
73
  private _isDuplicateKeyError;
@@ -11,8 +11,8 @@ class MongoStorageProvider {
11
11
  _batchSize;
12
12
  _client = null;
13
13
  constructor(db, opts = {}) {
14
- const nodesColl = opts.nodesCollection ?? 'sgdb_nodes';
15
- const edgesColl = opts.edgesCollection ?? 'sgdb_edges';
14
+ const nodesColl = opts.nodesCollection ?? 'grafiodb_nodes';
15
+ const edgesColl = opts.edgesCollection ?? 'grafiodb_edges';
16
16
  this._nodes = db.collection(nodesColl);
17
17
  this._edges = db.collection(edgesColl);
18
18
  this._graphId = opts.graphId ?? 'default';
@@ -79,55 +79,178 @@ 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 getAllNodes(limit, orderBy, transaction) {
82
+ async getNodesByIds(ids, transaction) {
83
83
  const session = transaction?.context;
84
- const nodes = [];
85
- const cursor = this._nodes.find({ graphId: this._graphId }, { session }).batchSize(this._batchSize);
86
- if (orderBy)
87
- cursor.sort(orderBy.field, orderBy.direction);
88
- if (limit)
89
- cursor.limit(limit);
84
+ const cursor = this._nodes.find({ graphId: this._graphId, id: { $in: ids } }, { session });
85
+ const nodes = new Map();
90
86
  for await (const doc of cursor) {
91
- nodes.push(this._docToNode(doc));
87
+ nodes.set(doc.id, this._docToNode(doc));
92
88
  }
93
89
  return nodes;
94
90
  }
95
- async getNodesByType(type, transaction) {
96
- const session = transaction?.context;
97
- const docs = await this._nodes.find({ graphId: this._graphId, type }, { session }).toArray();
98
- return docs.map(d => this._docToNode(d));
91
+ async getNodeCount(options) {
92
+ const session = options?.transaction?.context;
93
+ const filter = this._buildNodeFilter(options);
94
+ return this._nodes.countDocuments(filter, { session });
99
95
  }
100
- async getNodesByProperty(key, value, nodeType, transaction) {
101
- const session = transaction?.context;
102
- const filter = {
103
- graphId: this._graphId,
104
- [`properties.${key}`]: value,
96
+ async aggregateNodeProperty(key, options) {
97
+ const session = options?.transaction?.context;
98
+ const matchFilter = this._buildNodeFilter(options);
99
+ const propFilter = { $exists: true, $type: 'number' };
100
+ if (matchFilter.$and) {
101
+ matchFilter.$and.push({ [`properties.${key}`]: propFilter });
102
+ }
103
+ else {
104
+ matchFilter[`properties.${key}`] = propFilter;
105
+ }
106
+ const pipeline = [
107
+ { $match: matchFilter },
108
+ {
109
+ $group: {
110
+ _id: null,
111
+ count: { $sum: 1 },
112
+ sum: { $sum: `$properties.${key}` },
113
+ avg: { $avg: `$properties.${key}` },
114
+ min: { $min: `$properties.${key}` },
115
+ max: { $max: `$properties.${key}` },
116
+ },
117
+ },
118
+ ];
119
+ const result = await this._nodes.aggregate(pipeline, { session }).next();
120
+ if (!result) {
121
+ return { count: 0 };
122
+ }
123
+ return {
124
+ count: result.count,
125
+ sum: result.sum,
126
+ avg: result.avg,
127
+ min: result.min,
128
+ max: result.max,
105
129
  };
106
- if (nodeType !== undefined) {
107
- filter.type = nodeType;
130
+ }
131
+ async getNodes(options) {
132
+ const session = options?.transaction?.context;
133
+ const filter = this._buildNodeFilter(options);
134
+ const nodes = [];
135
+ const cursor = this._nodes.find(filter, { session }).batchSize(this._batchSize);
136
+ if (options?.orderBy) {
137
+ const { field, direction } = options.orderBy;
138
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
139
+ cursor.sort(sortField, direction);
140
+ }
141
+ if (options?.limit) {
142
+ cursor.limit(options.limit);
108
143
  }
109
- const docs = await this._nodes.find(filter, { session }).toArray();
110
- return docs.map(d => this._docToNode(d));
144
+ for await (const doc of cursor) {
145
+ nodes.push(this._docToNode(doc));
146
+ }
147
+ return nodes;
111
148
  }
112
- async getTotalNodeCount(transaction) {
149
+ async hasEdge(id, transaction) {
113
150
  const session = transaction?.context;
114
- return this._nodes.countDocuments({ graphId: this._graphId }, { session });
151
+ const doc = await this._edges.findOne({ graphId: this._graphId, id }, { projection: { _id: 1 }, session });
152
+ return doc !== null;
115
153
  }
116
- async getEdgesByProperty(key, value, edgeType, transaction) {
154
+ async getEdge(id, transaction) {
117
155
  const session = transaction?.context;
118
- const filter = {
119
- graphId: this._graphId,
120
- [`properties.${key}`]: value,
156
+ const doc = await this._edges.findOne({ graphId: this._graphId, id }, { session });
157
+ return doc ? this._docToEdge(doc) : undefined;
158
+ }
159
+ async getEdgeCount(options) {
160
+ const session = options?.transaction?.context;
161
+ const filter = this._buildEdgeFilter(options);
162
+ return this._edges.countDocuments(filter, { session });
163
+ }
164
+ async aggregateEdgeProperty(key, options) {
165
+ const session = options?.transaction?.context;
166
+ const matchFilter = this._buildEdgeFilter(options);
167
+ const propFilter = { $exists: true, $type: 'number' };
168
+ if (matchFilter.$and) {
169
+ matchFilter.$and.push({ [`properties.${key}`]: propFilter });
170
+ }
171
+ else {
172
+ matchFilter[`properties.${key}`] = propFilter;
173
+ }
174
+ const pipeline = [
175
+ { $match: matchFilter },
176
+ {
177
+ $group: {
178
+ _id: null,
179
+ count: { $sum: 1 },
180
+ sum: { $sum: `$properties.${key}` },
181
+ avg: { $avg: `$properties.${key}` },
182
+ min: { $min: `$properties.${key}` },
183
+ max: { $max: `$properties.${key}` },
184
+ },
185
+ },
186
+ ];
187
+ const result = await this._edges.aggregate(pipeline, { session }).next();
188
+ if (!result) {
189
+ return { count: 0 };
190
+ }
191
+ return {
192
+ count: result.count,
193
+ sum: result.sum,
194
+ avg: result.avg,
195
+ min: result.min,
196
+ max: result.max,
121
197
  };
122
- if (edgeType !== undefined) {
123
- filter.type = edgeType;
198
+ }
199
+ async getEdges(options) {
200
+ const session = options?.transaction?.context;
201
+ const filter = this._buildEdgeFilter(options);
202
+ const edges = [];
203
+ const cursor = this._edges.find(filter, { session }).batchSize(this._batchSize);
204
+ if (options?.orderBy) {
205
+ const { field, direction } = options.orderBy;
206
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
207
+ cursor.sort(sortField, direction);
124
208
  }
125
- const docs = await this._edges.find(filter, { session }).toArray();
126
- return docs.map(d => this._docToEdge(d));
209
+ if (options?.limit) {
210
+ cursor.limit(options.limit);
211
+ }
212
+ for await (const doc of cursor) {
213
+ edges.push(this._docToEdge(doc));
214
+ }
215
+ return edges;
127
216
  }
128
- async getTotalEdgeCount(transaction) {
129
- const session = transaction?.context;
130
- return this._edges.countDocuments({ graphId: this._graphId }, { session });
217
+ async getEdgesBySource(nodeId, options) {
218
+ const session = options?.transaction?.context;
219
+ const baseFilter = { graphId: this._graphId, sourceId: nodeId };
220
+ const filter = this._buildEdgeFilter(options, baseFilter);
221
+ const edges = [];
222
+ const cursor = this._edges.find(filter, { session }).batchSize(this._batchSize);
223
+ if (options?.orderBy) {
224
+ const { field, direction } = options.orderBy;
225
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
226
+ cursor.sort(sortField, direction);
227
+ }
228
+ if (options?.limit) {
229
+ cursor.limit(options.limit);
230
+ }
231
+ for await (const doc of cursor) {
232
+ edges.push(this._docToEdge(doc));
233
+ }
234
+ return edges;
235
+ }
236
+ async getEdgesByTarget(nodeId, options) {
237
+ const session = options?.transaction?.context;
238
+ const baseFilter = { graphId: this._graphId, targetId: nodeId };
239
+ const filter = this._buildEdgeFilter(options, baseFilter);
240
+ const edges = [];
241
+ const cursor = this._edges.find(filter, { session }).batchSize(this._batchSize);
242
+ if (options?.orderBy) {
243
+ const { field, direction } = options.orderBy;
244
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
245
+ cursor.sort(sortField, direction);
246
+ }
247
+ if (options?.limit) {
248
+ cursor.limit(options.limit);
249
+ }
250
+ for await (const doc of cursor) {
251
+ edges.push(this._docToEdge(doc));
252
+ }
253
+ return edges;
131
254
  }
132
255
  async insertEdge(edge, transaction) {
133
256
  const now = Date.now();
@@ -160,50 +283,6 @@ class MongoStorageProvider {
160
283
  const session = transaction?.context;
161
284
  await this._edges.deleteOne({ graphId: this._graphId, id }, { session });
162
285
  }
163
- async hasEdge(id, transaction) {
164
- const session = transaction?.context;
165
- const doc = await this._edges.findOne({ graphId: this._graphId, id }, { projection: { _id: 1 }, session });
166
- return doc !== null;
167
- }
168
- async getEdge(id, transaction) {
169
- const session = transaction?.context;
170
- const doc = await this._edges.findOne({ graphId: this._graphId, id }, { session });
171
- return doc ? this._docToEdge(doc) : undefined;
172
- }
173
- async getAllEdges(limit, orderBy, transaction) {
174
- const session = transaction?.context;
175
- const edges = [];
176
- const cursor = this._edges.find({ graphId: this._graphId }, { session }).batchSize(this._batchSize);
177
- if (orderBy)
178
- cursor.sort(orderBy.field, orderBy.direction);
179
- if (limit)
180
- cursor.limit(limit);
181
- for await (const doc of cursor) {
182
- edges.push(this._docToEdge(doc));
183
- }
184
- return edges;
185
- }
186
- async getEdgesByType(type, transaction) {
187
- const session = transaction?.context;
188
- const docs = await this._edges.find({ graphId: this._graphId, type }, { session }).toArray();
189
- return docs.map(d => this._docToEdge(d));
190
- }
191
- async getEdgesBySource(nodeId, type, transaction) {
192
- const session = transaction?.context;
193
- const filter = { graphId: this._graphId, sourceId: nodeId };
194
- if (type)
195
- filter.type = type;
196
- const docs = await this._edges.find(filter, { session }).toArray();
197
- return docs.map(d => this._docToEdge(d));
198
- }
199
- async getEdgesByTarget(nodeId, type, transaction) {
200
- const session = transaction?.context;
201
- const filter = { graphId: this._graphId, targetId: nodeId };
202
- if (type)
203
- filter.type = type;
204
- const docs = await this._edges.find(filter, { session }).toArray();
205
- return docs.map(d => this._docToEdge(d));
206
- }
207
286
  async exportJSON() {
208
287
  const nodes = [];
209
288
  const edges = [];
@@ -294,39 +373,41 @@ class MongoStorageProvider {
294
373
  }
295
374
  }
296
375
  }
297
- async createIndex(target, propertyKey, type) {
376
+ async createIndex(target, propertyKey) {
298
377
  if (target === 'node') {
299
378
  const indexFields = { graphId: 1, [`properties.${propertyKey}`]: 1 };
300
- if (type && type !== '*') {
301
- indexFields['type'] = 1;
302
- await this._nodes.createIndex(indexFields, {
303
- name: `node_graphId_type_${propertyKey}`,
304
- background: true
305
- });
306
- }
307
- else {
308
- await this._nodes.createIndex(indexFields, {
309
- name: `node_graphId_${propertyKey}`,
310
- background: true
311
- });
312
- }
379
+ await this._nodes.createIndex(indexFields, {
380
+ name: `node_graphId_${propertyKey}`,
381
+ background: true
382
+ });
383
+ await this._nodes.createIndex({ ...indexFields, type: 1 }, {
384
+ name: `node_graphId_type_${propertyKey}`,
385
+ background: true
386
+ });
313
387
  }
314
388
  else {
315
389
  const indexFields = { graphId: 1, [`properties.${propertyKey}`]: 1 };
316
- if (type && type !== '*') {
317
- indexFields['type'] = 1;
318
- await this._edges.createIndex(indexFields, {
319
- name: `edge_graphId_type_${propertyKey}`,
320
- background: true
321
- });
322
- }
323
- else {
324
- await this._edges.createIndex(indexFields, {
325
- name: `edge_graphId_${propertyKey}`,
326
- background: true
327
- });
390
+ await this._edges.createIndex(indexFields, {
391
+ name: `edge_graphId_${propertyKey}`,
392
+ background: true
393
+ });
394
+ await this._edges.createIndex({ ...indexFields, type: 1 }, {
395
+ name: `edge_graphId_type_${propertyKey}`,
396
+ background: true
397
+ });
398
+ }
399
+ }
400
+ async hasIndex(target, propertyKey) {
401
+ const collection = target === 'node' ? this._nodes : this._edges;
402
+ const indexNameWithoutType = `${target === 'node' ? 'node' : 'edge'}_graphId_${propertyKey}`;
403
+ const indexNameWithType = `${target === 'node' ? 'node' : 'edge'}_graphId_type_${propertyKey}`;
404
+ for await (const index of collection.listIndexes()) {
405
+ const name = index.name;
406
+ if (name === indexNameWithoutType || name === indexNameWithType) {
407
+ return true;
328
408
  }
329
409
  }
410
+ return false;
330
411
  }
331
412
  async addProperty(target, id, key, value, transaction) {
332
413
  if (!(0, grafio_1.isPrimitive)(value)) {
@@ -432,6 +513,150 @@ class MongoStorageProvider {
432
513
  session.endSession();
433
514
  }
434
515
  }
516
+ _buildNodeFilter(options, baseFilter = {}) {
517
+ const filter = { graphId: this._graphId, ...baseFilter };
518
+ const andConditions = [];
519
+ if (options?.filter) {
520
+ if (options.filter.types && options.filter.types.length > 0) {
521
+ filter.type = { $in: options.filter.types };
522
+ }
523
+ if (options.filter.properties && options.filter.properties.length > 0) {
524
+ for (const prop of options.filter.properties) {
525
+ const propFilter = this._buildPropertyFilter(prop);
526
+ if (propFilter) {
527
+ andConditions.push(propFilter);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ if (andConditions.length > 0) {
533
+ filter.$and = andConditions;
534
+ }
535
+ return filter;
536
+ }
537
+ _buildEdgeFilter(options, baseFilter = {}) {
538
+ const filter = { graphId: this._graphId, ...baseFilter };
539
+ const andConditions = [];
540
+ if (options?.filter) {
541
+ if (options.filter.types && options.filter.types.length > 0) {
542
+ filter.type = { $in: options.filter.types };
543
+ }
544
+ if (options.filter.properties && options.filter.properties.length > 0) {
545
+ for (const prop of options.filter.properties) {
546
+ const propFilter = this._buildPropertyFilter(prop);
547
+ if (propFilter) {
548
+ andConditions.push(propFilter);
549
+ }
550
+ }
551
+ }
552
+ }
553
+ if (andConditions.length > 0) {
554
+ filter.$and = andConditions;
555
+ }
556
+ return filter;
557
+ }
558
+ _buildPropertyFilter(prop) {
559
+ if (prop.AND && prop.AND.length > 0) {
560
+ const andFilters = [];
561
+ for (const subProp of prop.AND) {
562
+ const subFilter = this._buildPropertyFilter(subProp);
563
+ if (subFilter) {
564
+ andFilters.push(subFilter);
565
+ }
566
+ }
567
+ if (andFilters.length > 0) {
568
+ return { $and: andFilters };
569
+ }
570
+ return undefined;
571
+ }
572
+ if (prop.OR && prop.OR.length > 0) {
573
+ const orFilters = [];
574
+ for (const subProp of prop.OR) {
575
+ const subFilter = this._buildPropertyFilter(subProp);
576
+ if (subFilter) {
577
+ orFilters.push(subFilter);
578
+ }
579
+ }
580
+ if (orFilters.length > 0) {
581
+ return { $or: orFilters };
582
+ }
583
+ return undefined;
584
+ }
585
+ if (prop.key === undefined) {
586
+ return undefined;
587
+ }
588
+ const op = prop.op ?? '=';
589
+ const propPath = `properties.${prop.key}`;
590
+ switch (op) {
591
+ case '=':
592
+ return { [propPath]: prop.value };
593
+ case '<>':
594
+ return { [propPath]: { $ne: prop.value } };
595
+ case '>':
596
+ return { [propPath]: { $gt: prop.value } };
597
+ case '<':
598
+ return { [propPath]: { $lt: prop.value } };
599
+ case '>=':
600
+ return { [propPath]: { $gte: prop.value } };
601
+ case '<=':
602
+ return { [propPath]: { $lte: prop.value } };
603
+ case 'CONTAINS':
604
+ return { [propPath]: { $regex: String(prop.value), $options: 'i' } };
605
+ case 'STARTS_WITH':
606
+ return { [propPath]: { $regex: `^${this._escapeRegex(String(prop.value))}`, $options: 'i' } };
607
+ case 'ENDS_WITH':
608
+ return { [propPath]: { $regex: `${this._escapeRegex(String(prop.value))}$`, $options: 'i' } };
609
+ case 'IN':
610
+ return { [propPath]: { $in: Array.isArray(prop.value) ? prop.value : [prop.value] } };
611
+ case 'NOT_IN':
612
+ return { [propPath]: { $nin: Array.isArray(prop.value) ? prop.value : [prop.value] } };
613
+ case 'IS_NULL':
614
+ return { $or: [{ [propPath]: null }, { [propPath]: { $exists: false } }] };
615
+ case 'IS_NOT_NULL':
616
+ return { $and: [{ [propPath]: { $exists: true } }, { [propPath]: { $ne: null } }] };
617
+ default:
618
+ return { [propPath]: prop.value };
619
+ }
620
+ }
621
+ _buildSimplePropertyFilter(prop) {
622
+ if (prop.key === undefined) {
623
+ return undefined;
624
+ }
625
+ const op = prop.op ?? '=';
626
+ switch (op) {
627
+ case '=':
628
+ return prop.value;
629
+ case '<>':
630
+ return { $ne: prop.value };
631
+ case '>':
632
+ return { $gt: prop.value };
633
+ case '<':
634
+ return { $lt: prop.value };
635
+ case '>=':
636
+ return { $gte: prop.value };
637
+ case '<=':
638
+ return { $lte: prop.value };
639
+ case 'CONTAINS':
640
+ return { $regex: String(prop.value), $options: 'i' };
641
+ case 'STARTS_WITH':
642
+ return { $regex: `^${this._escapeRegex(String(prop.value))}`, $options: 'i' };
643
+ case 'ENDS_WITH':
644
+ return { $regex: `${this._escapeRegex(String(prop.value))}$`, $options: 'i' };
645
+ case 'IN':
646
+ return { $in: Array.isArray(prop.value) ? prop.value : [prop.value] };
647
+ case 'NOT_IN':
648
+ return { $nin: Array.isArray(prop.value) ? prop.value : [prop.value] };
649
+ case 'IS_NULL':
650
+ return { $or: [{ [prop.key]: null }, { [prop.key]: { $exists: false } }] };
651
+ case 'IS_NOT_NULL':
652
+ return { $and: [{ [prop.key]: { $exists: true } }, { [prop.key]: { $ne: null } }] };
653
+ default:
654
+ return prop.value;
655
+ }
656
+ }
657
+ _escapeRegex(str) {
658
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
659
+ }
435
660
  _docToNode(doc) {
436
661
  return {
437
662
  id: doc.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grafio-mongo",
3
- "version": "2.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": ">=6.0.0",
19
+ "grafio": ">=7.1.0",
20
20
  "mongodb": ">=5.0.0"
21
21
  },
22
22
  "devDependencies": {