From e3cddebd2bb1b03a88bcc42ff928b2e16e3b6471 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Sun, 29 May 2022 08:25:56 -0400 Subject: [PATCH 01/13] added timestamp --- src/middleware/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 57fc4d6..d7fb5ad 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -26,7 +26,7 @@ export function expressRateLimiter( 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. + const timeStamp = new Date().valueOf(); // 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 From 81d645b4fd7ef07bb6c4b794695ca156dd4399a0 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Mon, 30 May 2022 22:05:22 -0400 Subject: [PATCH 02/13] wrote the framework for the express middleware. --- package-lock.json | 133 +++++++++++++++++++++++++++-- package.json | 1 + src/@types/rateLimit.d.ts | 2 +- src/middleware/index.ts | 79 +++++++++++++---- src/middleware/rateLimiterSetup.ts | 30 +++++++ src/rateLimiters/tokenBucket.ts | 6 +- 6 files changed, 225 insertions(+), 26 deletions(-) create mode 100644 src/middleware/rateLimiterSetup.ts diff --git a/package-lock.json b/package-lock.json index 05bd366..af77977 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "graphql": "^16.5.0", + "ioredis": "^5.0.5", "redis": "^4.1.0" }, "devDependencies": { @@ -1864,6 +1865,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz", + "integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg==" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3617,7 +3623,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3667,6 +3672,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4754,6 +4767,29 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.0.5.tgz", + "integrity": "sha512-H+u9YB/cBckDO5lt5+S34gGN1EuIBjjaXk31LivQWfX3G1cqZPYCiwF9qCOkqK2NsKVk+saoUN+fLBz5tc2gFw==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.0.1", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5885,6 +5921,16 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6084,8 +6130,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -6599,6 +6644,14 @@ "@redis/time-series": "1.0.3" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=", + "engines": { + "node": ">=4" + } + }, "node_modules/redis-mock": { "version": "0.56.3", "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.56.3.tgz", @@ -6608,6 +6661,17 @@ "node": ">=6" } }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -7012,6 +7076,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -8978,6 +9047,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@ioredis/commands": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz", + "integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg==" + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -10326,7 +10400,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -10359,6 +10432,11 @@ "object-keys": "^1.1.1" } }, + "denque": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -11165,6 +11243,22 @@ "side-channel": "^1.0.4" } }, + "ioredis": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.0.5.tgz", + "integrity": "sha512-H+u9YB/cBckDO5lt5+S34gGN1EuIBjjaXk31LivQWfX3G1cqZPYCiwF9qCOkqK2NsKVk+saoUN+fLBz5tc2gFw==", + "requires": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.0.1", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -12002,6 +12096,16 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -12160,8 +12264,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "natural-compare": { "version": "1.4.0", @@ -12524,12 +12627,25 @@ "@redis/time-series": "1.0.3" } }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, "redis-mock": { "version": "0.56.3", "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.56.3.tgz", "integrity": "sha512-ynaJhqk0Qf3Qajnwvy4aOjS4Mdf9IBkELWtjd+NYhpiqu4QCNq6Vf3Q7c++XRPGiKiwRj9HWr0crcwy7EiPjYQ==", "dev": true }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -12833,6 +12949,11 @@ } } }, + "standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", diff --git a/package.json b/package.json index a154acd..2bd02a7 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "graphql": "^16.5.0", + "ioredis": "^5.0.5", "redis": "^4.1.0" } } diff --git a/src/@types/rateLimit.d.ts b/src/@types/rateLimit.d.ts index 91ec955..812af0d 100644 --- a/src/@types/rateLimit.d.ts +++ b/src/@types/rateLimit.d.ts @@ -15,7 +15,7 @@ interface RateLimiter { interface RateLimiterResponse { success: boolean; - tokens?: number; + tokens: number; } interface RedisBucket { diff --git a/src/middleware/index.ts b/src/middleware/index.ts index d7fb5ad..ad1e04f 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,7 +1,10 @@ -import { RedisClientOptions } from 'redis'; -import { Request, Response, NextFunction, RequestHandler } from 'express'; +import Redis, { RedisOptions } from 'ioredis'; import { GraphQLSchema } from 'graphql/type/schema'; -import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights'; +import { Request, Response, NextFunction, RequestHandler } from 'express'; + +import buildTypeWeightsFromSchema, { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights'; +import setupRateLimiter from './rateLimiterSetup'; +import getQueryTypeComplexity from '../analysis/typeComplexityAnalysis'; // 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 @@ -12,7 +15,7 @@ import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights'; * @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 {RedisClientOptions} RedisOptions // TODO add dsecription * @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 @@ -20,24 +23,68 @@ import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights'; * @throws ValidationError if GraphQL Schema is invalid */ export function expressRateLimiter( - rateLimiter: RateLimiterSelection, + rateLimiterAlgo: RateLimiterSelection, rateLimiterOptions: RateLimiterOptions, schema: GraphQLSchema, - redisClientOptions: RedisClientOptions, + redisClientOptions: RedisOptions, typeWeightConfig: TypeWeightConfig = defaultTypeWeightsConfig ): RequestHandler { - const timeStamp = new Date().valueOf(); - // 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 + /** + * build the type weight object, create the redis client and instantiate the ratelimiter + * before returning the express middleware that calculates query complexity and throttles the requests + */ + // TODO: Throw ValidationError if schema is invalid + const typeWeightObject = buildTypeWeightsFromSchema(schema, typeWeightConfig); + // TODO: Throw error if connection is unsuccessful + const redisClient = new Redis(redisClientOptions); // Default port is 6379 automatically + const rateLimiter = setupRateLimiter(rateLimiterAlgo, rateLimiterOptions, redisClient); + + return async (req: Request, res: Response, next: NextFunction): Promise => { + const requestTimestamp = new Date().valueOf(); + const { query }: { query: string } = req.body; + if (!query) { + // FIXME: Throw an error here? Code currently passes this on to whatever is next + console.log('There is no query on the request'); + return next(); + } + + /** + * There are numorous ways to get the ip address off of the request object. + * - the header 'x-forward-for' will hold the originating ip address if a proxy is placed infront of the server. This would be commen for a production build. + * - req.ips wwill hold an array of ip addresses in'x-forward-for' header. client is likely at index zero + * - req.ip will have the ip address + * - req.socket.remoteAddress is an insatnce of net.socket which is used as another method of getting the ip address + * + * req.ip and req.ips will worx in express but not with other frameworks + */ + const ip: string = req.ips[0] || req.ip; + // FIXME: this will only work with type complexity + const queryComplexity = getQueryTypeComplexity(query, typeWeightObject); - 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')); + try { + const rateLimiterResponse = await rateLimiter.processRequest( + ip, + requestTimestamp, + queryComplexity + ); + if (rateLimiterResponse.success === false) { + // TODO: add a header 'Retry-After' with the time to wait untill next query will succeed + res.status(429).json({ + timestamp: requestTimestamp, + complexity: queryComplexity, + tokens: rateLimiterResponse.tokens, + }); + } + res.locals.graphqlGate = { + timestamp: requestTimestamp, + complexity: queryComplexity, + tokens: rateLimiterResponse.tokens, + }; + return next(); + } catch (err) { + return next(err); + } }; - return middleware; } export default expressRateLimiter; diff --git a/src/middleware/rateLimiterSetup.ts b/src/middleware/rateLimiterSetup.ts new file mode 100644 index 0000000..3e43d38 --- /dev/null +++ b/src/middleware/rateLimiterSetup.ts @@ -0,0 +1,30 @@ +import Redis from 'ioredis'; +import TokenBucket from '../rateLimiters/tokenBucket'; + +export default function setupRateLimiter( + selection: RateLimiterSelection, + options: RateLimiterOptions, + client: Redis +) { + switch (selection) { + case 'TOKEN_BUCKET': + // todo validate options + return new TokenBucket(options.bucketSize, options.refillRate, client); + break; + case 'LEAKY_BUCKET': + throw new Error('Leaky Bucket algonithm has not be implemented.'); + break; + case 'FIXED_WINDOW': + throw new Error('Fixed Window algonithm has not be implemented.'); + break; + case 'SLIDING_WINDOW_LOG': + throw new Error('Sliding Window Log has not be implemented.'); + break; + case 'SLIDING_WINDOW_COUNTER': + throw new Error('Sliding Window Counter algonithm has not be implemented.'); + break; + default: + throw new Error('Selected rate limiting algorithm is not suppported'); + break; + } +} diff --git a/src/rateLimiters/tokenBucket.ts b/src/rateLimiters/tokenBucket.ts index 7f34d2b..dc92171 100644 --- a/src/rateLimiters/tokenBucket.ts +++ b/src/rateLimiters/tokenBucket.ts @@ -1,4 +1,4 @@ -import { RedisClientType } from 'redis'; +import Redis from 'ioredis'; /** * The TokenBucket instance of a RateLimiter limits requests based on a unique user ID. @@ -13,7 +13,7 @@ class TokenBucket implements RateLimiter { private refillRate: number; - private client: RedisClientType; + private client: Redis; /** * Create a new instance of a TokenBucket rate limiter that can be connected to any database store @@ -21,7 +21,7 @@ class TokenBucket implements RateLimiter { * @param refillRate rate at which the token bucket is refilled * @param client redis client where rate limiter will cache information */ - constructor(capacity: number, refillRate: number, client: RedisClientType) { + constructor(capacity: number, refillRate: number, client: Redis) { this.capacity = capacity; this.refillRate = refillRate; this.client = client; From acc95cca1bd20ec8e53c2689b03f17db120555e0 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Mon, 30 May 2022 22:21:44 -0400 Subject: [PATCH 03/13] added some comments ofr easiser draft review --- src/middleware/index.ts | 5 +++++ src/middleware/rateLimiterSetup.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/middleware/index.ts b/src/middleware/index.ts index ad1e04f..ef1f518 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -39,6 +39,7 @@ export function expressRateLimiter( const redisClient = new Redis(redisClientOptions); // Default port is 6379 automatically const rateLimiter = setupRateLimiter(rateLimiterAlgo, rateLimiterOptions, redisClient); + // return the rate limiting middleware return async (req: Request, res: Response, next: NextFunction): Promise => { const requestTimestamp = new Date().valueOf(); const { query }: { query: string } = req.body; @@ -57,11 +58,15 @@ export function expressRateLimiter( * * req.ip and req.ips will worx in express but not with other frameworks */ + // check for a proxied ip address before using the ip address on request const ip: string = req.ips[0] || req.ip; + // FIXME: this will only work with type complexity const queryComplexity = getQueryTypeComplexity(query, typeWeightObject); try { + // process the request and conditinoally respond to client with status code 429 o + // r pass the request onto the next middleware function const rateLimiterResponse = await rateLimiter.processRequest( ip, requestTimestamp, diff --git a/src/middleware/rateLimiterSetup.ts b/src/middleware/rateLimiterSetup.ts index 3e43d38..4c5de39 100644 --- a/src/middleware/rateLimiterSetup.ts +++ b/src/middleware/rateLimiterSetup.ts @@ -1,6 +1,15 @@ import Redis from 'ioredis'; import TokenBucket from '../rateLimiters/tokenBucket'; +/** + * Instatieate the rateLimiting algorithm class based on the developer selection and options + * + * @export + * @param {RateLimiterSelection} selection + * @param {RateLimiterOptions} options + * @param {Redis} client + * @return {*} + */ export default function setupRateLimiter( selection: RateLimiterSelection, options: RateLimiterOptions, @@ -24,6 +33,7 @@ export default function setupRateLimiter( throw new Error('Sliding Window Counter algonithm has not be implemented.'); break; default: + // typescript should never let us invoke this function with anything other than the options above throw new Error('Selected rate limiting algorithm is not suppported'); break; } From 1d47887b3a5c7dd6127375cf9b596a4f5a5f64c8 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Mon, 30 May 2022 22:32:09 -0400 Subject: [PATCH 04/13] refactored token bucket tests to use ioredis --- test/rateLimiters/tokenBucket.test.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/test/rateLimiters/tokenBucket.test.ts b/test/rateLimiters/tokenBucket.test.ts index 34aa391..32453fd 100644 --- a/test/rateLimiters/tokenBucket.test.ts +++ b/test/rateLimiters/tokenBucket.test.ts @@ -1,28 +1,27 @@ -import redis from 'redis-mock'; -import { RedisClientType } from 'redis'; +import * as ioredis from 'ioredis'; import TokenBucket from '../../src/rateLimiters/tokenBucket'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const RedisMock = require('ioredis-mock'); + const CAPACITY = 10; // FIXME: Changing the refill rate effects test outcomes. const REFILL_RATE = 1; // 1 token per second let limiter: TokenBucket; -let client: RedisClientType; +let client: ioredis.Redis; let timestamp: number; const user1 = '1'; const user2 = '2'; const user3 = '3'; const user4 = '4'; -async function getBucketFromClient( - redisClient: RedisClientType, - uuid: string -): Promise { +async function getBucketFromClient(redisClient: ioredis.Redis, uuid: string): Promise { return redisClient.get(uuid).then((res) => JSON.parse(res || '{}')); } async function setTokenCountInClient( - redisClient: RedisClientType, + redisClient: ioredis.Redis, uuid: string, tokens: number, time: number @@ -36,7 +35,7 @@ xdescribe('Test TokenBucket Rate Limiter', () => { // Initialize a new token bucket before each test // create a mock user // intialze the token bucket algorithm - client = redis.createClient(); + client = new RedisMock(); limiter = new TokenBucket(CAPACITY, REFILL_RATE, client); timestamp = new Date().valueOf(); }); @@ -197,7 +196,7 @@ xdescribe('Test TokenBucket Rate Limiter', () => { }); test('bucket allows custom refill rates', async () => { - const doubleRefillClient: RedisClientType = redis.createClient(); + const doubleRefillClient: ioredis.Redis = new RedisMock(); limiter = new TokenBucket(CAPACITY, 2, doubleRefillClient); await setTokenCountInClient(doubleRefillClient, user1, 0, timestamp); From 4074bc82ec61b10563e7e11661fcebc01514b503 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Mon, 30 May 2022 22:37:03 -0400 Subject: [PATCH 05/13] installed ioredis-mock --- package-lock.json | 146 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 147 insertions(+) diff --git a/package-lock.json b/package-lock.json index af77977..8782b71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", "husky": "^8.0.1", + "ioredis-mock": "^8.2.2", "jest": "^28.1.0", "lint-staged": "^12.4.1", "prettier": "2.6.2", @@ -1865,6 +1866,12 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/as-callback": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ioredis/as-callback/-/as-callback-3.0.0.tgz", + "integrity": "sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==", + "dev": true + }, "node_modules/@ioredis/commands": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz", @@ -4304,6 +4311,32 @@ "bser": "2.1.1" } }, + "node_modules/fengari": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.4.tgz", + "integrity": "sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g==", + "dev": true, + "dependencies": { + "readline-sync": "^1.4.9", + "sprintf-js": "^1.1.1", + "tmp": "^0.0.33" + } + }, + "node_modules/fengari-interop": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fengari-interop/-/fengari-interop-0.1.3.tgz", + "integrity": "sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw==", + "dev": true, + "peerDependencies": { + "fengari": "^0.1.0" + } + }, + "node_modules/fengari/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4790,6 +4823,24 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ioredis-mock": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/ioredis-mock/-/ioredis-mock-8.2.2.tgz", + "integrity": "sha512-XyJfcF6pqcLHwAYtldkzaLtjRxPw7d8U0FUfjgQ5U/d0vVhFxiXbqsILR4FEOp+ygzyZgBA8xye+uPKu74IH1A==", + "dev": true, + "dependencies": { + "@ioredis/as-callback": "^3.0.0", + "@ioredis/commands": "^1.1.1", + "fengari": "^0.1.4", + "fengari-interop": "^0.1.3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "ioredis": "5.x" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -6279,6 +6330,15 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -6631,6 +6691,15 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/redis": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/redis/-/redis-4.1.0.tgz", @@ -7302,6 +7371,18 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -9047,6 +9128,12 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@ioredis/as-callback": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ioredis/as-callback/-/as-callback-3.0.0.tgz", + "integrity": "sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==", + "dev": true + }, "@ioredis/commands": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz", @@ -10922,6 +11009,32 @@ "bser": "2.1.1" } }, + "fengari": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.4.tgz", + "integrity": "sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g==", + "dev": true, + "requires": { + "readline-sync": "^1.4.9", + "sprintf-js": "^1.1.1", + "tmp": "^0.0.33" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, + "fengari-interop": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fengari-interop/-/fengari-interop-0.1.3.tgz", + "integrity": "sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw==", + "dev": true, + "requires": {} + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -11259,6 +11372,18 @@ "standard-as-callback": "^2.1.0" } }, + "ioredis-mock": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/ioredis-mock/-/ioredis-mock-8.2.2.tgz", + "integrity": "sha512-XyJfcF6pqcLHwAYtldkzaLtjRxPw7d8U0FUfjgQ5U/d0vVhFxiXbqsILR4FEOp+ygzyZgBA8xye+uPKu74IH1A==", + "dev": true, + "requires": { + "@ioredis/as-callback": "^3.0.0", + "@ioredis/commands": "^1.1.1", + "fengari": "^0.1.4", + "fengari-interop": "^0.1.3" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -12377,6 +12502,12 @@ "word-wrap": "^1.2.3" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -12614,6 +12745,12 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, + "readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dev": true + }, "redis": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/redis/-/redis-4.1.0.tgz", @@ -13111,6 +13248,15 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 2bd02a7..2f81f2b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", "husky": "^8.0.1", + "ioredis-mock": "^8.2.2", "jest": "^28.1.0", "lint-staged": "^12.4.1", "prettier": "2.6.2", From 3ac1c94faab873d6b43db3566e0a56c734578191 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Tue, 31 May 2022 08:13:26 -0400 Subject: [PATCH 06/13] refactored middleware and complexityAnalysis to use variables --- src/analysis/typeComplexityAnalysis.ts | 6 +++++- src/middleware/index.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index 585550b..2af89b9 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -15,7 +15,11 @@ import { parse } from 'graphql'; * @param {TypeWeightObject} typeWeights * @param {string} complexityOption */ -function getQueryTypeComplexity(queryString: string, typeWeights: TypeWeightObject): number { +function getQueryTypeComplexity( + queryString: string, + queryVariables: any, + typeWeights: TypeWeightObject +): number { throw Error('getQueryComplexity is not implemented.'); } diff --git a/src/middleware/index.ts b/src/middleware/index.ts index ef1f518..f8287f2 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -42,7 +42,7 @@ export function expressRateLimiter( // return the rate limiting middleware return async (req: Request, res: Response, next: NextFunction): Promise => { const requestTimestamp = new Date().valueOf(); - const { query }: { query: string } = req.body; + const { query, variables }: { query: string; variables: any } = req.body; if (!query) { // FIXME: Throw an error here? Code currently passes this on to whatever is next console.log('There is no query on the request'); @@ -62,7 +62,7 @@ export function expressRateLimiter( const ip: string = req.ips[0] || req.ip; // FIXME: this will only work with type complexity - const queryComplexity = getQueryTypeComplexity(query, typeWeightObject); + const queryComplexity = getQueryTypeComplexity(query, variables, typeWeightObject); try { // process the request and conditinoally respond to client with status code 429 o From 18c06d27141728f2b161e5fed8852020da77d37c Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Tue, 31 May 2022 08:19:24 -0400 Subject: [PATCH 07/13] removed query variables parameter in complexityAnalysis. An issue for future work --- src/analysis/typeComplexityAnalysis.ts | 7 ++----- src/middleware/index.ts | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index 2af89b9..a1d7174 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -15,11 +15,8 @@ import { parse } from 'graphql'; * @param {TypeWeightObject} typeWeights * @param {string} complexityOption */ -function getQueryTypeComplexity( - queryString: string, - queryVariables: any, - typeWeights: TypeWeightObject -): number { +// TODO add queryVaribables parameter +function getQueryTypeComplexity(queryString: string, typeWeights: TypeWeightObject): number { throw Error('getQueryComplexity is not implemented.'); } diff --git a/src/middleware/index.ts b/src/middleware/index.ts index f8287f2..19b892f 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -62,7 +62,8 @@ export function expressRateLimiter( const ip: string = req.ips[0] || req.ip; // FIXME: this will only work with type complexity - const queryComplexity = getQueryTypeComplexity(query, variables, typeWeightObject); + // TODO: add query varibales parameter + const queryComplexity = getQueryTypeComplexity(query, typeWeightObject); try { // process the request and conditinoally respond to client with status code 429 o From 0e9447fd13afdff610ba16622954de211d82c69b Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Tue, 31 May 2022 08:24:36 -0400 Subject: [PATCH 08/13] refactored middleware to to .send an rejected request. --- src/middleware/index.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 19b892f..3515371 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -75,11 +75,8 @@ export function expressRateLimiter( ); if (rateLimiterResponse.success === false) { // TODO: add a header 'Retry-After' with the time to wait untill next query will succeed - res.status(429).json({ - timestamp: requestTimestamp, - complexity: queryComplexity, - tokens: rateLimiterResponse.tokens, - }); + // FIXME: send information about query complexity, tokens, etc, to the client on rejected query + res.status(429).send(); } res.locals.graphqlGate = { timestamp: requestTimestamp, From 2063e3f307789795e8e4883be41e470b69210073 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Wed, 1 Jun 2022 12:20:18 -0400 Subject: [PATCH 09/13] middleware is now parseing and validating the query before processing complexity. --- src/middleware/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 3515371..661afd3 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,4 +1,5 @@ import Redis, { RedisOptions } from 'ioredis'; +import { parse, validate } from 'graphql'; import { GraphQLSchema } from 'graphql/type/schema'; import { Request, Response, NextFunction, RequestHandler } from 'express'; @@ -62,8 +63,13 @@ export function expressRateLimiter( const ip: string = req.ips[0] || req.ip; // FIXME: this will only work with type complexity - // TODO: add query varibales parameter - const queryComplexity = getQueryTypeComplexity(query, typeWeightObject); + const queryAST = parse(query); + // validate the query against the schema. The GraphQL validation function returns an array of errors. + const validationErrors = validate(schema, queryAST); + // check if the length of the returned GraphQL Errors array is greater than zero. If it is, there were errors. Call next so that the GraphQL server can handle those. + if (validationErrors.length > 0) return next(); + + const queryComplexity = getQueryTypeComplexity(queryAST, variables, typeWeightObject); try { // process the request and conditinoally respond to client with status code 429 o From cc97c8e7ffe85ebca062205deb47fa402521dde9 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Wed, 1 Jun 2022 12:39:09 -0400 Subject: [PATCH 10/13] refactored the type complexity analysis function to accept query variables and the query AST --- src/analysis/typeComplexityAnalysis.ts | 9 +++- test/analysis/typeComplexityAnalysis.test.ts | 50 ++++++++++++-------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index a1d7174..cc603f3 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -1,4 +1,4 @@ -import { parse } from 'graphql'; +import { DocumentNode } from 'graphql'; /** * This function should @@ -13,10 +13,15 @@ import { parse } from 'graphql'; * * @param {string} queryString * @param {TypeWeightObject} typeWeights + * @param {any | undefined} varibales * @param {string} complexityOption */ // TODO add queryVaribables parameter -function getQueryTypeComplexity(queryString: string, typeWeights: TypeWeightObject): number { +function getQueryTypeComplexity( + queryString: DocumentNode, + varibales: any | undefined, + typeWeights: TypeWeightObject +): number { throw Error('getQueryComplexity is not implemented.'); } diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index e1bf05a..69297b9 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -1,3 +1,4 @@ +import { parse } from 'graphql'; import getQueryTypeComplexity from '../../src/analysis/typeComplexityAnalysis'; /** @@ -168,45 +169,46 @@ const typeWeights: TypeWeightObject = { xdescribe('Test getQueryTypeComplexity function', () => { let query = ''; + const variables: any | undefined = undefined; describe('Calculates the correct type complexity for queries', () => { test('with one feild', () => { query = `Query { scalars { num } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + Scalars 1 + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + Scalars 1 }); test('with two or more fields', () => { query = `Query { scalars { num } test { name } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); test('with one level of nested fields', () => { query = `Query { scalars { num, test { name } } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); test('with multiple levels of nesting', () => { query = `Query { scalars { num, test { name, scalars { id } } } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(4); // Query 1 + scalars 1 + test 1 + scalars 1 + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // Query 1 + scalars 1 + test 1 + scalars 1 }); test('with aliases', () => { query = `Query { foo: scalar { num } bar: scalar { id }}`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + scalar 1 + scalar 1 + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalar 1 + scalar 1 }); test('with all scalar fields', () => { query = `Query { scalars { id, num, float, bool, string } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + scalar 1 + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + scalar 1 }); test('with arguments and variables', () => { query = `Query { hero(episode: EMPIRE) { id, name } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1 + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 query = `Query { human(id: 1) { id, name, appearsIn } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + human/character 1 + appearsIn/episode + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + human/character 1 + appearsIn/episode // argument passed in as a variable - query = `Query { hero(episode: $ep) { id, name } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1 + query = `Query varibaleQuery ($ep: Episode){ hero(episode: $ep) { id, name } }`; + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 }); test('with fragments', () => { @@ -225,7 +227,7 @@ xdescribe('Test getQueryTypeComplexity function', () => { appearsIn } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(5); // Query 1 + 2*(character 1 + appearsIn/episode 1) + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(5); // Query 1 + 2*(character 1 + appearsIn/episode 1) }); test('with inline fragments', () => { @@ -241,7 +243,7 @@ xdescribe('Test getQueryTypeComplexity function', () => { } } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1) + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1) }); /** @@ -255,12 +257,14 @@ xdescribe('Test getQueryTypeComplexity function', () => { name } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(false); // ? + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(false); // ? }); - test('with lists detrmined by arguments', () => { + test('with lists detrmined by arguments and variables', () => { query = `Query {reviews(episode: EMPIRE, first: 3) { stars, commentary } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(4); // 1 Query + 3 reviews + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // 1 Query + 3 reviews + query = `Query queryVaribales($first: Int) {reviews(episode: EMPIRE, first: $first) { stars, commentary } }`; + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // 1 Query + 3 reviews }); test('with nested lists', () => { @@ -276,7 +280,7 @@ xdescribe('Test getQueryTypeComplexity function', () => { } } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(17); // 1 Query + 1 human/character + (5 friends/character X 3 friends/characters) + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(17); // 1 Query + 1 human/character + (5 friends/character X 3 friends/characters) }); test('accounting for __typename feild', () => { @@ -294,7 +298,7 @@ xdescribe('Test getQueryTypeComplexity function', () => { } } }`; - expect(getQueryTypeComplexity(query, typeWeights)).toBe(5); // 1 Query + 4 search results + expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(5); // 1 Query + 4 search results }); // todo: directives @skip, @include and custom directives @@ -302,11 +306,17 @@ xdescribe('Test getQueryTypeComplexity function', () => { // todo: expand on error handling test('Throws an error if for a bad query', () => { query = `Query { hello { hi } }`; // type doesn't exist - expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error'); + expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( + 'Error' + ); query = `Query { hero(episode: EMPIRE){ starship } }`; // field doesn't exist - expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error'); + expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( + 'Error' + ); query = `Query { hero(episode: EMPIRE) { id, name }`; // missing a closing bracket - expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error'); + expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( + 'Error' + ); }); }); From b24fcb9828d42d34de5095ab7361270f85fb0674 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Wed, 1 Jun 2022 12:44:07 -0400 Subject: [PATCH 11/13] fixed tests with variables --- test/analysis/typeComplexityAnalysis.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index 69297b9..112b320 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -169,7 +169,7 @@ const typeWeights: TypeWeightObject = { xdescribe('Test getQueryTypeComplexity function', () => { let query = ''; - const variables: any | undefined = undefined; + let variables: any | undefined; describe('Calculates the correct type complexity for queries', () => { test('with one feild', () => { query = `Query { scalars { num } }`; @@ -207,6 +207,7 @@ xdescribe('Test getQueryTypeComplexity function', () => { query = `Query { human(id: 1) { id, name, appearsIn } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + human/character 1 + appearsIn/episode // argument passed in as a variable + variables = { ep: 'EMPIRE' }; query = `Query varibaleQuery ($ep: Episode){ hero(episode: $ep) { id, name } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 }); @@ -263,6 +264,7 @@ xdescribe('Test getQueryTypeComplexity function', () => { test('with lists detrmined by arguments and variables', () => { query = `Query {reviews(episode: EMPIRE, first: 3) { stars, commentary } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // 1 Query + 3 reviews + variables = { first: 3 }; query = `Query queryVaribales($first: Int) {reviews(episode: EMPIRE, first: $first) { stars, commentary } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // 1 Query + 3 reviews }); From 2eedcbe8797cf05aa9b7e224abb2d2979b6e9ea8 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Thu, 2 Jun 2022 10:17:08 -0400 Subject: [PATCH 12/13] refactored the import of express rate limeter to work in test file --- test/middleware/express.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts index ff90782..6e63afa 100644 --- a/test/middleware/express.test.ts +++ b/test/middleware/express.test.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction, RequestHandler } from 'express'; import { GraphQLSchema, buildSchema } from 'graphql'; import * as ioredis from 'ioredis'; -import expressRateLimitMiddleware from '../../src/middleware/index'; +import { expressRateLimiter as expressRateLimitMiddleware } from '../../src/middleware/index'; // eslint-disable-next-line @typescript-eslint/no-var-requires const RedisMock = require('ioredis-mock'); From 92aa047cbf2b45a28876cae32ec4ea57808ce5fc Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Thu, 2 Jun 2022 11:48:37 -0400 Subject: [PATCH 13/13] added a description for redis client options --- src/middleware/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 1f4846b..75b0ee3 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -16,7 +16,7 @@ import getQueryTypeComplexity from '../analysis/typeComplexityAnalysis'; * @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} RedisOptions // TODO add dsecription + * @param {RedisClientOptions} RedisOptions ioredis connection options https://ioredis.readthedocs.io/en/stable/API/#new_Redis * @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