rate-limiter-flexible 7.1.1 → 7.2.0

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/README.md CHANGED
@@ -26,12 +26,8 @@ Memory limiter also works in the browser.
26
26
 
27
27
  **Friendly.** No matter which node package you prefer: [`valkey-glide`](https://www.npmjs.com/package/@valkey/valkey-glide) or [`iovalkey`](https://www.npmjs.com/package/iovalkey), `redis` or `ioredis`, `sequelize`/`typeorm` or `knex`, `memcached`, native driver or `mongoose`. It works with all of them.
28
28
 
29
- **Safe for using with valkey cluster.** [`valkey-glide`](https://www.npmjs.com/package/@valkey/valkey-glide) implementation, [RateLimiterValkeyGlide](https://github.com/animir/node-rate-limiter-flexible/wiki/Valkey-Glide), is being tested to ensure compatibility and high performance.
30
-
31
29
  **In-memory blocks.** Avoid extra requests to store with [inMemoryBlockOnConsumed](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#inmemoryblockonconsumed).
32
30
 
33
- Allow **traffic bursts** with [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter).
34
-
35
31
  **Deno compatible** See [this example](https://gist.github.com/animir/d06ca92931677f330d3f2d4c6c3108e4)
36
32
 
37
33
  It uses a **fixed window**, as it is much faster than a rolling window.
@@ -123,12 +119,12 @@ Full documentation is on [Wiki](https://github.com/animir/node-rate-limiter-flex
123
119
  Some copy/paste examples on Wiki:
124
120
  * [Minimal protection against password brute-force](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#minimal-protection-against-password-brute-force)
125
121
  * [Login endpoint protection](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection)
122
+ * [Apply Block Strategy](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#apply-in-memory-block-strategy-to-avoid-extra-requests-to-store)
123
+ * [Setup Insurance Strategy](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#setup-insurance-strategy-for-store-limiters)
126
124
  * [Websocket connection prevent flooding](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding)
127
125
  * [Dynamic block duration](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#dynamic-block-duration)
128
126
  * [Authorized users specific limits](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users)
129
127
  * [Different limits for different parts of application](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#different-limits-for-different-parts-of-application)
130
- * [Apply Block Strategy](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#apply-in-memory-block-strategy-to-avoid-extra-requests-to-store)
131
- * [Setup Insurance Strategy](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#setup-insurance-strategy-for-store-limiters)
132
128
  * [Third-party API, crawler, bot rate limiting](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#third-party-api-crawler-bot-rate-limiting)
133
129
 
134
130
  ### Migration from other packages
@@ -139,19 +135,21 @@ Some copy/paste examples on Wiki:
139
135
 
140
136
  * [Options](https://github.com/animir/node-rate-limiter-flexible/wiki/Options)
141
137
  * [API methods](https://github.com/animir/node-rate-limiter-flexible/wiki/API-methods)
142
- * Valkey: [ValkeyGlide](https://github.com/animir/node-rate-limiter-flexible/wiki/Valkey-Glide) or [iovalkey](https://github.com/animir/node-rate-limiter-flexible/wiki/IoValkey)
143
- * [Redis](https://github.com/animir/node-rate-limiter-flexible/wiki/Redis)
144
- * [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
138
+
139
+ * [Drizzle](https://github.com/animir/node-rate-limiter-flexible/wiki/Drizzle)
145
140
  * [DynamoDb](https://github.com/animir/node-rate-limiter-flexible/wiki/DynamoDB)
146
141
  * [Etcd](https://github.com/animir/node-rate-limiter-flexible/wiki/Etcd)
147
- * [Prisma](https://github.com/animir/node-rate-limiter-flexible/wiki/Prisma)
148
- * [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter) Traffic burst support
142
+ * [Memcached](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
143
+ * [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
149
144
  * [Mongo](https://github.com/animir/node-rate-limiter-flexible/wiki/Mongo) (with [sharding support](https://github.com/animir/node-rate-limiter-flexible/wiki/Mongo#mongodb-sharding-options))
150
145
  * [MySQL](https://github.com/animir/node-rate-limiter-flexible/wiki/MySQL) (support Sequelize and Knex)
151
146
  * [Postgres](https://github.com/animir/node-rate-limiter-flexible/wiki/PostgreSQL) (support Sequelize, TypeORM and Knex)
147
+ * [Prisma](https://github.com/animir/node-rate-limiter-flexible/wiki/Prisma)
148
+ * [Redis](https://github.com/animir/node-rate-limiter-flexible/wiki/Redis)
152
149
  * [SQLite](https://github.com/animir/node-rate-limiter-flexible/wiki/SQLite)
150
+ * Valkey: [iovalkey](https://github.com/animir/node-rate-limiter-flexible/wiki/IoValkey) or [ValkeyGlide](https://github.com/animir/node-rate-limiter-flexible/wiki/Valkey-Glide)
153
151
  * [RateLimiterCluster](https://github.com/animir/node-rate-limiter-flexible/wiki/Cluster) ([PM2 cluster docs read here](https://github.com/animir/node-rate-limiter-flexible/wiki/PM2-cluster))
154
- * [Memcached](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
152
+ * [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter) Traffic burst support
155
153
  * [RateLimiterUnion](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterUnion) Combine 2 or more limiters to act as single
156
154
  * [RLWrapperBlackAndWhite](https://github.com/animir/node-rate-limiter-flexible/wiki/Black-and-White-lists) Black and White lists
157
155
  * [RateLimiterQueue](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterQueue) Rate limiter with FIFO queue
package/index.js CHANGED
@@ -12,6 +12,7 @@ const BurstyRateLimiter = require('./lib/BurstyRateLimiter');
12
12
  const RateLimiterRes = require('./lib/RateLimiterRes');
13
13
  const RateLimiterDynamo = require('./lib/RateLimiterDynamo');
14
14
  const RateLimiterPrisma = require('./lib/RateLimiterPrisma');
15
+ const RateLimiterDrizzle = require('./lib/RateLimiterDrizzle');
15
16
  const RateLimiterValkey = require('./lib/RateLimiterValkey');
16
17
  const RateLimiterValkeyGlide = require('./lib/RateLimiterValkeyGlide');
17
18
  const RateLimiterSQLite = require('./lib/RateLimiterSQLite');
@@ -39,5 +40,6 @@ module.exports = {
39
40
  RateLimiterValkeyGlide,
40
41
  RateLimiterSQLite,
41
42
  RateLimiterEtcd,
43
+ RateLimiterDrizzle,
42
44
  RateLimiterEtcdNonAtomic,
43
45
  };
@@ -0,0 +1,170 @@
1
+ let drizzleOperators = null;
2
+ const CLEANUP_INTERVAL_MS = 300000; // 5 minutes
3
+ const EXPIRED_THRESHOLD_MS = 3600000; // 1 hour
4
+
5
+ class RateLimiterDrizzleError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'RateLimiterDrizzleError';
9
+ }
10
+ }
11
+
12
+ function getDrizzleOperators() {
13
+ if (drizzleOperators) return drizzleOperators;
14
+
15
+ try {
16
+ const { and, or, gt, lt, eq, isNull , sql } = require('drizzle-orm');
17
+ drizzleOperators = { and, or, gt, lt, eq, isNull , sql };
18
+ return drizzleOperators;
19
+ } catch (error) {
20
+ throw new RateLimiterDrizzleError(
21
+ 'drizzle-orm is not installed. Please install drizzle-orm to use RateLimiterDrizzle.'
22
+ );
23
+ }
24
+ }
25
+
26
+ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
27
+ const RateLimiterRes = require('./RateLimiterRes');
28
+
29
+ class RateLimiterDrizzle extends RateLimiterStoreAbstract {
30
+ constructor(opts) {
31
+ super(opts);
32
+
33
+ if (!opts?.schema) {
34
+ throw new RateLimiterDrizzleError('Drizzle schema is required');
35
+ }
36
+
37
+ if (!opts?.storeClient) {
38
+ throw new RateLimiterDrizzleError('Drizzle client is required');
39
+ }
40
+
41
+ this.schema = opts.schema;
42
+ this.drizzleClient = opts.storeClient;
43
+ this.clearExpiredByTimeout = opts.clearExpiredByTimeout ?? true;
44
+
45
+ if (this.clearExpiredByTimeout) {
46
+ this._clearExpiredHourAgo();
47
+ }
48
+ }
49
+
50
+ _getRateLimiterRes(rlKey, changedPoints, result) {
51
+ const res = new RateLimiterRes();
52
+
53
+ let doc = result;
54
+ res.isFirstInDuration = doc.points === changedPoints;
55
+ res.consumedPoints = doc.points;
56
+ res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
57
+ res.msBeforeNext = doc.expire !== null
58
+ ? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
59
+ : -1;
60
+
61
+ return res;
62
+ }
63
+
64
+ async _upsert(key, points, msDuration, forceExpire = false) {
65
+ if (!this.drizzleClient) {
66
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
67
+ }
68
+
69
+ const { eq , sql } = getDrizzleOperators();
70
+ const now = new Date();
71
+ const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;
72
+
73
+ const query = await this.drizzleClient.transaction(async (tx) => {
74
+ const [existingRecord] = await tx
75
+ .select()
76
+ .from(this.schema)
77
+ .where(eq(this.schema.key, key))
78
+ .limit(1);
79
+
80
+ const shouldUpdateExpire =
81
+ forceExpire ||
82
+ !existingRecord?.expire ||
83
+ existingRecord?.expire <= now ||
84
+ newExpire === null;
85
+
86
+ const [data] = await tx
87
+ .insert(this.schema)
88
+ .values({
89
+ key,
90
+ points,
91
+ expire: newExpire,
92
+ })
93
+ .onConflictDoUpdate({
94
+ target: this.schema.key,
95
+ set: {
96
+ points: !shouldUpdateExpire
97
+ ? sql`${this.schema.points} + ${points}`
98
+ : points,
99
+ ...(shouldUpdateExpire && { expire: newExpire }),
100
+ },
101
+ })
102
+ .returning();
103
+
104
+ return data;
105
+ })
106
+
107
+ return query
108
+ }
109
+
110
+ async _get(rlKey) {
111
+ if (!this.drizzleClient) {
112
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
113
+ }
114
+
115
+ const { and, or, gt, eq, isNull } = getDrizzleOperators();
116
+
117
+ const [response] = await this.drizzleClient
118
+ .select()
119
+ .from(this.schema)
120
+ .where(
121
+ and(
122
+ eq(this.schema.key, rlKey),
123
+ or(gt(this.schema.expire, new Date()), isNull(this.schema.expire))
124
+ )
125
+ )
126
+ .limit(1);
127
+
128
+ return response || null;
129
+
130
+ }
131
+
132
+ async _delete(rlKey) {
133
+ if (!this.drizzleClient) {
134
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
135
+ }
136
+
137
+ const { eq } = getDrizzleOperators();
138
+
139
+ const [result] = await this.drizzleClient
140
+ .delete(this.schema)
141
+ .where(eq(this.schema.key, rlKey))
142
+ .returning({ key: this.schema.key });
143
+
144
+ return !!result?.key
145
+ }
146
+
147
+ _clearExpiredHourAgo() {
148
+ if (this._clearExpiredTimeoutId) {
149
+ clearTimeout(this._clearExpiredTimeoutId);
150
+ }
151
+
152
+ const { lt } = getDrizzleOperators();
153
+
154
+ this._clearExpiredTimeoutId = setTimeout(async () => {
155
+ try {
156
+ await this.drizzleClient
157
+ .delete(this.schema)
158
+ .where(lt(this.schema.expire, new Date(Date.now() - EXPIRED_THRESHOLD_MS)));
159
+ } catch (error) {
160
+ console.warn('Failed to clear expired records:', error);
161
+ }
162
+
163
+ this._clearExpiredHourAgo();
164
+ }, CLEANUP_INTERVAL_MS);
165
+
166
+ this._clearExpiredTimeoutId.unref();
167
+ }
168
+ }
169
+
170
+ module.exports = RateLimiterDrizzle;
package/lib/index.d.ts CHANGED
@@ -241,6 +241,10 @@ interface IRateLimiterStoreNoAutoExpiryOptions extends IRateLimiterStoreOptions
241
241
  clearExpiredByTimeout?: boolean;
242
242
  }
243
243
 
244
+ interface IRateLimiterStoreNoAutoExpiryOptionsAndSchema extends IRateLimiterStoreNoAutoExpiryOptions {
245
+ schema: any;
246
+ }
247
+
244
248
  interface IRateLimiterMongoOptions extends IRateLimiterStoreOptions {
245
249
  indexKeyPrefix?: {
246
250
  [key: string]: any;
@@ -366,6 +370,10 @@ export class RateLimiterPrisma extends RateLimiterStoreAbstract {
366
370
  constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
367
371
  }
368
372
 
373
+ export class RateLimiterDrizzle extends RateLimiterStoreAbstract {
374
+ constructor(opts: IRateLimiterStoreNoAutoExpiryOptionsAndSchema, cb?: ICallbackReady);
375
+ }
376
+
369
377
  export class RateLimiterMemcache extends RateLimiterStoreAbstract { }
370
378
 
371
379
  export class RateLimiterUnion {
@@ -439,7 +447,7 @@ interface IRateLimiterValkeyGlideOptions extends IRateLimiterStoreOptions {
439
447
  * - KEYS[1]: The key being rate limited
440
448
  * - ARGV[1]: Points to consume (as string, use tonumber() to convert)
441
449
  * - ARGV[2]: Duration in seconds (as string, use tonumber() to convert)
442
- *
450
+ *
443
451
  * Must return an array with exactly two elements:
444
452
  * - [0]: Consumed points (number)
445
453
  * - [1]: TTL in milliseconds (number)
@@ -449,7 +457,7 @@ interface IRateLimiterValkeyGlideOptions extends IRateLimiterStoreOptions {
449
457
  /**
450
458
  * Custom name for the function library, defaults to 'ratelimiter'.
451
459
  * The name is used to identify the library of the Lua function.
452
- * A custom name should be used only if you want to use different
460
+ * A custom name should be used only if you want to use different
453
461
  * libraries for different rate limiters.
454
462
  * @default 'ratelimiter'
455
463
  */
@@ -462,9 +470,9 @@ interface IRateLimiterValkeyGlideOptions extends IRateLimiterStoreOptions {
462
470
  export class RateLimiterValkeyGlide extends RateLimiterStoreAbstract {
463
471
  /**
464
472
  * Creates a new instance of RateLimiterValkeyGlide
465
- *
473
+ *
466
474
  * @param opts Configuration options
467
- *
475
+ *
468
476
  * @example
469
477
  * ```typescript
470
478
  * // Basic usage
@@ -473,24 +481,24 @@ export class RateLimiterValkeyGlide extends RateLimiterStoreAbstract {
473
481
  * points: 5,
474
482
  * duration: 1
475
483
  * });
476
- *
484
+ *
477
485
  * // With custom Lua function
478
486
  * const customScript = `local key = KEYS[1]
479
487
  * local pointsToConsume = tonumber(ARGV[1]) or 0
480
488
  * local secDuration = tonumber(ARGV[2]) or 0
481
- *
489
+ *
482
490
  * -- Custom implementation
483
491
  * -- ...
484
- *
492
+ *
485
493
  * -- Must return exactly two values: [consumed_points, ttl_in_ms]
486
494
  * return {consumed, ttl}`;
487
- *
495
+ *
488
496
  * const rateLimiter = new RateLimiterValkeyGlide({
489
497
  * storeClient: glideClient,
490
498
  * points: 5,
491
499
  * customFunction: customScript
492
500
  * });
493
- *
501
+ *
494
502
  * // With insurance limiter
495
503
  * const rateLimiter = new RateLimiterValkeyGlide({
496
504
  * storeClient: primaryGlideClient,
@@ -508,7 +516,7 @@ export class RateLimiterValkeyGlide extends RateLimiterStoreAbstract {
508
516
  /**
509
517
  * Close the rate limiter and release resources
510
518
  * Note: The method won't close the Valkey client, as it may be shared with other instances.
511
- *
519
+ *
512
520
  * @returns Promise that resolves when the rate limiter is closed
513
521
  */
514
522
  close(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rate-limiter-flexible",
3
- "version": "7.1.1",
3
+ "version": "7.2.0",
4
4
  "description": "Node.js rate limiter by key and protection from DDoS and Brute-Force attacks in process Memory, Redis, MongoDb, Memcached, MySQL, PostgreSQL, Cluster or PM",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -10,7 +10,8 @@
10
10
  "valkey-cluster:down": "docker-compose -f docker-compose.valkey-cluster.yml down -v",
11
11
  "test:valkey-cluster": "VALKEY_CLUSTER_PORT=7001 mocha test/RateLimiterValkeyGlide.test.js -- -g 'RateLimiterValkeyGlide with cluster client'",
12
12
  "prisma:postgres": "prisma generate --schema=./test/RateLimiterPrisma/Postgres/schema.prisma && prisma db push --schema=./test/RateLimiterPrisma/Postgres/schema.prisma",
13
- "test": "npm run prisma:postgres && nyc --reporter=html --reporter=text mocha",
13
+ "drizzle:postgres": "cd ./test/RateLimiterDrizzle/Postgres && drizzle-kit push",
14
+ "test": "npm run prisma:postgres && npm run drizzle:postgres && nyc --reporter=html --reporter=text mocha",
14
15
  "debug-test": "mocha --inspect-brk lib/**/**.test.js",
15
16
  "coveralls": "cat ./coverage/lcov.info | coveralls",
16
17
  "eslint": "eslint --quiet lib/**/**.js test/**/**.js",
@@ -34,6 +35,7 @@
34
35
  "mysql",
35
36
  "postgres",
36
37
  "prisma",
38
+ "drizzle",
37
39
  "koa",
38
40
  "express",
39
41
  "hapi",
@@ -53,15 +55,18 @@
53
55
  "devDependencies": {
54
56
  "@aws-sdk/client-dynamodb": "^3.431.0",
55
57
  "@prisma/client": "^5.8.0",
58
+ "@valkey/valkey-glide": "^1.3.1",
56
59
  "better-sqlite3": "^11.9.0",
57
60
  "chai": "^4.1.2",
58
61
  "coveralls": "^3.0.1",
59
- "etcd3": "^1.1.2",
62
+ "drizzle-kit": "^0.31.4",
63
+ "drizzle-orm": "^0.44.3",
60
64
  "eslint": "^4.19.1",
61
65
  "eslint-config-airbnb-base": "^12.1.0",
62
66
  "eslint-plugin-import": "^2.7.0",
63
67
  "eslint-plugin-node": "^6.0.1",
64
68
  "eslint-plugin-security": "^1.4.0",
69
+ "etcd3": "^1.1.2",
65
70
  "ioredis": "^5.3.2",
66
71
  "iovalkey": "^0.3.1",
67
72
  "istanbul": "^1.1.0-alpha.1",
@@ -69,12 +74,12 @@
69
74
  "memcached-mock": "^0.1.0",
70
75
  "mocha": "^10.2.0",
71
76
  "nyc": "^15.1.0",
77
+ "pg": "^8.16.3",
72
78
  "prisma": "^5.8.0",
73
79
  "redis": "^4.6.8",
74
80
  "redis-mock": "^0.48.0",
75
81
  "sinon": "^17.0.1",
76
- "sqlite3": "^5.1.7",
77
- "@valkey/valkey-glide": "^1.3.1"
82
+ "sqlite3": "^5.1.7"
78
83
  },
79
84
  "browser": {
80
85
  "cluster": false,