@falkordb/mcpserver 1.1.0 → 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 +0 -5
- package/README.md +28 -10
- package/dist/config/index.js +0 -5
- package/dist/index.js +0 -3
- package/dist/mcp/tools.js +0 -121
- package/dist/services/falkordb.service.js +2 -2
- package/package.json +1 -3
- package/dist/services/redis.service.js +0 -179
- package/dist/services/redis.service.test.js +0 -399
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
|
package/dist/config/index.js
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
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.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
|
-
});
|