• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

typeorm / typeorm / 16679002736

01 Aug 2025 03:30PM UTC coverage: 76.353% (-0.09%) from 76.44%
16679002736

push

github

web-flow
feat(11528): add Redis 5.x support with backward compatibility wite peer dependency to allow (#11585)

* feat: add Redis 5 support to cache implementation

- Add version detection for Redis client to handle API differences
- Support Redis 5 Promise-based API while maintaining backward compatibility
- Update methods to use appropriate API based on Redis version
- Add tests for Redis 5 compatibility

Redis 5 introduced Promise-based API as default, replacing the callback-based
API. This change detects the Redis version and uses the appropriate API calls
to ensure compatibility with Redis 3, 4, and 5.

Closes #11528

* feat: add Redis 5 support to cache implementation

Implement automatic version detection for Redis client libraries to support
Redis 3, 4, and 5 seamlessly. The implementation uses runtime API testing
to determine the appropriate Redis client behavior without breaking existing
functionality.

Changes include:
  - Dynamic Redis version detection based on client API characteristics
  - Promise-based API support for Redis 5.x
  - Backward compatibility with Redis 3.x and 4.x callback-based APIs
  - Safe fallback mechanism defaulting to Redis 3 behavior
  - Updated peer dependency to include Redis 5.x versions

The cache implementation now automatically adapts to the installed Redis version, ensuring optimal performance and compatibility across all supported Redis client versions while maintaining full backward compatibility.

* fix: delete wrong migration guide

* feat: add package-lock.json

* refactor: optimize Redis client creation to reduce memory usage
Eliminate unnecessary Redis client recreation by using explicit tempClient
variable management, reducing potential client instances while maintaining
full Redis 3/4/5 compatibility and accurate version detection.

* refactor: improve Redis version detection to avoid cache pollution
Replace test key creation method with client method signature analysis
to prevent potential cache pollution a... (continued)

9322 of 12898 branches covered (72.27%)

Branch coverage included in aggregate %.

0 of 46 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

19015 of 24215 relevant lines covered (78.53%)

119338.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.94
/src/cache/RedisQueryResultCache.ts
1
import { QueryResultCache } from "./QueryResultCache"
2
import { QueryResultCacheOptions } from "./QueryResultCacheOptions"
3
import { PlatformTools } from "../platform/PlatformTools"
24✔
4
import { DataSource } from "../data-source/DataSource"
5
import { QueryRunner } from "../query-runner/QueryRunner"
6
import { TypeORMError } from "../error/TypeORMError"
24✔
7

8
/**
9
 * Caches query result into Redis database.
10
 */
11
export class RedisQueryResultCache implements QueryResultCache {
24✔
12
    // -------------------------------------------------------------------------
13
    // Protected Properties
14
    // -------------------------------------------------------------------------
15

16
    /**
17
     * Redis module instance loaded dynamically.
18
     */
19
    protected redis: any
20

21
    /**
22
     * Connected redis client.
23
     */
24
    protected client: any
25

26
    /**
27
     * Type of the Redis Client (redis or ioredis).
28
     */
29
    protected clientType: "redis" | "ioredis" | "ioredis/cluster"
30

31
    /**
32
     * Redis major version number
33
     */
34
    protected redisMajorVersion: number | undefined
35

36
    // -------------------------------------------------------------------------
37
    // Constructor
38
    // -------------------------------------------------------------------------
39

40
    constructor(
41
        protected connection: DataSource,
×
42
        clientType: "redis" | "ioredis" | "ioredis/cluster",
43
    ) {
44
        this.clientType = clientType
×
45
        this.redis = this.loadRedis()
×
46
    }
47

48
    // -------------------------------------------------------------------------
49
    // Public Methods
50
    // -------------------------------------------------------------------------
51

52
    /**
53
     * Creates a connection with given cache provider.
54
     */
55
    async connect(): Promise<void> {
56
        const cacheOptions: any = this.connection.options.cache
×
57
        if (this.clientType === "redis") {
×
NEW
58
            const clientOptions = {
×
59
                ...cacheOptions?.options,
60
            }
61

62
            // Create initial client to test Redis version
NEW
63
            let tempClient = this.redis.createClient(clientOptions)
×
NEW
64
            const isRedis4Plus = typeof tempClient.connect === "function"
×
65

NEW
66
            if (isRedis4Plus) {
×
67
                // Redis 4+ detected, recreate with legacyMode for Redis 4.x
68
                // (Redis 5 will ignore legacyMode if not needed)
NEW
69
                clientOptions.legacyMode = true
×
NEW
70
                tempClient = this.redis.createClient(clientOptions)
×
71
            }
72

73
            // Set as the main client
NEW
74
            this.client = tempClient
×
75

UNCOV
76
            if (
×
77
                typeof this.connection.options.cache === "object" &&
×
78
                this.connection.options.cache.ignoreErrors
79
            ) {
80
                this.client.on("error", (err: any) => {
×
81
                    this.connection.logger.log("warn", err)
×
82
                })
83
            }
84

85
            // Connect if Redis 4+
NEW
86
            if (typeof this.client.connect === "function") {
×
UNCOV
87
                await this.client.connect()
×
88
            }
89

90
            // Detect precise version after connection is established
NEW
91
            this.detectRedisVersion()
×
92
        } else if (this.clientType === "ioredis") {
×
93
            if (cacheOptions && cacheOptions.port) {
×
94
                if (cacheOptions.options) {
×
95
                    this.client = new this.redis(
×
96
                        cacheOptions.port,
97
                        cacheOptions.options,
98
                    )
99
                } else {
100
                    this.client = new this.redis(cacheOptions.port)
×
101
                }
102
            } else if (cacheOptions && cacheOptions.options) {
×
103
                this.client = new this.redis(cacheOptions.options)
×
104
            } else {
105
                this.client = new this.redis()
×
106
            }
107
        } else if (this.clientType === "ioredis/cluster") {
×
108
            if (
×
109
                cacheOptions &&
×
110
                cacheOptions.options &&
111
                Array.isArray(cacheOptions.options)
112
            ) {
113
                this.client = new this.redis.Cluster(cacheOptions.options)
×
114
            } else if (
×
115
                cacheOptions &&
×
116
                cacheOptions.options &&
117
                cacheOptions.options.startupNodes
118
            ) {
119
                this.client = new this.redis.Cluster(
×
120
                    cacheOptions.options.startupNodes,
121
                    cacheOptions.options.options,
122
                )
123
            } else {
124
                throw new TypeORMError(
×
125
                    `options.startupNodes required for ${this.clientType}.`,
126
                )
127
            }
128
        }
129
    }
130

131
    /**
132
     * Disconnects the connection
133
     */
134
    async disconnect(): Promise<void> {
NEW
135
        if (this.isRedis5OrHigher()) {
×
136
            // Redis 5+ uses quit() that returns a Promise
NEW
137
            await this.client.quit()
×
NEW
138
            this.client = undefined
×
NEW
139
            return
×
140
        }
141

142
        // Redis 3/4 callback style
143
        return new Promise<void>((ok, fail) => {
×
144
            this.client.quit((err: any, result: any) => {
×
145
                if (err) return fail(err)
×
146
                ok()
×
147
                this.client = undefined
×
148
            })
149
        })
150
    }
151

152
    /**
153
     * Creates table for storing cache if it does not exist yet.
154
     */
155
    async synchronize(queryRunner: QueryRunner): Promise<void> {}
156

157
    /**
158
     * Get data from cache.
159
     * Returns cache result if found.
160
     * Returns undefined if result is not cached.
161
     */
162
    getFromCache(
163
        options: QueryResultCacheOptions,
164
        queryRunner?: QueryRunner,
165
    ): Promise<QueryResultCacheOptions | undefined> {
NEW
166
        const key = options.identifier || options.query
×
NEW
167
        if (!key) return Promise.resolve(undefined)
×
168

NEW
169
        if (this.isRedis5OrHigher()) {
×
170
            // Redis 5+ Promise-based API
NEW
171
            return this.client.get(key).then((result: any) => {
×
NEW
172
                return result ? JSON.parse(result) : undefined
×
173
            })
174
        }
175

176
        // Redis 3/4 callback-based API
177
        return new Promise<QueryResultCacheOptions | undefined>((ok, fail) => {
×
NEW
178
            this.client.get(key, (err: any, result: any) => {
×
NEW
179
                if (err) return fail(err)
×
NEW
180
                ok(result ? JSON.parse(result) : undefined)
×
181
            })
182
        })
183
    }
184

185
    /**
186
     * Checks if cache is expired or not.
187
     */
188
    isExpired(savedCache: QueryResultCacheOptions): boolean {
189
        return savedCache.time! + savedCache.duration < Date.now()
×
190
    }
191

192
    /**
193
     * Stores given query result in the cache.
194
     */
195
    async storeInCache(
196
        options: QueryResultCacheOptions,
197
        savedCache: QueryResultCacheOptions,
198
        queryRunner?: QueryRunner,
199
    ): Promise<void> {
NEW
200
        const key = options.identifier || options.query
×
NEW
201
        if (!key) return
×
202

NEW
203
        const value = JSON.stringify(options)
×
NEW
204
        const duration = options.duration
×
205

NEW
206
        if (this.isRedis5OrHigher()) {
×
207
            // Redis 5+ Promise-based API with PX option
NEW
208
            await this.client.set(key, value, {
×
209
                PX: duration,
210
            })
NEW
211
            return
×
212
        }
213

214
        // Redis 3/4 callback-based API
215
        return new Promise<void>((ok, fail) => {
×
NEW
216
            this.client.set(
×
217
                key,
218
                value,
219
                "PX",
220
                duration,
221
                (err: any, result: any) => {
NEW
222
                    if (err) return fail(err)
×
NEW
223
                    ok()
×
224
                },
225
            )
226
        })
227
    }
228

229
    /**
230
     * Clears everything stored in the cache.
231
     */
232
    async clear(queryRunner?: QueryRunner): Promise<void> {
NEW
233
        if (this.isRedis5OrHigher()) {
×
234
            // Redis 5+ Promise-based API
NEW
235
            await this.client.flushDb()
×
NEW
236
            return
×
237
        }
238

239
        // Redis 3/4 callback-based API
240
        return new Promise<void>((ok, fail) => {
×
241
            this.client.flushdb((err: any, result: any) => {
×
242
                if (err) return fail(err)
×
243
                ok()
×
244
            })
245
        })
246
    }
247

248
    /**
249
     * Removes all cached results by given identifiers from cache.
250
     */
251
    async remove(
252
        identifiers: string[],
253
        queryRunner?: QueryRunner,
254
    ): Promise<void> {
255
        await Promise.all(
×
256
            identifiers.map((identifier) => {
257
                return this.deleteKey(identifier)
×
258
            }),
259
        )
260
    }
261

262
    // -------------------------------------------------------------------------
263
    // Protected Methods
264
    // -------------------------------------------------------------------------
265

266
    /**
267
     * Removes a single key from redis database.
268
     */
269
    protected async deleteKey(key: string): Promise<void> {
NEW
270
        if (this.isRedis5OrHigher()) {
×
271
            // Redis 5+ Promise-based API
NEW
272
            await this.client.del(key)
×
NEW
273
            return
×
274
        }
275

276
        // Redis 3/4 callback-based API
277
        return new Promise<void>((ok, fail) => {
×
278
            this.client.del(key, (err: any, result: any) => {
×
279
                if (err) return fail(err)
×
280
                ok()
×
281
            })
282
        })
283
    }
284

285
    /**
286
     * Loads redis dependency.
287
     */
288
    protected loadRedis(): any {
289
        try {
×
290
            if (this.clientType === "ioredis/cluster") {
×
291
                return PlatformTools.load("ioredis")
×
292
            } else {
293
                return PlatformTools.load(this.clientType)
×
294
            }
295
        } catch {
296
            throw new TypeORMError(
×
297
                `Cannot use cache because ${this.clientType} is not installed. Please run "npm i ${this.clientType}".`,
298
            )
299
        }
300
    }
301

302
    /**
303
     * Detects the Redis version based on the connected client's API characteristics
304
     * without creating test keys in the database
305
     */
306
    private detectRedisVersion(): void {
NEW
307
        if (this.clientType !== "redis") return
×
308

NEW
309
        try {
×
310
            // Detect version by examining the client's method signatures
311
            // This avoids creating test keys in the database
NEW
312
            const setMethod = this.client.set
×
NEW
313
            if (setMethod && setMethod.length <= 3) {
×
314
                // Redis 5+ set method accepts fewer parameters (key, value, options)
NEW
315
                this.redisMajorVersion = 5
×
316
            } else {
317
                // Redis 3/4 set method requires more parameters (key, value, flag, duration, callback)
NEW
318
                this.redisMajorVersion = 3
×
319
            }
320
        } catch {
321
            // Default to Redis 3/4 for maximum compatibility
NEW
322
            this.redisMajorVersion = 3
×
323
        }
324
    }
325

326
    /**
327
     * Checks if Redis version is 5.x or higher
328
     */
329
    private isRedis5OrHigher(): boolean {
NEW
330
        if (this.clientType !== "redis") return false
×
NEW
331
        return (
×
332
            this.redisMajorVersion !== undefined && this.redisMajorVersion >= 5
×
333
        )
334
    }
335
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc