@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.
- package/.env.example +26 -0
- package/LICENSE +21 -0
- package/README.md +412 -0
- package/dist/config/index.js +27 -0
- package/dist/config/index.test.js +23 -0
- package/dist/errors/AppError.js +27 -0
- package/dist/errors/ErrorHandler.js +46 -0
- package/dist/errors/ErrorHandler.test.js +146 -0
- package/dist/index.js +234 -0
- package/dist/mcp/prompts.js +229 -0
- package/dist/mcp/resources.js +26 -0
- package/dist/mcp/tools.js +258 -0
- package/dist/models/mcp-client-config.js +34 -0
- package/dist/models/mcp-client-config.test.js +173 -0
- package/dist/models/mcp.types.js +4 -0
- package/dist/services/falkordb.service.js +175 -0
- package/dist/services/falkordb.service.test.js +489 -0
- package/dist/services/logger.service.js +151 -0
- package/dist/services/logger.service.test.js +115 -0
- package/dist/services/redis.service.js +179 -0
- package/dist/services/redis.service.test.js +399 -0
- package/dist/utils/connection-parser.js +71 -0
- package/dist/utils/connection-parser.test.js +232 -0
- package/package.json +99 -0
|
@@ -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
|
+
}
|