Skip to content

Commit 35a928d

Browse files
authored
Merge pull request #23 from oslabs-beta/em/complexityTests
Test suite for the type complexity analysis algorithm
2 parents f2e5764 + f0f9a01 commit 35a928d

File tree

4 files changed

+352
-3
lines changed

4 files changed

+352
-3
lines changed
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/rateLimiters/tokenBucket.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import { RedisClientType } from 'redis';
99
* 4. Otherwise, disallow the request and do not update the token total.
1010
*/
1111
class TokenBucket implements RateLimiter {
12-
capacity: number;
12+
private capacity: number;
1313

14-
refillRate: number;
14+
private refillRate: number;
1515

16-
client: RedisClientType;
16+
private client: RedisClientType;
1717

1818
/**
1919
* 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 {
2929
throw Error('TokenBucket refillRate and capacity must be positive');
3030
}
3131

32+
/**
33+
*
34+
*
35+
* @param {string} uuid - unique identifer used to throttle requests
36+
* @param {number} timestamp - time the request was recieved
37+
* @param {number} [tokens=1] - complexity of the query for throttling requests
38+
* @return {*} {Promise<RateLimiterResponse>}
39+
* @memberof TokenBucket
40+
*/
3241
async processRequest(
3342
uuid: string,
3443
timestamp: number,

test/analysis/buildTypeWeights.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import buildTypeWeightsFromSchema from '../../src/analysis/buildTypeWeights';
66
interface TestFields {
77
[index: string]: number;
88
}
9+
910
interface TestType {
1011
weight: number;
1112
fields: TestFields;
1213
}
14+
1315
interface TestTypeWeightObject {
1416
[index: string]: TestType;
1517
}
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import getQueryTypeComplexity from '../../src/analysis/typeComplexityAnalysis';
2+
3+
/**
4+
* Here is the schema that creates the followning 'typeWeightsObject' used for the tests
5+
*
6+
type Query {
7+
hero(episode: Episode): Character
8+
reviews(episode: Episode!, first: Int): [Review]
9+
search(text: String): [SearchResult]
10+
character(id: ID!): Character
11+
droid(id: ID!): Droid
12+
human(id: ID!): Human
13+
scalars: Scalars
14+
}
15+
16+
enum Episode {
17+
NEWHOPE
18+
EMPIRE
19+
JEDI
20+
}
21+
22+
interface Character {
23+
id: ID!
24+
name: String!
25+
friends(first: Int): [Character]
26+
appearsIn: [Episode]!
27+
}
28+
29+
type Human implements Character {
30+
id: ID!
31+
name: String!
32+
homePlanet: String
33+
friends(first: Int): [Character]
34+
appearsIn: [Episode]!
35+
}
36+
37+
type Droid implements Character {
38+
id: ID!
39+
name: String!
40+
friends(first: Int): [Character]
41+
primaryFunction: String
42+
appearsIn: [Episode]!
43+
}
44+
45+
type Review {
46+
episode: Episode
47+
stars: Int!
48+
commentary: String
49+
}
50+
51+
union SearchResult = Human | Droid
52+
53+
type Scalars {
54+
num: Int,
55+
id: ID,
56+
float: Float,
57+
bool: Boolean,
58+
string: String
59+
test: Test,
60+
}
61+
62+
type Test {
63+
name: String,
64+
variable: Scalars
65+
}
66+
67+
*
68+
* TODO: extend this schema to include mutations, subscriptions and pagination
69+
*
70+
type Mutation {
71+
createReview(episode: Episode, review: ReviewInput!): Review
72+
}
73+
type Subscription {
74+
reviewAdded(episode: Episode): Review
75+
}
76+
type FriendsConnection {
77+
totalCount: Int
78+
edges: [FriendsEdge]
79+
friends: [Character]
80+
pageInfo: PageInfo!
81+
}
82+
type FriendsEdge {
83+
cursor: ID!
84+
node: Character
85+
}
86+
type PageInfo {
87+
startCursor: ID
88+
endCursor: ID
89+
hasNextPage: Boolean!
90+
}
91+
92+
add
93+
friendsConnection(first: Int, after: ID): FriendsConnection!
94+
to character, human and droid
95+
*/
96+
97+
// this object is created by the schema above for use in all the tests below
98+
const typeWeights: TypeWeightObject = {
99+
query: {
100+
// object type
101+
weight: 1,
102+
fields: {
103+
// FIXME: update the function def that is supposed te be here to match implementation
104+
// FIXME: add the function definition for the 'search' field which returns a list
105+
reviews: (arg, type) => arg * type.weight,
106+
},
107+
},
108+
episode: {
109+
// enum
110+
weight: 0,
111+
fields: {},
112+
},
113+
character: {
114+
// interface
115+
weight: 1,
116+
fields: {
117+
id: 0,
118+
name: 0,
119+
// FIXME: add the function definition for the 'friends' field which returns a list
120+
},
121+
},
122+
human: {
123+
// implements an interface
124+
weight: 1,
125+
fields: {
126+
id: 0,
127+
name: 0,
128+
homePlanet: 0,
129+
},
130+
},
131+
droid: {
132+
// implements an interface
133+
weight: 1,
134+
fields: {
135+
id: 0,
136+
name: 0,
137+
},
138+
},
139+
review: {
140+
weight: 1,
141+
fields: {
142+
stars: 0,
143+
commentary: 0,
144+
},
145+
},
146+
searchResult: {
147+
// union type
148+
weight: 1,
149+
fields: {},
150+
},
151+
scalars: {
152+
weight: 1, // object weight is 1, all scalar feilds have weight 0
153+
fields: {
154+
num: 0,
155+
id: 0,
156+
float: 0,
157+
bool: 0,
158+
string: 0,
159+
},
160+
},
161+
test: {
162+
weight: 1,
163+
fields: {
164+
name: 0,
165+
},
166+
},
167+
};
168+
169+
xdescribe('Test getQueryTypeComplexity function', () => {
170+
let query = '';
171+
describe('Calculates the correct type complexity for queries', () => {
172+
test('with one feild', () => {
173+
query = `Query { scalars { num } }`;
174+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + Scalars 1
175+
});
176+
177+
test('with two or more fields', () => {
178+
query = `Query { scalars { num } test { name } }`;
179+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1
180+
});
181+
182+
test('with one level of nested fields', () => {
183+
query = `Query { scalars { num, test { name } } }`;
184+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1
185+
});
186+
187+
test('with multiple levels of nesting', () => {
188+
query = `Query { scalars { num, test { name, scalars { id } } } }`;
189+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(4); // Query 1 + scalars 1 + test 1 + scalars 1
190+
});
191+
192+
test('with aliases', () => {
193+
query = `Query { foo: scalar { num } bar: scalar { id }}`;
194+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + scalar 1 + scalar 1
195+
});
196+
197+
test('with all scalar fields', () => {
198+
query = `Query { scalars { id, num, float, bool, string } }`;
199+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + scalar 1
200+
});
201+
202+
test('with arguments and variables', () => {
203+
query = `Query { hero(episode: EMPIRE) { id, name } }`;
204+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1
205+
query = `Query { human(id: 1) { id, name, appearsIn } }`;
206+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(3); // Query 1 + human/character 1 + appearsIn/episode
207+
// argument passed in as a variable
208+
query = `Query { hero(episode: $ep) { id, name } }`;
209+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1
210+
});
211+
212+
test('with fragments', () => {
213+
query = `
214+
Query {
215+
leftComparison: hero(episode: EMPIRE) {
216+
...comparisonFields
217+
}
218+
rightComparison: hero(episode: JEDI) {
219+
...comparisonFields
220+
}
221+
}
222+
223+
fragment comparisonFields on Character {
224+
name
225+
appearsIn
226+
}
227+
}`;
228+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(5); // Query 1 + 2*(character 1 + appearsIn/episode 1)
229+
});
230+
231+
test('with inline fragments', () => {
232+
query = `
233+
Query {
234+
hero(episode: EMPIRE) {
235+
name
236+
... on Droid {
237+
primaryFunction
238+
}
239+
... on Human {
240+
homeplanet
241+
}
242+
}
243+
}`;
244+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(2); // Query 1 + hero/character 1)
245+
});
246+
247+
/**
248+
* FIXME: handle lists of unknown size. change the expected result Once we figure out the implementation.
249+
*/
250+
xtest('with lists of unknown size', () => {
251+
query = `
252+
Query {
253+
search(text: 'hi') {
254+
id
255+
name
256+
}
257+
}`;
258+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(false); // ?
259+
});
260+
261+
test('with lists detrmined by arguments', () => {
262+
query = `Query {reviews(episode: EMPIRE, first: 3) { stars, commentary } }`;
263+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(4); // 1 Query + 3 reviews
264+
});
265+
266+
test('with nested lists', () => {
267+
query = `
268+
query {
269+
human(id: 1) {
270+
name,
271+
friends(first: 5) {
272+
name,
273+
friends(first: 3){
274+
name
275+
}
276+
}
277+
}
278+
}`;
279+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(17); // 1 Query + 1 human/character + (5 friends/character X 3 friends/characters)
280+
});
281+
282+
test('accounting for __typename feild', () => {
283+
query = `
284+
query {
285+
search(text: "an", first: 4) {
286+
__typename
287+
... on Human {
288+
name
289+
homePlanet
290+
}
291+
... on Droid {
292+
name
293+
primaryFunction
294+
}
295+
}
296+
}`;
297+
expect(getQueryTypeComplexity(query, typeWeights)).toBe(5); // 1 Query + 4 search results
298+
});
299+
300+
// todo: directives @skip, @include and custom directives
301+
302+
// todo: expand on error handling
303+
test('Throws an error if for a bad query', () => {
304+
query = `Query { hello { hi } }`; // type doesn't exist
305+
expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error');
306+
query = `Query { hero(episode: EMPIRE){ starship } }`; // field doesn't exist
307+
expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error');
308+
query = `Query { hero(episode: EMPIRE) { id, name }`; // missing a closing bracket
309+
expect(() => getQueryTypeComplexity(query, typeWeights)).toThrow('Error');
310+
});
311+
});
312+
313+
xdescribe('Calculates the correct type complexity for mutations', () => {});
314+
315+
xdescribe('Calculates the correct type complexity for subscriptions', () => {});
316+
});

0 commit comments

Comments
 (0)