Skip to content

Commit 9175747

Browse files
committed
resolved mergo conflicts from dev
2 parents b24fcb9 + c118dc3 commit 9175747

File tree

10 files changed

+755
-228
lines changed

10 files changed

+755
-228
lines changed

.eslintrc.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
},
2020
"plugins": ["import", "prettier"],
2121
"rules": {
22-
22+
"no-plusplus": [2, {
23+
"allowForLoopAfterthoughts": true
24+
}],
2325
"prettier/prettier": [
2426
"error"
2527
]
2628
},
27-
"ignorePatterns": ["jest.config.js"]
29+
"ignorePatterns": ["jest.config.ts"]
2830
}

jest.config.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

jest.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Config } from '@jest/types';
2+
3+
const config: Config.InitialOptions = {
4+
verbose: true,
5+
roots: ['./test'],
6+
preset: 'ts-jest',
7+
testEnvironment: 'node',
8+
moduleFileExtensions: ['js', 'ts'],
9+
};
10+
11+
export default config;

package-lock.json

Lines changed: 280 additions & 189 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
"version": "1.0.0",
44
"description": "A GraphQL rate limiting library using query complexity analysis.",
55
"main": "index.js",
6+
"type": "module",
67
"scripts": {
78
"test": "jest --passWithNoTests",
89
"lint": "eslint src test",
9-
"lint:fix": "eslint --fix src test",
10+
"lint:fix": "eslint --fix src test @types",
1011
"prettier": "prettier --write .",
1112
"prepare": "husky install"
1213
},
@@ -25,9 +26,10 @@
2526
"@babel/core": "^7.17.12",
2627
"@babel/preset-env": "^7.17.12",
2728
"@babel/preset-typescript": "^7.17.12",
29+
"@types/ioredis": "^4.28.10",
30+
"@types/ioredis-mock": "^5.6.0",
2831
"@types/express": "^4.17.13",
2932
"@types/jest": "^27.5.1",
30-
"@types/redis-mock": "^0.17.1",
3133
"@typescript-eslint/eslint-plugin": "^5.24.0",
3234
"@typescript-eslint/parser": "^5.24.0",
3335
"babel-jest": "^28.1.0",
@@ -42,8 +44,8 @@
4244
"jest": "^28.1.0",
4345
"lint-staged": "^12.4.1",
4446
"prettier": "2.6.2",
45-
"redis-mock": "^0.56.3",
4647
"ts-jest": "^28.0.2",
48+
"ts-node": "^10.8.0",
4749
"typescript": "^4.6.4"
4850
},
4951
"lint-staged": {
@@ -52,7 +54,7 @@
5254
},
5355
"dependencies": {
5456
"graphql": "^16.5.0",
55-
"ioredis": "^5.0.5",
56-
"redis": "^4.1.0"
57+
"ioredis": "^5.0.5"
58+
5759
}
5860
}

src/@types/rateLimit.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,6 @@ interface TokenBucketOptions {
4040
}
4141

4242
// TODO: This will be a union type where we can specify Option types for other Rate Limiters
43-
type RateLimiterOptions = TokenBucketOptions;
43+
// Record<string, never> represents the empty object for alogorithms that don't require settings
44+
// and might be able to be removed in the future.
45+
type RateLimiterOptions = TokenBucketOptions | Record<string, never>;

src/middleware/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import getQueryTypeComplexity from '../analysis/typeComplexityAnalysis';
2121
* Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
2222
* @returns {RequestHandler} express middleware that computes the complexity of req.query and calls the next middleware
2323
* if the query is allowed or sends a 429 status if the request is blocked
24-
* @throws ValidationError if GraphQL Schema is invalid
24+
* FIXME: How about the specific GraphQLError?
25+
* @throws ValidationError if GraphQL Schema is invalid.
2526
*/
2627
export function expressRateLimiter(
2728
rateLimiterAlgo: RateLimiterSelection,

src/rateLimiters/tokenBucket.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,67 @@ class TokenBucket implements RateLimiter {
4343
timestamp: number,
4444
tokens = 1
4545
): Promise<RateLimiterResponse> {
46-
throw Error(`TokenBucket.processRequest not implemented, ${this}`);
46+
// set the expiry of key-value pairs in the cache to 24 hours
47+
const keyExpiry = 86400000;
48+
49+
// attempt to get the value for the uuid from the redis cache
50+
const bucketJSON = await this.client.get(uuid);
51+
52+
// if the response is null, we need to create a bucket for the user
53+
if (bucketJSON === null) {
54+
const newUserBucket: RedisBucket = {
55+
// conditionally set tokens depending on how many are requested comapred to the capacity
56+
tokens: tokens > this.capacity ? this.capacity : this.capacity - tokens,
57+
timestamp,
58+
};
59+
// reject the request, not enough tokens could even be in the bucket
60+
if (tokens > this.capacity) {
61+
await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserBucket));
62+
return { success: false, tokens: this.capacity };
63+
}
64+
await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserBucket));
65+
return { success: true, tokens: newUserBucket.tokens };
66+
}
67+
68+
// parse the returned string from redis and update their token budget based on the time lapse between queries
69+
const bucket: RedisBucket = await JSON.parse(bucketJSON);
70+
bucket.tokens = this.calculateTokenBudgetFromTimestamp(bucket, timestamp);
71+
72+
const updatedUserBucket = {
73+
// conditionally set tokens depending on how many are requested comapred to the bucket
74+
tokens: bucket.tokens < tokens ? bucket.tokens : bucket.tokens - tokens,
75+
timestamp,
76+
};
77+
if (bucket.tokens < tokens) {
78+
// reject the request, not enough tokens in bucket
79+
await this.client.setex(uuid, keyExpiry, JSON.stringify(updatedUserBucket));
80+
return { success: false, tokens: bucket.tokens };
81+
}
82+
await this.client.setex(uuid, keyExpiry, JSON.stringify(updatedUserBucket));
83+
return { success: true, tokens: updatedUserBucket.tokens };
4784
}
4885

4986
/**
5087
* Resets the rate limiter to the intial state by clearing the redis store.
5188
*/
52-
reset(): void {
53-
throw Error(`TokenBucket.reset not implemented, ${this}`);
89+
public reset(): void {
90+
this.client.flushall();
5491
}
92+
93+
/**
94+
* Calculates the tokens a user bucket should have given the time lapse between requests.
95+
*/
96+
private calculateTokenBudgetFromTimestamp = (
97+
bucket: RedisBucket,
98+
timestamp: number
99+
): number => {
100+
const timeSinceLastQueryInSeconds: number = Math.floor(
101+
(timestamp - bucket.timestamp) / 1000 // 1000 ms in a second
102+
);
103+
const tokensToAdd = timeSinceLastQueryInSeconds * this.refillRate;
104+
const updatedTokenCount = bucket.tokens + tokensToAdd;
105+
return updatedTokenCount > this.capacity ? this.capacity : updatedTokenCount;
106+
};
55107
}
56108

57109
export default TokenBucket;

0 commit comments

Comments
 (0)