-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New Feature: Added Rate Limit Management Support in Redis (#18)
* install prettier & settings * Add support cache redis - New fields have been added to the ISettings interface (strategyCache, redis). - A strategy to support storage in Redis has been implemented. Deletion is done automatically.
- Loading branch information
1 parent
1dd7d07
commit 22582f0
Showing
13 changed files
with
416 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"semi": true, | ||
"trailingComma": "es5", | ||
"singleQuote": false, | ||
"printWidth": 120, | ||
"tabWidth": 2, | ||
"useTabs": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,15 @@ | ||
{ | ||
"editor.formatOnSave": true, | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll": true | ||
}, | ||
"editor.defaultFormatter": "esbenp.prettier-vscode", | ||
"eslint.enable": true, | ||
"eslint.validate": ["javascript", "typescript", "json"], | ||
"editor.tabSize": 2 | ||
"editor.formatOnSave": true, | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll": "explicit" | ||
}, | ||
"eslint.enable": true, | ||
"eslint.validate": ["javascript", "typescript", "json"], | ||
"editor.tabSize": 2, | ||
"[javascript]": { | ||
"editor.defaultFormatter": "esbenp.prettier-vscode" | ||
}, | ||
"[typescript]": { | ||
"editor.defaultFormatter": "esbenp.prettier-vscode" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { ISettings } from "./interfaces/settings"; | ||
import { MemoryCacheRepository } from "./repositories/memory-cache.repository"; | ||
import { RedisCacheRepository } from "./repositories/redis-cache.repository"; | ||
import { getStrategyCache } from "./get-strategy-cache"; | ||
import { RedisCache } from "./interfaces/cache"; | ||
|
||
jest.mock("./repositories/memory-cache.repository"); | ||
jest.mock("./repositories/redis-cache.repository"); | ||
|
||
const settingsBase: ISettings = { | ||
policy: { | ||
type: "REQUEST_PER_SECONDS", | ||
periodWindow: 10, | ||
maxRequests: 10, | ||
}, | ||
}; | ||
|
||
describe("getStrategyCache unit test", () => { | ||
beforeEach(() => { | ||
(RedisCacheRepository.getInstance as jest.Mock).mockReturnValue( | ||
new RedisCacheRepository({} as unknown as RedisCache) | ||
); | ||
(MemoryCacheRepository.getInstance as jest.Mock).mockReturnValue( | ||
new MemoryCacheRepository() | ||
); | ||
}); | ||
|
||
it("should return an instance of MemoryCacheRepository when no strategy is specified", () => { | ||
const settings = settingsBase; | ||
expect(getStrategyCache(settings)).toBeInstanceOf(MemoryCacheRepository); | ||
}); | ||
|
||
it("should return an instance of MemoryCacheRepository when strategy is IN_MEMORY", () => { | ||
const settings: ISettings = { ...settingsBase, strategyCache: "IN_MEMORY" }; | ||
expect(getStrategyCache(settings)).toBeInstanceOf(MemoryCacheRepository); | ||
}); | ||
|
||
it("should throw an error when strategy is CUSTOM without a cache implementation", () => { | ||
const settings: ISettings = { | ||
...settingsBase, | ||
strategyCache: "CUSTOM", | ||
cache: undefined, | ||
} as unknown as ISettings; | ||
expect(() => getStrategyCache(settings)).toThrow( | ||
"When the property 'strategyCache' is 'CUSTOM', the property 'cache' is required." | ||
); | ||
}); | ||
|
||
it("should return a custom cache implementation when strategy is CUSTOM with a cache", () => { | ||
const customCache = { set: jest.fn(), get: jest.fn() }; | ||
const settings: ISettings = { | ||
...settingsBase, | ||
strategyCache: "CUSTOM", | ||
cache: customCache, | ||
} as unknown as ISettings; | ||
|
||
expect(getStrategyCache(settings)).toEqual(customCache); | ||
}); | ||
|
||
it("should throw an error when strategy is REDIS without Redis configuration", () => { | ||
const settings: ISettings = { | ||
...settingsBase, | ||
strategyCache: "REDIS", | ||
} as unknown as ISettings; | ||
|
||
expect(() => getStrategyCache(settings)).toThrow( | ||
"When the property 'strategyCache' is 'REDIS', the property 'redis' is required." | ||
); | ||
}); | ||
|
||
it("should return an instance of RedisCacheRepository when strategy is REDIS with Redis configuration", () => { | ||
const redisClient = {} as RedisCache; | ||
|
||
const settings: ISettings = { | ||
strategyCache: "REDIS", | ||
redis: redisClient, | ||
policy: { | ||
type: "REQUEST_PER_SECONDS", | ||
periodWindow: 10, | ||
maxRequests: 10, | ||
}, | ||
}; | ||
|
||
expect(getStrategyCache(settings)).toBeInstanceOf(RedisCacheRepository); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { ICache } from "./interfaces/cache"; | ||
import { ISettings } from "./interfaces/settings"; | ||
import { MemoryCacheRepository } from "./repositories/memory-cache.repository"; | ||
import { RedisCacheRepository } from "./repositories/redis-cache.repository"; | ||
|
||
export const getStrategyCache = (settings: ISettings): ICache => { | ||
// Default to IN_MEMORY if no strategy is specified or if IN_MEMORY is explicitly specified | ||
if (!settings.strategyCache || settings.strategyCache === "IN_MEMORY") { | ||
return MemoryCacheRepository.getInstance(); | ||
} | ||
|
||
// Ensure that a custom cache is provided if the strategy is CUSTOM | ||
if (settings.strategyCache === "CUSTOM") { | ||
if (!settings.cache) { | ||
throw new Error("When the property 'strategyCache' is 'CUSTOM', the property 'cache' is required."); | ||
} | ||
return settings.cache; | ||
} | ||
|
||
// Ensure that Redis configuration is provided if the strategy is REDIS | ||
if (settings.strategyCache === "REDIS") { | ||
if (!settings.redis) { | ||
throw new Error("When the property 'strategyCache' is 'REDIS', the property 'redis' is required."); | ||
} | ||
|
||
return RedisCacheRepository.getInstance(settings.redis); | ||
} | ||
|
||
// Return MemoryCacheRepository by default if no valid strategy is matched (fallback safety) | ||
return MemoryCacheRepository.getInstance(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,60 @@ | ||
/* eslint-disable no-unused-vars */ | ||
import { RequestExpress } from "../../core/interfaces/express"; | ||
import { PolicieRateLimit } from "../../core/interfaces/policies"; | ||
import { ICache } from "./cache"; | ||
import { CacheStrategy, ICache, RedisCache } from "./cache"; | ||
|
||
export type BlockRequestRule = (req: RequestExpress) => boolean; | ||
|
||
export interface ISettings { | ||
interface ISettingsBase { | ||
strategyCache?: CacheStrategy; | ||
|
||
/** | ||
* This atributte is opcional and needs to receive an classe to type ICache. | ||
* You can implement a custom Cache if you want as long as the interface is respected | ||
* @default MemoryCache | ||
* The object with settings to policy rate-limit. | ||
*/ | ||
cache?: ICache; | ||
policy: PolicieRateLimit; | ||
|
||
/** | ||
* This function can to be implemented to forbidden a request. | ||
* @param {IExpressRequest} req | ||
* @returns {boolean} | ||
*/ | ||
blockRequestRule?: BlockRequestRule; | ||
} | ||
|
||
interface ISettingsMemoryCache extends ISettingsBase { | ||
/** | ||
* The object with settings to policy rate-limit. | ||
* Specifies the type of cache to use. | ||
* @default IN_MEMORY | ||
*/ | ||
policy: PolicieRateLimit; | ||
strategyCache?: "IN_MEMORY"; | ||
} | ||
|
||
interface ISettingsRedisCache extends ISettingsBase { | ||
redis: RedisCache; | ||
|
||
/** | ||
* Specifies the type of cache to use. | ||
* @default IN_MEMORY | ||
*/ | ||
strategyCache?: "REDIS"; | ||
} | ||
|
||
interface ISettingsCustomCache extends ISettingsBase { | ||
/** | ||
* Specifies the type of cache to use. | ||
* @default IN_MEMORY | ||
*/ | ||
strategyCache?: "CUSTOM"; | ||
|
||
/** | ||
* This atributte is opcional and needs to receive an classe to type ICache. | ||
* You can implement a custom Cache if you want as long as the interface is respected | ||
* @default MemoryCache | ||
*/ | ||
cache: ICache; | ||
} | ||
|
||
export type ISettings = | ||
| ISettingsMemoryCache | ||
| ISettingsRedisCache | ||
| ISettingsCustomCache; |
Oops, something went wrong.