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 +11 -256
- package/dist/MongoStorageProvider.d.ts +38 -14
- package/dist/MongoStorageProvider.js +331 -106
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
# grafio-mongo
|
|
2
2
|
|
|
3
|
-
MongoDB storage backend for
|
|
3
|
+
MongoDB storage backend for **grafio** — a graph database with pluggable storage architecture.
|
|
4
4
|
|
|
5
|
-
|
|
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** —
|
|
12
|
-
- **Multiple
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
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
|
-
##
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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 ?? '
|
|
15
|
-
const edgesColl = opts.edgesCollection ?? '
|
|
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
|
|
82
|
+
async getNodesByIds(ids, transaction) {
|
|
83
83
|
const session = transaction?.context;
|
|
84
|
-
const
|
|
85
|
-
const
|
|
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.
|
|
87
|
+
nodes.set(doc.id, this._docToNode(doc));
|
|
92
88
|
}
|
|
93
89
|
return nodes;
|
|
94
90
|
}
|
|
95
|
-
async
|
|
96
|
-
const session = transaction?.context;
|
|
97
|
-
const
|
|
98
|
-
return
|
|
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
|
|
101
|
-
const session = transaction?.context;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
144
|
+
for await (const doc of cursor) {
|
|
145
|
+
nodes.push(this._docToNode(doc));
|
|
146
|
+
}
|
|
147
|
+
return nodes;
|
|
111
148
|
}
|
|
112
|
-
async
|
|
149
|
+
async hasEdge(id, transaction) {
|
|
113
150
|
const session = transaction?.context;
|
|
114
|
-
|
|
151
|
+
const doc = await this._edges.findOne({ graphId: this._graphId, id }, { projection: { _id: 1 }, session });
|
|
152
|
+
return doc !== null;
|
|
115
153
|
}
|
|
116
|
-
async
|
|
154
|
+
async getEdge(id, transaction) {
|
|
117
155
|
const session = transaction?.context;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
|
129
|
-
const session = transaction?.context;
|
|
130
|
-
|
|
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
|
|
376
|
+
async createIndex(target, propertyKey) {
|
|
298
377
|
if (target === 'node') {
|
|
299
378
|
const indexFields = { graphId: 1, [`properties.${propertyKey}`]: 1 };
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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": "
|
|
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": ">=
|
|
19
|
+
"grafio": ">=7.1.0",
|
|
20
20
|
"mongodb": ">=5.0.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|