├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── jest.config.js
├── lib
├── default-options.spec.ts
├── default-options.ts
├── index.ts
├── rate-limiter.decorator.spec.ts
├── rate-limiter.decorator.ts
├── rate-limiter.guard.spec.ts
├── rate-limiter.guard.ts
├── rate-limiter.interface.spec.ts
├── rate-limiter.interface.ts
├── rate-limiter.module.spec.ts
└── rate-limiter.module.ts
├── package-lock.json
├── package.json
├── tsconfig.json
└── tslint.json
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test with Jest
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [10.x, 12.x, 14.x]
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | - run: npm ci
23 | - run: npm run build
24 | - run: npm run test
25 | env:
26 | CI: true
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vim
2 | *.swp
3 | *.*~
4 |
5 | # dependencies
6 | /node_modules
7 |
8 | # misc
9 | npm-debug.log
10 | .DS_Store
11 |
12 | # dist
13 | /dist
14 |
15 | /coverage
16 |
17 | # Example NestJS
18 | examples/dist
19 | examples/node_modules
20 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | examples
3 | coverage
4 | lib
5 | .gitignore
6 | .prettierrc
7 | jest.config.js
8 | package-lock.json
9 | tsconfig.json
10 | tslint.json
11 | README.md
12 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "none",
4 | "tabWidth": 4,
5 | "printWidth": 155,
6 | "useTabs": true,
7 | "semi": false,
8 | "bracketSpacing": true
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Onur Ozkan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## No longer maintained
2 |
3 |
4 |
5 |
6 |
7 | Rate Limiter Module for NestJS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | # Documentation Map
19 | - [Description](https://github.com/ozkanonur/nestjs-rate-limiter#description)
20 | - [Installation](https://github.com/ozkanonur/nestjs-rate-limiter#installation)
21 | - [Basic Usage](https://github.com/ozkanonur/nestjs-rate-limiter#basic-usage)
22 | - [Include Module](https://github.com/ozkanonur/nestjs-rate-limiter#include-module)
23 | - [Using Guard](https://github.com/ozkanonur/nestjs-rate-limiter#using-guard)
24 | - [With Decorator](https://github.com/ozkanonur/nestjs-rate-limiter#with-decorator)
25 | - [With All Options](https://github.com/ozkanonur/nestjs-rate-limiter#with-all-options)
26 | - [Fastify based Graphql](https://github.com/ozkanonur/nestjs-rate-limiter#fastify-based-graphql)
27 | - [Options](https://github.com/ozkanonur/nestjs-rate-limiter#options)
28 | - [for](https://github.com/ozkanonur/nestjs-rate-limiter#-for)
29 | - [type](https://github.com/ozkanonur/nestjs-rate-limiter#-type)
30 | - [keyPrefix](https://github.com/ozkanonur/nestjs-rate-limiter#-keyPrefix)
31 | - [points](https://github.com/ozkanonur/nestjs-rate-limiter#-points)
32 | - [pointsConsumed](https://github.com/ozkanonur/nestjs-rate-limiter#-pointsConsumed)
33 | - [inmemoryBlockOnConsumed](https://github.com/ozkanonur/nestjs-rate-limiter#-inmemoryBlockOnConsumed)
34 | - [duration](https://github.com/ozkanonur/nestjs-rate-limiter#-duration)
35 | - [blockDuration](https://github.com/ozkanonur/nestjs-rate-limiter#-blockDuration)
36 | - [inmemoryBlockDuration](https://github.com/ozkanonur/nestjs-rate-limiter#-inmemoryBlockDuration)
37 | - [queueEnabled](https://github.com/ozkanonur/nestjs-rate-limiter#-queueEnabled)
38 | - [whiteList](https://github.com/ozkanonur/nestjs-rate-limiter#-whiteList)
39 | - [blackList](https://github.com/ozkanonur/nestjs-rate-limiter#-blackList)
40 | - [storeClient](https://github.com/ozkanonur/nestjs-rate-limiter#-storeClient)
41 | - [insuranceLimiter](https://github.com/ozkanonur/nestjs-rate-limiter#-insuranceLimiter)
42 | - [storeType](https://github.com/ozkanonur/nestjs-rate-limiter#-storeType)
43 | - [dbName](https://github.com/ozkanonur/nestjs-rate-limiter#-dbName)
44 | - [tableName](https://github.com/ozkanonur/nestjs-rate-limiter#-tableName)
45 | - [tableCreated](https://github.com/ozkanonur/nestjs-rate-limiter#-tableCreated)
46 | - [clearExpiredByTimeout](https://github.com/ozkanonur/nestjs-rate-limiter#-clearExpiredByTimeout)
47 | - [execEvenly](https://github.com/ozkanonur/nestjs-rate-limiter#-execEvenly)
48 | - [execEvenlyMinDelayMs](https://github.com/ozkanonur/nestjs-rate-limiter#-execEvenlyMinDelayMs)
49 | - [indexKeyPrefix](https://github.com/ozkanonur/nestjs-rate-limiter#-indexKeyPrefix)
50 | - [maxQueueSize](https://github.com/ozkanonur/nestjs-rate-limiter#-maxQueueSize)
51 | - [omitResponseHeaders](https://github.com/ozkanonur/nestjs-rate-limiter#-omitResponseHeaders)
52 | - [errorMessage](https://github.com/ozkanonur/nestjs-rate-limiter#-errorMessage)
53 | - [customResponseSchema](https://github.com/ozkanonur/nestjs-rate-limiter#-customResponseSchema)
54 | - [Override Functions](https://github.com/ozkanonur/nestjs-rate-limiter#override-functions)
55 | - [Benchmarks](https://github.com/ozkanonur/nestjs-rate-limiter#benchmarks)
56 | - [TODO List](https://github.com/ozkanonur/nestjs-rate-limiter#todo)
57 |
58 | # Description
59 |
60 | `nestjs-rate-limiter` is a module which adds in configurable rate limiting for [Nest](https://github.com/nestjs/nest) applications.
61 |
62 | Under the hood it uses [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible).
63 |
64 | # Installation
65 |
66 | ```bash
67 | npm i --save nestjs-rate-limiter
68 | ```
69 |
70 | Or if you use Yarn:
71 |
72 | ```bash
73 | yarn add nestjs-rate-limiter
74 | ```
75 |
76 | # Requirements
77 |
78 | `nestjs-rate-limiter` is built to work with Nest 6 and newer versions.
79 |
80 | # Basic Usage
81 |
82 | ### Include Module
83 |
84 | First you need to import this module into your main application module:
85 |
86 | > app.module.ts
87 |
88 | ```ts
89 | import { RateLimiterModule } from 'nestjs-rate-limiter'
90 |
91 | @Module({
92 | imports: [RateLimiterModule],
93 | })
94 | export class ApplicationModule {}
95 | ```
96 |
97 | ### Using Guard
98 |
99 | Now you need to register the guard. You can do this only on some routes:
100 |
101 | > app.controller.ts
102 |
103 | ```ts
104 | import { RateLimiterGuard } from 'nestjs-rate-limiter'
105 |
106 | @UseGuards(RateLimiterGuard)
107 | @Get('/login')
108 | public async login() {
109 | console.log('hello')
110 | }
111 | ```
112 |
113 | Or you can choose to register the guard globally:
114 |
115 | > app.module.ts
116 |
117 | ```ts
118 | import { APP_GUARD } from '@nestjs/core'
119 | import { RateLimiterModule, RateLimiterGuard } from 'nestjs-rate-limiter'
120 |
121 | @Module({
122 | imports: [RateLimiterModule],
123 | providers: [
124 | {
125 | provide: APP_GUARD,
126 | useClass: RateLimiterGuard,
127 | },
128 | ],
129 | })
130 | export class ApplicationModule {}
131 | ```
132 |
133 | ### With Decorator
134 |
135 | You can use the `@RateLimit` decorator to specify the points and duration for rate limiting on a per controller or per
136 | route basis:
137 |
138 | > app.controller.ts
139 |
140 | ```ts
141 | import { RateLimit } from 'nestjs-rate-limiter'
142 |
143 | @RateLimit({ keyPrefix: 'sign-up', points: 1, duration: 60, errorMessage: 'Accounts cannot be created more than once in per minute' })
144 | @Get('/signup')
145 | public async signUp() {
146 | console.log('hello')
147 | }
148 | ```
149 |
150 | ### Dynamic Keyprefix
151 |
152 | ```ts
153 | import { RateLimit } from 'nestjs-rate-limiter'
154 |
155 | @RateLimit({
156 | keyPrefix: () => programmaticFuncThatReturnsValue(),
157 | points: 1,
158 | duration: 60,
159 | customResponseSchema: () => { return { timestamp: '1611479696', message: 'Request has been blocked' }}
160 | })
161 | @Get('/example')
162 | public async example() {
163 | console.log('hello')
164 | }
165 | ```
166 |
167 | ### With All Options
168 |
169 | The usage of the limiter options is as in the code block below. For an explanation of the each option, please see [options](https://github.com/ozkanonur/nestjs-rate-limiter#options)
.
170 |
171 | ```ts
172 | @Module({
173 | imports: [
174 | // All the values here are defaults.
175 | RateLimiterModule.register({
176 | for: 'Express',
177 | type: 'Memory',
178 | keyPrefix: 'global',
179 | points: 4,
180 | pointsConsumed: 1,
181 | inmemoryBlockOnConsumed: 0,
182 | duration: 1,
183 | blockDuration: 0,
184 | inmemoryBlockDuration: 0,
185 | queueEnabled: false,
186 | whiteList: [],
187 | blackList: [],
188 | storeClient: undefined,
189 | insuranceLimiter: undefined,
190 | storeType: undefined,
191 | dbName: undefined,
192 | tableName: undefined,
193 | tableCreated: undefined,
194 | clearExpiredByTimeout: undefined,
195 | execEvenly: false,
196 | execEvenlyMinDelayMs: undefined,
197 | indexKeyPrefix: {},
198 | maxQueueSize: 100,
199 | omitResponseHeaders: false,
200 | errorMessage: 'Rate limit exceeded',
201 | logger: true,
202 | customResponseSchema: undefined
203 | }),
204 | ],
205 | providers: [
206 | {
207 | provide: APP_GUARD,
208 | useClass: RateLimiterGuard,
209 | },
210 | ],
211 | })
212 | export class ApplicationModule {}
213 | ```
214 |
215 | ### Fastify based Graphql
216 | If you want to use this library on a fastify based graphql server, you need to override the graphql context in the app.module as shown below.
217 | ```ts
218 | GraphQLModule.forRoot({
219 | context: ({ request, reply }) => {
220 | return { req: request, res: reply }
221 | },
222 | }),
223 | ```
224 |
225 | # Options
226 |
227 | #### ● for
228 | Default: 'Express'
229 |
230 | Type: 'Express' | 'Fastify' | 'Microservice' | 'ExpressGraphql' | 'FastifyGraphql'
231 |
232 |
233 | In this option, you specify what the technology is running under the Nest application. The wrong value causes to limiter not working.
234 |
235 | #### ● type
236 | Default: 'Memory'
237 |
238 | Type: 'Memory' | 'Redis' | 'Memcache' | 'Postgres' | 'MySQL' | 'Mongo'
239 |
240 |
241 | Here you define where the limiter data will be stored. Each option plays a different role in limiter performance, to see that please check [benchmarks](https://github.com/ozkanonur/nestjs-rate-limiter#benchmarks).
242 |
243 | #### ● keyPrefix
244 | Default: 'global'
245 |
246 | Type: string
247 |
248 |
249 | For creating several limiters with different options to apply different modules/endpoints.
250 |
251 | Set to empty string '', if keys should be stored without prefix.
252 |
253 | Note: for some limiters it should correspond to Storage requirements for tables or collections name, as keyPrefix may be used as their name.
254 |
255 | #### ● points
256 | Default: 4
257 |
258 | Type: number
259 |
260 |
261 | Maximum number of points can be consumed over duration.
262 |
263 | #### ● pointsConsumed
264 | Default: 1
265 |
266 | Type: number
267 |
268 |
269 | You can consume more than 1 point per invocation of the rate limiter.
270 |
271 | For instance if you have a limit of 100 points per 60 seconds, and pointsConsumed is set to 10, the user will effectively be able to make 10 requests per 60 seconds.
272 |
273 | #### ● inmemoryBlockOnConsumed
274 | Default: 0
275 |
276 | Type: number
277 |
278 |
279 | For Redis, Memcached, MongoDB, MySQL, PostgreSQL, etc.
280 |
281 | Can be used against DDoS attacks. In-memory blocking works in current process memory and for consume method only.
282 |
283 | It blocks a key in memory for msBeforeNext milliseconds from the last consume result, if inmemoryBlockDuration is not set. This helps to avoid extra requests.
284 | It is not necessary to increment counter on store, if all points are consumed already.
285 |
286 | #### ● duration
287 | Default: 1
288 |
289 | Type: number
290 |
291 |
292 | Number of seconds before consumed points are reset.
293 |
294 | Keys never expire, if duration is 0.
295 |
296 | #### ● blockDuration
297 | Default: 0
298 |
299 | Type: number
300 |
301 |
302 | If positive number and consumed more than points in current duration, block for blockDuration seconds.
303 |
304 | #### ● inmemoryBlockDuration
305 | Default: 0
306 |
307 | Type: number
308 |
309 |
310 | For Redis, Memcached, MongoDB, MySQL, PostgreSQL, etc.
311 |
312 | Block key for inmemoryBlockDuration seconds, if inmemoryBlockOnConsumed or more points are consumed. Set it the same as blockDuration option for distributed application to have consistent result on all processes.
313 |
314 | #### ● queueEnabled
315 | Default: false
316 |
317 | Type: boolean
318 |
319 |
320 | It activates the queue mechanism, and holds the incoming requests for duration
value.
321 |
322 | #### ● whiteList
323 | Default: []
324 |
325 | Type: string[]
326 |
327 |
328 | If the IP is white listed, consume resolved no matter how many points consumed.
329 |
330 | #### ● blackList
331 | Default: []
332 |
333 | Type: string[]
334 |
335 |
336 | If the IP is black listed, consume rejected anytime. Blacklisted IPs are blocked on code level not in store/memory. Think of it as of requests filter.
337 |
338 | #### ● storeClient
339 | Default: undefined
340 |
341 | Type: any
342 |
343 |
344 | Required for Redis, Memcached, MongoDB, MySQL, PostgreSQL, etc.
345 |
346 | Have to be redis, ioredis, memcached, mongodb, pg, mysql2, mysql or any other related pool or connection.
347 |
348 | #### ● insuranceLimiter
349 | Default: undefined
350 |
351 | Type: any
352 |
353 |
354 | Default: undefined For Redis, Memcached, MongoDB, MySQL, PostgreSQL.
355 |
356 | Instance of RateLimiterAbstract extended object to store limits, when database comes up with any error.
357 |
358 | All data from insuranceLimiter is NOT copied to parent limiter, when error gone
359 |
360 | Note: insuranceLimiter automatically setup blockDuration and execEvenly to same values as in parent to avoid unexpected behaviour.
361 |
362 | #### ● storeType
363 | Default: storeClient.constructor.name
364 |
365 | Type: any
366 |
367 |
368 | For MySQL and PostgreSQL
369 | It is required only for Knex and have to be set to 'knex'
370 |
371 | #### ● dbName
372 | Default for MySQL, Postgres & Mongo: 'rate-limiter'
373 |
374 | Type: string
375 |
376 |
377 | Database where limits are stored. It is created during creating a limiter. Doesn't work with Mongoose, as mongoose connection is established to exact database.
378 |
379 | #### ● tableName
380 | Default: equals to 'keyPrefix' option
381 |
382 | Type: string
383 |
384 |
385 | For MongoDB, MySQL, PostgreSQL.
386 |
387 | By default, limiter creates a table for each unique keyPrefix. tableName option sets table/collection name where values should be store.
388 |
389 | #### ● tableCreated
390 | Default: false
391 |
392 | Type: boolean
393 |
394 |
395 | Does not create a table for rate limiter, if tableCreated is true
.
396 |
397 | #### ● clearExpiredByTimeout
398 | Default for MySQL and PostgreSQL: true
399 |
400 | Type: boolean
401 |
402 |
403 | Rate limiter deletes data expired more than 1 hour ago every 5 minutes.
404 |
405 | #### ● execEvenly
406 | Default: false
407 |
408 | Type: boolean
409 |
410 |
411 | Delay action to be executed evenly over duration First action in duration is executed without delay. All next allowed actions in current duration are delayed by formula msBeforeDurationEnd / (remainingPoints + 2) with minimum delay of duration * 1000 / points It allows to cut off load peaks similar way to Leaky Bucket.
412 |
413 | Note: it isn't recommended to use it for long duration and few points, as it may delay action for too long with default execEvenlyMinDelayMs.
414 |
415 | #### ● execEvenlyMinDelayMs
416 | Default: duration * 1000 / points
417 |
418 | Type: number
419 |
420 |
421 | Sets minimum delay in milliseconds, when action is delayed with execEvenly
422 |
423 | #### ● indexKeyPrefix
424 | Default: {}
425 |
426 | Type: {}
427 |
428 |
429 | Object which is used to create combined index by {...indexKeyPrefix, key: 1} attributes.
430 |
431 | #### ● maxQueueSize
432 | Default: 100
433 |
434 | Type: number
435 |
436 |
437 | Determines the maximum number of requests in the queue and returns 429
as response to requests that over of the maxQueueSize.
438 |
439 | #### ● omitResponseHeaders
440 | Default: false
441 |
442 | Type: boolean
443 |
444 |
445 | Whether or not the rate limit headers (X-Retry-After
, X-RateLimit-Limit
, X-Retry-Remaining
, X-Retry-Reset
) should be omitted in the response.
446 |
447 |
448 | #### ● errorMessage
449 | Default: 'Rate limit exceeded'
450 |
451 | Type: string
452 |
453 |
454 | errorMessage option can change the error message of rate limiter exception.
455 |
456 | #### ● logger
457 | Default: true
458 |
459 | Type: boolean
460 |
461 |
462 | logger option allows to enable or disable logging from library.
463 |
464 | #### ● customResponseSchema
465 | Default: undefined
466 |
467 | Type: string
468 |
469 |
470 | customResponseSchema option allows to provide customizable response schemas
471 |
472 | # Override Functions
473 |
474 | It's possible to override getIpFromRequest
function by extending RateLimiterGuard
class.
475 |
476 | ```ts
477 | import { RateLimiterGuard } from 'nestjs-rate-limiter'
478 | import type { Request } from 'express'
479 |
480 | class ExampleRateLimiterGuard extends RateLimiterGuard {
481 | protected getIpFromRequest(request: Request): string {
482 | return request.get('x-forwarded-for');
483 | }
484 | }
485 | ```
486 |
487 | # Benchmarks
488 |
489 | 1000 concurrent clients with maximum 2000 requests per sec during 30 seconds.
490 |
491 | ```
492 | 1. Memory 0.34 ms
493 | 2. Redis 2.45 ms
494 | 3. Memcached 3.89 ms
495 | 4. Mongo 4.75 ms
496 | ```
497 |
498 | 500 concurrent clients with maximum 1000 req per sec during 30 seconds
499 |
500 | ```
501 | 5. PostgreSQL 7.48 ms (with connection pool max 100)
502 | 6. MySQL 14.59 ms (with connection pool 100)
503 | ```
504 |
505 | ## TODO
506 | - [ ] Support Websocket
507 | - [ ] Support Rpc
508 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ['/lib'],
3 | transform: {
4 | '^.+\\.ts?$': 'ts-jest',
5 | },
6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts?$',
7 | moduleFileExtensions: ['ts', 'js', 'jsx', 'json', 'node'],
8 | collectCoverage: true,
9 | coverageDirectory: "./coverage",
10 | coverageThreshold: {
11 | global: {
12 | functions: 70,
13 | lines: 80,
14 | statements: 80,
15 | },
16 | },
17 | }
--------------------------------------------------------------------------------
/lib/default-options.spec.ts:
--------------------------------------------------------------------------------
1 | import { defaultRateLimiterOptions } from './default-options'
2 |
3 | describe('defaultRateLimiterOptions', () => {
4 | it('should validate that defaultRateLimiterOptions exists', async () => {
5 | expect(defaultRateLimiterOptions).toBeDefined()
6 | })
7 |
8 | it('should validate the defaultRateLimiterOptions', async () => {
9 | expect(defaultRateLimiterOptions.for).toBe('Express')
10 | expect(defaultRateLimiterOptions.type).toBe('Memory')
11 | expect(defaultRateLimiterOptions.keyPrefix).toBe('global')
12 | expect(defaultRateLimiterOptions.points).toBe(4)
13 | expect(defaultRateLimiterOptions.pointsConsumed).toBe(1)
14 | expect(defaultRateLimiterOptions.inmemoryBlockOnConsumed).toBe(0)
15 | expect(defaultRateLimiterOptions.duration).toBe(1)
16 | expect(defaultRateLimiterOptions.blockDuration).toBe(0)
17 | expect(defaultRateLimiterOptions.inmemoryBlockDuration).toBe(0)
18 | expect(defaultRateLimiterOptions.queueEnabled).toBe(false)
19 | expect(defaultRateLimiterOptions.whiteList.length).toBe(0)
20 | expect(defaultRateLimiterOptions.blackList.length).toBe(0)
21 | expect(defaultRateLimiterOptions.storeClient).toBeUndefined()
22 | expect(defaultRateLimiterOptions.insuranceLimiter).toBeUndefined()
23 | expect(defaultRateLimiterOptions.dbName).toBe('rate-limiter')
24 | expect(defaultRateLimiterOptions.tableName).toBeUndefined()
25 | expect(defaultRateLimiterOptions.tableCreated).toBeUndefined()
26 | expect(defaultRateLimiterOptions.clearExpiredByTimeout).toBeUndefined()
27 | expect(defaultRateLimiterOptions.execEvenly).toBe(false)
28 | expect(defaultRateLimiterOptions.execEvenlyMinDelayMs).toBeUndefined()
29 | expect(Object.keys(defaultRateLimiterOptions.indexKeyPrefix).length).toBe(0)
30 | expect(defaultRateLimiterOptions.maxQueueSize).toBe(100)
31 | expect(defaultRateLimiterOptions.omitResponseHeaders).toBe(false)
32 | expect(defaultRateLimiterOptions.errorMessage).toBe('Rate limit exceeded')
33 | expect(defaultRateLimiterOptions.customResponseSchema).toBeUndefined()
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/lib/default-options.ts:
--------------------------------------------------------------------------------
1 | import { RateLimiterOptions } from './rate-limiter.interface'
2 |
3 | export const defaultRateLimiterOptions: RateLimiterOptions = {
4 | for: 'Express',
5 | type: 'Memory',
6 | keyPrefix: 'global',
7 | points: 4,
8 | pointsConsumed: 1,
9 | inmemoryBlockOnConsumed: 0,
10 | duration: 1,
11 | blockDuration: 0,
12 | inmemoryBlockDuration: 0,
13 | queueEnabled: false,
14 | whiteList: [],
15 | blackList: [],
16 | storeClient: undefined,
17 | insuranceLimiter: undefined,
18 | storeType: undefined,
19 | dbName: 'rate-limiter',
20 | tableName: undefined,
21 | tableCreated: undefined,
22 | clearExpiredByTimeout: undefined,
23 | execEvenly: false,
24 | execEvenlyMinDelayMs: undefined,
25 | indexKeyPrefix: {},
26 | maxQueueSize: 100,
27 | omitResponseHeaders: false,
28 | errorMessage: 'Rate limit exceeded',
29 | customResponseSchema: undefined,
30 | logger: true
31 | }
32 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './rate-limiter.decorator'
2 | export * from './rate-limiter.interface'
3 | export * from './rate-limiter.module'
4 | export * from './rate-limiter.guard'
5 |
--------------------------------------------------------------------------------
/lib/rate-limiter.decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { RateLimit } from './rate-limiter.decorator'
2 | import { RateLimiterOptions } from './rate-limiter.interface'
3 |
4 | describe('RateLimit', () => {
5 | it('should validate that RateLimit decorator exists', async () => {
6 | expect(RateLimit).toBeDefined()
7 | })
8 |
9 | it('should verify RateLimit Method decorator can be created with empty options', async () => {
10 | const options: RateLimiterOptions = {}
11 |
12 | const decorator: MethodDecorator = RateLimit(options)
13 |
14 | expect(decorator).toBeDefined()
15 | })
16 |
17 | it('should verify RateLimit can decorate a method and be called', async () => {
18 | const options: RateLimiterOptions = {}
19 | const testFn = jest.fn()
20 |
21 | class TestController {
22 | @RateLimit(options)
23 | run() {
24 | testFn()
25 | }
26 | }
27 |
28 | const controller = new TestController()
29 | controller.run()
30 |
31 | expect(testFn).toHaveBeenCalled()
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/lib/rate-limiter.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common'
2 | import { RateLimiterOptions } from './rate-limiter.interface'
3 |
4 | export const RateLimit = (options: RateLimiterOptions): MethodDecorator => SetMetadata('rateLimit', options)
5 |
--------------------------------------------------------------------------------
/lib/rate-limiter.guard.spec.ts:
--------------------------------------------------------------------------------
1 | import { RateLimit } from './rate-limiter.decorator'
2 | import { RateLimiterOptions } from './rate-limiter.interface'
3 |
4 | describe('RateLimit', () => {
5 | it('should validate that RateLimit decorator exists', async () => {
6 | expect(RateLimit).toBeDefined()
7 | })
8 |
9 | it('should verify RateLimit Method decorator can be created with empty options', async () => {
10 | const options: RateLimiterOptions = {}
11 |
12 | const decorator: MethodDecorator = RateLimit(options)
13 |
14 | expect(decorator).toBeDefined()
15 | })
16 |
17 | it('should verify RateLimit can decorate a method and be called', async () => {
18 | const options: RateLimiterOptions = {}
19 | const testFn = jest.fn()
20 |
21 | class TestController {
22 | @RateLimit(options)
23 | run() {
24 | testFn()
25 | }
26 | }
27 |
28 | const controller = new TestController()
29 | controller.run()
30 |
31 | expect(testFn).toHaveBeenCalled()
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/lib/rate-limiter.guard.ts:
--------------------------------------------------------------------------------
1 | import { Reflector } from '@nestjs/core'
2 | import { Injectable, ExecutionContext, Inject, HttpStatus, Logger, HttpException, CanActivate } from '@nestjs/common'
3 | import {
4 | RateLimiterMemory,
5 | RateLimiterRes,
6 | RateLimiterAbstract,
7 | RateLimiterRedis,
8 | IRateLimiterStoreOptions,
9 | RateLimiterMemcache,
10 | RateLimiterPostgres,
11 | RateLimiterMySQL,
12 | RateLimiterMongo,
13 | RateLimiterQueue,
14 | RLWrapperBlackAndWhite
15 | } from 'rate-limiter-flexible'
16 | import { RateLimiterOptions } from './rate-limiter.interface'
17 | import { defaultRateLimiterOptions } from './default-options'
18 |
19 | @Injectable()
20 | export class RateLimiterGuard implements CanActivate {
21 | private rateLimiters: Map = new Map()
22 | private specificOptions: RateLimiterOptions
23 | private queueLimiter: RateLimiterQueue
24 |
25 | constructor(@Inject('RATE_LIMITER_OPTIONS') private options: RateLimiterOptions, @Inject('Reflector') private readonly reflector: Reflector) {}
26 |
27 | async getRateLimiter(options?: RateLimiterOptions): Promise {
28 | this.options = { ...defaultRateLimiterOptions, ...this.options }
29 | this.specificOptions = null
30 | this.specificOptions = options
31 |
32 | const limiterOptions: RateLimiterOptions = {
33 | ...this.options,
34 | ...options
35 | }
36 |
37 | const { ...libraryArguments } = limiterOptions
38 |
39 | let rateLimiter: RateLimiterAbstract = this.rateLimiters.get(libraryArguments.keyPrefix)
40 |
41 | if (libraryArguments.execEvenlyMinDelayMs === undefined)
42 | libraryArguments.execEvenlyMinDelayMs = (this.options.duration * 1000) / this.options.points
43 |
44 | if (!rateLimiter) {
45 | const logger = this.specificOptions?.logger || this.options.logger
46 | switch (this.specificOptions?.type || this.options.type) {
47 | case 'Memory':
48 | rateLimiter = new RateLimiterMemory(libraryArguments)
49 | if (logger) {
50 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMemory')
51 | }
52 | break
53 | case 'Redis':
54 | rateLimiter = new RateLimiterRedis(libraryArguments as IRateLimiterStoreOptions)
55 | if (logger) {
56 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterRedis')
57 | }
58 | break
59 | case 'Memcache':
60 | rateLimiter = new RateLimiterMemcache(libraryArguments as IRateLimiterStoreOptions)
61 | if (logger) {
62 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMemcache')
63 | }
64 | break
65 | case 'Postgres':
66 | if (libraryArguments.storeType === undefined) libraryArguments.storeType = this.options.storeClient.constructor.name
67 |
68 | libraryArguments.tableName = this.specificOptions?.tableName || this.options.tableName
69 | if (libraryArguments.tableName === undefined) {
70 | libraryArguments.tableName = this.specificOptions?.keyPrefix || this.options.keyPrefix
71 | }
72 |
73 | if (libraryArguments.tableCreated === undefined) libraryArguments.tableCreated = false
74 | if (libraryArguments.clearExpiredByTimeout === undefined) libraryArguments.clearExpiredByTimeout = true
75 |
76 | rateLimiter = await new Promise((resolve, reject) => {
77 | const limiter = new RateLimiterPostgres(libraryArguments as IRateLimiterStoreOptions, (err) => {
78 | if (err) {
79 | reject(err)
80 | } else {
81 | resolve(limiter)
82 | }
83 | })
84 | })
85 | if (logger) {
86 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterPostgres')
87 | }
88 | break
89 | case 'MySQL':
90 | if (libraryArguments.storeType === undefined) libraryArguments.storeType = this.options.storeClient.constructor.name
91 |
92 | libraryArguments.tableName = this.specificOptions?.tableName || this.options.tableName
93 | if (libraryArguments.tableName === undefined) {
94 | libraryArguments.tableName = this.specificOptions?.keyPrefix || this.options.keyPrefix
95 | }
96 |
97 | if (libraryArguments.tableCreated === undefined) libraryArguments.tableCreated = false
98 | if (libraryArguments.clearExpiredByTimeout === undefined) libraryArguments.clearExpiredByTimeout = true
99 |
100 | rateLimiter = await new Promise((resolve, reject) => {
101 | const limiter = new RateLimiterMySQL(libraryArguments as IRateLimiterStoreOptions, (err) => {
102 | if (err) {
103 | reject(err)
104 | } else {
105 | resolve(limiter)
106 | }
107 | })
108 | })
109 | if (logger) {
110 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMySQL')
111 | }
112 | break
113 | case 'Mongo':
114 | if (libraryArguments.storeType === undefined) libraryArguments.storeType = this.options.storeClient.constructor.name
115 |
116 | libraryArguments.tableName = this.specificOptions?.tableName || this.options.tableName
117 | if (libraryArguments.tableName === undefined) {
118 | libraryArguments.tableName = this.specificOptions?.keyPrefix || this.options.keyPrefix
119 | }
120 |
121 | rateLimiter = new RateLimiterMongo(libraryArguments as IRateLimiterStoreOptions)
122 | if (logger) {
123 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMongo')
124 | }
125 | break
126 | default:
127 | throw new Error(`Invalid "type" option provided to RateLimiterGuard. Value was ${limiterOptions.type}`)
128 | }
129 |
130 | this.rateLimiters.set(limiterOptions.keyPrefix, rateLimiter)
131 | }
132 |
133 | if (this.specificOptions?.queueEnabled || this.options.queueEnabled) {
134 | this.queueLimiter = new RateLimiterQueue(rateLimiter, {
135 | maxQueueSize: this.specificOptions?.maxQueueSize || this.options.maxQueueSize
136 | })
137 | }
138 |
139 | rateLimiter = new RLWrapperBlackAndWhite({
140 | limiter: rateLimiter,
141 | whiteList: this.specificOptions?.whiteList || this.options.whiteList,
142 | blackList: this.specificOptions?.blackList || this.options.blackList,
143 | runActionAnyway: false
144 | })
145 |
146 | return rateLimiter
147 | }
148 |
149 | async canActivate(context: ExecutionContext): Promise {
150 | let points: number = this.specificOptions?.points || this.options.points
151 | let pointsConsumed: number = this.specificOptions?.pointsConsumed || this.options.pointsConsumed
152 |
153 | const reflectedOptions: RateLimiterOptions = this.reflector.get('rateLimit', context.getHandler())
154 |
155 | if (reflectedOptions) {
156 | if (reflectedOptions.points) {
157 | points = reflectedOptions.points
158 | }
159 |
160 | if (reflectedOptions.pointsConsumed) {
161 | pointsConsumed = reflectedOptions.pointsConsumed
162 | }
163 | }
164 |
165 | const request = this.httpHandler(context).req
166 | const response = this.httpHandler(context).res
167 |
168 | const rateLimiter: RateLimiterAbstract = await this.getRateLimiter(reflectedOptions)
169 | const key = this.getIpFromRequest(request)
170 |
171 | await this.responseHandler(response, key, rateLimiter, points, pointsConsumed)
172 | return true
173 | }
174 |
175 | protected getIpFromRequest(request: { ip: string }): string {
176 | return request.ip?.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/)?.[0]
177 | }
178 |
179 | private httpHandler(context: ExecutionContext) {
180 | if (this.options.for === 'ExpressGraphql') {
181 | return {
182 | req: context.getArgByIndex(2).req,
183 | res: context.getArgByIndex(2).req.res
184 | }
185 | } else if (this.options.for === 'FastifyGraphql') {
186 | return {
187 | req: context.getArgByIndex(2).req,
188 | res: context.getArgByIndex(2).res
189 | }
190 | } else {
191 | return {
192 | req: context.switchToHttp().getRequest(),
193 | res: context.switchToHttp().getResponse()
194 | }
195 | }
196 | }
197 |
198 | private async setResponseHeaders(response: any, points: number, rateLimiterResponse: RateLimiterRes) {
199 | response.header('Retry-After', Math.ceil(rateLimiterResponse.msBeforeNext / 1000))
200 | response.header('X-RateLimit-Limit', points)
201 | response.header('X-Retry-Remaining', rateLimiterResponse.remainingPoints)
202 | response.header('X-Retry-Reset', new Date(Date.now() + rateLimiterResponse.msBeforeNext).toUTCString())
203 | }
204 |
205 | private async responseHandler(response: any, key: any, rateLimiter: RateLimiterAbstract, points: number, pointsConsumed: number) {
206 | try {
207 | if (this.specificOptions?.queueEnabled || this.options.queueEnabled) await this.queueLimiter.removeTokens(1)
208 | else {
209 | const rateLimiterResponse: RateLimiterRes = await rateLimiter.consume(key, pointsConsumed)
210 | if (!this.specificOptions?.omitResponseHeaders && !this.options.omitResponseHeaders)
211 | this.setResponseHeaders(response, points, rateLimiterResponse)
212 | }
213 | } catch (rateLimiterResponse) {
214 | response.header('Retry-After', Math.ceil(rateLimiterResponse.msBeforeNext / 1000))
215 | if (typeof this.specificOptions?.customResponseSchema === 'function' || typeof this.options.customResponseSchema === 'function') {
216 | const errorBody = this.specificOptions?.customResponseSchema || this.options.customResponseSchema
217 | throw new HttpException(errorBody(rateLimiterResponse), HttpStatus.TOO_MANY_REQUESTS)
218 | } else {
219 | throw new HttpException(this.specificOptions?.errorMessage || this.options.errorMessage, HttpStatus.TOO_MANY_REQUESTS)
220 | }
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/lib/rate-limiter.interface.spec.ts:
--------------------------------------------------------------------------------
1 | import { RateLimiterOptionsFactory, RateLimiterModuleAsyncOptions, RateLimiterOptions } from './rate-limiter.interface'
2 |
3 | describe('RateLimiterOptionsFactory', () => {
4 | it('should validate that RateLimiterOptionsFactory exists', async () => {
5 | const rateLimiterOptionsFactory: RateLimiterOptionsFactory = {
6 | createRateLimiterOptions: jest.fn()
7 | }
8 | expect(rateLimiterOptionsFactory).toBeDefined()
9 | expect(rateLimiterOptionsFactory.createRateLimiterOptions).toBeDefined()
10 | })
11 | })
12 |
13 | describe('RateLimiterModuleAsyncOptions', () => {
14 | it('should validate that RateLimiterModuleAsyncOptions with no properties', async () => {
15 | const rateLimiterModuleAsyncOptions: RateLimiterModuleAsyncOptions = {}
16 | expect(rateLimiterModuleAsyncOptions).toBeDefined()
17 | expect(rateLimiterModuleAsyncOptions.useExisting).toBeUndefined()
18 | })
19 |
20 | it('should validate that RateLimiterModuleAsyncOptions with optional properties', async () => {
21 | const rateLimiterModuleAsyncOptions: RateLimiterModuleAsyncOptions = {
22 | useFactory: jest.fn(),
23 | inject: ['test']
24 | }
25 | expect(rateLimiterModuleAsyncOptions).toBeDefined()
26 | expect(rateLimiterModuleAsyncOptions.useFactory).toBeDefined()
27 | expect(rateLimiterModuleAsyncOptions.inject.length).toBe(1)
28 | })
29 | })
30 |
31 | describe('RateLimiterOptions', () => {
32 | it('should validate that RateLimiterOptions with no properties', async () => {
33 | const rateLimiterOptions: RateLimiterOptions = {}
34 | expect(rateLimiterOptions).toBeDefined()
35 | expect(rateLimiterOptions.for).toBeUndefined()
36 | })
37 |
38 | it('should validate that RateLimiterOptions with no properties', async () => {
39 | const rateLimiterOptions: RateLimiterOptions = {
40 | for: 'Express',
41 | type: 'Memory',
42 | points: 2,
43 | pointsConsumed: 3,
44 | dbName: 'test'
45 | }
46 | expect(rateLimiterOptions).toBeDefined()
47 | expect(rateLimiterOptions.for).toBe('Express')
48 | expect(rateLimiterOptions.type).toBe('Memory')
49 | expect(rateLimiterOptions.points).toBe(2)
50 | expect(rateLimiterOptions.pointsConsumed).toBe(3)
51 | expect(rateLimiterOptions.dbName).toBe('test')
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/lib/rate-limiter.interface.ts:
--------------------------------------------------------------------------------
1 | import { Provider } from '@nestjs/common'
2 | import { ModuleMetadata, Type } from '@nestjs/common/interfaces'
3 | import { RateLimiterRes } from 'rate-limiter-flexible'
4 |
5 | export interface RateLimiterOptions {
6 | for?: 'Express' | 'Fastify' | 'Microservice' | 'ExpressGraphql' | 'FastifyGraphql'
7 | type?: 'Memory' | 'Redis' | 'Memcache' | 'Postgres' | 'MySQL' | 'Mongo'
8 | keyPrefix?: string
9 | points?: number
10 | pointsConsumed?: number
11 | inmemoryBlockDuration?: number
12 | duration?: number
13 | blockDuration?: number
14 | inmemoryBlockOnConsumed?: number
15 | queueEnabled?: boolean
16 | whiteList?: string[]
17 | blackList?: string[]
18 | storeClient?: any
19 | insuranceLimiter?: any
20 | storeType?: string
21 | dbName?: string
22 | tableName?: string
23 | tableCreated?: boolean
24 | clearExpiredByTimeout?: boolean
25 | execEvenly?: boolean
26 | execEvenlyMinDelayMs?: number
27 | indexKeyPrefix?: {}
28 | maxQueueSize?: number
29 | omitResponseHeaders?: boolean
30 | errorMessage?: string
31 | logger?: boolean
32 | customResponseSchema?: (rateLimiterResponse: RateLimiterRes) => {}
33 | }
34 |
35 | export interface RateLimiterOptionsFactory {
36 | createRateLimiterOptions(): Promise | RateLimiterOptions
37 | }
38 |
39 | export interface RateLimiterModuleAsyncOptions extends Pick {
40 | useExisting?: Type
41 | useClass?: Type
42 | useFactory?: (...args: any[]) => Promise | RateLimiterOptions
43 | inject?: any[]
44 | extraProviders?: Provider[]
45 | }
46 |
--------------------------------------------------------------------------------
/lib/rate-limiter.module.spec.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Provider } from '@nestjs/common'
2 | import { RateLimiterModule } from './rate-limiter.module'
3 | import { RateLimiterOptions, RateLimiterModuleAsyncOptions } from './rate-limiter.interface'
4 |
5 | describe('RateLimiterModule', () => {
6 | it('should validate that RateLimiterModule exists', async () => {
7 | expect(RateLimiterModule).toBeDefined()
8 | })
9 |
10 | it('should register RateLimiterModule with empty options', async () => {
11 | const rateLimiterOptions: RateLimiterOptions = {}
12 |
13 | const registeredDynamicModule: DynamicModule = RateLimiterModule.register(rateLimiterOptions)
14 |
15 | expect(registeredDynamicModule).toBeDefined()
16 | expect(typeof registeredDynamicModule.module).toBeDefined()
17 | expect(registeredDynamicModule.providers.length).toBe(1)
18 | const rateLimitOptionsProvider: any = registeredDynamicModule.providers[0]
19 | expect(rateLimitOptionsProvider.provide).toBe('RATE_LIMITER_OPTIONS')
20 | expect(rateLimitOptionsProvider.useValue).toBeDefined()
21 | })
22 |
23 | it('should register RateLimiterModule with default options', async () => {
24 | const rateLimiterOptions: RateLimiterOptions = {}
25 |
26 | const registeredDynamicModule: DynamicModule = RateLimiterModule.register()
27 |
28 | expect(registeredDynamicModule).toBeDefined()
29 | expect(typeof registeredDynamicModule.module).toBeDefined()
30 | expect(registeredDynamicModule.providers.length).toBe(1)
31 | const rateLimitOptionsProvider: any = registeredDynamicModule.providers[0]
32 | expect(rateLimitOptionsProvider.provide).toBe('RATE_LIMITER_OPTIONS')
33 | expect(rateLimitOptionsProvider.useValue).toBeDefined()
34 |
35 | const options: RateLimiterOptions = rateLimitOptionsProvider.useValue
36 |
37 | expect(options.for).toBe('Express')
38 | })
39 |
40 | it('should register async RateLimiterModule with async options', async () => {
41 | const rateLimiterOptions: RateLimiterModuleAsyncOptions = {}
42 |
43 | const registeredDynamicModule: DynamicModule = RateLimiterModule.registerAsync(rateLimiterOptions)
44 |
45 | expect(registeredDynamicModule).toBeDefined()
46 | expect(typeof registeredDynamicModule.module).toBeDefined()
47 | expect(registeredDynamicModule.providers.length).toBe(2)
48 | const rateLimitOptionsProvider: any = registeredDynamicModule.providers[0]
49 | expect(rateLimitOptionsProvider.provide).toBe('RATE_LIMITER_OPTIONS')
50 | expect(rateLimitOptionsProvider.useFactory).toBeDefined()
51 | expect(rateLimitOptionsProvider.inject).toBeDefined()
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/lib/rate-limiter.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, DynamicModule, Provider } from '@nestjs/common'
2 | import { defaultRateLimiterOptions } from './default-options'
3 | import { RateLimiterOptions, RateLimiterModuleAsyncOptions, RateLimiterOptionsFactory } from './rate-limiter.interface'
4 |
5 | @Module({
6 | exports: ['RATE_LIMITER_OPTIONS'],
7 | providers: [{ provide: 'RATE_LIMITER_OPTIONS', useValue: defaultRateLimiterOptions }]
8 | })
9 | export class RateLimiterModule {
10 | static register(options: RateLimiterOptions = defaultRateLimiterOptions): DynamicModule {
11 | return {
12 | module: RateLimiterModule,
13 | providers: [{ provide: 'RATE_LIMITER_OPTIONS', useValue: options }]
14 | }
15 | }
16 |
17 | static registerAsync(options: RateLimiterModuleAsyncOptions): DynamicModule {
18 | return {
19 | module: RateLimiterModule,
20 | imports: options.imports,
21 | providers: [...this.createAsyncProviders(options), ...(options.extraProviders || [])]
22 | }
23 | }
24 |
25 | private static createAsyncProviders(options: RateLimiterModuleAsyncOptions): Provider[] {
26 | if (options.useExisting || options.useFactory) {
27 | return [this.createAsyncOptionsProvider(options)]
28 | }
29 | return [
30 | this.createAsyncOptionsProvider(options),
31 | {
32 | provide: options.useClass,
33 | useClass: options.useClass
34 | }
35 | ]
36 | }
37 |
38 | private static createAsyncOptionsProvider(options: RateLimiterModuleAsyncOptions): Provider {
39 | if (options.useFactory) {
40 | return {
41 | provide: 'RATE_LIMITER_OPTIONS',
42 | useFactory: options.useFactory,
43 | inject: options.inject || []
44 | }
45 | }
46 | return {
47 | provide: 'RATE_LIMITER_OPTIONS',
48 | useFactory: async (optionsFactory: RateLimiterOptionsFactory) => optionsFactory.createRateLimiterOptions(),
49 | inject: [options.useExisting || options.useClass]
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nestjs-rate-limiter",
3 | "version": "3.1.0",
4 | "description": "Highly configurable and extensible rate limiter library",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/ozkanonur/nestjs-rate-limiter.git"
8 | },
9 | "keywords": [
10 | "nestjs",
11 | "nest",
12 | "rate-limiter",
13 | "request-limiter",
14 | "security"
15 | ],
16 | "main": "dist/index.js",
17 | "author": "Onur Ozkan ",
18 | "contributors": [
19 | "Onur Ozkan ",
20 | "Ryan Dowling "
21 | ],
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/ozkanonur/nestjs-rate-limiter/issues"
25 | },
26 | "homepage": "https://github.com/ozkanonur/nestjs-rate-limiter#readme",
27 | "publishConfig": {
28 | "access": "public"
29 | },
30 | "scripts": {
31 | "build": "rm -rf dist && tsc -p tsconfig.json",
32 | "lint": "prettier --write lib",
33 | "test": "jest --config ./jest.config.js"
34 | },
35 | "dependencies": {
36 | "rate-limiter-flexible": "2.1.10"
37 | },
38 | "devDependencies": {
39 | "@nestjs/common": "latest",
40 | "@nestjs/core": "latest",
41 | "@types/jest": "^26.0.15",
42 | "@types/node": "^14.11.1",
43 | "jest": "^26.6.3",
44 | "prettier": "^2.1.1",
45 | "reflect-metadata": "0.1.13",
46 | "rxjs": "^6.6.3",
47 | "ts-node": "^9.0.0",
48 | "ts-jest": "^26.4.4",
49 | "typescript": "^4.0.2"
50 | }
51 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "noImplicitAny": false,
6 | "removeComments": true,
7 | "noLib": false,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es6",
11 | "sourceMap": false,
12 | "outDir": "./dist",
13 | "rootDir": "./lib",
14 | "skipLibCheck": true
15 | },
16 | "include": [
17 | "lib/**/*"
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "jsRules": {
7 | "no-unused-expression": true
8 | },
9 | "rules": {
10 | "quotemark": [
11 | true,
12 | "single"
13 | ],
14 | "member-access": [
15 | false
16 | ],
17 | "ordered-imports": [
18 | false
19 | ],
20 | "max-line-length": [
21 | true,
22 | 155
23 | ],
24 | "member-ordering": [
25 | false
26 | ],
27 | "interface-name": [
28 | false
29 | ],
30 | "arrow-parens": false,
31 | "object-literal-sort-keys": false
32 | },
33 | "rulesDirectory": []
34 | }
35 |
--------------------------------------------------------------------------------