rate-limiter-flexible 6.1.0 → 6.2.1

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.
@@ -2,6 +2,17 @@ const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract");
2
2
  const RateLimiterRes = require("./RateLimiterRes");
3
3
 
4
4
  class RateLimiterSQLite extends RateLimiterStoreAbstract {
5
+ /**
6
+ * Internal store type used to determine the SQLite client in use.
7
+ * It can be one of the following:
8
+ * - `"sqlite3".
9
+ * - `"better-sqlite3".
10
+ *
11
+ * @type {("sqlite3" | "better-sqlite3" | null)}
12
+ * @private
13
+ */
14
+ _internalStoreType = null;
15
+
5
16
  /**
6
17
  * @callback callback
7
18
  * @param {Object} err
@@ -10,66 +21,151 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
10
21
  * @param {callback} cb
11
22
  * Defaults {
12
23
  * ... see other in RateLimiterStoreAbstract
13
- *
14
- * storeClient: sqliteClient, // SQLite database instance
24
+ * storeClient: sqliteClient, // SQLite database instance (sqlite3, better-sqlite3, or knex instance)
25
+ * storeType: 'sqlite3' | 'better-sqlite3' | 'knex', // Optional, defaults to 'sqlite3'
15
26
  * tableName: 'string',
27
+ * tableCreated: boolean,
28
+ * clearExpiredByTimeout: boolean,
16
29
  * }
17
30
  */
18
31
  constructor(opts, cb = null) {
19
32
  super(opts);
20
33
 
21
34
  this.client = opts.storeClient;
35
+ this.storeType = opts.storeType || "sqlite3";
22
36
  this.tableName = opts.tableName;
23
37
  this.tableCreated = opts.tableCreated || false;
24
38
  this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
25
39
 
26
- if (!/^[A-Za-z0-9_]*$/.test(this.tableName)) {
27
- const err = new Error("Table name must contain only letters and numbers");
28
- if (typeof cb === "function") {
29
- return cb(err);
30
- }
31
- throw err;
32
- }
40
+ this._validateStoreTypes(cb);
41
+ this._validateStoreClient(cb);
42
+ this._setInternalStoreType(cb);
43
+ this._validateTableName(cb);
33
44
 
34
45
  if (!this.tableCreated) {
35
46
  this._createDbAndTable()
36
47
  .then(() => {
37
48
  this.tableCreated = true;
38
-
39
- if (this.clearExpiredByTimeout) {
40
- this._clearExpiredHourAgo();
41
- }
42
- if (typeof cb === "function") {
43
- cb();
44
- }
49
+ if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
50
+ if (typeof cb === "function") cb();
45
51
  })
46
52
  .catch((err) => {
47
- if (typeof cb === "function") {
48
- cb(err);
49
- } else {
50
- throw err;
51
- }
53
+ if (typeof cb === "function") cb(err);
54
+ else throw err;
52
55
  });
53
56
  } else {
54
- if (this.clearExpiredByTimeout) {
55
- this._clearExpiredHourAgo();
57
+ if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
58
+ if (typeof cb === "function") cb();
59
+ }
60
+ }
61
+ _validateStoreTypes(cb) {
62
+ const validStoreTypes = ["sqlite3", "better-sqlite3", "knex"];
63
+ if (!validStoreTypes.includes(this.storeType)) {
64
+ const err = new Error(
65
+ `storeType must be one of: ${validStoreTypes.join(", ")}`
66
+ );
67
+ if (typeof cb === "function") return cb(err);
68
+ throw err;
69
+ }
70
+ }
71
+ _validateStoreClient(cb) {
72
+ if (this.storeType === "sqlite3") {
73
+ if (typeof this.client.run !== "function") {
74
+ const err = new Error(
75
+ "storeClient must be an instance of sqlite3.Database when storeType is 'sqlite3' or no storeType was provided"
76
+ );
77
+ if (typeof cb === "function") return cb(err);
78
+ throw err;
79
+ }
80
+ } else if (this.storeType === "better-sqlite3") {
81
+ if (
82
+ typeof this.client.prepare !== "function" ||
83
+ typeof this.client.run !== "undefined"
84
+ ) {
85
+ const err = new Error(
86
+ "storeClient must be an instance of better-sqlite3.Database when storeType is 'better-sqlite3'"
87
+ );
88
+ if (typeof cb === "function") return cb(err);
89
+ throw err;
56
90
  }
57
- if (typeof cb === "function") {
58
- cb();
91
+ } else if (this.storeType === "knex") {
92
+ if (typeof this.client.raw !== "function") {
93
+ const err = new Error(
94
+ "storeClient must be an instance of Knex when storeType is 'knex'"
95
+ );
96
+ if (typeof cb === "function") return cb(err);
97
+ throw err;
59
98
  }
60
99
  }
61
100
  }
101
+ _setInternalStoreType(cb) {
102
+ if (this.storeType === "knex") {
103
+ const knexClientType = this.client.client.config.client;
104
+ if (knexClientType === "sqlite3") {
105
+ this._internalStoreType = "sqlite3";
106
+ } else if (knexClientType === "better-sqlite3") {
107
+ this._internalStoreType = "better-sqlite3";
108
+ } else {
109
+ const err = new Error(
110
+ "Knex must be configured with 'sqlite3' or 'better-sqlite3' for RateLimiterSQLite"
111
+ );
112
+ if (typeof cb === "function") return cb(err);
113
+ throw err;
114
+ }
115
+ } else {
116
+ this._internalStoreType = this.storeType;
117
+ }
118
+ }
119
+ _validateTableName(cb) {
120
+ if (!/^[A-Za-z0-9_]*$/.test(this.tableName)) {
121
+ const err = new Error("Table name must contain only letters and numbers");
122
+ if (typeof cb === "function") return cb(err);
123
+ throw err;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Acquires the database connection based on the storeType.
129
+ * @returns {Promise<Object>} The database client or connection
130
+ */
131
+ async _getConnection() {
132
+ if (this.storeType === "knex") {
133
+ return this.client.client.acquireConnection(); // Acquire raw connection from knex pool
134
+ }
135
+ return this.client; // For sqlite3 and better-sqlite3, return the client directly
136
+ }
137
+
138
+ /**
139
+ * Releases the database connection if necessary.
140
+ * @param {Object} conn The database client or connection
141
+ */
142
+ _releaseConnection(conn) {
143
+ if (this.storeType === "knex") {
144
+ this.client.client.releaseConnection(conn);
145
+ }
146
+ // No release needed for direct sqlite3 or better-sqlite3 clients
147
+ }
148
+
62
149
  async _createDbAndTable() {
63
- return new Promise((resolve, reject) => {
64
- const createTableSQL = this._getCreateTableSQL();
65
- this.client.run(createTableSQL, (err) => {
66
- if (err) {
67
- reject(err);
68
- } else {
69
- resolve();
70
- }
71
- });
72
- });
150
+ const conn = await this._getConnection();
151
+ try {
152
+ switch (this._internalStoreType) {
153
+ case "sqlite3":
154
+ await new Promise((resolve, reject) => {
155
+ conn.run(this._getCreateTableSQL(), (err) =>
156
+ err ? reject(err) : resolve()
157
+ );
158
+ });
159
+ break;
160
+ case "better-sqlite3":
161
+ conn.prepare(this._getCreateTableSQL()).run();
162
+ break;
163
+ default:
164
+ throw new Error("Unsupported internalStoreType");
165
+ }
166
+ } finally {
167
+ this._releaseConnection(conn);
168
+ }
73
169
  }
74
170
 
75
171
  _getCreateTableSQL() {
@@ -81,26 +177,33 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
81
177
  }
82
178
 
83
179
  _clearExpiredHourAgo() {
84
- if (this._clearExpiredTimeoutId) {
85
- clearTimeout(this._clearExpiredTimeoutId);
86
- }
180
+ if (this._clearExpiredTimeoutId) clearTimeout(this._clearExpiredTimeoutId);
87
181
  this._clearExpiredTimeoutId = setTimeout(() => {
88
- this.clearExpired(Date.now() - 3600000) // Never rejected
89
- .then(() => {
90
- this._clearExpiredHourAgo();
91
- });
92
- }, 300000);
182
+ this.clearExpired(Date.now() - 3600000) // 1 hour ago
183
+ .then(() => this._clearExpiredHourAgo());
184
+ }, 300000); // Every 5 minutes
93
185
  this._clearExpiredTimeoutId.unref();
94
186
  }
95
187
 
96
- clearExpired(nowMs) {
97
- return new Promise((resolve) => {
98
- this.client.run(
99
- `DELETE FROM ${this.tableName} WHERE expire < ?`,
100
- [nowMs],
101
- () => resolve()
102
- );
103
- });
188
+ async clearExpired(nowMs) {
189
+ const sql = `DELETE FROM ${this.tableName} WHERE expire < ?`;
190
+ const conn = await this._getConnection();
191
+ try {
192
+ switch (this._internalStoreType) {
193
+ case "sqlite3":
194
+ await new Promise((resolve, reject) => {
195
+ conn.run(sql, [nowMs], (err) => (err ? reject(err) : resolve()));
196
+ });
197
+ break;
198
+ case "better-sqlite3":
199
+ conn.prepare(sql).run(nowMs);
200
+ break;
201
+ default:
202
+ throw new Error("Unsupported internalStoreType");
203
+ }
204
+ } finally {
205
+ this._releaseConnection(conn);
206
+ }
104
207
  }
105
208
 
106
209
  _getRateLimiterRes(rlKey, changedPoints, result) {
@@ -111,96 +214,124 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
111
214
  res.msBeforeNext = result.expire
112
215
  ? Math.max(result.expire - Date.now(), 0)
113
216
  : -1;
114
-
115
217
  return res;
116
218
  }
117
219
 
118
- async _upsertTransaction(rlKey, points, msDuration, forceExpire) {
119
- return new Promise((resolve, reject) => {
120
- const dateNow = Date.now();
121
- const newExpire = msDuration > 0 ? dateNow + msDuration : null;
122
-
123
- const upsertQuery = forceExpire
124
- ? `INSERT OR REPLACE INTO ${this.tableName} (key, points, expire) VALUES (?, ?, ?) RETURNING points, expire;`
125
- : `
126
- INSERT INTO ${this.tableName} (key, points, expire)
127
- VALUES (?, ?, ?)
128
- ON CONFLICT(key) DO UPDATE SET
129
- points = CASE
130
- WHEN expire IS NULL OR expire > ? THEN points + excluded.points
131
- ELSE excluded.points
132
- END,
133
- expire = CASE
134
- WHEN expire IS NULL OR expire > ? THEN expire
135
- ELSE excluded.expire
136
- END
137
- RETURNING points, expire;
138
- `;
139
-
140
- const upsertParams = forceExpire
141
- ? [rlKey, points, newExpire]
142
- : [rlKey, points, newExpire, dateNow, dateNow];
143
-
144
- this.client.serialize(() => {
145
- this.client.run("SAVEPOINT rate_limiter_trx;", (savepointErr) => {
146
- if (savepointErr) return reject(savepointErr);
147
-
148
- this.client.get(upsertQuery, upsertParams, (queryErr, row) => {
149
- if (queryErr) {
150
- return this.client.run(
151
- "ROLLBACK TO SAVEPOINT rate_limiter_trx;",
152
- () => reject(queryErr)
220
+ async _upsertTransactionSQLite3(conn, upsertQuery, upsertParams) {
221
+ return await new Promise((resolve, reject) => {
222
+ conn.serialize(() => {
223
+ conn.run("SAVEPOINT rate_limiter_trx;", (err) => {
224
+ if (err) return reject(err);
225
+ conn.get(upsertQuery, upsertParams, (err, row) => {
226
+ if (err) {
227
+ conn.run("ROLLBACK TO SAVEPOINT rate_limiter_trx;", () =>
228
+ reject(err)
153
229
  );
230
+ return;
154
231
  }
155
-
156
- this.client.run("RELEASE SAVEPOINT rate_limiter_trx;", () =>
157
- resolve(row)
158
- );
232
+ conn.run("RELEASE SAVEPOINT rate_limiter_trx;", () => resolve(row));
159
233
  });
160
234
  });
161
235
  });
162
236
  });
163
237
  }
238
+
239
+ async _upsertTransactionBetterSQLite3(conn, upsertQuery, upsertParams) {
240
+ return conn.transaction(() =>
241
+ conn.prepare(upsertQuery).get(...upsertParams)
242
+ )();
243
+ }
244
+ async _upsertTransaction(rlKey, points, msDuration, forceExpire) {
245
+ const dateNow = Date.now();
246
+ const newExpire = msDuration > 0 ? dateNow + msDuration : null;
247
+ const upsertQuery = forceExpire
248
+ ? `INSERT OR REPLACE INTO ${this.tableName} (key, points, expire) VALUES (?, ?, ?) RETURNING points, expire`
249
+ : `INSERT INTO ${this.tableName} (key, points, expire)
250
+ VALUES (?, ?, ?)
251
+ ON CONFLICT(key) DO UPDATE SET
252
+ points = CASE WHEN expire IS NULL OR expire > ? THEN points + excluded.points ELSE excluded.points END,
253
+ expire = CASE WHEN expire IS NULL OR expire > ? THEN expire ELSE excluded.expire END
254
+ RETURNING points, expire`;
255
+ const upsertParams = forceExpire
256
+ ? [rlKey, points, newExpire]
257
+ : [rlKey, points, newExpire, dateNow, dateNow];
258
+
259
+ const conn = await this._getConnection();
260
+ try {
261
+ switch (this._internalStoreType) {
262
+ case "sqlite3":
263
+ return this._upsertTransactionSQLite3(
264
+ conn,
265
+ upsertQuery,
266
+ upsertParams
267
+ );
268
+ case "better-sqlite3":
269
+ return this._upsertTransactionBetterSQLite3(
270
+ conn,
271
+ upsertQuery,
272
+ upsertParams
273
+ );
274
+ default:
275
+ throw new Error("Unsupported internalStoreType");
276
+ }
277
+ } finally {
278
+ this._releaseConnection(conn);
279
+ }
280
+ }
281
+
164
282
  _upsert(rlKey, points, msDuration, forceExpire = false) {
165
283
  if (!this.tableCreated) {
166
- return Promise.reject(Error("Table is not created yet"));
284
+ return Promise.reject(new Error("Table is not created yet"));
167
285
  }
168
286
  return this._upsertTransaction(rlKey, points, msDuration, forceExpire);
169
287
  }
170
- _get(rlKey) {
171
- return new Promise((resolve, reject) => {
172
- this.client.get(
173
- `SELECT points, expire FROM ${this.tableName} WHERE key = ? AND (expire > ? OR expire IS NULL)`,
174
- [rlKey, Date.now()],
175
- (err, row) => {
176
- if (err) {
177
- reject(err);
178
- } else {
179
- resolve(row || null);
180
- }
181
- }
182
- );
183
- });
288
+
289
+ async _get(rlKey) {
290
+ const sql = `SELECT points, expire FROM ${this.tableName} WHERE key = ? AND (expire > ? OR expire IS NULL)`;
291
+ const now = Date.now();
292
+ const conn = await this._getConnection();
293
+ try {
294
+ switch (this._internalStoreType) {
295
+ case "sqlite3":
296
+ return await new Promise((resolve, reject) => {
297
+ conn.get(sql, [rlKey, now], (err, row) =>
298
+ err ? reject(err) : resolve(row || null)
299
+ );
300
+ });
301
+ case "better-sqlite3":
302
+ return conn.prepare(sql).get(rlKey, now) || null;
303
+ default:
304
+ throw new Error("Unsupported internalStoreType");
305
+ }
306
+ } finally {
307
+ this._releaseConnection(conn);
308
+ }
184
309
  }
185
310
 
186
- _delete(rlKey) {
311
+ async _delete(rlKey) {
187
312
  if (!this.tableCreated) {
188
- return Promise.reject(Error("Table is not created yet"));
313
+ return Promise.reject(new Error("Table is not created yet"));
314
+ }
315
+ const sql = `DELETE FROM ${this.tableName} WHERE key = ?`;
316
+ const conn = await this._getConnection();
317
+ try {
318
+ switch (this._internalStoreType) {
319
+ case "sqlite3":
320
+ return await new Promise((resolve, reject) => {
321
+ conn.run(sql, [rlKey], function (err) {
322
+ if (err) reject(err);
323
+ else resolve(this.changes > 0);
324
+ });
325
+ });
326
+ case "better-sqlite3":
327
+ const result = conn.prepare(sql).run(rlKey);
328
+ return result.changes > 0;
329
+ default:
330
+ throw new Error("Unsupported internalStoreType");
331
+ }
332
+ } finally {
333
+ this._releaseConnection(conn);
189
334
  }
190
-
191
- return new Promise((resolve, reject) => {
192
- this.client.run(
193
- `DELETE FROM ${this.tableName} WHERE key = ?`,
194
- [rlKey],
195
- function (err) {
196
- if (err) {
197
- reject(err);
198
- } else {
199
- resolve(this.changes > 0);
200
- }
201
- }
202
- );
203
- });
204
335
  }
205
336
  }
206
337
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rate-limiter-flexible",
3
- "version": "6.1.0",
3
+ "version": "6.2.1",
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": {
@@ -45,6 +45,7 @@
45
45
  "devDependencies": {
46
46
  "@aws-sdk/client-dynamodb": "^3.431.0",
47
47
  "@prisma/client": "^5.8.0",
48
+ "better-sqlite3": "^11.9.0",
48
49
  "chai": "^4.1.2",
49
50
  "coveralls": "^3.0.1",
50
51
  "eslint": "^4.19.1",
@@ -55,6 +56,7 @@
55
56
  "ioredis": "^5.3.2",
56
57
  "iovalkey": "^0.3.1",
57
58
  "istanbul": "^1.1.0-alpha.1",
59
+ "knex": "^3.1.0",
58
60
  "memcached-mock": "^0.1.0",
59
61
  "mocha": "^10.2.0",
60
62
  "nyc": "^15.1.0",