Skip to content

Commit 946f616

Browse files
committed
Merge branch 'em/tokenBucketAlgo' of https://github.com/oslabs-beta/GraphQL-Gate into em/tokenBucketAlgo
2 parents cabdebe + 8e55d04 commit 946f616

File tree

10 files changed

+835
-27
lines changed

10 files changed

+835
-27
lines changed

package-lock.json

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@babel/preset-typescript": "^7.17.12",
2828
"@types/ioredis": "^4.28.10",
2929
"@types/ioredis-mock": "^5.6.0",
30+
"@types/express": "^4.17.13",
3031
"@types/jest": "^27.5.1",
3132
"@typescript-eslint/eslint-plugin": "^5.24.0",
3233
"@typescript-eslint/parser": "^5.24.0",
@@ -52,5 +53,6 @@
5253
"dependencies": {
5354
"graphql": "^16.5.0",
5455
"ioredis": "^5.0.5"
56+
5557
}
5658
}

src/@types/buildTypeWeights.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
interface Fields {
2-
[index: string]: number;
2+
readonly [index: string]: number | ((arg: number, type: Type) => number);
33
}
44

55
interface Type {
6-
weight: number;
7-
fields: Fields;
6+
readonly weight: number;
7+
readonly fields: Fields;
88
}
99

1010
interface TypeWeightObject {
11-
[index: string]: Type;
11+
readonly [index: string]: Type;
1212
}
1313

1414
interface TypeWeightConfig {

src/@types/rateLimit.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,22 @@ interface RedisBucket {
2222
tokens: number;
2323
timestamp: number;
2424
}
25+
26+
type RateLimiterSelection =
27+
| 'TOKEN_BUCKET'
28+
| 'LEAKY_BUCKET'
29+
| 'FIXED_WINDOW'
30+
| 'SLIDING_WINDOW_LOG'
31+
| 'SLIDING_WINDOW_COUNTER';
32+
33+
/**
34+
* @type {number} bucketSize - Size of the token bucket
35+
* @type {number} refillRate - Rate at which tokens are added to the bucket in seconds
36+
*/
37+
interface TokenBucketOptions {
38+
bucketSize: number;
39+
refillRate: number;
40+
}
41+
42+
// TODO: This will be a union type where we can specify Option types for other Rate Limiters
43+
type RateLimiterOptions = TokenBucketOptions;

src/analysis/buildTypeWeights.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
import { GraphQLSchema } from 'graphql/type/schema';
22

3+
/**
4+
* Default TypeWeight Configuration:
5+
* mutation: 10
6+
* object: 1
7+
* scalar: 0
8+
* connection: 2
9+
*/
10+
export const defaultTypeWeightsConfig: TypeWeightConfig = {
11+
mutation: 10,
12+
object: 1,
13+
scalar: 0,
14+
connection: 2,
15+
};
16+
317
/**
418
* The default typeWeightsConfig object is based off of Shopifys implementation of query
519
* cost analysis. Our function should input a users configuration of type weights or fall
@@ -10,16 +24,11 @@ import { GraphQLSchema } from 'graphql/type/schema';
1024
* - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
1125
*
1226
* @param schema
13-
* @param typeWeightsConfig
27+
* @param typeWeightsConfig Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
1428
*/
1529
function buildTypeWeightsFromSchema(
1630
schema: GraphQLSchema,
17-
typeWeightsConfig: TypeWeightConfig = {
18-
mutation: 10,
19-
object: 1,
20-
scalar: 0,
21-
connection: 2,
22-
}
31+
typeWeightsConfig: TypeWeightConfig = defaultTypeWeightsConfig
2332
): TypeWeightObject {
2433
throw Error(`getTypeWeightsFromSchema is not implemented.`);
2534
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { parse } from 'graphql';
2+
3+
/**
4+
* This function should
5+
* 1. validate the query using graphql methods
6+
* 2. parse the query string using the graphql parse method
7+
* 3. itreate through the query AST and
8+
* - cross reference the type weight object to check type weight
9+
* - total all the eweights of all types in the query
10+
* 4. return the total as the query complexity
11+
*
12+
* TO DO: extend the functionality to work for mutations and subscriptions
13+
*
14+
* @param {string} queryString
15+
* @param {TypeWeightObject} typeWeights
16+
* @param {string} complexityOption
17+
*/
18+
function getQueryTypeComplexity(queryString: string, typeWeights: TypeWeightObject): number {
19+
throw Error('getQueryComplexity is not implemented.');
20+
}
21+
22+
export default getQueryTypeComplexity;

src/middleware/index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { RedisClientOptions } from 'redis';
2+
import { Request, Response, NextFunction, RequestHandler } from 'express';
3+
import { GraphQLSchema } from 'graphql/type/schema';
4+
import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights';
5+
6+
// FIXME: Will the developer be responsible for first parsing the schema from a file?
7+
// Can consider accepting a string representing a the filepath to a schema
8+
// FIXME: Should a 429 status be sent by default or do we allow the user to handle blocked requests?
9+
10+
/**
11+
* Primary entry point for adding GraphQL Rate Limiting middleware to an Express Server
12+
* @param {RateLimiterSelection} rateLimiter Specify rate limiting algorithm to be used
13+
* @param {RateLimiterOptions} options Specify the appropriate options for the selected rateLimiter
14+
* @param {GraphQLSchema} schema GraphQLSchema object
15+
* @param {RedisClientOptions} redisClientOptions valid node-redis connection options. See https://github.com/redis/node-redis/blob/HEAD/docs/client-configuration.md
16+
* @param {TypeWeightConfig} typeWeightConfig Optional type weight configuration for the GraphQL Schema.
17+
* Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
18+
* @returns {RequestHandler} express middleware that computes the complexity of req.query and calls the next middleware
19+
* if the query is allowed or sends a 429 status if the request is blocked
20+
* @throws ValidationError if GraphQL Schema is invalid
21+
*/
22+
export function expressRateLimiter(
23+
rateLimiter: RateLimiterSelection,
24+
rateLimiterOptions: RateLimiterOptions,
25+
schema: GraphQLSchema,
26+
redisClientOptions: RedisClientOptions,
27+
typeWeightConfig: TypeWeightConfig = defaultTypeWeightsConfig
28+
): RequestHandler {
29+
// TODO: Set 'timestamp' on res.locals to record when the request is received in UNIX format. HTTP does not inlude this.
30+
// TODO: Parse the schema to create a TypeWeightObject. Throw ValidationError if schema is invalid
31+
// TODO: Connect to Redis store using provided options. Default to localhost:6379
32+
// TODO: Configure the selected RateLimtier
33+
// TODO: Configure the complexity analysis algorithm to run for incoming requests
34+
35+
const middleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
36+
// TODO: Parse query from req.query, compute complexity and pass necessary info to rate limiter
37+
// TODO: Call next if query is successful, send 429 status if query blocked, call next(err) with any thrown errors
38+
next(Error('Express rate limiting middleware not implemented'));
39+
};
40+
return middleware;
41+
}
42+
43+
export default expressRateLimiter;

0 commit comments

Comments
 (0)