express-rate-limiter-ts
Version:
A powerful, flexible and modern rate limiter middleware for Express.js written in TypeScript.
121 lines (120 loc) • 4.75 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostgresStore = void 0;
const pg_1 = require("pg");
// PostgresStore: Fully working, async, counter and ban supported store for production.
class PostgresStore {
constructor(options) {
this.windowMs = options.windowMs;
this.client = new pg_1.Client({ connectionString: options.connectionString });
this.connect();
}
// Start connection
async connect() {
await this.client.connect();
// Create required tables (idempotent)
await this.client.query(`
CREATE TABLE IF NOT EXISTS rate_limiter_counters (
key TEXT PRIMARY KEY,
count INTEGER NOT NULL,
expires_at BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS rate_limiter_bans (
key TEXT PRIMARY KEY,
reason TEXT,
expires_at BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS rate_limiter_stats (
route TEXT PRIMARY KEY,
count INTEGER NOT NULL,
last_triggered BIGINT NOT NULL
);
`);
}
// Increment counter and check limit
async incr(key) {
await this.connect();
const now = Date.now();
const expiresAt = now + this.windowMs;
// UPSERT to increment counter
const res = await this.client.query(`INSERT INTO rate_limiter_counters (key, count, expires_at)
VALUES ($1, 1, $2)
ON CONFLICT (key) DO UPDATE SET count = rate_limiter_counters.count + 1, expires_at = $2
RETURNING count, expires_at`, [key, expiresAt]);
return {
current: res.rows[0].count,
remaining: 0, // Remaining limit will be calculated in middleware
resetTime: new Date(Number(res.rows[0].expires_at)),
exceeded: false,
};
}
// Read counter
async get(key) {
await this.connect();
const res = await this.client.query('SELECT count, expires_at FROM rate_limiter_counters WHERE key = $1', [key]);
if (res.rows.length === 0)
return { count: 0, resetTime: Date.now() + this.windowMs };
return { count: res.rows[0].count, resetTime: Number(res.rows[0].expires_at) };
}
// Reset counter
async reset(key) {
await this.connect();
await this.client.query('DELETE FROM rate_limiter_counters WHERE key = $1', [key]);
}
// Record statistic
async recordStat(route) {
await this.connect();
const now = Date.now();
await this.client.query(`INSERT INTO rate_limiter_stats (route, count, last_triggered)
VALUES ($1, 1, $2)
ON CONFLICT (route) DO UPDATE SET count = rate_limiter_stats.count + 1, last_triggered = $2`, [route, now]);
}
// Get statistics
async getStats() {
await this.connect();
const res = await this.client.query('SELECT route, count, last_triggered FROM rate_limiter_stats');
const stats = {};
for (const row of res.rows) {
stats[row.route] = {
count: row.count,
lastTriggered: new Date(Number(row.last_triggered)),
};
}
return stats;
}
// Apply ban
async ban(key, seconds, reason) {
await this.connect();
const expiresAt = Date.now() + seconds * 1000;
await this.client.query(`INSERT INTO rate_limiter_bans (key, reason, expires_at)
VALUES ($1, $2, $3)
ON CONFLICT (key) DO UPDATE SET reason = $2, expires_at = $3`, [key, reason, expiresAt]);
}
// Check ban
async isBanned(key) {
await this.connect();
const res = await this.client.query('SELECT reason, expires_at FROM rate_limiter_bans WHERE key = $1', [key]);
if (res.rows.length && res.rows[0].expires_at > Date.now()) {
return { banned: true, banExpiresAt: new Date(Number(res.rows[0].expires_at)), reason: res.rows[0].reason };
}
return { banned: false };
}
// Get banned keys
async getBanned() {
await this.connect();
const now = Date.now();
const res = await this.client.query('SELECT key, reason, expires_at FROM rate_limiter_bans WHERE expires_at > $1', [now]);
const result = {};
for (const row of res.rows) {
result[row.key] = { banExpiresAt: new Date(Number(row.expires_at)), reason: row.reason };
}
return result;
}
// Remove ban
async unban(key) {
await this.connect();
await this.client.query('DELETE FROM rate_limiter_bans WHERE key = $1', [key]);
await this.client.query('DELETE FROM rate_limiter_counters WHERE key = $1', [key]);
}
}
exports.PostgresStore = PostgresStore;