Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@babel/core": "^7.17.12",
"@babel/preset-env": "^7.17.12",
"@babel/preset-typescript": "^7.17.12",
"@types/express": "^4.17.13",
"@types/jest": "^27.5.1",
"@types/redis-mock": "^0.17.1",
"@typescript-eslint/eslint-plugin": "^5.24.0",
Expand All @@ -49,7 +50,7 @@
"*.{js,ts,css,md}": "prettier --write --ignore-unknown"
},
"dependencies": {
"redis": "^4.1.0",
"graphql": "^16.5.0"
"graphql": "^16.5.0",
"redis": "^4.1.0"
}
}
19 changes: 19 additions & 0 deletions src/@types/rateLimit.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,22 @@ interface RedisBucket {
tokens: number;
timestamp: number;
}

type RateLimiterSelection =
| 'TOKEN_BUCKET'
| 'LEAKY_BUCKET'
| 'FIXED_WINDOW'
| 'SLIDING_WINDOW_LOG'
| 'SLIDING_WINDOW_COUNTER';

/**
* @type {number} bucketSize - Size of the token bucket
* @type {number} refillRate - Rate at which tokens are added to the bucket in seconds
*/
interface TokenBucketOptions {
bucketSize: number;
refillRate: number;
}

// TODO: This will be a union type where we can specify Option types for other Rate Limiters
type RateLimiterOptions = TokenBucketOptions;
23 changes: 16 additions & 7 deletions src/analysis/buildTypeWeights.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { GraphQLSchema } from 'graphql/type/schema';

/**
* Default TypeWeight Configuration:
* mutation: 10
* object: 1
* scalar: 0
* connection: 2
*/
export const defaultTypeWeightsConfig: TypeWeightConfig = {
mutation: 10,
object: 1,
scalar: 0,
connection: 2,
};

/**
* The default typeWeightsConfig object is based off of Shopifys implementation of query
* cost analysis. Our function should input a users configuration of type weights or fall
Expand All @@ -10,16 +24,11 @@ import { GraphQLSchema } from 'graphql/type/schema';
* - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
*
* @param schema
* @param typeWeightsConfig
* @param typeWeightsConfig Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
*/
function buildTypeWeightsFromSchema(
schema: GraphQLSchema,
typeWeightsConfig: TypeWeightConfig = {
mutation: 10,
object: 1,
scalar: 0,
connection: 2,
}
typeWeightsConfig: TypeWeightConfig = defaultTypeWeightsConfig
): TypeWeightObject {
throw Error(`getTypeWeightsFromSchema is not implemented.`);
}
Expand Down
43 changes: 43 additions & 0 deletions src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { RedisClientOptions } from 'redis';
import { Request, Response, NextFunction, RequestHandler } from 'express';
import { GraphQLSchema } from 'graphql/type/schema';
import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights';

// FIXME: Will the developer be responsible for first parsing the schema from a file?
// Can consider accepting a string representing a the filepath to a schema
// FIXME: Should a 429 status be sent by default or do we allow the user to handle blocked requests?

/**
* Primary entry point for adding GraphQL Rate Limiting middleware to an Express Server
* @param {RateLimiterSelection} rateLimiter Specify rate limiting algorithm to be used
* @param {RateLimiterOptions} options Specify the appropriate options for the selected rateLimiter
* @param {GraphQLSchema} schema GraphQLSchema object
* @param {RedisClientOptions} redisClientOptions valid node-redis connection options. See https://github.com/redis/node-redis/blob/HEAD/docs/client-configuration.md
* @param {TypeWeightConfig} typeWeightConfig Optional type weight configuration for the GraphQL Schema.
* Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
* @returns {RequestHandler} express middleware that computes the complexity of req.query and calls the next middleware
* if the query is allowed or sends a 429 status if the request is blocked
* @throws ValidationError if GraphQL Schema is invalid
*/
export function expressRateLimiter(
rateLimiter: RateLimiterSelection,
rateLimiterOptions: RateLimiterOptions,
schema: GraphQLSchema,
redisClientOptions: RedisClientOptions,
typeWeightConfig: TypeWeightConfig = defaultTypeWeightsConfig
): RequestHandler {
// TODO: Set 'timestamp' on res.locals to record when the request is received in UNIX format. HTTP does not inlude this.
// TODO: Parse the schema to create a TypeWeightObject. Throw ValidationError if schema is invalid
// TODO: Connect to Redis store using provided options. Default to localhost:6379
// TODO: Configure the selected RateLimtier
// TODO: Configure the complexity analysis algorithm to run for incoming requests

const middleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
// TODO: Parse query from req.query, compute complexity and pass necessary info to rate limiter
// TODO: Call next if query is successful, send 429 status if query blocked, call next(err) with any thrown errors
next(Error('Express rate limiting middleware not implemented'));
};
return middleware;
}

export default expressRateLimiter;