@falkordb/mcpserver 1.0.1 → 1.1.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 CHANGED
@@ -17,10 +17,5 @@ FALKORDB_PASSWORD=
17
17
  # Set to 'true' to use read-only queries by default (useful for replica instances)
18
18
  FALKORDB_DEFAULT_READONLY=false
19
19
 
20
- # Redis Configuration (for key-value operations)
21
- REDIS_URL=redis://localhost:6379
22
- REDIS_USERNAME=
23
- REDIS_PASSWORD=
24
-
25
20
  # Logging Configuration (optional)
26
21
  ENABLE_FILE_LOGGING=false
package/README.md CHANGED
@@ -17,7 +17,6 @@ FalkorDB MCP Server enables AI assistants like Claude to interact with FalkorDB
17
17
  This server implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), allowing AI models to:
18
18
  - **Query graph databases** using OpenCypher (with read-only mode support)
19
19
  - **Create and manage** nodes and relationships
20
- - **Store and retrieve** key-value data
21
20
  - **List and explore** multiple graphs
22
21
  - **Delete graphs** when needed
23
22
  - **Read-only queries** for replica instances or to prevent accidental writes
@@ -54,6 +53,32 @@ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_
54
53
  }
55
54
  ```
56
55
 
56
+ ### Running with npx
57
+
58
+ You can run the server directly from the command line using npx:
59
+
60
+ **Using inline environment variables:**
61
+
62
+ ```bash
63
+ # Run with stdio transport (default)
64
+ FALKORDB_HOST=localhost FALKORDB_PORT=6379 npx -y @falkordb/mcpserver
65
+
66
+ # Run with HTTP transport
67
+ MCP_TRANSPORT=http MCP_PORT=3005 FALKORDB_HOST=localhost FALKORDB_PORT=6379 npx -y @falkordb/mcpserver
68
+ ```
69
+
70
+ **Using a .env file:**
71
+
72
+ ```bash
73
+ # Using dotenv-cli to load environment variables from .env
74
+ npx dotenv-cli -e .env -- npx @falkordb/mcpserver
75
+ ```
76
+
77
+ This is useful for:
78
+ - Quick testing and development
79
+ - Running the server standalone without Claude Desktop
80
+ - Custom integrations and scripting
81
+
57
82
  ### Installation
58
83
 
59
84
  1. **Clone and install:**
@@ -72,19 +97,14 @@ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_
72
97
  ```env
73
98
  # Environment Configuration
74
99
  NODE_ENV=development
75
-
100
+
76
101
  # FalkorDB Configuration
77
102
  FALKORDB_HOST=localhost
78
103
  FALKORDB_PORT=6379
79
104
  FALKORDB_USERNAME= # Optional
80
105
  FALKORDB_PASSWORD= # Optional
81
106
  FALKORDB_DEFAULT_READONLY=false # Set to 'true' for read-only mode (useful for replicas)
82
-
83
- # Redis Configuration (for key-value operations)
84
- REDIS_URL=redis://localhost:6379
85
- REDIS_USERNAME= # Optional
86
- REDIS_PASSWORD= # Optional
87
-
107
+
88
108
  # Logging Configuration (optional)
89
109
  ENABLE_FILE_LOGGING=false
90
110
  ```
@@ -136,7 +156,6 @@ There's also a dedicated `query_graph_readonly` tool that always executes querie
136
156
  ```text
137
157
  "Create a new person named Alice who knows Bob"
138
158
  "Add a 'WORKS_AT' relationship between Alice and TechCorp"
139
- "Store my API key in the database"
140
159
  ```
141
160
 
142
161
  ### 📊 Explore Structure
@@ -195,7 +214,6 @@ src/
195
214
  ├── index.ts # MCP server entry point
196
215
  ├── services/ # Core business logic
197
216
  │ ├── falkordb.service.ts # FalkorDB operations
198
- │ ├── redis.service.ts # Key-value operations
199
217
  │ └── logger.service.ts # Logging and MCP notifications
200
218
  ├── mcp/ # MCP protocol implementations
201
219
  │ ├── tools.ts # MCP tool definitions
@@ -15,11 +15,6 @@ export const config = {
15
15
  password: process.env.FALKORDB_PASSWORD || '',
16
16
  defaultReadOnly: process.env.FALKORDB_DEFAULT_READONLY === 'true',
17
17
  },
18
- redis: {
19
- url: process.env.REDIS_URL || 'redis://localhost:6379',
20
- username: process.env.REDIS_USERNAME || '',
21
- password: process.env.REDIS_PASSWORD || '',
22
- },
23
18
  mcp: {
24
19
  transport: (process.env.MCP_TRANSPORT || 'stdio'),
25
20
  apiKey: process.env.MCP_API_KEY || '',
package/dist/index.js CHANGED
@@ -6,7 +6,6 @@ import { falkorDBService } from './services/falkordb.service.js';
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
- import { redisService } from './services/redis.service.js';
10
9
  import { errorHandler } from './errors/ErrorHandler.js';
11
10
  import { logger } from './services/logger.service.js';
12
11
  import { config } from './config/index.js';
@@ -39,7 +38,6 @@ const gracefulShutdown = async (signal) => {
39
38
  });
40
39
  }
41
40
  await falkorDBService.close();
42
- await redisService.close();
43
41
  await logger.info('All services closed successfully');
44
42
  process.exit(0);
45
43
  }
@@ -81,7 +79,6 @@ async function initializeServices() {
81
79
  await logger.info('Initializing FalkorDB MCP server...');
82
80
  try {
83
81
  await falkorDBService.initialize();
84
- await redisService.initialize();
85
82
  await logger.info('All services initialized successfully');
86
83
  }
87
84
  catch (error) {
package/dist/mcp/tools.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  import { falkorDBService } from '../services/falkordb.service.js';
3
- import { redisService } from '../services/redis.service.js';
4
3
  import { logger } from '../services/logger.service.js';
5
4
  import { AppError, CommonErrors } from '../errors/AppError.js';
6
5
  import { config } from '../config/index.js';
@@ -18,17 +17,6 @@ const deleteGraphSchema = {
18
17
  graphName: z.string().describe("The name of the graph to delete"),
19
18
  confirmDelete: z.literal(true).describe("Must be set to true to confirm deletion. This is a safety measure to prevent accidental data loss."),
20
19
  };
21
- const setKeySchema = {
22
- key: z.string().describe("The key to set"),
23
- value: z.string().describe("The value to set"),
24
- };
25
- const getKeySchema = {
26
- key: z.string().describe("The key to get."),
27
- };
28
- const deleteKeySchema = {
29
- key: z.string().describe("The key to delete"),
30
- confirmDelete: z.literal(true).describe("Must be set to true to confirm deletion. This is a safety measure to prevent accidental data loss."),
31
- };
32
20
  function registerQueryGraphTool(server) {
33
21
  server.registerTool("query_graph", {
34
22
  title: "Query Graph",
@@ -140,119 +128,10 @@ function registerDeleteGraphTool(server) {
140
128
  }
141
129
  });
142
130
  }
143
- function registerListKeysTool(server) {
144
- server.registerTool("list_keys", {
145
- title: "List Keys",
146
- description: "List all keys in Redis",
147
- inputSchema: {},
148
- }, async () => {
149
- try {
150
- const keys = await redisService.listKeys();
151
- await logger.debug('List keys tool executed', { count: keys.length });
152
- return {
153
- content: [{
154
- type: "text",
155
- text: keys.join("\n"),
156
- }]
157
- };
158
- }
159
- catch (error) {
160
- await logger.error('List keys tool execution failed', error instanceof Error ? error : new Error(String(error)));
161
- throw error;
162
- }
163
- });
164
- }
165
- function registerSetKeyTool(server) {
166
- // Register set_key tool
167
- server.registerTool("set_key", {
168
- title: "Set Key",
169
- description: "Set a key in Redis",
170
- inputSchema: setKeySchema,
171
- }, async (args) => {
172
- const { key, value } = z.object(setKeySchema).parse(args);
173
- try {
174
- if (!key?.trim()) {
175
- throw new AppError(CommonErrors.INVALID_INPUT, 'Key is required and cannot be empty', true);
176
- }
177
- if (value === undefined || value === null) {
178
- throw new AppError(CommonErrors.INVALID_INPUT, 'Value is required', true);
179
- }
180
- await redisService.set(key, value);
181
- await logger.debug('Set key tool executed successfully', { key });
182
- return {
183
- content: [{
184
- type: "text",
185
- text: `Key ${key} set successfully`
186
- }]
187
- };
188
- }
189
- catch (error) {
190
- await logger.error('Set key tool execution failed', error instanceof Error ? error : new Error(String(error)), { key });
191
- throw error;
192
- }
193
- });
194
- }
195
- function registerGetKeyTool(server) {
196
- // Register get_key tool
197
- server.registerTool("get_key", {
198
- title: "Get Key",
199
- description: "Get a key from Redis",
200
- inputSchema: getKeySchema,
201
- }, async (args) => {
202
- const { key } = z.object(getKeySchema).parse(args);
203
- try {
204
- if (!key?.trim()) {
205
- throw new AppError(CommonErrors.INVALID_INPUT, 'Key is required and cannot be empty', true);
206
- }
207
- const value = await redisService.get(key);
208
- await logger.debug('Get key tool executed successfully', { key, hasValue: value !== null });
209
- return {
210
- content: [{
211
- type: "text",
212
- text: `Key ${key} is ${value ?? 'null (not found)'}`
213
- }]
214
- };
215
- }
216
- catch (error) {
217
- await logger.error('Get key tool execution failed', error instanceof Error ? error : new Error(String(error)), { key });
218
- throw error;
219
- }
220
- });
221
- }
222
- function registerDeleteKeyTool(server) {
223
- server.registerTool("delete_key", {
224
- title: "Delete Key",
225
- description: "Permanently delete a key from Redis. WARNING: This action is irreversible. You must set confirmDelete to true to proceed.",
226
- inputSchema: deleteKeySchema,
227
- }, async (args) => {
228
- const { key } = z.object(deleteKeySchema).parse(args);
229
- try {
230
- if (!key?.trim()) {
231
- throw new AppError(CommonErrors.INVALID_INPUT, 'Key is required and cannot be empty', true);
232
- }
233
- await redisService.delete(key);
234
- await logger.debug('Delete key tool executed successfully', { key });
235
- return {
236
- content: [{
237
- type: "text",
238
- text: `Key ${key} deleted`
239
- }]
240
- };
241
- }
242
- catch (error) {
243
- await logger.error('Delete key tool execution failed', error instanceof Error ? error : new Error(String(error)), { key });
244
- throw error;
245
- }
246
- });
247
- }
248
131
  export default function registerAllTools(server) {
249
132
  // Register query_graph tools
250
133
  registerQueryGraphTool(server);
251
134
  registerQueryGraphReadOnlyTool(server);
252
135
  registerListGraphsTool(server);
253
136
  registerDeleteGraphTool(server);
254
- registerSetKeyTool(server);
255
- registerGetKeyTool(server);
256
- registerDeleteKeyTool(server);
257
- registerListKeysTool(server);
258
137
  }
@@ -41,8 +41,8 @@ class FalkorDBService {
41
41
  host: config.falkorDB.host,
42
42
  port: config.falkorDB.port,
43
43
  },
44
- password: config.falkorDB.password,
45
- username: config.falkorDB.username,
44
+ ...(config.falkorDB.username && { username: config.falkorDB.username }),
45
+ ...(config.falkorDB.password && { password: config.falkorDB.password }),
46
46
  });
47
47
  // Test connection
48
48
  const connection = await this.client.connection;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkordb/mcpserver",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Model Context Protocol server for FalkorDB graph databases - enables AI assistants to query and manage graph data using natural language",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -32,7 +32,6 @@
32
32
  "claude",
33
33
  "opencypher",
34
34
  "cypher",
35
- "redis",
36
35
  "graph",
37
36
  "database",
38
37
  "typescript",
@@ -85,7 +84,6 @@
85
84
  "dotenv": "^17.2.1",
86
85
  "falkordb": "^6.3.0",
87
86
  "platformdirs": "^4.3.8-rc3",
88
- "redis": "^4.0.0",
89
87
  "zod": "^3.23.0"
90
88
  },
91
89
  "publishConfig": {
@@ -1,179 +0,0 @@
1
- import { createClient } from 'redis';
2
- import { config } from '../config/index.js';
3
- import { AppError, CommonErrors } from '../errors/AppError.js';
4
- import { logger } from './logger.service.js';
5
- class RedisService {
6
- client = null;
7
- maxRetries = 5;
8
- retryCount = 0;
9
- initializingPromise = null;
10
- constructor() {
11
- // Don't initialize in constructor - use explicit initialization
12
- }
13
- sanitizeUrl(url) {
14
- try {
15
- const parsed = new URL(url);
16
- if (parsed.username || parsed.password) {
17
- parsed.username = parsed.username ? '***' : '';
18
- parsed.password = parsed.password ? '***' : '';
19
- }
20
- return parsed.toString();
21
- }
22
- catch {
23
- return '<invalid-url>';
24
- }
25
- }
26
- async initialize() {
27
- // Idempotency guard: don't overwrite an already-connected client
28
- if (this.client) {
29
- return;
30
- }
31
- if (this.initializingPromise) {
32
- return this.initializingPromise;
33
- }
34
- this.retryCount = 0;
35
- this.initializingPromise = this._initialize();
36
- try {
37
- await this.initializingPromise;
38
- }
39
- finally {
40
- this.initializingPromise = null;
41
- }
42
- }
43
- async _initialize() {
44
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
45
- try {
46
- // Fire-and-forget: non-critical connection attempt log
47
- logger.info('Attempting to connect to Redis', {
48
- url: this.sanitizeUrl(config.redis.url),
49
- attempt: attempt + 1
50
- });
51
- this.client = createClient({
52
- url: config.redis.url,
53
- username: config.redis.username,
54
- password: config.redis.password,
55
- });
56
- await this.client.connect();
57
- await this.client.ping();
58
- // Fire-and-forget: non-critical success log
59
- logger.info('Successfully connected to Redis');
60
- this.retryCount = 0;
61
- return;
62
- }
63
- catch (error) {
64
- // Clean up failed client before retrying or throwing
65
- if (this.client) {
66
- try {
67
- await this.client.disconnect();
68
- }
69
- catch {
70
- // Ignore disconnect errors
71
- }
72
- this.client = null;
73
- }
74
- if (attempt < this.maxRetries) {
75
- this.retryCount = attempt + 1;
76
- const delay = Math.min(5000 * 2 ** attempt, 30000) + Math.random() * 1000;
77
- // Fire-and-forget: non-critical retry log
78
- logger.warn('Failed to connect to Redis, retrying...', {
79
- attempt: this.retryCount,
80
- maxRetries: this.maxRetries,
81
- nextRetryMs: Math.round(delay),
82
- error: error instanceof Error ? error.message : String(error)
83
- });
84
- await new Promise(resolve => setTimeout(resolve, delay));
85
- }
86
- else {
87
- const appError = new AppError(CommonErrors.CONNECTION_FAILED, `Failed to connect to Redis after ${this.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`, true);
88
- await logger.error('Redis connection failed permanently', appError);
89
- throw appError;
90
- }
91
- }
92
- }
93
- }
94
- async get(key) {
95
- if (!this.client) {
96
- throw new AppError(CommonErrors.CONNECTION_FAILED, 'Redis client not initialized. Call initialize() first.', true);
97
- }
98
- try {
99
- const value = await this.client.get(key);
100
- logger.debug('Redis GET operation completed', { key, hasValue: value !== null });
101
- return value;
102
- }
103
- catch (error) {
104
- const appError = new AppError(CommonErrors.OPERATION_FAILED, `Failed to get key '${key}' from Redis: ${error instanceof Error ? error.message : String(error)}`, true);
105
- await logger.error('Redis GET operation failed', appError, { key });
106
- throw appError;
107
- }
108
- }
109
- async set(key, value) {
110
- if (!this.client) {
111
- throw new AppError(CommonErrors.CONNECTION_FAILED, 'Redis client not initialized. Call initialize() first.', true);
112
- }
113
- try {
114
- await this.client.set(key, value);
115
- logger.debug('Redis SET operation completed', { key });
116
- }
117
- catch (error) {
118
- const appError = new AppError(CommonErrors.OPERATION_FAILED, `Failed to set key '${key}' in Redis: ${error instanceof Error ? error.message : String(error)}`, true);
119
- await logger.error('Redis SET operation failed', appError, { key });
120
- throw appError;
121
- }
122
- }
123
- async delete(key) {
124
- if (!this.client) {
125
- throw new AppError(CommonErrors.CONNECTION_FAILED, 'Redis client not initialized. Call initialize() first.', true);
126
- }
127
- try {
128
- await this.client.del(key);
129
- logger.debug('Redis DEL operation completed', { key });
130
- }
131
- catch (error) {
132
- const appError = new AppError(CommonErrors.OPERATION_FAILED, `Failed to delete key '${key}' from Redis: ${error instanceof Error ? error.message : String(error)}`, true);
133
- await logger.error('Redis DEL operation failed', appError, { key });
134
- throw appError;
135
- }
136
- }
137
- async listKeys() {
138
- if (!this.client) {
139
- throw new AppError(CommonErrors.CONNECTION_FAILED, 'Redis client not initialized. Call initialize() first.', true);
140
- }
141
- try {
142
- let cursor = 0;
143
- const allKeys = [];
144
- do {
145
- const result = await this.client.scan(cursor, {
146
- MATCH: '*',
147
- COUNT: 1000
148
- });
149
- allKeys.push(...result.keys);
150
- // Depending on the redis client, cursor may be a string; normalize to number
151
- cursor = typeof result.cursor === 'string' ? Number(result.cursor) : result.cursor;
152
- } while (cursor !== 0);
153
- logger.debug('Redis KEYS operation completed', { count: allKeys.length });
154
- return allKeys;
155
- }
156
- catch (error) {
157
- const appError = new AppError(CommonErrors.OPERATION_FAILED, `Failed to list keys in Redis: ${error instanceof Error ? error.message : String(error)}`, true);
158
- await logger.error('Redis KEYS operation failed', appError);
159
- throw appError;
160
- }
161
- }
162
- async close() {
163
- if (this.client) {
164
- try {
165
- await this.client.quit();
166
- logger.info('Redis connection closed successfully');
167
- }
168
- catch (error) {
169
- // Fire-and-forget: best-effort log during cleanup
170
- logger.error('Error closing Redis connection', error instanceof Error ? error : new Error(String(error)));
171
- }
172
- finally {
173
- this.client = null;
174
- this.retryCount = 0;
175
- }
176
- }
177
- }
178
- }
179
- export const redisService = new RedisService();
@@ -1,399 +0,0 @@
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
- });