@falkordb/mcpserver 1.0.1

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.
@@ -0,0 +1,399 @@
1
+ import { redisService } from './redis.service';
2
+ import { AppError, CommonErrors } from '../errors/AppError.js';
3
+ // Mock the logger service
4
+ jest.mock('./logger.service.js', () => ({
5
+ logger: {
6
+ info: jest.fn().mockResolvedValue(undefined),
7
+ warn: jest.fn().mockResolvedValue(undefined),
8
+ error: jest.fn().mockResolvedValue(undefined),
9
+ debug: jest.fn().mockResolvedValue(undefined),
10
+ }
11
+ }));
12
+ // Mock the config
13
+ jest.mock('../config/index.js', () => ({
14
+ config: {
15
+ redis: {
16
+ url: 'redis://localhost:6379',
17
+ username: 'testuser',
18
+ password: 'testpass'
19
+ }
20
+ }
21
+ }));
22
+ // Mock the Redis library
23
+ jest.mock('redis', () => {
24
+ const mockConnect = jest.fn();
25
+ const mockPing = jest.fn();
26
+ const mockGet = jest.fn();
27
+ const mockSet = jest.fn();
28
+ const mockDel = jest.fn();
29
+ const mockScan = jest.fn();
30
+ const mockQuit = jest.fn();
31
+ const mockClient = {
32
+ connect: mockConnect,
33
+ ping: mockPing,
34
+ get: mockGet,
35
+ set: mockSet,
36
+ del: mockDel,
37
+ scan: mockScan,
38
+ quit: mockQuit
39
+ };
40
+ return {
41
+ createClient: jest.fn().mockReturnValue(mockClient),
42
+ mockConnect,
43
+ mockPing,
44
+ mockGet,
45
+ mockSet,
46
+ mockDel,
47
+ mockScan,
48
+ mockQuit,
49
+ mockClient
50
+ };
51
+ });
52
+ describe('Redis Service', () => {
53
+ let mockRedis;
54
+ beforeAll(async () => {
55
+ // Access the mocks
56
+ mockRedis = await import('redis');
57
+ });
58
+ beforeEach(() => {
59
+ jest.clearAllMocks();
60
+ // Reset service state
61
+ redisService.client = null;
62
+ redisService.retryCount = 0;
63
+ redisService.initializingPromise = null;
64
+ });
65
+ describe('initialize', () => {
66
+ it('should successfully initialize and connect to Redis', async () => {
67
+ // Arrange
68
+ mockRedis.mockConnect.mockResolvedValue(undefined);
69
+ mockRedis.mockPing.mockResolvedValue('PONG');
70
+ // Act
71
+ await redisService.initialize();
72
+ // Assert
73
+ expect(mockRedis.createClient).toHaveBeenCalledWith({
74
+ url: 'redis://localhost:6379',
75
+ username: 'testuser',
76
+ password: 'testpass',
77
+ });
78
+ expect(mockRedis.mockConnect).toHaveBeenCalled();
79
+ expect(mockRedis.mockPing).toHaveBeenCalled();
80
+ expect(redisService.client).not.toBeNull();
81
+ expect(redisService.retryCount).toBe(0);
82
+ expect(redisService.initializingPromise).toBeNull();
83
+ });
84
+ it('should await ongoing initialization if already initializing', async () => {
85
+ // Arrange
86
+ mockRedis.mockConnect.mockResolvedValue(undefined);
87
+ mockRedis.mockPing.mockResolvedValue('PONG');
88
+ // Act - start two initializations concurrently
89
+ const init1 = redisService.initialize();
90
+ const init2 = redisService.initialize();
91
+ await Promise.all([init1, init2]);
92
+ // Assert - createClient should only be called once
93
+ expect(mockRedis.createClient).toHaveBeenCalledTimes(1);
94
+ expect(redisService.client).not.toBeNull();
95
+ });
96
+ it('should retry connection on failure and eventually succeed', async () => {
97
+ // Arrange
98
+ const connectError = new Error('Connection failed');
99
+ mockRedis.mockConnect
100
+ .mockRejectedValueOnce(connectError)
101
+ .mockRejectedValueOnce(connectError)
102
+ .mockResolvedValueOnce(undefined);
103
+ mockRedis.mockPing.mockResolvedValue('PONG');
104
+ // Mock setTimeout to avoid actual delays
105
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => {
106
+ setImmediate(callback);
107
+ return {};
108
+ });
109
+ // Act
110
+ await redisService.initialize();
111
+ // Assert
112
+ expect(mockRedis.createClient).toHaveBeenCalledTimes(3);
113
+ expect(redisService.client).not.toBeNull();
114
+ expect(redisService.retryCount).toBe(0);
115
+ // Cleanup
116
+ setTimeoutSpy.mockRestore();
117
+ });
118
+ it('should throw AppError after max retries exceeded', async () => {
119
+ // Arrange
120
+ const connectError = new Error('Connection failed');
121
+ mockRedis.mockConnect.mockRejectedValue(connectError);
122
+ // Mock setTimeout to avoid actual delays
123
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => {
124
+ setImmediate(callback);
125
+ return {};
126
+ });
127
+ // Act & Assert
128
+ try {
129
+ await redisService.initialize();
130
+ fail('Expected initialize to throw AppError');
131
+ }
132
+ catch (error) {
133
+ expect(error).toBeInstanceOf(AppError);
134
+ expect(error.name).toBe(CommonErrors.CONNECTION_FAILED);
135
+ }
136
+ expect(mockRedis.createClient).toHaveBeenCalledTimes(6); // 1 initial + 5 retries
137
+ // Cleanup
138
+ setTimeoutSpy.mockRestore();
139
+ });
140
+ it('should handle ping failure during connection test', async () => {
141
+ // Arrange
142
+ const pingError = new Error('Ping failed');
143
+ mockRedis.mockConnect.mockResolvedValue(undefined);
144
+ mockRedis.mockPing.mockRejectedValue(pingError);
145
+ // Mock setTimeout to avoid actual delays
146
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => {
147
+ setImmediate(callback);
148
+ return {};
149
+ });
150
+ // Act & Assert
151
+ await expect(redisService.initialize()).rejects.toThrow(AppError);
152
+ // Cleanup
153
+ setTimeoutSpy.mockRestore();
154
+ });
155
+ });
156
+ describe('get', () => {
157
+ it('should get a value from Redis', async () => {
158
+ // Arrange
159
+ const key = 'testKey';
160
+ const expectedValue = 'testValue';
161
+ mockRedis.mockGet.mockResolvedValue(expectedValue);
162
+ // Force client to be available
163
+ redisService.client = mockRedis.mockClient;
164
+ // Act
165
+ const result = await redisService.get(key);
166
+ // Assert
167
+ expect(mockRedis.mockGet).toHaveBeenCalledWith(key);
168
+ expect(result).toBe(expectedValue);
169
+ });
170
+ it('should return null when key does not exist', async () => {
171
+ // Arrange
172
+ const key = 'nonExistentKey';
173
+ mockRedis.mockGet.mockResolvedValue(null);
174
+ // Force client to be available
175
+ redisService.client = mockRedis.mockClient;
176
+ // Act
177
+ const result = await redisService.get(key);
178
+ // Assert
179
+ expect(mockRedis.mockGet).toHaveBeenCalledWith(key);
180
+ expect(result).toBeNull();
181
+ });
182
+ it('should throw AppError if client is not initialized', async () => {
183
+ // Arrange
184
+ redisService.client = null;
185
+ // Act & Assert
186
+ await expect(redisService.get('testKey'))
187
+ .rejects
188
+ .toThrow(AppError);
189
+ await expect(redisService.get('testKey'))
190
+ .rejects
191
+ .toThrow('Redis client not initialized');
192
+ });
193
+ it('should throw AppError when get operation fails', async () => {
194
+ // Arrange
195
+ const key = 'testKey';
196
+ const getError = new Error('Redis GET failed');
197
+ mockRedis.mockGet.mockRejectedValue(getError);
198
+ // Force client to be available
199
+ redisService.client = mockRedis.mockClient;
200
+ // Act & Assert
201
+ try {
202
+ await redisService.get(key);
203
+ fail('Expected get to throw AppError');
204
+ }
205
+ catch (error) {
206
+ expect(error).toBeInstanceOf(AppError);
207
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
208
+ }
209
+ });
210
+ });
211
+ describe('set', () => {
212
+ it('should set a value in Redis', async () => {
213
+ // Arrange
214
+ const key = 'testKey';
215
+ const value = 'testValue';
216
+ mockRedis.mockSet.mockResolvedValue('OK');
217
+ // Force client to be available
218
+ redisService.client = mockRedis.mockClient;
219
+ // Act
220
+ await redisService.set(key, value);
221
+ // Assert
222
+ expect(mockRedis.mockSet).toHaveBeenCalledWith(key, value);
223
+ });
224
+ it('should throw AppError if client is not initialized', async () => {
225
+ // Arrange
226
+ redisService.client = null;
227
+ // Act & Assert
228
+ await expect(redisService.set('testKey', 'testValue'))
229
+ .rejects
230
+ .toThrow(AppError);
231
+ await expect(redisService.set('testKey', 'testValue'))
232
+ .rejects
233
+ .toThrow('Redis client not initialized');
234
+ });
235
+ it('should throw AppError when set operation fails', async () => {
236
+ // Arrange
237
+ const key = 'testKey';
238
+ const value = 'testValue';
239
+ const setError = new Error('Redis SET failed');
240
+ mockRedis.mockSet.mockRejectedValue(setError);
241
+ // Force client to be available
242
+ redisService.client = mockRedis.mockClient;
243
+ // Act & Assert
244
+ try {
245
+ await redisService.set(key, value);
246
+ fail('Expected set to throw AppError');
247
+ }
248
+ catch (error) {
249
+ expect(error).toBeInstanceOf(AppError);
250
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
251
+ }
252
+ });
253
+ });
254
+ describe('close', () => {
255
+ it('should close the client connection successfully', async () => {
256
+ // Arrange
257
+ mockRedis.mockQuit.mockResolvedValue('OK');
258
+ redisService.client = mockRedis.mockClient;
259
+ redisService.retryCount = 3;
260
+ // Act
261
+ await redisService.close();
262
+ // Assert
263
+ expect(mockRedis.mockQuit).toHaveBeenCalled();
264
+ expect(redisService.client).toBeNull();
265
+ expect(redisService.retryCount).toBe(0);
266
+ });
267
+ it('should handle close error gracefully', async () => {
268
+ // Arrange
269
+ const closeError = new Error('Close failed');
270
+ mockRedis.mockQuit.mockRejectedValue(closeError);
271
+ redisService.client = mockRedis.mockClient;
272
+ redisService.retryCount = 2;
273
+ // Act
274
+ await redisService.close();
275
+ // Assert
276
+ expect(mockRedis.mockQuit).toHaveBeenCalled();
277
+ expect(redisService.client).toBeNull();
278
+ expect(redisService.retryCount).toBe(0);
279
+ });
280
+ it('should not throw if client is already null', async () => {
281
+ // Arrange
282
+ redisService.client = null;
283
+ // Act & Assert
284
+ await expect(redisService.close()).resolves.not.toThrow();
285
+ expect(mockRedis.mockQuit).not.toHaveBeenCalled();
286
+ });
287
+ });
288
+ describe('delete', () => {
289
+ it('should delete a key from Redis', async () => {
290
+ // Arrange
291
+ const key = 'testKey';
292
+ mockRedis.mockDel.mockResolvedValue(1);
293
+ // Force client to be available
294
+ redisService.client = mockRedis.mockClient;
295
+ // Act
296
+ await redisService.delete(key);
297
+ // Assert
298
+ expect(mockRedis.mockDel).toHaveBeenCalledWith(key);
299
+ });
300
+ it('should throw AppError if client is not initialized', async () => {
301
+ // Arrange
302
+ redisService.client = null;
303
+ // Act & Assert
304
+ await expect(redisService.delete('testKey'))
305
+ .rejects
306
+ .toThrow(AppError);
307
+ await expect(redisService.delete('testKey'))
308
+ .rejects
309
+ .toThrow('Redis client not initialized');
310
+ });
311
+ it('should throw AppError when delete operation fails', async () => {
312
+ // Arrange
313
+ const key = 'testKey';
314
+ const delError = new Error('Redis DEL failed');
315
+ mockRedis.mockDel.mockRejectedValue(delError);
316
+ // Force client to be available
317
+ redisService.client = mockRedis.mockClient;
318
+ // Act & Assert
319
+ try {
320
+ await redisService.delete(key);
321
+ fail('Expected delete to throw AppError');
322
+ }
323
+ catch (error) {
324
+ expect(error).toBeInstanceOf(AppError);
325
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
326
+ }
327
+ });
328
+ });
329
+ describe('listKeys', () => {
330
+ it('should list all keys from Redis using scan iteration', async () => {
331
+ // Arrange
332
+ mockRedis.mockScan
333
+ .mockResolvedValueOnce({ cursor: 5, keys: ['key1', 'key2'] })
334
+ .mockResolvedValueOnce({ cursor: 10, keys: ['key3', 'key4'] })
335
+ .mockResolvedValueOnce({ cursor: 0, keys: ['key5'] });
336
+ // Force client to be available
337
+ redisService.client = mockRedis.mockClient;
338
+ // Act
339
+ const result = await redisService.listKeys();
340
+ // Assert
341
+ expect(mockRedis.mockScan).toHaveBeenCalledTimes(3);
342
+ expect(mockRedis.mockScan).toHaveBeenCalledWith(0, { MATCH: '*', COUNT: 1000 });
343
+ expect(mockRedis.mockScan).toHaveBeenCalledWith(5, { MATCH: '*', COUNT: 1000 });
344
+ expect(mockRedis.mockScan).toHaveBeenCalledWith(10, { MATCH: '*', COUNT: 1000 });
345
+ expect(result).toEqual(['key1', 'key2', 'key3', 'key4', 'key5']);
346
+ });
347
+ it('should handle cursor as string and convert to number', async () => {
348
+ // Arrange
349
+ mockRedis.mockScan
350
+ .mockResolvedValueOnce({ cursor: '5', keys: ['key1'] })
351
+ .mockResolvedValueOnce({ cursor: '0', keys: ['key2'] });
352
+ // Force client to be available
353
+ redisService.client = mockRedis.mockClient;
354
+ // Act
355
+ const result = await redisService.listKeys();
356
+ // Assert
357
+ expect(mockRedis.mockScan).toHaveBeenCalledTimes(2);
358
+ expect(result).toEqual(['key1', 'key2']);
359
+ });
360
+ it('should return empty array when no keys exist', async () => {
361
+ // Arrange
362
+ mockRedis.mockScan.mockResolvedValueOnce({ cursor: 0, keys: [] });
363
+ // Force client to be available
364
+ redisService.client = mockRedis.mockClient;
365
+ // Act
366
+ const result = await redisService.listKeys();
367
+ // Assert
368
+ expect(mockRedis.mockScan).toHaveBeenCalledTimes(1);
369
+ expect(result).toEqual([]);
370
+ });
371
+ it('should throw AppError if client is not initialized', async () => {
372
+ // Arrange
373
+ redisService.client = null;
374
+ // Act & Assert
375
+ await expect(redisService.listKeys())
376
+ .rejects
377
+ .toThrow(AppError);
378
+ await expect(redisService.listKeys())
379
+ .rejects
380
+ .toThrow('Redis client not initialized');
381
+ });
382
+ it('should throw AppError when listKeys operation fails', async () => {
383
+ // Arrange
384
+ const scanError = new Error('Redis SCAN failed');
385
+ mockRedis.mockScan.mockRejectedValue(scanError);
386
+ // Force client to be available
387
+ redisService.client = mockRedis.mockClient;
388
+ // Act & Assert
389
+ try {
390
+ await redisService.listKeys();
391
+ fail('Expected listKeys to throw AppError');
392
+ }
393
+ catch (error) {
394
+ expect(error).toBeInstanceOf(AppError);
395
+ expect(error.name).toBe(CommonErrors.OPERATION_FAILED);
396
+ }
397
+ });
398
+ });
399
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Utility to parse FalkorDB connection strings
3
+ */
4
+ /**
5
+ * Parse a FalkorDB connection string
6
+ * Format: falkordb://[username:password@]host:port
7
+ *
8
+ * @param connectionString The connection string to parse
9
+ * @returns Parsed connection options
10
+ */
11
+ export function parseFalkorDBConnectionString(connectionString) {
12
+ try {
13
+ // Default values
14
+ const defaultOptions = {
15
+ host: 'localhost',
16
+ port: 6379
17
+ };
18
+ // Handle empty or undefined input
19
+ if (!connectionString) {
20
+ return defaultOptions;
21
+ }
22
+ // Remove protocol prefix if present
23
+ let cleanString = connectionString;
24
+ if (cleanString.startsWith('falkordb://')) {
25
+ cleanString = cleanString.substring('falkordb://'.length);
26
+ }
27
+ // Parse authentication if present - use lastIndexOf to handle '@' in password
28
+ let auth = '';
29
+ let hostPort = cleanString;
30
+ const lastAtIndex = cleanString.lastIndexOf('@');
31
+ if (lastAtIndex !== -1) {
32
+ auth = cleanString.slice(0, lastAtIndex);
33
+ hostPort = cleanString.slice(lastAtIndex + 1);
34
+ }
35
+ // Parse host and port
36
+ let host = 'localhost';
37
+ let port = 6379;
38
+ if (hostPort.includes(':')) {
39
+ const parts = hostPort.split(':');
40
+ host = parts[0] || 'localhost';
41
+ port = parseInt(parts[1], 10) || 6379;
42
+ }
43
+ else {
44
+ host = hostPort || 'localhost';
45
+ }
46
+ // Parse username and password - handle multiple ':' in password
47
+ let username = undefined;
48
+ let password = undefined;
49
+ if (auth && auth.includes(':')) {
50
+ const firstColonIndex = auth.indexOf(':');
51
+ username = auth.slice(0, firstColonIndex) || undefined;
52
+ password = auth.slice(firstColonIndex + 1) || undefined;
53
+ }
54
+ else if (auth) {
55
+ password = auth;
56
+ }
57
+ return {
58
+ host,
59
+ port,
60
+ username,
61
+ password
62
+ };
63
+ }
64
+ catch (error) {
65
+ console.error('Error parsing connection string:', error);
66
+ return {
67
+ host: 'localhost',
68
+ port: 6379
69
+ };
70
+ }
71
+ }