grafio-mongo 2.0.0 → 3.0.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
@@ -211,6 +211,99 @@ const page = await engine.query(
211
211
  | `LIMIT` | ✅ Literal + `$param` | Evaluated at runtime |
212
212
  | `CREATE` / `DELETE` / `SET` / `REMOVE` / `MERGE` | ❌ Rejected | Validation gate prevents execution |
213
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
+
214
307
  ### Variable-length Edge Syntax
215
308
 
216
309
  | Syntax | Meaning |
@@ -222,6 +315,50 @@ const page = await engine.query(
222
315
 
223
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.
224
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
+
225
362
  ## Graph Operations
226
363
 
227
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.
@@ -246,8 +383,8 @@ const path = await graph.traverse(alice.id, bob.id, { method: 'bfs' });
246
383
  // Type filtering
247
384
  const allPersons = await graph.getNodesByType('Person');
248
385
 
249
- // Property queries
250
- const adults = await graph.getNodesByProperty('age', 30);
386
+ // Property queries with operators
387
+ const adults = await graph.getNodes({ filter: { properties: [{ key: 'age', value: 25, op: '>' }] } });
251
388
 
252
389
  // DAG check and topological sort
253
390
  const isDag = await graph.isDAG();
@@ -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,34 @@ 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
+ getNodeCount(options?: StorageQueryOptions): Promise<number>;
30
+ aggregateNodeProperty(key: string, options?: StorageQueryOptions): Promise<{
31
+ count: number;
32
+ sum?: number;
33
+ avg?: number;
34
+ min?: number;
35
+ max?: number;
36
+ }>;
37
+ getNodes(options?: StorageQueryOptions): Promise<NodeData[]>;
30
38
  hasEdge(id: string, transaction?: ITransactionHandle): Promise<boolean>;
31
39
  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[]>;
40
+ getEdgeCount(options?: StorageQueryOptions): Promise<number>;
41
+ aggregateEdgeProperty(key: string, options?: StorageQueryOptions): Promise<{
42
+ count: number;
43
+ sum?: number;
44
+ avg?: number;
45
+ min?: number;
46
+ max?: number;
47
+ }>;
48
+ getEdges(options?: StorageQueryOptions): Promise<EdgeData[]>;
49
+ getEdgesBySource(nodeId: string, options?: StorageQueryOptions): Promise<EdgeData[]>;
50
+ getEdgesByTarget(nodeId: string, options?: StorageQueryOptions): Promise<EdgeData[]>;
51
+ insertEdge(edge: EdgeData, transaction?: ITransactionHandle): Promise<void>;
52
+ deleteEdge(id: string, transaction?: ITransactionHandle): Promise<void>;
36
53
  exportJSON(): Promise<GraphData>;
37
54
  importJSON(data: GraphData): Promise<void>;
38
- createIndex(target: 'node' | 'edge', propertyKey: string, type?: string): Promise<void>;
55
+ createIndex(target: 'node' | 'edge', propertyKey: string): Promise<void>;
56
+ hasIndex(target: 'node' | 'edge', propertyKey: string): Promise<boolean>;
39
57
  addProperty(target: 'node' | 'edge', id: string, key: string, value: unknown, transaction?: ITransactionHandle): Promise<void>;
40
58
  updateProperty(target: 'node' | 'edge', id: string, key: string, value: unknown, transaction?: ITransactionHandle): Promise<void>;
41
59
  deleteProperty(target: 'node' | 'edge', id: string, key: string, transaction?: ITransactionHandle): Promise<void>;
@@ -44,6 +62,11 @@ export declare class MongoStorageProvider implements IStorageProvider {
44
62
  beginTransaction(): Promise<ITransactionHandle>;
45
63
  commitTransaction(handle: ITransactionHandle): Promise<void>;
46
64
  rollbackTransaction(handle: ITransactionHandle): Promise<void>;
65
+ private _buildNodeFilter;
66
+ private _buildEdgeFilter;
67
+ private _buildPropertyFilter;
68
+ private _buildSimplePropertyFilter;
69
+ private _escapeRegex;
47
70
  private _docToNode;
48
71
  private _docToEdge;
49
72
  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,169 @@ 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) {
83
- const session = transaction?.context;
82
+ async getNodeCount(options) {
83
+ const session = options?.transaction?.context;
84
+ const filter = this._buildNodeFilter(options);
85
+ return this._nodes.countDocuments(filter, { session });
86
+ }
87
+ async aggregateNodeProperty(key, options) {
88
+ const session = options?.transaction?.context;
89
+ const matchFilter = this._buildNodeFilter(options);
90
+ const propFilter = { $exists: true, $type: 'number' };
91
+ if (matchFilter.$and) {
92
+ matchFilter.$and.push({ [`properties.${key}`]: propFilter });
93
+ }
94
+ else {
95
+ matchFilter[`properties.${key}`] = propFilter;
96
+ }
97
+ const pipeline = [
98
+ { $match: matchFilter },
99
+ {
100
+ $group: {
101
+ _id: null,
102
+ count: { $sum: 1 },
103
+ sum: { $sum: `$properties.${key}` },
104
+ avg: { $avg: `$properties.${key}` },
105
+ min: { $min: `$properties.${key}` },
106
+ max: { $max: `$properties.${key}` },
107
+ },
108
+ },
109
+ ];
110
+ const result = await this._nodes.aggregate(pipeline, { session }).next();
111
+ if (!result) {
112
+ return { count: 0 };
113
+ }
114
+ return {
115
+ count: result.count,
116
+ sum: result.sum,
117
+ avg: result.avg,
118
+ min: result.min,
119
+ max: result.max,
120
+ };
121
+ }
122
+ async getNodes(options) {
123
+ const session = options?.transaction?.context;
124
+ const filter = this._buildNodeFilter(options);
84
125
  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);
126
+ const cursor = this._nodes.find(filter, { session }).batchSize(this._batchSize);
127
+ if (options?.orderBy) {
128
+ const { field, direction } = options.orderBy;
129
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
130
+ cursor.sort(sortField, direction);
131
+ }
132
+ if (options?.limit) {
133
+ cursor.limit(options.limit);
134
+ }
90
135
  for await (const doc of cursor) {
91
136
  nodes.push(this._docToNode(doc));
92
137
  }
93
138
  return nodes;
94
139
  }
95
- async getNodesByType(type, transaction) {
140
+ async hasEdge(id, transaction) {
96
141
  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));
142
+ const doc = await this._edges.findOne({ graphId: this._graphId, id }, { projection: { _id: 1 }, session });
143
+ return doc !== null;
99
144
  }
100
- async getNodesByProperty(key, value, nodeType, transaction) {
145
+ async getEdge(id, transaction) {
101
146
  const session = transaction?.context;
102
- const filter = {
103
- graphId: this._graphId,
104
- [`properties.${key}`]: value,
105
- };
106
- if (nodeType !== undefined) {
107
- filter.type = nodeType;
108
- }
109
- const docs = await this._nodes.find(filter, { session }).toArray();
110
- return docs.map(d => this._docToNode(d));
147
+ const doc = await this._edges.findOne({ graphId: this._graphId, id }, { session });
148
+ return doc ? this._docToEdge(doc) : undefined;
111
149
  }
112
- async getTotalNodeCount(transaction) {
113
- const session = transaction?.context;
114
- return this._nodes.countDocuments({ graphId: this._graphId }, { session });
150
+ async getEdgeCount(options) {
151
+ const session = options?.transaction?.context;
152
+ const filter = this._buildEdgeFilter(options);
153
+ return this._edges.countDocuments(filter, { session });
115
154
  }
116
- async getEdgesByProperty(key, value, edgeType, transaction) {
117
- const session = transaction?.context;
118
- const filter = {
119
- graphId: this._graphId,
120
- [`properties.${key}`]: value,
155
+ async aggregateEdgeProperty(key, options) {
156
+ const session = options?.transaction?.context;
157
+ const matchFilter = this._buildEdgeFilter(options);
158
+ const propFilter = { $exists: true, $type: 'number' };
159
+ if (matchFilter.$and) {
160
+ matchFilter.$and.push({ [`properties.${key}`]: propFilter });
161
+ }
162
+ else {
163
+ matchFilter[`properties.${key}`] = propFilter;
164
+ }
165
+ const pipeline = [
166
+ { $match: matchFilter },
167
+ {
168
+ $group: {
169
+ _id: null,
170
+ count: { $sum: 1 },
171
+ sum: { $sum: `$properties.${key}` },
172
+ avg: { $avg: `$properties.${key}` },
173
+ min: { $min: `$properties.${key}` },
174
+ max: { $max: `$properties.${key}` },
175
+ },
176
+ },
177
+ ];
178
+ const result = await this._edges.aggregate(pipeline, { session }).next();
179
+ if (!result) {
180
+ return { count: 0 };
181
+ }
182
+ return {
183
+ count: result.count,
184
+ sum: result.sum,
185
+ avg: result.avg,
186
+ min: result.min,
187
+ max: result.max,
121
188
  };
122
- if (edgeType !== undefined) {
123
- filter.type = edgeType;
189
+ }
190
+ async getEdges(options) {
191
+ const session = options?.transaction?.context;
192
+ const filter = this._buildEdgeFilter(options);
193
+ const edges = [];
194
+ const cursor = this._edges.find(filter, { session }).batchSize(this._batchSize);
195
+ if (options?.orderBy) {
196
+ const { field, direction } = options.orderBy;
197
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
198
+ cursor.sort(sortField, direction);
199
+ }
200
+ if (options?.limit) {
201
+ cursor.limit(options.limit);
202
+ }
203
+ for await (const doc of cursor) {
204
+ edges.push(this._docToEdge(doc));
124
205
  }
125
- const docs = await this._edges.find(filter, { session }).toArray();
126
- return docs.map(d => this._docToEdge(d));
206
+ return edges;
127
207
  }
128
- async getTotalEdgeCount(transaction) {
129
- const session = transaction?.context;
130
- return this._edges.countDocuments({ graphId: this._graphId }, { session });
208
+ async getEdgesBySource(nodeId, options) {
209
+ const session = options?.transaction?.context;
210
+ const baseFilter = { graphId: this._graphId, sourceId: nodeId };
211
+ const filter = this._buildEdgeFilter(options, baseFilter);
212
+ const edges = [];
213
+ const cursor = this._edges.find(filter, { session }).batchSize(this._batchSize);
214
+ if (options?.orderBy) {
215
+ const { field, direction } = options.orderBy;
216
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
217
+ cursor.sort(sortField, direction);
218
+ }
219
+ if (options?.limit) {
220
+ cursor.limit(options.limit);
221
+ }
222
+ for await (const doc of cursor) {
223
+ edges.push(this._docToEdge(doc));
224
+ }
225
+ return edges;
226
+ }
227
+ async getEdgesByTarget(nodeId, options) {
228
+ const session = options?.transaction?.context;
229
+ const baseFilter = { graphId: this._graphId, targetId: nodeId };
230
+ const filter = this._buildEdgeFilter(options, baseFilter);
231
+ const edges = [];
232
+ const cursor = this._edges.find(filter, { session }).batchSize(this._batchSize);
233
+ if (options?.orderBy) {
234
+ const { field, direction } = options.orderBy;
235
+ const sortField = field === 'createdOn' || field === 'updatedOn' ? field : `properties.${field}`;
236
+ cursor.sort(sortField, direction);
237
+ }
238
+ if (options?.limit) {
239
+ cursor.limit(options.limit);
240
+ }
241
+ for await (const doc of cursor) {
242
+ edges.push(this._docToEdge(doc));
243
+ }
244
+ return edges;
131
245
  }
132
246
  async insertEdge(edge, transaction) {
133
247
  const now = Date.now();
@@ -160,50 +274,6 @@ class MongoStorageProvider {
160
274
  const session = transaction?.context;
161
275
  await this._edges.deleteOne({ graphId: this._graphId, id }, { session });
162
276
  }
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
277
  async exportJSON() {
208
278
  const nodes = [];
209
279
  const edges = [];
@@ -294,39 +364,41 @@ class MongoStorageProvider {
294
364
  }
295
365
  }
296
366
  }
297
- async createIndex(target, propertyKey, type) {
367
+ async createIndex(target, propertyKey) {
298
368
  if (target === 'node') {
299
369
  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
- }
370
+ await this._nodes.createIndex(indexFields, {
371
+ name: `node_graphId_${propertyKey}`,
372
+ background: true
373
+ });
374
+ await this._nodes.createIndex({ ...indexFields, type: 1 }, {
375
+ name: `node_graphId_type_${propertyKey}`,
376
+ background: true
377
+ });
313
378
  }
314
379
  else {
315
380
  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
- });
381
+ await this._edges.createIndex(indexFields, {
382
+ name: `edge_graphId_${propertyKey}`,
383
+ background: true
384
+ });
385
+ await this._edges.createIndex({ ...indexFields, type: 1 }, {
386
+ name: `edge_graphId_type_${propertyKey}`,
387
+ background: true
388
+ });
389
+ }
390
+ }
391
+ async hasIndex(target, propertyKey) {
392
+ const collection = target === 'node' ? this._nodes : this._edges;
393
+ const indexNameWithoutType = `${target === 'node' ? 'node' : 'edge'}_graphId_${propertyKey}`;
394
+ const indexNameWithType = `${target === 'node' ? 'node' : 'edge'}_graphId_type_${propertyKey}`;
395
+ for await (const index of collection.listIndexes()) {
396
+ const name = index.name;
397
+ if (name === indexNameWithoutType || name === indexNameWithType) {
398
+ return true;
328
399
  }
329
400
  }
401
+ return false;
330
402
  }
331
403
  async addProperty(target, id, key, value, transaction) {
332
404
  if (!(0, grafio_1.isPrimitive)(value)) {
@@ -432,6 +504,150 @@ class MongoStorageProvider {
432
504
  session.endSession();
433
505
  }
434
506
  }
507
+ _buildNodeFilter(options, baseFilter = {}) {
508
+ const filter = { graphId: this._graphId, ...baseFilter };
509
+ const andConditions = [];
510
+ if (options?.filter) {
511
+ if (options.filter.types && options.filter.types.length > 0) {
512
+ filter.type = { $in: options.filter.types };
513
+ }
514
+ if (options.filter.properties && options.filter.properties.length > 0) {
515
+ for (const prop of options.filter.properties) {
516
+ const propFilter = this._buildPropertyFilter(prop);
517
+ if (propFilter) {
518
+ andConditions.push(propFilter);
519
+ }
520
+ }
521
+ }
522
+ }
523
+ if (andConditions.length > 0) {
524
+ filter.$and = andConditions;
525
+ }
526
+ return filter;
527
+ }
528
+ _buildEdgeFilter(options, baseFilter = {}) {
529
+ const filter = { graphId: this._graphId, ...baseFilter };
530
+ const andConditions = [];
531
+ if (options?.filter) {
532
+ if (options.filter.types && options.filter.types.length > 0) {
533
+ filter.type = { $in: options.filter.types };
534
+ }
535
+ if (options.filter.properties && options.filter.properties.length > 0) {
536
+ for (const prop of options.filter.properties) {
537
+ const propFilter = this._buildPropertyFilter(prop);
538
+ if (propFilter) {
539
+ andConditions.push(propFilter);
540
+ }
541
+ }
542
+ }
543
+ }
544
+ if (andConditions.length > 0) {
545
+ filter.$and = andConditions;
546
+ }
547
+ return filter;
548
+ }
549
+ _buildPropertyFilter(prop) {
550
+ if (prop.AND && prop.AND.length > 0) {
551
+ const andFilters = [];
552
+ for (const subProp of prop.AND) {
553
+ const subFilter = this._buildPropertyFilter(subProp);
554
+ if (subFilter) {
555
+ andFilters.push(subFilter);
556
+ }
557
+ }
558
+ if (andFilters.length > 0) {
559
+ return { $and: andFilters };
560
+ }
561
+ return undefined;
562
+ }
563
+ if (prop.OR && prop.OR.length > 0) {
564
+ const orFilters = [];
565
+ for (const subProp of prop.OR) {
566
+ const subFilter = this._buildPropertyFilter(subProp);
567
+ if (subFilter) {
568
+ orFilters.push(subFilter);
569
+ }
570
+ }
571
+ if (orFilters.length > 0) {
572
+ return { $or: orFilters };
573
+ }
574
+ return undefined;
575
+ }
576
+ if (prop.key === undefined) {
577
+ return undefined;
578
+ }
579
+ const op = prop.op ?? '=';
580
+ const propPath = `properties.${prop.key}`;
581
+ switch (op) {
582
+ case '=':
583
+ return { [propPath]: prop.value };
584
+ case '<>':
585
+ return { [propPath]: { $ne: prop.value } };
586
+ case '>':
587
+ return { [propPath]: { $gt: prop.value } };
588
+ case '<':
589
+ return { [propPath]: { $lt: prop.value } };
590
+ case '>=':
591
+ return { [propPath]: { $gte: prop.value } };
592
+ case '<=':
593
+ return { [propPath]: { $lte: prop.value } };
594
+ case 'CONTAINS':
595
+ return { [propPath]: { $regex: String(prop.value), $options: 'i' } };
596
+ case 'STARTS_WITH':
597
+ return { [propPath]: { $regex: `^${this._escapeRegex(String(prop.value))}`, $options: 'i' } };
598
+ case 'ENDS_WITH':
599
+ return { [propPath]: { $regex: `${this._escapeRegex(String(prop.value))}$`, $options: 'i' } };
600
+ case 'IN':
601
+ return { [propPath]: { $in: Array.isArray(prop.value) ? prop.value : [prop.value] } };
602
+ case 'NOT_IN':
603
+ return { [propPath]: { $nin: Array.isArray(prop.value) ? prop.value : [prop.value] } };
604
+ case 'IS_NULL':
605
+ return { $or: [{ [propPath]: null }, { [propPath]: { $exists: false } }] };
606
+ case 'IS_NOT_NULL':
607
+ return { $and: [{ [propPath]: { $exists: true } }, { [propPath]: { $ne: null } }] };
608
+ default:
609
+ return { [propPath]: prop.value };
610
+ }
611
+ }
612
+ _buildSimplePropertyFilter(prop) {
613
+ if (prop.key === undefined) {
614
+ return undefined;
615
+ }
616
+ const op = prop.op ?? '=';
617
+ switch (op) {
618
+ case '=':
619
+ return prop.value;
620
+ case '<>':
621
+ return { $ne: prop.value };
622
+ case '>':
623
+ return { $gt: prop.value };
624
+ case '<':
625
+ return { $lt: prop.value };
626
+ case '>=':
627
+ return { $gte: prop.value };
628
+ case '<=':
629
+ return { $lte: prop.value };
630
+ case 'CONTAINS':
631
+ return { $regex: String(prop.value), $options: 'i' };
632
+ case 'STARTS_WITH':
633
+ return { $regex: `^${this._escapeRegex(String(prop.value))}`, $options: 'i' };
634
+ case 'ENDS_WITH':
635
+ return { $regex: `${this._escapeRegex(String(prop.value))}$`, $options: 'i' };
636
+ case 'IN':
637
+ return { $in: Array.isArray(prop.value) ? prop.value : [prop.value] };
638
+ case 'NOT_IN':
639
+ return { $nin: Array.isArray(prop.value) ? prop.value : [prop.value] };
640
+ case 'IS_NULL':
641
+ return { $or: [{ [prop.key]: null }, { [prop.key]: { $exists: false } }] };
642
+ case 'IS_NOT_NULL':
643
+ return { $and: [{ [prop.key]: { $exists: true } }, { [prop.key]: { $ne: null } }] };
644
+ default:
645
+ return prop.value;
646
+ }
647
+ }
648
+ _escapeRegex(str) {
649
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
650
+ }
435
651
  _docToNode(doc) {
436
652
  return {
437
653
  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.0.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.0.0",
20
20
  "mongodb": ">=5.0.0"
21
21
  },
22
22
  "devDependencies": {