Skip to content

Commit 1b0ac1b

Browse files
authored
Merge pull request #34 from oslabs-beta/em/typeComplexityAlgo
Type complexity algorithm
2 parents 4cd7382 + 9ef5624 commit 1b0ac1b

File tree

15 files changed

+266
-77
lines changed

15 files changed

+266
-77
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@
2626
"error"
2727
]
2828
},
29-
"ignorePatterns": ["jest.*"]
29+
"ignorePatterns": ["jest.*", "build/*"]
3030
}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,5 @@ dist
102102

103103
# TernJS port file
104104
.tern-port
105+
106+
build/*

.vscode/launch.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [{
7+
"type": "node",
8+
"request": "launch",
9+
"name": "Jest Tests",
10+
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
11+
"args": [
12+
"-i", "--verbose", "--no-cache"
13+
],
14+
// "preLaunchTask": "build",
15+
// "internalConsoleOptions": "openOnSessionStart",
16+
// "outFiles": [
17+
// "${workspaceRoot}/dist/**/*"
18+
// ],
19+
// "envFile": "${workspaceRoot}/.env"
20+
}]
21+
}

.vscode/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,19 @@
55
"source.fixAll.eslint": true
66
},
77
"editor.formatOnSave": true,
8+
"configurations": [{
9+
"type": "node",
10+
"request": "launch",
11+
"name": "Jest Tests",
12+
"program": "${workspaceRoot}\\node_modules\\jest\\bin\\jest.js",
13+
"args": [
14+
"-i"
15+
],
16+
// "preLaunchTask": "build",
17+
"internalConsoleOptions": "openOnSessionStart",
18+
"outFiles": [
19+
"${workspaceRoot}/dist/**/*"
20+
],
21+
"envFile": "${workspaceRoot}/.env"
22+
}]
823
}

src/@types/buildTypeWeights.d.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
interface Fields {
2-
[index: string]: number | ((args: ArgumentNode[]) => number);
1+
export interface Fields {
2+
[index: string]: FieldWeight;
33
}
4-
5-
interface Type {
4+
export type WeightFunction = (args: ArgumentNode[]) => number;
5+
export type FieldWeight = number | WeightFunction;
6+
export interface Type {
67
readonly weight: number;
78
readonly fields: Fields;
89
}
9-
10-
interface TypeWeightObject {
10+
export interface TypeWeightObject {
1111
[index: string]: Type;
1212
}
13-
14-
interface TypeWeightConfig {
13+
export interface TypeWeightConfig {
1514
mutation?: number;
1615
query?: number;
1716
object?: number;

src/@types/rateLimit.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
interface RateLimiter {
1+
export interface RateLimiter {
22
/**
33
* Checks if a request is allowed under the given conditions and withdraws the specified number of tokens
44
* @param uuid Unique identifier for the user associated with the request
@@ -13,17 +13,17 @@ interface RateLimiter {
1313
) => Promise<RateLimiterResponse>;
1414
}
1515

16-
interface RateLimiterResponse {
16+
export interface RateLimiterResponse {
1717
success: boolean;
1818
tokens: number;
1919
}
2020

21-
interface RedisBucket {
21+
export interface RedisBucket {
2222
tokens: number;
2323
timestamp: number;
2424
}
2525

26-
type RateLimiterSelection =
26+
export type RateLimiterSelection =
2727
| 'TOKEN_BUCKET'
2828
| 'LEAKY_BUCKET'
2929
| 'FIXED_WINDOW'
@@ -34,12 +34,12 @@ type RateLimiterSelection =
3434
* @type {number} bucketSize - Size of the token bucket
3535
* @type {number} refillRate - Rate at which tokens are added to the bucket in seconds
3636
*/
37-
interface TokenBucketOptions {
37+
export interface TokenBucketOptions {
3838
bucketSize: number;
3939
refillRate: number;
4040
}
4141

4242
// TODO: This will be a union type where we can specify Option types for other Rate Limiters
4343
// Record<string, never> represents the empty object for alogorithms that don't require settings
4444
// and might be able to be removed in the future.
45-
type RateLimiterOptions = TokenBucketOptions | Record<string, never>;
45+
export type RateLimiterOptions = TokenBucketOptions | Record<string, never>;

src/analysis/ASTnodefunctions.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/* eslint-disable @typescript-eslint/no-use-before-define */
2+
import {
3+
DocumentNode,
4+
FieldNode,
5+
SelectionSetNode,
6+
DefinitionNode,
7+
Kind,
8+
SelectionNode,
9+
ArgumentNode,
10+
} from 'graphql';
11+
import { FieldWeight, TypeWeightObject } from '../@types/buildTypeWeights';
12+
13+
// TODO: handle variables and arguments
14+
// ! this is not functional
15+
const getArgObj = (args: ArgumentNode[]): { [index: string]: any } => {
16+
const argObj: { [index: string]: any } = {};
17+
for (let i = 0; i < args.length; i + 1) {
18+
const node = args[i];
19+
if (args[i].value.kind !== Kind.VARIABLE) {
20+
if (args[i].value.kind === Kind.INT) {
21+
// FIXME: this does not work
22+
argObj[args[i].name.value] = args[i].value;
23+
}
24+
}
25+
}
26+
return argObj;
27+
};
28+
/**
29+
* The AST node functions call each other following the nested structure below
30+
* Each function handles a specific GraphQL AST node type
31+
*
32+
* AST nodes call each other in the following way
33+
*
34+
* Document Node
35+
* |
36+
* Definiton Node
37+
* (operation and fragment definitons)
38+
* / \
39+
* |-----> Selection Set Node not done
40+
* | /
41+
* | Selection Node
42+
* | (Field, Inline fragment and fragment spread)
43+
* | | \ \
44+
* |--Field Node not done not done
45+
*
46+
*/
47+
48+
export function fieldNode(
49+
node: FieldNode,
50+
typeWeights: TypeWeightObject,
51+
variables: any | undefined,
52+
parentName: string
53+
): number {
54+
let complexity = 0;
55+
// console.log('fieldNode', node, parentName);
56+
// check if the field name is in the type weight object.
57+
if (node.name.value.toLocaleLowerCase() in typeWeights) {
58+
// if it is, than the field is an object type, add itss type weight to the total
59+
complexity += typeWeights[node.name.value].weight;
60+
// call the function to handle selection set node with selectionSet property if it is not undefined
61+
if (node.selectionSet) {
62+
complexity += selectionSetNode(
63+
node.selectionSet,
64+
typeWeights,
65+
variables,
66+
node.name.value
67+
);
68+
}
69+
} else {
70+
// otherwise the field is a scalar or a list.
71+
const fieldWeight: FieldWeight = typeWeights[parentName].fields[node.name.value];
72+
if (typeof fieldWeight === 'number') {
73+
// if the feild weight is a number, add the number to the total complexity
74+
complexity += fieldWeight;
75+
} else if (node.arguments) {
76+
// otherwise the the feild weight is a list, invoke the function with variables
77+
// TODO: calculate the complexity for lists with arguments and varibales
78+
// ! this is not functional
79+
// iterate through the arguments to build the object to
80+
complexity += fieldWeight([...node.arguments]);
81+
}
82+
}
83+
return complexity;
84+
}
85+
86+
export function selectionNode(
87+
node: SelectionNode,
88+
typeWeights: TypeWeightObject,
89+
variables: any | undefined,
90+
parentName: string
91+
): number {
92+
let complexity = 0;
93+
// console.log('selectionNode', node, parentName);
94+
// check the kind property against the set of selection nodes that are possible
95+
if (node.kind === Kind.FIELD) {
96+
// call the function that handle field nodes
97+
complexity += fieldNode(node, typeWeights, variables, parentName);
98+
}
99+
// TODO: add checks for Kind.FRAGMENT_SPREAD and Kind.INLINE_FRAGMENT here
100+
return complexity;
101+
}
102+
103+
export function selectionSetNode(
104+
node: SelectionSetNode,
105+
typeWeights: TypeWeightObject,
106+
variables: any | undefined,
107+
parentName: string
108+
): number {
109+
let complexity = 0;
110+
// iterate shrough the 'selections' array on the seletion set node
111+
for (let i = 0; i < node.selections.length; i += 1) {
112+
// call the function to handle seletion nodes
113+
// pass the current parent through because selection sets act only as intermediaries
114+
complexity += selectionNode(node.selections[i], typeWeights, variables, parentName);
115+
}
116+
return complexity;
117+
}
118+
119+
export function definitionNode(
120+
node: DefinitionNode,
121+
typeWeights: TypeWeightObject,
122+
variables: any | undefined
123+
): number {
124+
let complexity = 0;
125+
// check the kind property against the set of definiton nodes that are possible
126+
if (node.kind === Kind.OPERATION_DEFINITION) {
127+
// check if the operation is in the type weights object.
128+
if (node.operation.toLocaleLowerCase() in typeWeights) {
129+
// if it is, it is an object type, add it's type weight to the total
130+
complexity += typeWeights[node.operation].weight;
131+
// call the function to handle selection set node with selectionSet property if it is not undefined
132+
if (node.selectionSet)
133+
complexity += selectionSetNode(
134+
node.selectionSet,
135+
typeWeights,
136+
variables,
137+
node.operation
138+
);
139+
}
140+
}
141+
// TODO: add checks for Kind.FRAGMENT_DEFINITION here (there are other type definition nodes that i think we can ignore. see ast.d.ts in 'graphql')
142+
return complexity;
143+
}
144+
145+
export function documentNode(
146+
node: DocumentNode,
147+
typeWeights: TypeWeightObject,
148+
variables: any | undefined
149+
): number {
150+
let complexity = 0;
151+
// iterate through 'definitions' array on the document node
152+
for (let i = 0; i < node.definitions.length; i += 1) {
153+
// call the function to handle the various types of definition nodes
154+
complexity += definitionNode(node.definitions[i], typeWeights, variables);
155+
}
156+
return complexity;
157+
}

src/analysis/buildTypeWeights.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { Maybe } from 'graphql/jsutils/Maybe';
2020
import { ObjMap } from 'graphql/jsutils/ObjMap';
2121
import { GraphQLSchema } from 'graphql/type/schema';
22+
import { TypeWeightConfig, TypeWeightObject } from '../@types/buildTypeWeights';
2223

2324
export const KEYWORDS = ['first', 'last', 'limit'];
2425

@@ -81,6 +82,7 @@ function parseQuery(
8182
queryFields[field].args.forEach((arg: GraphQLArgument) => {
8283
// If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query
8384
// should be dependent on both the weight of the resolved type and the limiting argument.
85+
// FIXME: Can nonnull wrap list types?
8486
if (KEYWORDS.includes(arg.name) && isListType(resolveType)) {
8587
// Get the type that comprises the list
8688
const listType = resolveType.ofType;
@@ -125,7 +127,7 @@ function parseQuery(
125127
}
126128
});
127129

128-
// if the field is a scalar or an enum set weight accordingly
130+
// if the field is a scalar or an enum set weight accordingly. It is not a list in this case
129131
if (isScalarType(resolveType) || isEnumType(resolveType)) {
130132
result.query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
131133
}
@@ -152,7 +154,7 @@ function parseTypes(
152154

153155
// Handle Object, Interface, Enum and Union types
154156
Object.keys(typeMap).forEach((type) => {
155-
const typeName = type.toLowerCase();
157+
const typeName: string = type.toLowerCase();
156158

157159
const currentType: GraphQLNamedType = typeMap[type];
158160
// Get all types that aren't Query or Mutation or a built in type that starts with '__'
@@ -219,7 +221,7 @@ function buildTypeWeightsFromSchema(
219221
...typeWeightsConfig,
220222
};
221223

222-
// Confirm that any custom weights are positive
224+
// Confirm that any custom weights are non-negative
223225
Object.entries(typeWeights).forEach((value: [string, number]) => {
224226
if (value[1] < 0) {
225227
throw new Error(`Type weights cannot be negative. Received: ${value[0]}: ${value[1]} `);
Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
import { DocumentNode } from 'graphql';
2+
import { TypeWeightObject } from '../@types/buildTypeWeights';
3+
import { documentNode } from './ASTnodefunctions';
24

35
/**
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
6+
* Calculate the complexity for the query by recursivly traversing through the query AST,
7+
* checking the query fields against the type weight object and totaling the weights of every field.
118
*
12-
* TO DO: extend the functionality to work for mutations and subscriptions
9+
* TO DO: extend the functionality to work for mutations and subscriptions and directives
1310
*
14-
* @param {string} queryString
15-
* @param {TypeWeightObject} typeWeights
11+
* @param {string} queryAST
1612
* @param {any | undefined} varibales
17-
* @param {string} complexityOption
13+
* @param {TypeWeightObject} typeWeights
1814
*/
19-
// TODO add queryVaribables parameter
2015
function getQueryTypeComplexity(
21-
queryString: DocumentNode,
22-
varibales: any | undefined,
16+
queryAST: DocumentNode,
17+
variables: any | undefined,
2318
typeWeights: TypeWeightObject
2419
): number {
25-
throw Error('getQueryComplexity is not implemented.');
20+
let complexity = 0;
21+
complexity += documentNode(queryAST, typeWeights, variables);
22+
return complexity;
2623
}
2724

2825
export default getQueryTypeComplexity;

0 commit comments

Comments
 (0)