Skip to content

Commit

Permalink
Merge pull request #258 from animir/prisma-support
Browse files Browse the repository at this point in the history
Implement RateLimiterPrisma
  • Loading branch information
animir committed Feb 15, 2024
2 parents 013ab5c + a7d1608 commit c795d68
Show file tree
Hide file tree
Showing 10 changed files with 628 additions and 23 deletions.
19 changes: 17 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,24 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 20.x]
node-version: [16.x, 18.x, 20.x]
redis-version: [6, 7]

services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: secret
POSTGRES_USER: root
ports:
- 5432:5432
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout repository
uses: actions/checkout@v4.0.0
Expand All @@ -62,6 +77,6 @@ jobs:

- name: Start DynamoDB local
uses: rrainn/dynamodb-action@v3.0.0

- run: npm install
- run: npm run test
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
[![Coverage Status](https://coveralls.io/repos/animir/node-rate-limiter-flexible/badge.svg?branch=master)](https://coveralls.io/r/animir/node-rate-limiter-flexible?branch=master)
[![npm version](https://badge.fury.io/js/rate-limiter-flexible.svg)](https://www.npmjs.com/package/rate-limiter-flexible)
![npm](https://img.shields.io/npm/dm/rate-limiter-flexible.svg)
[![node version][node-image]][node-url]
[![deno version](https://img.shields.io/badge/deno-^1.5.3-lightgrey?logo=deno)](https://github.com/denoland/deno)

[node-image]: https://img.shields.io/badge/node.js-%3E=_14.0-green.svg?style=flat-square
[node-image]: https://img.shields.io/badge/node.js-%3E=_16.0-green.svg?style=flat-square
[node-url]: http://nodejs.org/download/

<img src="img/rlflx-logo-small.png" width="50" alt="Logo"/>
Expand Down Expand Up @@ -104,12 +103,13 @@ const headers = {
* no race conditions
* no production dependencies
* TypeScript declaration bundled
* allow traffic burst with [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter)
* Block Strategy against really powerful DDoS attacks (like 100k requests per sec) [Read about it and benchmarking here](https://github.com/animir/node-rate-limiter-flexible/wiki/In-memory-Block-Strategy)
* Insurance Strategy as emergency solution if database / store is down [Read about Insurance Strategy here](https://github.com/animir/node-rate-limiter-flexible/wiki/Insurance-Strategy)
* works in Cluster or PM2 without additional software [See RateLimiterCluster benchmark and detailed description here](https://github.com/animir/node-rate-limiter-flexible/wiki/Cluster)
* useful `get`, `set`, `block`, `delete`, `penalty` and `reward` methods

Full documentation is on [Wiki](https://github.com/animir/node-rate-limiter-flexible/wiki)

### Middlewares, plugins and other packages
* [Express middleware](https://github.com/animir/node-rate-limiter-flexible/wiki/Express-Middleware)
* [Koa middleware](https://github.com/animir/node-rate-limiter-flexible/wiki/Koa-Middleware)
Expand Down Expand Up @@ -137,15 +137,16 @@ Some copy/paste examples on Wiki:

* [Options](https://github.com/animir/node-rate-limiter-flexible/wiki/Options)
* [API methods](https://github.com/animir/node-rate-limiter-flexible/wiki/API-methods)
* [Redis](https://github.com/animir/node-rate-limiter-flexible/wiki/Redis)
* [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
* [DynamoDb](https://github.com/animir/node-rate-limiter-flexible/wiki/DynamoDB)
* [Prisma](https://github.com/animir/node-rate-limiter-flexible/wiki/Prisma)
* [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter) Traffic burst support
* [RateLimiterRedis](https://github.com/animir/node-rate-limiter-flexible/wiki/Redis)
* [RateLimiterDynamo](https://github.com/animir/node-rate-limiter-flexible/wiki/DynamoDB)
* [RateLimiterMemcache](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
* [RateLimiterMongo](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))
* [RateLimiterMySQL](https://github.com/animir/node-rate-limiter-flexible/wiki/MySQL) (support Sequelize and Knex)
* [RateLimiterPostgres](https://github.com/animir/node-rate-limiter-flexible/wiki/PostgreSQL) (support Sequelize, TypeORM and Knex)
* [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))
* [MySQL](https://github.com/animir/node-rate-limiter-flexible/wiki/MySQL) (support Sequelize and Knex)
* [Postgres](https://github.com/animir/node-rate-limiter-flexible/wiki/PostgreSQL) (support Sequelize, TypeORM and Knex)
* [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))
* [RateLimiterMemory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
* [Memcache](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
* [RateLimiterUnion](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterUnion) Combine 2 or more limiters to act as single
* [RLWrapperBlackAndWhite](https://github.com/animir/node-rate-limiter-flexible/wiki/Black-and-White-lists) Black and White lists
* [RateLimiterQueue](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterQueue) Rate limiter with FIFO queue
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ services:
container_name: dynamo
ports:
- 8000:8000
postgres:
image: postgres:latest
restart: always
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const RateLimiterQueue = require('./lib/RateLimiterQueue');
const BurstyRateLimiter = require('./lib/BurstyRateLimiter');
const RateLimiterRes = require('./lib/RateLimiterRes');
const RateLimiterDynamo = require('./lib/RateLimiterDynamo');
const RateLimiterPrisma = require('./lib/RateLimiterPrisma');

module.exports = {
RateLimiterRedis,
Expand All @@ -27,5 +28,6 @@ module.exports = {
RateLimiterQueue,
BurstyRateLimiter,
RateLimiterRes,
RateLimiterDynamo
RateLimiterDynamo,
RateLimiterPrisma,
};
126 changes: 126 additions & 0 deletions lib/RateLimiterPrisma.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
const RateLimiterRes = require('./RateLimiterRes');

class RateLimiterPrisma extends RateLimiterStoreAbstract {
/**
* Constructor for the rate limiter
* @param {Object} opts - Options for the rate limiter
*/
constructor(opts) {
super(opts);

this.modelName = opts.tableName || 'RateLimiterFlexible';
this.prismaClient = opts.storeClient;
this.clearExpiredByTimeout = opts.clearExpiredByTimeout || true;

if (!this.prismaClient) {
throw new Error('Prisma client is not provided');
}

if (this.clearExpiredByTimeout) {
this._clearExpiredHourAgo();
}
}

_getRateLimiterRes(rlKey, changedPoints, result) {
const res = new RateLimiterRes();

let doc = result;

res.isFirstInDuration = doc.points === changedPoints;
res.consumedPoints = doc.points;

res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
res.msBeforeNext = doc.expire !== null
? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
: -1;

return res;
}

_upsert(key, points, msDuration, forceExpire = false) {
if (!this.prismaClient) {
return Promise.reject(new Error('Prisma client is not established'));
}

const now = new Date();
const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;

return this.prismaClient.$transaction(async (prisma) => {
const existingRecord = await prisma[this.modelName].findFirst({
where: { key: key },
});

if (existingRecord) {
// Determine if we should update the expire field
const shouldUpdateExpire = forceExpire || !existingRecord.expire || existingRecord.expire <= now || newExpire === null;

return prisma[this.modelName].update({
where: { key: key },
data: {
points: !shouldUpdateExpire ? existingRecord.points + points : points,
...(shouldUpdateExpire && { expire: newExpire }),
},
});
} else {
return prisma[this.modelName].create({
data: {
key: key,
points: points,
expire: newExpire,
},
});
}
});
}

_get(rlKey) {
if (!this.prismaClient) {
return Promise.reject(new Error('Prisma client is not established'));
}

return this.prismaClient[this.modelName].findFirst({
where: {
AND: [
{ key: rlKey },
{
OR: [
{ expire: { gt: new Date() } },
{ expire: null },
],
},
],
},
});
}

_delete(rlKey) {
if (!this.prismaClient) {
return Promise.reject(new Error('Prisma client is not established'));
}

return this.prismaClient[this.modelName].deleteMany({
where: {
key: rlKey,
},
}).then(res => res.count > 0);
}

_clearExpiredHourAgo() {
if (this._clearExpiredTimeoutId) {
clearTimeout(this._clearExpiredTimeoutId);
}
this._clearExpiredTimeoutId = setTimeout(async () => {
await this.prismaClient[this.modelName].deleteMany({
where: {
expire: {
lt: new Date(Date.now() - 3600000),
},
},
});
this._clearExpiredHourAgo();
}, 300000); // Clear every 5 minutes
}
}

module.exports = RateLimiterPrisma;
3 changes: 2 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const LIMITER_TYPES = {
REDIS: 'redis',
MYSQL: 'mysql',
POSTGRES: 'postgres',
DYNAMO: 'dynamo'
DYNAMO: 'dynamo',
PRISMA: 'prisma',
};

const ERR_UNKNOWN_LIMITER_TYPE_MESSAGE = 'Unknown limiter type. Use one of LIMITER_TYPES constants.';
Expand Down
4 changes: 4 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ export class RateLimiterPostgres extends RateLimiterStoreAbstract {
constructor(opts: IRateLimiterPostgresOptions, cb?: ICallbackReady);
}

export class RateLimiterPrisma extends RateLimiterStoreAbstract {
constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
}

export class RateLimiterMemcache extends RateLimiterStoreAbstract { }

export class RateLimiterUnion {
Expand Down
22 changes: 13 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "rate-limiter-flexible",
"version": "4.0.1",
"version": "5.0.0",
"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",
"main": "index.js",
"scripts": {
"dc:up": "docker-compose -f docker-compose.yml up -d",
"dc:down": "docker-compose -f docker-compose.yml down",
"test": "nyc --reporter=html --reporter=text mocha",
"prisma:postgres": "prisma generate --schema=./test/RateLimiterPrisma/Postgres/schema.prisma && prisma db push --schema=./test/RateLimiterPrisma/Postgres/schema.prisma",
"test": "npm run prisma:postgres && nyc --reporter=html --reporter=text mocha",
"debug-test": "mocha --inspect-brk lib/**/**.test.js",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"eslint": "eslint --quiet lib/**/**.js test/**/**.js",
Expand All @@ -17,21 +18,22 @@
"url": "git+https://github.com/animir/node-rate-limiter-flexible.git"
},
"keywords": [
"ratelimter",
"authorization",
"security",
"rate",
"limit",
"ratelimter",
"brute",
"force",
"bruteforce",
"throttle",
"redis",
"mongodb",
"dynamodb",
"mysql",
"postgres",
"prisma",
"koa",
"express",
"hapi",
"auth",
"ddos",
"queue"
"hapi"
],
"author": "animir <animirr@gmail.com>",
"license": "ISC",
Expand All @@ -42,6 +44,7 @@
"types": "./lib/index.d.ts",
"devDependencies": {
"@aws-sdk/client-dynamodb": "^3.431.0",
"@prisma/client": "^5.8.0",
"chai": "^4.1.2",
"coveralls": "^3.0.1",
"eslint": "^4.19.1",
Expand All @@ -54,6 +57,7 @@
"memcached-mock": "^0.1.0",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"prisma": "^5.8.0",
"redis": "^4.6.8",
"redis-mock": "^0.48.0",
"sinon": "^17.0.1"
Expand Down
Loading

0 comments on commit c795d68

Please sign in to comment.