import { AppError } from '../common/errors/app-error'; import { Injectable } from '@nestjs/common'; import { DatabaseService } from '../database/database.service'; import { hashToken } from './tokens'; @Injectable() export class RateLimitService { constructor(private readonly db: DatabaseService) {} async consume(key: string, limit: number, seconds: number): Promise { const digest = hashToken(key); const [row] = await this.db.$queryRaw< Array<{ hits: number; expiresAt: Date }> >` INSERT INTO rate_limits (key, hits, expires_at) VALUES (${digest}, 1, NOW() + ${seconds} * INTERVAL '1 second') ON CONFLICT (key) DO UPDATE SET hits = CASE WHEN rate_limits.expires_at <= NOW() THEN 1 ELSE rate_limits.hits + 1 END, expires_at = CASE WHEN rate_limits.expires_at <= NOW() THEN NOW() + ${seconds} * INTERVAL '1 second' ELSE rate_limits.expires_at END RETURNING hits, expires_at AS "expiresAt" `; if (row.hits > limit) throw new AppError( 'RATE_LIMITED', key.split(':')[0], undefined, Math.max(1, Math.ceil((row.expiresAt.getTime() - Date.now()) / 1000)), ); } }