Skip to content

Commit 2af4220

Browse files
authored
Merge pull request #18887 from mozilla/gql-rate-limit-support
task(graphql): Support using new rate-limitting library for customs in graphql-api
2 parents 8d562d2 + 0f14515 commit 2af4220

31 files changed

Lines changed: 819 additions & 141 deletions

libs/accounts/rate-limit/jest.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,14 @@ export default {
88
},
99
moduleFileExtensions: ['ts', 'js', 'html'],
1010
coverageDirectory: '../../../coverage/libs/accounts/rate-limit',
11+
reporters: [
12+
'default',
13+
[
14+
'jest-junit',
15+
{
16+
outputDirectory: 'artifacts/tests/lib/accounts/rate-limit',
17+
outputName: 'nestjs-customs-jest-unit-results.xml',
18+
},
19+
],
20+
],
1121
};

libs/accounts/rate-limit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"dependencies": {},
55
"type": "commonjs",
66
"main": "./index.cjs",
7+
"types": "./index.d.ts",
78
"private": true
89
}

libs/accounts/rate-limit/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
export * from './lib/rate-limit';
6+
export * from './lib/rate-limit.provider';
67
export * from './lib/config';
78
export * from './lib/error';
89
export * from './lib/models';

libs/accounts/rate-limit/src/lib/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ export class RateLimitConfig {
1111
@IsString()
1212
@IsArray()
1313
rules!: Array<string> | string;
14+
15+
@IsArray()
16+
ignoreIPs!: Array<string>;
17+
18+
@IsArray()
19+
ignoreEmails!: Array<string>;
20+
21+
@IsArray()
22+
ignoreUIDs!: Array<string>;
1423
}
1524

1625
/**
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { Test, TestingModule } from '@nestjs/testing';
6+
import { ConfigService } from '@nestjs/config';
7+
import {
8+
RateLimitClient,
9+
RateLimitProvider,
10+
RateLimitRedisClient,
11+
} from './rate-limit.provider';
12+
import { RateLimit } from './rate-limit';
13+
import { StatsDService } from '../../../../shared/metrics/statsd/src';
14+
15+
const mockStatsd = jest.fn();
16+
jest.mock('hot-shots', () => {
17+
return {
18+
StatsD: function (...args: any) {
19+
return mockStatsd(...args);
20+
},
21+
};
22+
});
23+
24+
describe('RateLimitProvider', () => {
25+
let rateLimit: RateLimit;
26+
27+
const mockConfig = {
28+
rules: ['test : ip : 1 : 1 second : 1 second '],
29+
};
30+
const mockConfigService = {
31+
get: jest.fn().mockImplementation((key: string) => {
32+
if (key === 'rateLimit') {
33+
return mockConfig;
34+
}
35+
return null;
36+
}),
37+
};
38+
39+
beforeEach(async () => {
40+
jest.clearAllMocks();
41+
const module: TestingModule = await Test.createTestingModule({
42+
providers: [
43+
RateLimitProvider,
44+
{
45+
provide: ConfigService,
46+
useValue: mockConfigService,
47+
},
48+
{
49+
provide: RateLimitRedisClient,
50+
useValue: {},
51+
},
52+
{
53+
provide: StatsDService,
54+
useValue: {},
55+
},
56+
],
57+
}).compile();
58+
59+
rateLimit = await module.resolve<RateLimit>(RateLimitClient);
60+
});
61+
62+
it('should provide statsd', async () => {
63+
expect(rateLimit).toBeDefined();
64+
});
65+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { ConfigService } from '@nestjs/config';
6+
import { parseConfigRules, RateLimitConfig } from './config';
7+
import { RateLimit } from './rate-limit';
8+
import Redis, { RedisOptions } from 'ioredis';
9+
import { StatsD, StatsDService } from '@fxa/shared/metrics/statsd';
10+
import { Provider } from '@nestjs/common';
11+
12+
export const RateLimitRedisClient = Symbol('RATE_LIMIT__REDIS_CLIENT');
13+
14+
export const RateLimitRedisProvider: Provider = {
15+
provide: RateLimitRedisClient,
16+
useFactory: (config: ConfigService) => {
17+
const redisConfig = config.get<RedisOptions>('redis');
18+
if (redisConfig == null) {
19+
throw new Error('Missing config for redis');
20+
}
21+
22+
const redisRateLimitConfig = config.get<RedisOptions>('redis.customs');
23+
if (redisRateLimitConfig == null) {
24+
throw new Error('Missing config for redis.customs');
25+
}
26+
27+
return new Redis({
28+
...redisConfig,
29+
...redisRateLimitConfig,
30+
});
31+
},
32+
inject: [ConfigService],
33+
};
34+
35+
export const RateLimitClient = Symbol('RATE_LIMIT_CLIENT');
36+
export const RateLimitProvider = {
37+
provide: RateLimitClient,
38+
useFactory: (config: ConfigService, redis: Redis.Redis, statsd: StatsD) => {
39+
const rateLimitConfig = config.get<RateLimitConfig>('rateLimit');
40+
if (rateLimitConfig == null) {
41+
throw new Error('Missing rateLimit config');
42+
}
43+
44+
const rules = parseConfigRules(rateLimitConfig.rules);
45+
return new RateLimit(
46+
{
47+
rules,
48+
ignoreEmails: rateLimitConfig.ignoreEmails,
49+
ignoreIPs: rateLimitConfig.ignoreIPs,
50+
ignoreUIDs: rateLimitConfig.ignoreUIDs,
51+
},
52+
redis,
53+
statsd
54+
);
55+
},
56+
inject: [ConfigService, RateLimitRedisClient, StatsDService],
57+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": ["../../../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7+
"rules": {}
8+
},
9+
{
10+
"files": ["*.ts", "*.tsx"],
11+
"rules": {}
12+
},
13+
{
14+
"files": ["*.js", "*.jsx"],
15+
"rules": {}
16+
}
17+
]
18+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Customs
2+
3+
This library supports adding customs (i.e. rate-limiting) as a nestjs service. It was initially designed to work with
4+
FxA's graphql-api, but could be repurposed to work else where.
5+
6+
## Building
7+
8+
Run `nx build customs` to build the library.
9+
10+
## Running unit tests
11+
12+
Run `nx test-unit customs` to execute the unit tests via [Jest](https://jestjs.io).
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Config } from 'jest';
2+
3+
/* eslint-disable */
4+
const config: Config = {
5+
displayName: 'shared-nestjs-customs',
6+
preset: '../../../../jest.preset.js',
7+
testEnvironment: 'node',
8+
transform: {
9+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
10+
},
11+
moduleFileExtensions: ['ts', 'js', 'html'],
12+
coverageDirectory: '../../../../coverage/libs/nestjs/customs',
13+
reporters: [
14+
'default',
15+
[
16+
'jest-junit',
17+
{
18+
outputDirectory: 'artifacts/tests/lib/shared/nestjs/customs',
19+
outputName: 'nestjs-customs-jest-unit-results.xml',
20+
},
21+
],
22+
],
23+
};
24+
25+
export default config;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@fxa/shared/nestjs/customs",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "commonjs",
6+
"main": "./index.cjs",
7+
"types": "./index.d.ts",
8+
"dependencies": {}
9+
}

0 commit comments

Comments
 (0)