Skip to content

Commit b7e6d3d

Browse files
committed
feat: add Redis cluster support and enhance Redis service functionality
1 parent 7400bdc commit b7e6d3d

File tree

7 files changed

+100
-26
lines changed

7 files changed

+100
-26
lines changed

eslint.config.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ export default tseslint.config(
2727
globals: globals.jest
2828
}
2929
},
30-
eslintPluginPrettierRecommended,
30+
{
31+
...eslintPluginPrettierRecommended,
32+
rules: {
33+
'prettier/prettier': 'off',
34+
'arrow-parens': ['error', 'always'],
35+
'@typescript-eslint/no-base-to-string': 'off'
36+
}
37+
},
3138
{
3239
linterOptions: {
3340
reportUnusedDisableDirectives: 'error'

examples/nest-redis-example/src/app.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class AppService {
1616
const info = await redis.info();
1717
return {
1818
message: 'Redis connection successful',
19-
serverInfo: info.split('\r\n').slice(0, 10), // 只返回前10行信息
19+
serverInfo: info,
2020
};
2121
}
2222

packages/node-redis/lib/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ export { RedisModule } from './redis.module';
55
export { RedisService } from './redis.service';
66

77
// Types and Interfaces
8-
export type { RedisClientType } from 'redis';
9-
export type { RedisModuleOptions, RedisModuleAsyncOptions, RedisOptionsFactory, RedisOptions } from './interfaces';
8+
export type { RedisClientType, RedisClusterType, RedisClusterOptions } from 'redis';
9+
export type { RedisOptions } from './interfaces/redis-options.interface';
10+
export type { RedisModuleOptions, RedisModuleAsyncOptions } from './interfaces/redis-module-options.interface';
11+
export type { RedisOptionsFactory } from './interfaces/redis-factory.interface';
1012

1113
// Constants
1214
export { REDIS_CLIENT } from './redis.constants';

packages/node-redis/lib/interfaces/redis-options.interface.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RedisClientOptions, RedisModules, RedisFunctions, RedisScripts } from 'redis';
1+
import { RedisClientOptions, RedisModules, RedisFunctions, RedisScripts, RedisClusterOptions } from 'redis';
22

33
/**
44
* Interface defining Redis options.
@@ -15,4 +15,9 @@ export interface RedisOptions<
1515
* See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details
1616
*/
1717
url?: string;
18+
19+
/**
20+
* Redis cluster configuration
21+
*/
22+
cluster?: RedisClusterOptions;
1823
}

packages/node-redis/lib/redis.providers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Provider, Type } from '@nestjs/common';
2-
import { createClient, RedisClientType } from 'redis';
2+
import { createClient, createCluster, RedisClientType, RedisClusterType } from 'redis';
33
import { REDIS_CLIENT } from './redis.constants';
44
import { RedisOptions } from './interfaces';
55
import { MODULE_OPTIONS_TOKEN } from './redis.module-definition';
@@ -8,7 +8,13 @@ import { RedisOptionsFactory } from './interfaces/redis-factory.interface';
88

99
export const createRedisClient = (): Provider => ({
1010
provide: REDIS_CLIENT,
11-
useFactory: async (options: RedisOptions): Promise<RedisClientType> => {
11+
useFactory: async (options: RedisOptions): Promise<RedisClientType | RedisClusterType> => {
12+
if (options.cluster) {
13+
const cluster = createCluster(options.cluster);
14+
await cluster.connect();
15+
return cluster as unknown as RedisClusterType;
16+
}
17+
1218
const client = createClient(options) as RedisClientType;
1319
await client.connect();
1420
return client;

packages/node-redis/lib/redis.service.spec.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ describe('RedisService', () => {
1010
module = await Test.createTestingModule({
1111
imports: [
1212
RedisModule.forRoot({
13-
url: 'redis://localhost:6379'
13+
cluster: {
14+
rootNodes: [
15+
{
16+
url: 'redis://127.0.0.1:7380',
17+
password: 'mycluster'
18+
}
19+
]
20+
}
1421
})
1522
]
1623
}).compile();
@@ -36,9 +43,10 @@ describe('RedisService', () => {
3643
try {
3744
const result = await client.ping();
3845
expect(result).toBe('PONG');
39-
} catch (error: unknown) {
40-
// If Redis is not available, skip the test
41-
console.log('Redis not available, skipping connection test');
46+
} catch (error) {
47+
console.log(`Redis not available, skipping connection test: ${String(error)}`);
48+
// Skip test if Redis is not available
49+
return;
4250
}
4351
});
4452

@@ -62,8 +70,8 @@ describe('RedisService', () => {
6270
// Verify deletion
6371
const afterDelete = await client.get(testKey);
6472
expect(afterDelete).toBeNull();
65-
} catch (error: unknown) {
66-
console.log('Redis operations failed, skipping test:', (error as Error).message);
73+
} catch (error) {
74+
console.log(`Redis operations failed, skipping test: ${String(error)}`);
6775
}
6876
});
6977

@@ -86,7 +94,7 @@ describe('RedisService', () => {
8694
// Cleanup
8795
await client.del(counterKey);
8896
} catch (error: unknown) {
89-
console.log('Redis counter operations failed, skipping test:', (error as Error).message);
97+
console.log(`Redis counter operations failed, skipping test: ${String(error)}`);
9098
}
9199
});
92100

@@ -103,13 +111,13 @@ describe('RedisService', () => {
103111
expect(immediate).toBe('expiry-test');
104112

105113
// Wait for expiry
106-
await new Promise(resolve => setTimeout(resolve, 1100));
114+
await new Promise((resolve) => setTimeout(resolve, 1100));
107115

108116
// Should not exist after expiry
109117
const afterExpiry = await client.get(expiryKey);
110118
expect(afterExpiry).toBeNull();
111119
} catch (error: unknown) {
112-
console.log('Redis expiry operations failed, skipping test:', (error as Error).message);
120+
console.log(`Redis expiry operations failed, skipping test: ${String(error)}`);
113121
}
114122
});
115123

@@ -134,7 +142,7 @@ describe('RedisService', () => {
134142
// Cleanup
135143
await client.del(listKey);
136144
} catch (error: unknown) {
137-
console.log('Redis list operations failed, skipping test:', (error as Error).message);
145+
console.log(`Redis list operations failed, skipping test: ${String(error)}`);
138146
}
139147
});
140148

@@ -161,7 +169,7 @@ describe('RedisService', () => {
161169
// Cleanup
162170
await client.del(hashKey);
163171
} catch (error: unknown) {
164-
console.log('Redis hash operations failed, skipping test:', (error as Error).message);
172+
console.log(`Redis hash operations failed, skipping test: ${String(error)}`);
165173
}
166174
});
167175

@@ -176,9 +184,10 @@ describe('RedisService', () => {
176184

177185
// Cleanup
178186
await client.del('test:error:key');
179-
} catch (error: unknown) {
180-
// If Redis is not available, test should not fail
181-
console.log('Redis connection test failed, but this is expected if Redis is not running');
187+
} catch (error) {
188+
console.log(`Redis connection test failed, but this is expected if Redis is not running: ${String(error)}`);
189+
// Skip test if Redis is not available
190+
return;
182191
}
183192
});
184193

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,74 @@
11
import { Injectable, Inject } from '@nestjs/common';
2-
import type { RedisModules, RedisFunctions, RedisScripts, RedisClientType } from 'redis';
2+
import type { RedisModules, RedisFunctions, RedisScripts, RedisClientType, RedisClusterType } from 'redis';
33
import { REDIS_CLIENT } from './redis.constants';
44

55
/**
6-
* The redis connection manager (single instance).
6+
* The redis connection manager (single instance or cluster).
77
*/
88
@Injectable()
99
export class RedisService<
1010
M extends RedisModules = RedisModules,
1111
F extends RedisFunctions = RedisFunctions,
1212
S extends RedisScripts = RedisScripts
1313
> {
14-
constructor(@Inject(REDIS_CLIENT) private readonly client: RedisClientType<M, F, S>) {}
14+
constructor(@Inject(REDIS_CLIENT) private readonly client: RedisClientType<M, F, S> | RedisClusterType<M, F, S>) {}
1515

1616
/**
1717
* Get the redis client instance.
1818
*/
19-
getClient(): RedisClientType<M, F, S> {
19+
getClient(): RedisClientType<M, F, S> | RedisClusterType<M, F, S> {
2020
return this.client;
2121
}
2222

23+
/**
24+
* Get the redis cluster client instance.
25+
*/
26+
getCluster(): RedisClusterType<M, F, S> {
27+
return this.client as unknown as RedisClusterType<M, F, S>;
28+
}
29+
30+
/**
31+
* Check if the client is in cluster mode.
32+
*/
33+
isClusterMode(): boolean {
34+
// check if the client is a cluster client
35+
if ('masters' in this.client) {
36+
return true;
37+
}
38+
return false;
39+
}
40+
41+
/**
42+
* Get client type information.
43+
*/
44+
getClientType(): 'single' | 'cluster' {
45+
return this.isClusterMode() ? 'cluster' : 'single';
46+
}
47+
48+
/**
49+
* Get cluster information if in cluster mode.
50+
*/
51+
getClusterInfo() {
52+
if (!this.isClusterMode()) {
53+
return 'single';
54+
}
55+
const cluster = this.getCluster();
56+
try {
57+
const masters = cluster.masters;
58+
return {
59+
type: 'cluster',
60+
masters: masters.length,
61+
nodes: masters.map((node) => node.toString())
62+
};
63+
} catch (error) {
64+
throw new Error(`Failed to get cluster info: ${String(error)}`);
65+
}
66+
}
67+
2368
/**
2469
* Quit the redis client instance.
2570
*/
2671
async onApplicationShutdown(): Promise<void> {
27-
await this.client.quit();
72+
await this.client.close();
2873
}
2974
}

0 commit comments

Comments
 (0)