diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts new file mode 100644 index 0000000..585550b --- /dev/null +++ b/src/analysis/typeComplexityAnalysis.ts @@ -0,0 +1,22 @@ +import { parse } from 'graphql'; + +/** + * This function should + * 1. validate the query using graphql methods + * 2. parse the query string using the graphql parse method + * 3. itreate through the query AST and + * - cross reference the type weight object to check type weight + * - total all the eweights of all types in the query + * 4. return the total as the query complexity + * + * TO DO: extend the functionality to work for mutations and subscriptions + * + * @param {string} queryString + * @param {TypeWeightObject} typeWeights + * @param {string} complexityOption + */ +function getQueryTypeComplexity(queryString: string, typeWeights: TypeWeightObject): number { + throw Error('getQueryComplexity is not implemented.'); +} + +export default getQueryTypeComplexity; diff --git a/src/rateLimiters/tokenBucket.ts b/src/rateLimiters/tokenBucket.ts index 378c974..7f34d2b 100644 --- a/src/rateLimiters/tokenBucket.ts +++ b/src/rateLimiters/tokenBucket.ts @@ -9,11 +9,11 @@ import { RedisClientType } from 'redis'; * 4. Otherwise, disallow the request and do not update the token total. */ class TokenBucket implements RateLimiter { - capacity: number; + private capacity: number; - refillRate: number; + private refillRate: number; - client: RedisClientType; + private client: RedisClientType; /** * Create a new instance of a TokenBucket rate limiter that can be connected to any database store @@ -29,6 +29,15 @@ class TokenBucket implements RateLimiter { throw Error('TokenBucket refillRate and capacity must be positive'); } + /** + * + * + * @param {string} uuid - unique identifer used to throttle requests + * @param {number} timestamp - time the request was recieved + * @param {number} [tokens=1] - complexity of the query for throttling requests + * @return {*} {Promise} + * @memberof TokenBucket + */ async processRequest( uuid: string, timestamp: number, diff --git a/test/analysis/buildTypeWeights.test.ts b/test/analysis/buildTypeWeights.test.ts index 5ded759..1cfc307 100644 --- a/test/analysis/buildTypeWeights.test.ts +++ b/test/analysis/buildTypeWeights.test.ts @@ -6,10 +6,12 @@ import buildTypeWeightsFromSchema from '../../src/analysis/buildTypeWeights'; interface TestFields { [index: string]: number; } + interface TestType { weight: number; fields: TestFields; } + interface TestTypeWeightObject { [index: string]: TestType; } diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts new file mode 100644 index 0000000..e1bf05a --- /dev/null +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -0,0 +1,316 @@ +import getQueryTypeComplexity from '../../src/analysis/typeComplexityAnalysis'; + +/** + * Here is the schema that creates the followning 'typeWeightsObject' used for the tests + * + type Query { + hero(episode: Episode): Character + reviews(episode: Episode!, first: Int): [Review] + search(text: String): [SearchResult] + character(id: ID!): Character + droid(id: ID!): Droid + human(id: ID!): Human + scalars: Scalars + } + + enum Episode { + NEWHOPE + EMPIRE + JEDI + } + + interface Character { + id: ID! + name: String! + friends(first: Int): [Character] + appearsIn: [Episode]! + } + + type Human implements Character { + id: ID! + name: String! + homePlanet: String + friends(first: Int): [Character] + appearsIn: [Episode]! + } + + type Droid implements Character { + id: ID! + name: String! + friends(first: Int): [Character] + primaryFunction: String + appearsIn: [Episode]! + } + + type Review { + episode: Episode + stars: Int! + commentary: String + } + + union SearchResult = Human | Droid + + type Scalars { + num: Int, + id: ID, + float: Float, + bool: Boolean, + string: String + test: Test, + } + + type Test { + name: String, + variable: Scalars + } + + * + * TODO: extend this schema to include mutations, subscriptions and pagination + * + type Mutation { + createReview(episode: Episode, review: ReviewInput!): Review + } + type Subscription { + reviewAdded(episode: Episode): Review + } + type FriendsConnection { + totalCount: Int + edges: [FriendsEdge] + friends: [Character] + pageInfo: PageInfo! + } + type FriendsEdge { + cursor: ID! + node: Character + } + type PageInfo { + startCursor: ID + endCursor: ID + hasNextPage: Boolean! + } + + add + friendsConnection(first: Int, after: ID): FriendsConnection! + to character, human and droid +*/ + +// this object is created by the schema above for use in all the tests below +const typeWeights: TypeWeightObject = { + query: { + // object type + weight: 1, + fields: { + // FIXME: update the function def that is supposed te be here to match implementation + // FIXME: add the function definition for the 'search' field which returns a list + reviews: (arg, type) => arg * type.weight, + }, + }, + episode: { + // enum + weight: 0, + fields: {}, + }, + character: { + // interface + weight: 1, + fields: { + id: 0, + name: 0, + // FIXME: add the function definition for the 'friends' field which returns a list + }, + }, + human: { + // implements an interface + weight: 1, + fields: { + id: 0, + name: 0, + homePlanet: 0, + }, + }, + droid: { + // implements an interface + weight: 1, + fields: { + id: 0, + name: 0, + }, + }, + review: { + weight: 1, + fields: { + stars: 0, + commentary: 0, + }, + }, + searchResult: { + // union type + weight: 1, + fields: {}, + }, + scalars: { + weight: 1, // object weight is 1, all scalar feilds have weight 0 + fields: { + num: 0, + id: 0, + float: 0, + bool: 0, + string: 0, + }, + }, + test: { + weight: 1, + fields: { + name: 0, + }, + }, +}; + +xdescribe('Test getQueryTypeComplexity function', () => { + let query = ''; + 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 + }); + + test('with two or more fields', () => { + query = `Query { scalars { num } test { name } }`; + expect(getQueryTypeComplexity(query, 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 + }); + + 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 + }); + + test('with aliases', () => { + query = `Query { foo: scalar { num } bar: scalar { id }}`; + expect(getQueryTypeComplexity(query, 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 + }); + + test('with arguments and variables', () => { + query = `Query { hero(episode: EMPIRE) { id, name } }`; + expect(getQueryTypeComplexity(query, 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 + // argument passed in as a variable + query = `Query { hero(episode: $ep) { id, name } }`; + expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1 + }); + + test('with fragments', () => { + query = ` + Query { + leftComparison: hero(episode: EMPIRE) { + ...comparisonFields + } + rightComparison: hero(episode: JEDI) { + ...comparisonFields + } + } + + fragment comparisonFields on Character { + name + appearsIn + } + }`; + expect(getQueryTypeComplexity(query, typeWeights)).toBe(5); // Query 1 + 2*(character 1 + appearsIn/episode 1) + }); + + test('with inline fragments', () => { + query = ` + Query { + hero(episode: EMPIRE) { + name + ... on Droid { + primaryFunction + } + ... on Human { + homeplanet + } + } + }`; + expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1) + }); + + /** + * FIXME: handle lists of unknown size. change the expected result Once we figure out the implementation. + */ + xtest('with lists of unknown size', () => { + query = ` + Query { + search(text: 'hi') { + id + name + } + }`; + expect(getQueryTypeComplexity(query, typeWeights)).toBe(false); // ? + }); + + test('with lists detrmined by arguments', () => { + query = `Query {reviews(episode: EMPIRE, first: 3) { stars, commentary } }`; + expect(getQueryTypeComplexity(query, typeWeights)).toBe(4); // 1 Query + 3 reviews + }); + + test('with nested lists', () => { + query = ` + query { + human(id: 1) { + name, + friends(first: 5) { + name, + friends(first: 3){ + name + } + } + } + }`; + expect(getQueryTypeComplexity(query, typeWeights)).toBe(17); // 1 Query + 1 human/character + (5 friends/character X 3 friends/characters) + }); + + test('accounting for __typename feild', () => { + query = ` + query { + search(text: "an", first: 4) { + __typename + ... on Human { + name + homePlanet + } + ... on Droid { + name + primaryFunction + } + } + }`; + expect(getQueryTypeComplexity(query, typeWeights)).toBe(5); // 1 Query + 4 search results + }); + + // todo: directives @skip, @include and custom directives + + // 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'); + query = `Query { hero(episode: EMPIRE){ starship } }`; // field doesn't exist + expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error'); + query = `Query { hero(episode: EMPIRE) { id, name }`; // missing a closing bracket + expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error'); + }); + }); + + xdescribe('Calculates the correct type complexity for mutations', () => {}); + + xdescribe('Calculates the correct type complexity for subscriptions', () => {}); +});