From 4da0ef0cd4c85d375223fc104debfced5a901aa9 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Feb 2024 16:52:56 +0100 Subject: [PATCH 1/9] implement RateLimiterPrisma --- README.md | 2 - docker-compose.yml | 8 + index.js | 4 +- lib/RateLimiterPrisma.js | 126 +++++ lib/constants.js | 3 +- lib/index.d.ts | 4 + package.json | 20 +- .../RateLimiterPrismaPostgres.test.js | 430 ++++++++++++++++++ test/RateLimiterPrisma/Postgres/schema.prisma | 14 + 9 files changed, 599 insertions(+), 12 deletions(-) create mode 100644 lib/RateLimiterPrisma.js create mode 100644 test/RateLimiterPrisma/Postgres/RateLimiterPrismaPostgres.test.js create mode 100644 test/RateLimiterPrisma/Postgres/schema.prisma diff --git a/README.md b/README.md index 745280e..551c740 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![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] @@ -104,7 +103,6 @@ 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) diff --git a/docker-compose.yml b/docker-compose.yml index 5a6d2cf..53ce591 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/index.js b/index.js index 3d06545..2064766 100644 --- a/index.js +++ b/index.js @@ -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, @@ -27,5 +28,6 @@ module.exports = { RateLimiterQueue, BurstyRateLimiter, RateLimiterRes, - RateLimiterDynamo + RateLimiterDynamo, + RateLimiterPrisma, }; diff --git a/lib/RateLimiterPrisma.js b/lib/RateLimiterPrisma.js new file mode 100644 index 0000000..497d3eb --- /dev/null +++ b/lib/RateLimiterPrisma.js @@ -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; diff --git a/lib/constants.js b/lib/constants.js index 2da360f..e24313c 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -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.'; diff --git a/lib/index.d.ts b/lib/index.d.ts index 36e2731..8f0f7f7 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -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 { diff --git a/package.json b/package.json index 16d3db5..b392947 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "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", @@ -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 ", "license": "ISC", @@ -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", @@ -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" diff --git a/test/RateLimiterPrisma/Postgres/RateLimiterPrismaPostgres.test.js b/test/RateLimiterPrisma/Postgres/RateLimiterPrismaPostgres.test.js new file mode 100644 index 0000000..7ea3a50 --- /dev/null +++ b/test/RateLimiterPrisma/Postgres/RateLimiterPrismaPostgres.test.js @@ -0,0 +1,430 @@ +const { describe, it, beforeEach, afterEach } = require('mocha'); +const { expect } = require('chai'); +const { PrismaClient } = require('@prisma/client'); +const sinon = require('sinon'); +const RateLimiterPrisma = require('../../../lib/RateLimiterPrisma'); +const RateLimiterMemory = require("../../../lib/RateLimiterMemory"); + +const prisma = new PrismaClient(); + +after(async () => { + await prisma.$disconnect(); +}) + +describe('RateLimiterPrisma Postgres with fixed window', function RateLimiterPrismaTest() { + this.timeout(6000); + + beforeEach(async () => { + await prisma.rateLimiterFlexible.deleteMany({}); + }); + + afterEach(async () => { + await prisma.rateLimiterFlexible.deleteMany({}); + }); + + it('consume 1 point', async () => { + const testKey = 'consume1'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + tableName: 'RateLimiterFlexible', + points: 2, + duration: 5, + }); + + await rateLimiter.consume(testKey); + const record = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey(testKey) }, + }); + + expect(record.points).to.equal(1); + }); + + it('rejected when consume more than maximum points', async () => { + const testKey = 'consume2'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 5, + }); + + try { + await rateLimiter.consume(testKey, 2); + } catch (rejRes) { + expect(rejRes.msBeforeNext >= 0).to.equal(true); + } + }); + + it('execute evenly over duration', async () => { + const testKey = 'consumeEvenly'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 2, + duration: 5, + execEvenly: true, + }); + + await rateLimiter.consume(testKey); // First consume should pass immediately + + const timeFirstConsume = Date.now(); + try { + await rateLimiter.consume(testKey); // Second consume should be delayed evenly over the duration + const diff = Date.now() - timeFirstConsume; + expect(diff).to.be.greaterThan(2400).and.lessThan(5100); // Check if the delay is within the expected range + } catch (err) { + expect.fail(`Test failed: ${err.message}`); + } + }); + + it('execute evenly over duration with minimum delay 20 ms', async () => { + const testKey = 'consumeEvenlyMinDelay'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 100, + duration: 1, + execEvenly: true, + execEvenlyMinDelayMs: 20, + }); + + await rateLimiter.consume(testKey); + const timeFirstConsume = Date.now(); + + await new Promise(resolve => setTimeout(resolve, 20)); + await rateLimiter.consume(testKey); + + expect(Date.now() - timeFirstConsume >= 20).to.equal(true); + }); + + it('makes penalty', async () => { + const testKey = 'penalty1'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 3, + duration: 5, + }); + + await rateLimiter.consume(testKey); + await rateLimiter.penalty(testKey); + + const record = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey(testKey) }, + }); + + expect(record.points).to.equal(2); + }); + + it('reward points', async () => { + const testKey = 'reward'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 5, + }); + + await rateLimiter.consume(testKey); + await rateLimiter.reward(testKey); + + const record = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey(testKey) }, + }); + + expect(record.points).to.equal(0); + }); + + it('block key in memory when inMemory block options set up', async () => { + const testKey = 'blockmem'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 5, + inMemoryBlockOnConsumed: 2, + inMemoryBlockDuration: 10, + }); + + await rateLimiter.consume(testKey); + try { + await rateLimiter.consume(testKey); + } catch (rejRes) { + expect(rejRes.msBeforeNext > 5000 && rejRes.remainingPoints === 0).to.equal(true); + } + }); + + it('block key in memory for msBeforeNext milliseconds', async () => { + const testKey = 'blockmempoints'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 5, + inMemoryBlockOnConsumed: 1, + }); + + try { + await rateLimiter.consume(testKey); + const msBeforeExpire = rateLimiter._inMemoryBlockedKeys.msBeforeExpire(rateLimiter.getKey(testKey)); + expect(msBeforeExpire).to.be.greaterThan(0); + } catch (err) { + expect.fail(`Consume failed: ${err.message}`); + } + }); + + it('reject after block key in memory for msBeforeNext, if consumed more than points', async () => { + const testKey = 'blockmempointsreject'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 5, + inMemoryBlockOnConsumed: 1, + }); + + try { + await rateLimiter.consume(testKey, 2); + expect.fail('Expected consume to fail'); + } catch (err) { + const msBeforeExpire = rateLimiter._inMemoryBlockedKeys.msBeforeExpire(rateLimiter.getKey(testKey)); + expect(msBeforeExpire).to.be.greaterThan(0); + } + }); + + it('expire inMemory blocked key', async () => { + const testKey = 'blockmemexpire'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 1, + inMemoryBlockOnConsumed: 2, + inMemoryBlockDuration: 2, + }); + + try { + await rateLimiter.consume(testKey, 2); + } catch (err) { + // Expect the key to be blocked at this point + const blocked = rateLimiter._inMemoryBlockedKeys._keys[rateLimiter.getKey(testKey)] + expect(!!blocked).to.be.true; + + // Wait for the in-memory block to expire + await new Promise(resolve => setTimeout(resolve, 2001)); + + // Now the block should be expired + try { + await rateLimiter.consume(testKey); + // Consume should succeed, indicating block has expired + } catch (consumeErr) { + expect.fail('Expected consume to succeed after block expired'); + } + } + }); + it('consume using insuranceLimiter when PrismaClient error', async () => { + const testKey = 'prismaerror'; + + const insuranceLimiter = new RateLimiterMemory({ + points: 2, + duration: 2, + }); + + const rateLimiter = new RateLimiterPrisma({ + storeClient: { $transaction: () => Promise.reject(new Error('PrismaClient error')) }, + points: 1, + duration: 1, + insuranceLimiter: insuranceLimiter, + }); + + try { + const res = await rateLimiter.consume(testKey); + expect(res.remainingPoints === 1 && res.msBeforeNext > 1000).to.equal(true); + } catch (rej) { + expect.fail('Expected to fall back to insurance limiter'); + } + }); + + it('blocks key for block duration when consumed more than points', async () => { + const testKey = 'blockForDuration'; + const blockDuration = 2; // Duration for which the key should be blocked, in seconds + + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 1, + blockDuration: blockDuration, + }); + + try { + await rateLimiter.consume(testKey, 2); + expect.fail('Consume should not be successful, expected to throw an error'); + } catch (rej) { + expect(rej.msBeforeNext > 1000).to.equal(true) + } + }); + + it('reject with error, if internal block by blockDuration failed', async () => { + const testKey = 'blockDurationFail'; + const blockDuration = 2; // Block duration in seconds + + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 1, + blockDuration: blockDuration, + }); + + // Stubbing the internal block method to simulate a failure + sinon.stub(rateLimiter, '_block').callsFake(() => Promise.reject(new Error('Block failed'))); + + // Attempting to consume more points than allowed to trigger the block + try { + await rateLimiter.consume(testKey, 2); + expect.fail('Expected block to fail and throw an error'); + } catch (err) { + expect(err.message).to.equal('Block failed'); + } + }); + + it('block expires in blockDuration seconds', async () => { + const testKey = 'blockExpire'; + const blockDuration = 2; // Block duration in seconds + + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 1, + blockDuration: blockDuration, + }); + + try { + await rateLimiter.consume(testKey, 2); + expect.fail('Expected consume to fail and block the key'); + } catch (rej) { + } + + await new Promise(resolve => setTimeout(resolve, blockDuration * 1000)); + + try { + await rateLimiter.consume(testKey); + } catch (err) { + expect.fail('Expected consume to succeed after block expired'); + } + }); + + it('get points', async () => { + const testKey = 'getPointsTest'; + const totalPoints = 2; // Total points available + + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: totalPoints, + duration: 1, + }); + + await rateLimiter.consume(testKey); + const rateLimiterRecord = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey(testKey) }, + }); + expect(rateLimiterRecord.points).to.equal(totalPoints - 1); + + const limiterStatus = await rateLimiter.get(testKey); + expect(limiterStatus.remainingPoints).to.equal(totalPoints - 1); + }); + + it('return correct data with _getRateLimiterRes', async () => { + const rateLimiter = new RateLimiterPrisma({ points: 5, storeClient: prisma }); + const now = new Date(); + const rateLimiterResponse = { + points: 3, + expire: new Date(now.getTime() + 1000).toISOString(), + }; + + await prisma.rateLimiterFlexible.create({ + data: { + key: rateLimiter.getKey('test'), + points: rateLimiterResponse.points, + expire: rateLimiterResponse.expire, + }, + }); + + const record = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey('test')} + }); + + const res = rateLimiter._getRateLimiterRes('test', 1, { + points: rateLimiterResponse.points, + expire: rateLimiterResponse.expire, + }); + + expect(res.msBeforeNext <= 1000 + && res.consumedPoints === 3 + && res.isFirstInDuration === false + && res.remainingPoints === 2).to.equal(true); + }); + + it('delete key and return true', async () => { + const testKey = 'deleteTrueTest'; + const rateLimiter = new RateLimiterPrisma({ storeClient: prisma, points: 2, duration: 1 }); + + await prisma.rateLimiterFlexible.create({ + data: { key: rateLimiter.getKey(testKey), points: 1, expire: new Date().toISOString() }, + }); + + const deleteResult = await rateLimiter.delete(testKey); + expect(deleteResult).to.equal(true); + const record = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey(testKey) }, + }); + expect(record).to.be.null; + }); + + it('block key forever, if secDuration is 0', async () => { + const testKey = 'blockForeverTest'; + const rateLimiter = new RateLimiterPrisma({ + storeClient: prisma, + points: 1, + duration: 1, + }); + + await rateLimiter.block(testKey, 0); + // Check the key is blocked + const recordBefore = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey(testKey) }, + }); + expect(recordBefore).not.to.be.null; + expect(recordBefore.expire).to.equal(null); // Expecting expire to be null for indefinite block + + // Wait for some time and check if the key is still blocked + await new Promise(resolve => setTimeout(resolve, 1000)); + + const recordAfter = await prisma.rateLimiterFlexible.findUnique({ + where: { key: rateLimiter.getKey(testKey) }, + }); + expect(recordAfter).not.to.be.null; + expect(recordAfter.expire).to.equal(null); // Key should still be blocked indefinitely + }); + + // TODO fix test + it('set points by key forever', async () => { + const testKey = 'setForeverTest'; + const totalPoints = 12; // Setting the points to this value + + const rateLimiter = new RateLimiterPrisma({ storeClient: prisma, points: 1, duration: 1 }); + + const resSet = await rateLimiter.set(testKey, totalPoints, 0); + const res = await rateLimiter.get(testKey); + expect(res.remainingPoints).to.equal(0); + expect(res.consumedPoints).to.equal(totalPoints); + expect(res.msBeforeNext).to.equal(-1); // or your equivalent for 'forever' + await new Promise(resolve => setTimeout(resolve, 1100)); // Wait for a time beyond the rate limiter's default duration + + const resAfterWait = await rateLimiter.get(testKey); + expect(resAfterWait.remainingPoints).to.equal(0); + expect(resAfterWait.consumedPoints).to.equal(totalPoints); + expect(resAfterWait.msBeforeNext).to.equal(-1); // or your equivalent for 'forever' + }); + + it('get returns NULL if key is not set', async () => { + const testKey = 'nonExistentKey'; + + const rateLimiter = new RateLimiterPrisma({ storeClient: prisma, points: 1, duration: 1 }); + + const res = await rateLimiter.get(testKey); + expect(res).to.be.null; + }); + + +}); diff --git a/test/RateLimiterPrisma/Postgres/schema.prisma b/test/RateLimiterPrisma/Postgres/schema.prisma new file mode 100644 index 0000000..2dab50e --- /dev/null +++ b/test/RateLimiterPrisma/Postgres/schema.prisma @@ -0,0 +1,14 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = "postgres://root:secret@localhost:5432" +} + +model RateLimiterFlexible { + key String @id + points Int + expire DateTime? +} From 2ac86460998bd49a4b851f19086b4a3e5ab503ea Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Feb 2024 17:04:42 +0100 Subject: [PATCH 2/9] add Postgres to github actions --- .github/workflows/test.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bacdb0c..4880a19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,12 @@ jobs: node-version: [14.x, 16.x, 18.x, 20.x] redis-version: [6, 7] + services: + postgres: + image: postgres + ports: + - 5432:5432 + steps: - name: Checkout repository uses: actions/checkout@v4.0.0 @@ -60,6 +66,11 @@ jobs: - name: Start DynamoDB local uses: rrainn/dynamodb-action@v3.0.0 - + + - name: Start PostgreSQL + uses: supercharge/postgres-github-action@1.0.0 + with: + postgres-version: 'latest' + - run: npm install - run: npm run test From ba4231d4958b088097d1431de0d8e3b77070a08b Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Feb 2024 17:08:05 +0100 Subject: [PATCH 3/9] fix github actions --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4880a19..c061509 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,10 +67,5 @@ jobs: - name: Start DynamoDB local uses: rrainn/dynamodb-action@v3.0.0 - - name: Start PostgreSQL - uses: supercharge/postgres-github-action@1.0.0 - with: - postgres-version: 'latest' - - run: npm install - run: npm run test From 3a2db9fa71b505d4f6a8a08f14ef4e7e4fb59da2 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 14 Feb 2024 17:49:07 +0100 Subject: [PATCH 4/9] add main Wiki link --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 551c740..47c9d49 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ const headers = { * 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) From 081f2e2b77a4afc85d915ec5c4600ce2e50f9784 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 14 Feb 2024 17:56:50 +0100 Subject: [PATCH 5/9] fix test wait until postgres is started --- .github/workflows/test.yml | 9 +++++++++ test/RateLimiterPrisma/Postgres/schema.prisma | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c061509..8ef23ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,8 +45,17 @@ jobs: services: postgres: image: postgres + env: + POSTGRES_PASSWORD: root + POSTGRES_USER: secret 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 diff --git a/test/RateLimiterPrisma/Postgres/schema.prisma b/test/RateLimiterPrisma/Postgres/schema.prisma index 2dab50e..743028f 100644 --- a/test/RateLimiterPrisma/Postgres/schema.prisma +++ b/test/RateLimiterPrisma/Postgres/schema.prisma @@ -4,7 +4,7 @@ generator client { datasource db { provider = "postgresql" - url = "postgres://root:secret@localhost:5432" + url = "postgres://root:secret@127.0.0.1:5432" } model RateLimiterFlexible { From e3ecc10129fe85600a66ed82ccc68033fd3c3429 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 14 Feb 2024 17:59:14 +0100 Subject: [PATCH 6/9] fix test postgres creds --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ef23ca..b27ef8e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,8 +46,8 @@ jobs: postgres: image: postgres env: - POSTGRES_PASSWORD: root - POSTGRES_USER: secret + POSTGRES_PASSWORD: secret + POSTGRES_USER: root ports: - 5432:5432 # Set health checks to wait until postgres has started From a71b687a833f8b9dfca63ff70f947fdf4163d8b2 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 14 Feb 2024 18:03:23 +0100 Subject: [PATCH 7/9] drop node.js version 14 support --- .github/workflows/test.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b27ef8e..d06e78c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ 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: diff --git a/README.md b/README.md index 47c9d49..b49f244 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![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/ Logo From d9263ffbdf4fd63f218bace36a5f46d411331610 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 15 Feb 2024 17:48:33 +0100 Subject: [PATCH 8/9] update readme --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b49f244..e6e0fbc 100644 --- a/README.md +++ b/README.md @@ -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 From a7d1608aaa11b5ba9f2431856d383e9bd7bab437 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 15 Feb 2024 18:00:28 +0100 Subject: [PATCH 9/9] 5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b392947..149e519 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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": {