From e0e540d40d3d8ea6e9921170e3f62e9d586f84cc Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Wed, 1 Jun 2022 14:39:13 -0400 Subject: [PATCH 01/10] laid out the framework for the type complxity analysis algorithm --- src/analysis/typeComplexityAnalysis.ts | 80 ++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index cc603f3..2ace4da 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -1,4 +1,5 @@ -import { DocumentNode } from 'graphql'; +import { query } from 'express'; +import { ASTNode, DocumentNode, Kind } from 'graphql'; /** * This function should @@ -11,18 +12,85 @@ import { DocumentNode } from 'graphql'; * * TO DO: extend the functionality to work for mutations and subscriptions * - * @param {string} queryString - * @param {TypeWeightObject} typeWeights + * @param {string} queryAST * @param {any | undefined} varibales - * @param {string} complexityOption + * @param {TypeWeightObject} typeWeights */ // TODO add queryVaribables parameter function getQueryTypeComplexity( - queryString: DocumentNode, + queryAST: DocumentNode, varibales: any | undefined, typeWeights: TypeWeightObject ): number { - throw Error('getQueryComplexity is not implemented.'); + const recursive = (node: ASTNode, parent: ASTNode | null = null): number => { + /** + * pseudo code of the process + * + // if 'kind' property is 'Document' + // iterate through queryAST.definitions array + // call recursive with object + + // if 'kind' property is 'operationDefinition' + // check 'operation' value against the type weights and add to total + // call recursive with selectionSet property if it is not undefined + + // if 'kind' is 'selectionSet' + // iterate shrough the 'selections' array of fields + // if 'selectinSet' is not undefined, call recursive with the field + + // if 'kind' property is 'feild' + // check the fields name.value against the type weights and total + // if there is a match, it is an objcet type with feilds, + // call recursive with selectionSet property if it is not undefined + // if it is not a match, it is a scalar field, look in the parent.name.value to check type weights feilds + */ + + let complexity = 0; + const parentName: string = parent?.operation || parent?.name.value || null; + + if (node.kind === Kind.DOCUMENT) { + // if 'kind' property is a 'Document' + // iterate through queryAST.definitions array + for (let i = 0; i < node.definitions.length; i + 1) { + // call recursive with the definition node + complexity += recursive(node.definitions[i], node); + } + } else if (node.kind === Kind.OPERATION_DEFINITION) { + // if 'kind' property is 'operationDefinition' + // TODO: case-sensitive + if (node.operation in typeWeights) { + // check 'operation' value against the type weights and add to total + complexity += typeWeights[node.operation].weight; + // call recursive with selectionSet property if it is not undefined + if (node.selectionSet) complexity += recursive(node.selectionSet, node); + } + } else if (node.kind === Kind.SELECTION_SET) { + // if 'kind' is 'selectionSet' + // iterate shrough the 'selections' array of fields + for (let i = 0; i < node.selections.length; i + 1) { + // call recursive with the field + complexity += recursive(node.selections[i], parent); // passing the current parent through because selection sets act only as intermediaries + } + } else if (node.kind === Kind.FIELD) { + // if 'kind' property is 'field' + // check the fields name.value against the type weights and total + // TODO: case-sensitive + if (node.name.value in typeWeights) { + // if there is a match, it is an objcet type with feilds, + complexity += typeWeights[node.name.value].weight; + // call recursive with selectionSet property if it is not undefined + if (node.selectionSet) complexity += recursive(node.selectionSet, node); + // node.name.value in typeWeights[parent.operation || parent.name.value].fields + } else if (parent?.opeartion) { + // if it is not a match, it is a scalar field, look in the parent.name.value to check type weights feilds + // TODO: if it is a list, need to look at the parent + complexity += typeWeights[parent.name.value].fields[node.name.value]; + } + } + + return complexity; + }; + return recursive(queryAST); } export default getQueryTypeComplexity; From 610d7b5d884e80436020de9477fca6d215c9dbfd Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Wed, 1 Jun 2022 15:13:19 -0400 Subject: [PATCH 02/10] commiting changes. considering overall strategy --- src/analysis/typeComplexityAnalysis.ts | 61 +++++--------------- test/analysis/typeComplexityAnalysis.test.ts | 2 +- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index 2ace4da..ebe49f0 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -1,16 +1,10 @@ -import { query } from 'express'; import { ASTNode, DocumentNode, Kind } 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 + * Calculate the complexity for the query by recursivly traversing through the query AST, + * checking the query fields against the type weight object and totaling the weights of every field. * - * TO DO: extend the functionality to work for mutations and subscriptions + * TO DO: extend the functionality to work for mutations and subscriptions and directives * * @param {string} queryAST * @param {any | undefined} varibales @@ -22,75 +16,52 @@ function getQueryTypeComplexity( varibales: any | undefined, typeWeights: TypeWeightObject ): number { - const recursive = (node: ASTNode, parent: ASTNode | null = null): number => { - /** - * pseudo code of the process - * - // if 'kind' property is 'Document' - // iterate through queryAST.definitions array - // call recursive with object - - // if 'kind' property is 'operationDefinition' - // check 'operation' value against the type weights and add to total - // call recursive with selectionSet property if it is not undefined - - // if 'kind' is 'selectionSet' - // iterate shrough the 'selections' array of fields - // if 'selectinSet' is not undefined, call recursive with the field - - // if 'kind' property is 'feild' - // check the fields name.value against the type weights and total - // if there is a match, it is an objcet type with feilds, - // call recursive with selectionSet property if it is not undefined - // if it is not a match, it is a scalar field, look in the parent.name.value to check type weights feilds - */ - + const getComplexityOfNode = (node: ASTNode, parent: ASTNode = node): number => { let complexity = 0; - const parentName: string = parent?.operation || parent?.name.value || null; if (node.kind === Kind.DOCUMENT) { // if 'kind' property is a 'Document' // iterate through queryAST.definitions array for (let i = 0; i < node.definitions.length; i + 1) { // call recursive with the definition node - complexity += recursive(node.definitions[i], node); + complexity += getComplexityOfNode(node.definitions[i], node); } } else if (node.kind === Kind.OPERATION_DEFINITION) { // if 'kind' property is 'operationDefinition' // TODO: case-sensitive - if (node.operation in typeWeights) { + if (node.operation.toLocaleLowerCase() in typeWeights) { // check 'operation' value against the type weights and add to total complexity += typeWeights[node.operation].weight; // call recursive with selectionSet property if it is not undefined - if (node.selectionSet) complexity += recursive(node.selectionSet, node); + if (node.selectionSet) complexity += getComplexityOfNode(node.selectionSet, node); } } else if (node.kind === Kind.SELECTION_SET) { // if 'kind' is 'selectionSet' // iterate shrough the 'selections' array of fields for (let i = 0; i < node.selections.length; i + 1) { // call recursive with the field - complexity += recursive(node.selections[i], parent); // passing the current parent through because selection sets act only as intermediaries + complexity += getComplexityOfNode(node.selections[i], parent); // passing the current parent through because selection sets act only as intermediaries } } else if (node.kind === Kind.FIELD) { // if 'kind' property is 'field' // check the fields name.value against the type weights and total // TODO: case-sensitive - if (node.name.value in typeWeights) { + if (node.name.value.toLocaleLowerCase() in typeWeights) { // if there is a match, it is an objcet type with feilds, complexity += typeWeights[node.name.value].weight; // call recursive with selectionSet property if it is not undefined - if (node.selectionSet) complexity += recursive(node.selectionSet, node); + if (node.selectionSet) complexity += getComplexityOfNode(node.selectionSet, node); // node.name.value in typeWeights[parent.operation || parent.name.value].fields - } else if (parent?.opeartion) { - // if it is not a match, it is a scalar field, look in the parent.name.value to check type weights feilds - // TODO: if it is a list, need to look at the parent - complexity += typeWeights[parent.name.value].fields[node.name.value]; + } else { + // TODO: if it is not a match, it is a scalar field or list, + // if (parent?.objective !== null) { + // } + // const weight = typeWeights[parent.name.value].fields[node.name.value]; } } - return complexity; }; - return recursive(queryAST); + return getComplexityOfNode(queryAST); } export default getQueryTypeComplexity; diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index 112b320..77f64e0 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -167,7 +167,7 @@ const typeWeights: TypeWeightObject = { }, }; -xdescribe('Test getQueryTypeComplexity function', () => { +describe('Test getQueryTypeComplexity function', () => { let query = ''; let variables: any | undefined; describe('Calculates the correct type complexity for queries', () => { From cb183c0d8c3e157e3e7f3347693aba22952990d6 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Wed, 1 Jun 2022 22:56:53 -0400 Subject: [PATCH 03/10] started to refactor the complexity analysis to use seperate functions for every type of node --- src/analysis/ASTnodefunctions.ts | 85 ++++++++++++++++++++++++++ src/analysis/typeComplexityAnalysis.ts | 57 +++-------------- 2 files changed, 95 insertions(+), 47 deletions(-) create mode 100644 src/analysis/ASTnodefunctions.ts diff --git a/src/analysis/ASTnodefunctions.ts b/src/analysis/ASTnodefunctions.ts new file mode 100644 index 0000000..2e59cfe --- /dev/null +++ b/src/analysis/ASTnodefunctions.ts @@ -0,0 +1,85 @@ +import { + DocumentNode, + FieldNode, + SelectionSetNode, + DefinitionNode, + Kind, + SelectionNode, +} from 'graphql'; + +export function fieldNode( + node: FieldNode, + typeWeights: TypeWeightObject, + variables: any | undefined, + parent: FieldNode | DefinitionNode +): number { + const complexity = 0; + return complexity; +} + +export function selectionNode( + node: SelectionNode, + typeWeights: TypeWeightObject, + variables: any | undefined, + parent: DefinitionNode | FieldNode +): number { + let complexity = 0; + // check the kind property against the set of selection nodes that are possible + if (node.kind === Kind.FIELD) { + // call the function that handle field nodes and multiply the result into complexity to accound for nested fields + complexity *= fieldNode(node, typeWeights, variables, parent); + } + // TODO: add checks for Kind.FRAGMENT_SPREAD and Kind.INLINE_FRAGMENT here + return complexity; +} + +export function selectionSetNode( + node: SelectionSetNode, + typeWeights: TypeWeightObject, + variables: any | undefined, + parent: DefinitionNode | FieldNode +): number { + let complexity = 0; + // iterate shrough the 'selections' array on the seletion set node + for (let i = 0; i < node.selections.length; i + 1) { + // call the function to handle seletion nodes + // pass the current parent through because selection sets act only as intermediaries + complexity += selectionNode(node.selections[i], typeWeights, variables, parent); + } + return complexity; +} + +export function definitionNode( + node: DefinitionNode, + typeWeights: TypeWeightObject, + variables: any | undefined +): number { + let complexity = 0; + // check the kind property against the set of definiton nodes that are possible + if (node.kind === Kind.OPERATION_DEFINITION) { + // check if the operation is in the type weights object. + if (node.operation.toLocaleLowerCase() in typeWeights) { + // if it is, it is an object type, add it's type weight to the total + complexity += typeWeights[node.operation].weight; + // call the function to handle selection set node with selectionSet property if it is not undefined + if (node.selectionSet) + complexity += selectionSetNode(node.selectionSet, typeWeights, variables, node); + } + } + // 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') + return complexity; +} + +export function documentNode( + node: DocumentNode, + typeWeights: TypeWeightObject, + variables: any | undefined +): number { + let complexity = 0; + // iterate through 'definitions' array on the document node + for (let i = 0; i < node.definitions.length; i + 1) { + // call the function to handle the various types of definition nodes + complexity += definitionNode(node.definitions[i], typeWeights, variables); + } + return complexity; +} diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index ebe49f0..95c5018 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -1,4 +1,10 @@ import { ASTNode, DocumentNode, Kind } from 'graphql'; +import { + selectionSetNode, + fieldNode, + documentNode, + operationDefinitionNode, +} from './ASTnodefunctions'; /** * Calculate the complexity for the query by recursivly traversing through the query AST, @@ -13,55 +19,12 @@ import { ASTNode, DocumentNode, Kind } from 'graphql'; // TODO add queryVaribables parameter function getQueryTypeComplexity( queryAST: DocumentNode, - varibales: any | undefined, + variables: any | undefined, typeWeights: TypeWeightObject ): number { - const getComplexityOfNode = (node: ASTNode, parent: ASTNode = node): number => { - let complexity = 0; - - if (node.kind === Kind.DOCUMENT) { - // if 'kind' property is a 'Document' - // iterate through queryAST.definitions array - for (let i = 0; i < node.definitions.length; i + 1) { - // call recursive with the definition node - complexity += getComplexityOfNode(node.definitions[i], node); - } - } else if (node.kind === Kind.OPERATION_DEFINITION) { - // if 'kind' property is 'operationDefinition' - // TODO: case-sensitive - if (node.operation.toLocaleLowerCase() in typeWeights) { - // check 'operation' value against the type weights and add to total - complexity += typeWeights[node.operation].weight; - // call recursive with selectionSet property if it is not undefined - if (node.selectionSet) complexity += getComplexityOfNode(node.selectionSet, node); - } - } else if (node.kind === Kind.SELECTION_SET) { - // if 'kind' is 'selectionSet' - // iterate shrough the 'selections' array of fields - for (let i = 0; i < node.selections.length; i + 1) { - // call recursive with the field - complexity += getComplexityOfNode(node.selections[i], parent); // passing the current parent through because selection sets act only as intermediaries - } - } else if (node.kind === Kind.FIELD) { - // if 'kind' property is 'field' - // check the fields name.value against the type weights and total - // TODO: case-sensitive - if (node.name.value.toLocaleLowerCase() in typeWeights) { - // if there is a match, it is an objcet type with feilds, - complexity += typeWeights[node.name.value].weight; - // call recursive with selectionSet property if it is not undefined - if (node.selectionSet) complexity += getComplexityOfNode(node.selectionSet, node); - // node.name.value in typeWeights[parent.operation || parent.name.value].fields - } else { - // TODO: if it is not a match, it is a scalar field or list, - // if (parent?.objective !== null) { - // } - // const weight = typeWeights[parent.name.value].fields[node.name.value]; - } - } - return complexity; - }; - return getComplexityOfNode(queryAST); + let complexity = 0; + complexity += documentNode(queryAST, typeWeights, variables); + return complexity; } export default getQueryTypeComplexity; From 92a0a6a75729ee86957ff8c69076a0b0b37ad999 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Wed, 1 Jun 2022 23:41:01 -0400 Subject: [PATCH 04/10] refactored the type complexity analysis to use several functions, each handling the nuance of a different AST node. --- src/@types/buildTypeWeights.d.ts | 2 +- src/analysis/ASTnodefunctions.ts | 58 ++++++++++++++++++++++---- src/analysis/typeComplexityAnalysis.ts | 1 - 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/@types/buildTypeWeights.d.ts b/src/@types/buildTypeWeights.d.ts index cb1a9fe..b2c3d95 100644 --- a/src/@types/buildTypeWeights.d.ts +++ b/src/@types/buildTypeWeights.d.ts @@ -1,5 +1,5 @@ interface Fields { - readonly [index: string]: number | ((arg: number, type: Type) => number); + readonly [index: string]: number | ((arg: { [index: string]: any }) => number); } interface Type { diff --git a/src/analysis/ASTnodefunctions.ts b/src/analysis/ASTnodefunctions.ts index 2e59cfe..ebd408e 100644 --- a/src/analysis/ASTnodefunctions.ts +++ b/src/analysis/ASTnodefunctions.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ import { DocumentNode, FieldNode, @@ -5,15 +6,51 @@ import { DefinitionNode, Kind, SelectionNode, + ArgumentNode, } from 'graphql'; +// const getArgObj = (args: ArgumentNode[]): { [index: string]: any } => { +// const argObj: { [index: string]: any } = {}; +// for (let i = 0; i < args.length; i + 1) { +// if (args[i].kind === Kind.BOOLEAN) { +// argObj[args[i].name.value] = args[i].value.value; +// } +// } +// return argObj; +// }; + export function fieldNode( node: FieldNode, typeWeights: TypeWeightObject, variables: any | undefined, - parent: FieldNode | DefinitionNode + parentName: string ): number { - const complexity = 0; + let complexity = 0; + // check if the field name is in the type weight object. + if (node.name.value.toLocaleLowerCase() in typeWeights) { + // if it is, than the field is an object type, add itss type weight to the total + complexity += typeWeights[node.name.value].weight; + // call the function to handle selection set node with selectionSet property if it is not undefined + if (node.selectionSet) + complexity *= selectionSetNode( + node.selectionSet, + typeWeights, + variables, + node.name.value + ); + } else { + // otherwise the field is a scalar or a list. + const fieldWeight = typeWeights[parentName].fields[node.name.value]; + if (typeof fieldWeight === 'number') { + // if the feild weight is a number, add the number to the total complexity + complexity += fieldWeight; + } else { + // otherwise the the feild weight is a list, invoke the function with variables + // TODO: calculate the complexity for lists with arguments and varibales + // iterate through the arguments to build the object to + // complexity += fieldWeight(getArgObj(node.arguments)); + } + } return complexity; } @@ -21,13 +58,13 @@ export function selectionNode( node: SelectionNode, typeWeights: TypeWeightObject, variables: any | undefined, - parent: DefinitionNode | FieldNode + parentName: string ): number { let complexity = 0; // check the kind property against the set of selection nodes that are possible if (node.kind === Kind.FIELD) { - // call the function that handle field nodes and multiply the result into complexity to accound for nested fields - complexity *= fieldNode(node, typeWeights, variables, parent); + // call the function that handle field nodes + complexity += fieldNode(node, typeWeights, variables, parentName); } // TODO: add checks for Kind.FRAGMENT_SPREAD and Kind.INLINE_FRAGMENT here return complexity; @@ -37,14 +74,14 @@ export function selectionSetNode( node: SelectionSetNode, typeWeights: TypeWeightObject, variables: any | undefined, - parent: DefinitionNode | FieldNode + parentName: string ): number { let complexity = 0; // iterate shrough the 'selections' array on the seletion set node for (let i = 0; i < node.selections.length; i + 1) { // call the function to handle seletion nodes // pass the current parent through because selection sets act only as intermediaries - complexity += selectionNode(node.selections[i], typeWeights, variables, parent); + complexity += selectionNode(node.selections[i], typeWeights, variables, parentName); } return complexity; } @@ -63,7 +100,12 @@ export function definitionNode( complexity += typeWeights[node.operation].weight; // call the function to handle selection set node with selectionSet property if it is not undefined if (node.selectionSet) - complexity += selectionSetNode(node.selectionSet, typeWeights, variables, node); + complexity += selectionSetNode( + node.selectionSet, + typeWeights, + variables, + node.operation + ); } } // 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') diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index 95c5018..30339c1 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -16,7 +16,6 @@ import { * @param {any | undefined} varibales * @param {TypeWeightObject} typeWeights */ -// TODO add queryVaribables parameter function getQueryTypeComplexity( queryAST: DocumentNode, variables: any | undefined, From 8f5f86ecb3fc7ba33a2047e0ac8131868f13cc21 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Thu, 2 Jun 2022 07:50:11 -0400 Subject: [PATCH 05/10] debugging comlpexity algorithm --- .vscode/launch.json | 21 +++++++++++ .vscode/settings.json | 15 ++++++++ src/analysis/ASTnodefunctions.ts | 37 ++++++++++++++------ src/analysis/typeComplexityAnalysis.ts | 9 ++--- test/analysis/typeComplexityAnalysis.test.ts | 28 +++++++-------- 5 files changed, 78 insertions(+), 32 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..633f45c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [{ + "type": "node", + "request": "launch", + "name": "Jest Tests", + "program": "${workspaceRoot}\\node_modules\\jest\\bin\\jest.js", + "args": [ + "-i" + ], + // "preLaunchTask": "build", + "internalConsoleOptions": "openOnSessionStart", + "outFiles": [ + "${workspaceRoot}/dist/**/*" + ], + "envFile": "${workspaceRoot}/.env" + }] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c754b38..6dade31 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,4 +5,19 @@ "source.fixAll.eslint": true }, "editor.formatOnSave": true, + "configurations": [{ + "type": "node", + "request": "launch", + "name": "Jest Tests", + "program": "${workspaceRoot}\\node_modules\\jest\\bin\\jest.js", + "args": [ + "-i" + ], + // "preLaunchTask": "build", + "internalConsoleOptions": "openOnSessionStart", + "outFiles": [ + "${workspaceRoot}/dist/**/*" + ], + "envFile": "${workspaceRoot}/.env" + }] } \ No newline at end of file diff --git a/src/analysis/ASTnodefunctions.ts b/src/analysis/ASTnodefunctions.ts index ebd408e..d73e3a2 100644 --- a/src/analysis/ASTnodefunctions.ts +++ b/src/analysis/ASTnodefunctions.ts @@ -7,17 +7,23 @@ import { Kind, SelectionNode, ArgumentNode, + BooleanValueNode, } from 'graphql'; -// const getArgObj = (args: ArgumentNode[]): { [index: string]: any } => { -// const argObj: { [index: string]: any } = {}; -// for (let i = 0; i < args.length; i + 1) { -// if (args[i].kind === Kind.BOOLEAN) { -// argObj[args[i].name.value] = args[i].value.value; -// } -// } -// return argObj; -// }; +// TODO: handle variables and arguments +const getArgObj = (args: ArgumentNode[]): { [index: string]: any } => { + const argObj: { [index: string]: any } = {}; + for (let i = 0; i < args.length; i + 1) { + const node = args[i]; + if (args[i].value.kind !== Kind.VARIABLE) { + if (args[i].value.kind === Kind.INT) { + // FIXME: this does not work + argObj[args[i].name.value] = args[i].value; + } + } + } + return argObj; +}; export function fieldNode( node: FieldNode, @@ -26,18 +32,20 @@ export function fieldNode( parentName: string ): number { let complexity = 0; + // console.log('fieldNode', node, parentName); // check if the field name is in the type weight object. if (node.name.value.toLocaleLowerCase() in typeWeights) { // if it is, than the field is an object type, add itss type weight to the total complexity += typeWeights[node.name.value].weight; // call the function to handle selection set node with selectionSet property if it is not undefined - if (node.selectionSet) + if (node.selectionSet) { complexity *= selectionSetNode( node.selectionSet, typeWeights, variables, node.name.value ); + } } else { // otherwise the field is a scalar or a list. const fieldWeight = typeWeights[parentName].fields[node.name.value]; @@ -48,7 +56,11 @@ export function fieldNode( // otherwise the the feild weight is a list, invoke the function with variables // TODO: calculate the complexity for lists with arguments and varibales // iterate through the arguments to build the object to - // complexity += fieldWeight(getArgObj(node.arguments)); + // eslint-disable-next-line no-lonely-if + if (node.arguments) { + const argumentsCopy = [...node.arguments]; + complexity += fieldWeight(getArgObj(argumentsCopy)); + } } } return complexity; @@ -61,6 +73,7 @@ export function selectionNode( parentName: string ): number { let complexity = 0; + // console.log('selectionNode', node, parentName); // check the kind property against the set of selection nodes that are possible if (node.kind === Kind.FIELD) { // call the function that handle field nodes @@ -77,6 +90,7 @@ export function selectionSetNode( parentName: string ): number { let complexity = 0; + console.log('selectionSetNode', node.selections.length, parentName); // iterate shrough the 'selections' array on the seletion set node for (let i = 0; i < node.selections.length; i + 1) { // call the function to handle seletion nodes @@ -92,6 +106,7 @@ export function definitionNode( variables: any | undefined ): number { let complexity = 0; + console.log('definitionTode', node); // check the kind property against the set of definiton nodes that are possible if (node.kind === Kind.OPERATION_DEFINITION) { // check if the operation is in the type weights object. diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index 30339c1..f3da790 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -1,10 +1,5 @@ -import { ASTNode, DocumentNode, Kind } from 'graphql'; -import { - selectionSetNode, - fieldNode, - documentNode, - operationDefinitionNode, -} from './ASTnodefunctions'; +import { DocumentNode } from 'graphql'; +import { documentNode } from './ASTnodefunctions'; /** * Calculate the complexity for the query by recursivly traversing through the query AST, diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index 77f64e0..c4f901c 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -103,7 +103,7 @@ const typeWeights: TypeWeightObject = { 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, + reviews: (arg) => 1, }, }, episode: { @@ -172,36 +172,36 @@ describe('Test getQueryTypeComplexity function', () => { let variables: any | undefined; describe('Calculates the correct type complexity for queries', () => { test('with one feild', () => { - query = `Query { scalars { num } }`; + query = `query { scalars { num } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + Scalars 1 }); - test('with two or more fields', () => { + xtest('with two or more fields', () => { query = `Query { scalars { num } test { name } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); - test('with one level of nested fields', () => { + xtest('with one level of nested fields', () => { query = `Query { scalars { num, test { name } } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); - test('with multiple levels of nesting', () => { + xtest('with multiple levels of nesting', () => { query = `Query { scalars { num, test { name, scalars { id } } } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // Query 1 + scalars 1 + test 1 + scalars 1 }); - test('with aliases', () => { + xtest('with aliases', () => { query = `Query { foo: scalar { num } bar: scalar { id }}`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalar 1 + scalar 1 }); - test('with all scalar fields', () => { + xtest('with all scalar fields', () => { query = `Query { scalars { id, num, float, bool, string } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + scalar 1 }); - test('with arguments and variables', () => { + xtest('with arguments and variables', () => { query = `Query { hero(episode: EMPIRE) { id, name } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 query = `Query { human(id: 1) { id, name, appearsIn } }`; @@ -212,7 +212,7 @@ describe('Test getQueryTypeComplexity function', () => { expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 }); - test('with fragments', () => { + xtest('with fragments', () => { query = ` Query { leftComparison: hero(episode: EMPIRE) { @@ -231,7 +231,7 @@ describe('Test getQueryTypeComplexity function', () => { expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(5); // Query 1 + 2*(character 1 + appearsIn/episode 1) }); - test('with inline fragments', () => { + xtest('with inline fragments', () => { query = ` Query { hero(episode: EMPIRE) { @@ -261,7 +261,7 @@ describe('Test getQueryTypeComplexity function', () => { expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(false); // ? }); - test('with lists detrmined by arguments and variables', () => { + xtest('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 }; @@ -269,7 +269,7 @@ describe('Test getQueryTypeComplexity function', () => { expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // 1 Query + 3 reviews }); - test('with nested lists', () => { + xtest('with nested lists', () => { query = ` query { human(id: 1) { @@ -285,7 +285,7 @@ describe('Test getQueryTypeComplexity function', () => { 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', () => { + xtest('accounting for __typename feild', () => { query = ` query { search(text: "an", first: 4) { @@ -306,7 +306,7 @@ describe('Test getQueryTypeComplexity function', () => { // todo: directives @skip, @include and custom directives // todo: expand on error handling - test('Throws an error if for a bad query', () => { + xtest('Throws an error if for a bad query', () => { query = `Query { hello { hi } }`; // type doesn't exist expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( 'Error' From 57a8cd8ae15c06049ee4c63ed412692aa798a462 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Thu, 2 Jun 2022 09:12:55 -0400 Subject: [PATCH 06/10] type complexity analysis working for first test. changed i + 1 to i += 1 --- .vscode/launch.json | 14 ++++----- src/analysis/ASTnodefunctions.ts | 9 ++---- test/analysis/typeComplexityAnalysis.test.ts | 32 ++++++++++---------- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 633f45c..81ae313 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,15 +7,15 @@ "type": "node", "request": "launch", "name": "Jest Tests", - "program": "${workspaceRoot}\\node_modules\\jest\\bin\\jest.js", + "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", "args": [ - "-i" + "-i", "--verbose", "--no-cache" ], // "preLaunchTask": "build", - "internalConsoleOptions": "openOnSessionStart", - "outFiles": [ - "${workspaceRoot}/dist/**/*" - ], - "envFile": "${workspaceRoot}/.env" + // "internalConsoleOptions": "openOnSessionStart", + // "outFiles": [ + // "${workspaceRoot}/dist/**/*" + // ], + // "envFile": "${workspaceRoot}/.env" }] } \ No newline at end of file diff --git a/src/analysis/ASTnodefunctions.ts b/src/analysis/ASTnodefunctions.ts index d73e3a2..76c34a6 100644 --- a/src/analysis/ASTnodefunctions.ts +++ b/src/analysis/ASTnodefunctions.ts @@ -7,7 +7,6 @@ import { Kind, SelectionNode, ArgumentNode, - BooleanValueNode, } from 'graphql'; // TODO: handle variables and arguments @@ -39,7 +38,7 @@ export function fieldNode( complexity += typeWeights[node.name.value].weight; // call the function to handle selection set node with selectionSet property if it is not undefined if (node.selectionSet) { - complexity *= selectionSetNode( + complexity += selectionSetNode( node.selectionSet, typeWeights, variables, @@ -90,9 +89,8 @@ export function selectionSetNode( parentName: string ): number { let complexity = 0; - console.log('selectionSetNode', node.selections.length, parentName); // iterate shrough the 'selections' array on the seletion set node - for (let i = 0; i < node.selections.length; i + 1) { + for (let i = 0; i < node.selections.length; i += 1) { // call the function to handle seletion nodes // pass the current parent through because selection sets act only as intermediaries complexity += selectionNode(node.selections[i], typeWeights, variables, parentName); @@ -106,7 +104,6 @@ export function definitionNode( variables: any | undefined ): number { let complexity = 0; - console.log('definitionTode', node); // check the kind property against the set of definiton nodes that are possible if (node.kind === Kind.OPERATION_DEFINITION) { // check if the operation is in the type weights object. @@ -134,7 +131,7 @@ export function documentNode( ): number { let complexity = 0; // iterate through 'definitions' array on the document node - for (let i = 0; i < node.definitions.length; i + 1) { + for (let i = 0; i < node.definitions.length; i += 1) { // call the function to handle the various types of definition nodes complexity += definitionNode(node.definitions[i], typeWeights, variables); } diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index c4f901c..e4d797b 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -172,49 +172,49 @@ describe('Test getQueryTypeComplexity function', () => { let variables: any | undefined; describe('Calculates the correct type complexity for queries', () => { test('with one feild', () => { - query = `query { scalars { num } }`; + query = `uery { scalars { num } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + Scalars 1 }); - xtest('with two or more fields', () => { - query = `Query { scalars { num } test { name } }`; + test('with two or more fields', () => { + query = `query { scalars { num } test { name } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); xtest('with one level of nested fields', () => { - query = `Query { scalars { num, test { name } } }`; + query = `query { scalars { num, test { name } } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); xtest('with multiple levels of nesting', () => { - query = `Query { scalars { num, test { name, scalars { id } } } }`; + query = `query { scalars { num, test { name, scalars { id } } } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // Query 1 + scalars 1 + test 1 + scalars 1 }); xtest('with aliases', () => { - query = `Query { foo: scalar { num } bar: scalar { id }}`; + query = `query { foo: scalar { num } bar: scalar { id }}`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalar 1 + scalar 1 }); xtest('with all scalar fields', () => { - query = `Query { scalars { id, num, float, bool, string } }`; + query = `query { scalars { id, num, float, bool, string } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + scalar 1 }); xtest('with arguments and variables', () => { - query = `Query { hero(episode: EMPIRE) { id, name } }`; + query = `query { hero(episode: EMPIRE) { id, name } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 - query = `Query { human(id: 1) { id, name, appearsIn } }`; + 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 } }`; + query = `query varibaleQuery ($ep: Episode){ hero(episode: $ep) { id, name } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 }); xtest('with fragments', () => { query = ` - Query { + query { leftComparison: hero(episode: EMPIRE) { ...comparisonFields } @@ -233,7 +233,7 @@ describe('Test getQueryTypeComplexity function', () => { xtest('with inline fragments', () => { query = ` - Query { + query { hero(episode: EMPIRE) { name ... on Droid { @@ -252,7 +252,7 @@ describe('Test getQueryTypeComplexity function', () => { */ xtest('with lists of unknown size', () => { query = ` - Query { + query { search(text: 'hi') { id name @@ -307,15 +307,15 @@ describe('Test getQueryTypeComplexity function', () => { // todo: expand on error handling xtest('Throws an error if for a bad query', () => { - query = `Query { hello { hi } }`; // type doesn't exist + query = `query { hello { hi } }`; // type doesn't exist expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( 'Error' ); - query = `Query { hero(episode: EMPIRE){ starship } }`; // field doesn't exist + query = `query { hero(episode: EMPIRE){ starship } }`; // field doesn't exist expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( 'Error' ); - query = `Query { hero(episode: EMPIRE) { id, name }`; // missing a closing bracket + query = `query { hero(episode: EMPIRE) { id, name }`; // missing a closing bracket expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( 'Error' ); From 9beb1cebe403db9b3e824b4132167c12a20c7ff7 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Thu, 2 Jun 2022 09:28:15 -0400 Subject: [PATCH 07/10] passing all test up to and including aliases --- test/analysis/typeComplexityAnalysis.test.ts | 28 +++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index e4d797b..e13eb96 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -172,7 +172,7 @@ describe('Test getQueryTypeComplexity function', () => { let variables: any | undefined; describe('Calculates the correct type complexity for queries', () => { test('with one feild', () => { - query = `uery { scalars { num } }`; + query = `query { scalars { num } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + Scalars 1 }); @@ -181,22 +181,22 @@ describe('Test getQueryTypeComplexity function', () => { expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); - xtest('with one level of nested fields', () => { + test('with one level of nested fields', () => { query = `query { scalars { num, test { name } } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalars 1 + test 1 }); - xtest('with multiple levels of nesting', () => { + test('with multiple levels of nesting', () => { query = `query { scalars { num, test { name, scalars { id } } } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(4); // Query 1 + scalars 1 + test 1 + scalars 1 }); - xtest('with aliases', () => { - query = `query { foo: scalar { num } bar: scalar { id }}`; + test('with aliases', () => { + query = `query { foo: scalars { num } bar: scalars { id }}`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(3); // Query 1 + scalar 1 + scalar 1 }); - xtest('with all scalar fields', () => { + test('with all scalar fields', () => { query = `query { scalars { id, num, float, bool, string } }`; expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + scalar 1 }); @@ -304,22 +304,6 @@ describe('Test getQueryTypeComplexity function', () => { }); // todo: directives @skip, @include and custom directives - - // todo: expand on error handling - xtest('Throws an error if for a bad query', () => { - query = `query { hello { hi } }`; // type doesn't exist - expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( - 'Error' - ); - query = `query { hero(episode: EMPIRE){ starship } }`; // field doesn't exist - expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( - 'Error' - ); - query = `query { hero(episode: EMPIRE) { id, name }`; // missing a closing bracket - expect(() => getQueryTypeComplexity(parse(query), variables, typeWeights)).toThrow( - 'Error' - ); - }); }); xdescribe('Calculates the correct type complexity for mutations', () => {}); From 8d09a5c5a34b009b27761e30b7356dc187edd069 Mon Sep 17 00:00:00 2001 From: "[Evan McNeely]" Date: Thu, 2 Jun 2022 11:45:12 -0400 Subject: [PATCH 08/10] added a chart to describe the AST node connections --- src/analysis/ASTnodefunctions.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/analysis/ASTnodefunctions.ts b/src/analysis/ASTnodefunctions.ts index 76c34a6..7440d08 100644 --- a/src/analysis/ASTnodefunctions.ts +++ b/src/analysis/ASTnodefunctions.ts @@ -10,6 +10,7 @@ import { } from 'graphql'; // TODO: handle variables and arguments +// ! this is not functional const getArgObj = (args: ArgumentNode[]): { [index: string]: any } => { const argObj: { [index: string]: any } = {}; for (let i = 0; i < args.length; i + 1) { @@ -23,6 +24,25 @@ const getArgObj = (args: ArgumentNode[]): { [index: string]: any } => { } return argObj; }; +/** + * The AST node functions call each other following the nested structure below + * Each function handles a specific GraphQL AST node type + * + * AST nodes call each other in the following way + * + * Document Node + * | + * Definiton Node + * (operation and fragment definitons) + * / \ + * |-----> Selection Set Node not done + * | / + * | Selection Node + * | (Field, Inline fragment and fragment spread) + * | | \ \ + * |--Field Node not done not done + * + */ export function fieldNode( node: FieldNode, @@ -54,6 +74,7 @@ export function fieldNode( } else { // otherwise the the feild weight is a list, invoke the function with variables // TODO: calculate the complexity for lists with arguments and varibales + // ! this is not functional // iterate through the arguments to build the object to // eslint-disable-next-line no-lonely-if if (node.arguments) { From 4ea037f130ac53b122c366c34ecd8e55e806e899 Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Mon, 13 Jun 2022 18:16:31 -0400 Subject: [PATCH 09/10] exported types for succesful typescript build. --- .eslintrc.json | 2 +- .gitignore | 2 ++ src/@types/buildTypeWeights.d.ts | 15 +++++------- src/@types/rateLimit.d.ts | 12 +++++----- src/analysis/ASTnodefunctions.ts | 1 + src/analysis/buildTypeWeights.ts | 8 ++++--- src/analysis/typeComplexityAnalysis.ts | 1 + src/middleware/index.ts | 24 ++++++++++++-------- src/middleware/rateLimiterSetup.ts | 1 + src/rateLimiters/tokenBucket.ts | 3 ++- test/analysis/typeComplexityAnalysis.test.ts | 1 + test/rateLimiters/tokenBucket.test.ts | 1 + tsconfig.json | 5 ++-- 13 files changed, 44 insertions(+), 32 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 3fa0b0c..42201ab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,5 +26,5 @@ "error" ] }, - "ignorePatterns": ["jest.*"] + "ignorePatterns": ["jest.*", "build/*"] } diff --git a/.gitignore b/.gitignore index 6704566..cc1e49e 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ dist # TernJS port file .tern-port + +build/* \ No newline at end of file diff --git a/src/@types/buildTypeWeights.d.ts b/src/@types/buildTypeWeights.d.ts index 6be5de4..bda7bca 100644 --- a/src/@types/buildTypeWeights.d.ts +++ b/src/@types/buildTypeWeights.d.ts @@ -1,19 +1,16 @@ -interface Fields { +export interface Fields { [index: string]: FieldWeight; } -type WeightFunction = (args: ArgumentNode[]) => number; -type FieldWeight = number | WeightFunction; - -interface Type { +export type WeightFunction = (args: ArgumentNode[]) => number; +export type FieldWeight = number | WeightFunction; +export interface Type { readonly weight: number; readonly fields: Fields; } - -interface TypeWeightObject { +export interface TypeWeightObject { [index: string]: Type; } - -interface TypeWeightConfig { +export interface TypeWeightConfig { mutation?: number; query?: number; object?: number; diff --git a/src/@types/rateLimit.d.ts b/src/@types/rateLimit.d.ts index 2eded16..d240ff1 100644 --- a/src/@types/rateLimit.d.ts +++ b/src/@types/rateLimit.d.ts @@ -1,4 +1,4 @@ -interface RateLimiter { +export interface RateLimiter { /** * Checks if a request is allowed under the given conditions and withdraws the specified number of tokens * @param uuid Unique identifier for the user associated with the request @@ -13,17 +13,17 @@ interface RateLimiter { ) => Promise; } -interface RateLimiterResponse { +export interface RateLimiterResponse { success: boolean; tokens: number; } -interface RedisBucket { +export interface RedisBucket { tokens: number; timestamp: number; } -type RateLimiterSelection = +export type RateLimiterSelection = | 'TOKEN_BUCKET' | 'LEAKY_BUCKET' | 'FIXED_WINDOW' @@ -34,7 +34,7 @@ type RateLimiterSelection = * @type {number} bucketSize - Size of the token bucket * @type {number} refillRate - Rate at which tokens are added to the bucket in seconds */ -interface TokenBucketOptions { +export interface TokenBucketOptions { bucketSize: number; refillRate: number; } @@ -42,4 +42,4 @@ interface TokenBucketOptions { // TODO: This will be a union type where we can specify Option types for other Rate Limiters // Record represents the empty object for alogorithms that don't require settings // and might be able to be removed in the future. -type RateLimiterOptions = TokenBucketOptions | Record; +export type RateLimiterOptions = TokenBucketOptions | Record; diff --git a/src/analysis/ASTnodefunctions.ts b/src/analysis/ASTnodefunctions.ts index acb25a4..0caa817 100644 --- a/src/analysis/ASTnodefunctions.ts +++ b/src/analysis/ASTnodefunctions.ts @@ -8,6 +8,7 @@ import { SelectionNode, ArgumentNode, } from 'graphql'; +import { FieldWeight, TypeWeightObject } from '../@types/buildTypeWeights'; // TODO: handle variables and arguments // ! this is not functional diff --git a/src/analysis/buildTypeWeights.ts b/src/analysis/buildTypeWeights.ts index c5b3dd9..abb6bbd 100644 --- a/src/analysis/buildTypeWeights.ts +++ b/src/analysis/buildTypeWeights.ts @@ -19,6 +19,7 @@ import { import { Maybe } from 'graphql/jsutils/Maybe'; import { ObjMap } from 'graphql/jsutils/ObjMap'; import { GraphQLSchema } from 'graphql/type/schema'; +import { TypeWeightConfig, TypeWeightObject } from '../@types/buildTypeWeights'; export const KEYWORDS = ['first', 'last', 'limit']; @@ -81,6 +82,7 @@ function parseQuery( queryFields[field].args.forEach((arg: GraphQLArgument) => { // If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query // should be dependent on both the weight of the resolved type and the limiting argument. + // FIXME: Can nonnull wrap list types? if (KEYWORDS.includes(arg.name) && isListType(resolveType)) { // Get the type that comprises the list const listType = resolveType.ofType; @@ -125,7 +127,7 @@ function parseQuery( } }); - // if the field is a scalar or an enum set weight accordingly + // if the field is a scalar or an enum set weight accordingly. It is not a list in this case if (isScalarType(resolveType) || isEnumType(resolveType)) { result.query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT; } @@ -152,7 +154,7 @@ function parseTypes( // Handle Object, Interface, Enum and Union types Object.keys(typeMap).forEach((type) => { - const typeName = type.toLowerCase(); + const typeName: string = type.toLowerCase(); const currentType: GraphQLNamedType = typeMap[type]; // Get all types that aren't Query or Mutation or a built in type that starts with '__' @@ -219,7 +221,7 @@ function buildTypeWeightsFromSchema( ...typeWeightsConfig, }; - // Confirm that any custom weights are positive + // Confirm that any custom weights are non-negative Object.entries(typeWeights).forEach((value: [string, number]) => { if (value[1] < 0) { throw new Error(`Type weights cannot be negative. Received: ${value[0]}: ${value[1]} `); diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index f3da790..43c063a 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -1,4 +1,5 @@ import { DocumentNode } from 'graphql'; +import { TypeWeightObject } from '../@types/buildTypeWeights'; import { documentNode } from './ASTnodefunctions'; /** diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 75b0ee3..cb2f84d 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -6,6 +6,8 @@ import { Request, Response, NextFunction, RequestHandler } from 'express'; import buildTypeWeightsFromSchema, { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights'; import setupRateLimiter from './rateLimiterSetup'; import getQueryTypeComplexity from '../analysis/typeComplexityAnalysis'; +import { RateLimiterOptions, RateLimiterSelection } from '../@types/rateLimit'; +import { TypeWeightConfig } from '../@types/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 @@ -50,7 +52,6 @@ export function expressRateLimiter( 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. @@ -68,10 +69,12 @@ export function expressRateLimiter( // 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(); + if (validationErrors.length > 0) { + // FIXME: Customize this error to throw the GraphQLError + return next(Error('invalid query')); + } const queryComplexity = getQueryTypeComplexity(queryAST, variables, typeWeightObject); - try { // process the request and conditinoally respond to client with status code 429 o // r pass the request onto the next middleware function @@ -83,14 +86,15 @@ export function expressRateLimiter( if (rateLimiterResponse.success === false) { // TODO: add a header 'Retry-After' with the time to wait untill next query will succeed // FIXME: send information about query complexity, tokens, etc, to the client on rejected query - res.status(429).send(); + res.status(429).json({ graphqlGate: rateLimiterResponse }); + } else { + res.locals.graphqlGate = { + timestamp: requestTimestamp, + complexity: queryComplexity, + tokens: rateLimiterResponse.tokens, + }; + return next(); } - res.locals.graphqlGate = { - timestamp: requestTimestamp, - complexity: queryComplexity, - tokens: rateLimiterResponse.tokens, - }; - return next(); } catch (err) { return next(err); } diff --git a/src/middleware/rateLimiterSetup.ts b/src/middleware/rateLimiterSetup.ts index 4c5de39..734bc5f 100644 --- a/src/middleware/rateLimiterSetup.ts +++ b/src/middleware/rateLimiterSetup.ts @@ -1,4 +1,5 @@ import Redis from 'ioredis'; +import { RateLimiterOptions, RateLimiterSelection } from '../@types/rateLimit'; import TokenBucket from '../rateLimiters/tokenBucket'; /** diff --git a/src/rateLimiters/tokenBucket.ts b/src/rateLimiters/tokenBucket.ts index daea169..02cc429 100644 --- a/src/rateLimiters/tokenBucket.ts +++ b/src/rateLimiters/tokenBucket.ts @@ -1,4 +1,5 @@ import Redis from 'ioredis'; +import { RateLimiter, RateLimiterResponse, RedisBucket } from '../@types/rateLimit'; /** * The TokenBucket instance of a RateLimiter limits requests based on a unique user ID. @@ -98,7 +99,7 @@ class TokenBucket implements RateLimiter { timestamp: number ): number => { const timeSinceLastQueryInSeconds: number = Math.floor( - (timestamp - bucket.timestamp) / 1000 // 1000 ms in a second + (timestamp - bucket.timestamp) / 1000 // 1000 ms in a second FIXME: magic number if specifying custom timeframe ); const tokensToAdd = timeSinceLastQueryInSeconds * this.refillRate; const updatedTokenCount = bucket.tokens + tokensToAdd; diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index b76cea0..3901601 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -1,6 +1,7 @@ import { ArgumentNode } from 'graphql/language'; import { parse } from 'graphql'; import getQueryTypeComplexity from '../../src/analysis/typeComplexityAnalysis'; +import { TypeWeightObject } from '../../src/@types/buildTypeWeights'; /** * Here is the schema that creates the followning 'typeWeightsObject' used for the tests diff --git a/test/rateLimiters/tokenBucket.test.ts b/test/rateLimiters/tokenBucket.test.ts index a177943..834feb0 100644 --- a/test/rateLimiters/tokenBucket.test.ts +++ b/test/rateLimiters/tokenBucket.test.ts @@ -1,4 +1,5 @@ import * as ioredis from 'ioredis'; +import { RedisBucket } from '../../src/@types/rateLimit'; import TokenBucket from '../../src/rateLimiters/tokenBucket'; // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/tsconfig.json b/tsconfig.json index f8c4bf7..8e6273b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,8 +17,9 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": false, - "typeRoots": ["./@types", "node_modules/@types"], - "types": ["node", "jest"] + "typeRoots": ["src/@types", "node_modules/@types"], + "types": ["node", "jest"], + "outDir": "build/" }, "include": ["src/**/*.ts", "src/**/*.js", "test/**/*.ts", "test/**/*.js"], "exclude": ["node_modules", "**/*.spec.ts"] From 9ef5624dd7f505e055d5f3c21ae2b2da7d01c0c3 Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Mon, 13 Jun 2022 18:34:08 -0400 Subject: [PATCH 10/10] lint fix --- src/middleware/index.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/middleware/index.ts b/src/middleware/index.ts index cb2f84d..3e4944a 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -44,7 +44,11 @@ export function expressRateLimiter( const rateLimiter = setupRateLimiter(rateLimiterAlgo, rateLimiterOptions, redisClient); // return the rate limiting middleware - return async (req: Request, res: Response, next: NextFunction): Promise => { + return async ( + req: Request, + res: Response, + next: NextFunction + ): Promise>> => { const requestTimestamp = new Date().valueOf(); const { query, variables }: { query: string; variables: any } = req.body; if (!query) { @@ -83,18 +87,17 @@ export function expressRateLimiter( requestTimestamp, queryComplexity ); - if (rateLimiterResponse.success === false) { + if (!rateLimiterResponse.success) { // TODO: add a header 'Retry-After' with the time to wait untill next query will succeed // FIXME: send information about query complexity, tokens, etc, to the client on rejected query - res.status(429).json({ graphqlGate: rateLimiterResponse }); - } else { - res.locals.graphqlGate = { - timestamp: requestTimestamp, - complexity: queryComplexity, - tokens: rateLimiterResponse.tokens, - }; - return next(); + return res.status(429).json({ graphqlGate: rateLimiterResponse }); } + res.locals.graphqlGate = { + timestamp: requestTimestamp, + complexity: queryComplexity, + tokens: rateLimiterResponse.tokens, + }; + return next(); } catch (err) { return next(err); }