Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3f1a39b
Merge branch 'dev' of github.com:oslabs-beta/GraphQL-Gate into sh/fra…
shalarewicz Jul 9, 2022
fa3094b
preliminary fragment support.
shalarewicz Jul 9, 2022
70bc11b
Merge branch 'sh/fragment-tests' into sh/fragments
shalarewicz Jul 10, 2022
acebc1c
added typeWeight and variable parameters to ASTParser constructor
shalarewicz Jul 10, 2022
9495e8a
Merge branch 'sh/fragment-tests' into sh/fragments
shalarewicz Jul 10, 2022
f69f1fd
Merge branch 'sh/fragment-tests' into sh/fragments
shalarewicz Jul 10, 2022
7498ba1
added FRAGMENT_DEFINITION support.
shalarewicz Jul 10, 2022
7e82dba
Merge branch 'sh/fragment-tests' into sh/fragments
shalarewicz Jul 10, 2022
ee2ba0b
preliminary suppor for inline fragments on interfaces
shalarewicz Jul 10, 2022
fca1a98
accounted for case if inline fragment is missing type condition
shalarewicz Jul 10, 2022
5495620
Merge branch 'sh/fragment-tests' into sh/fragments
shalarewicz Jul 10, 2022
715700a
Merge branch 'sh/fragment-tests' into sh/fragments
shalarewicz Jul 10, 2022
e4f6a0b
inline fragment test correction
shalarewicz Jul 10, 2022
50eb5c8
updated uniontypeweight object
shalarewicz Jul 10, 2022
37eb463
Merge branch 'sh/fragment-tests' into sh/fragments
shalarewicz Jul 10, 2022
228cf62
more descriptive error message for fieldNode function
shalarewicz Jul 10, 2022
a2524b7
renamed AST parse file
shalarewicz Jul 10, 2022
81d105c
Revert "renamed AST parse file" for easier review of changes
shalarewicz Jul 11, 2022
24754d4
updated type weight tests for unions in prep for refactor mentioned i…
shalarewicz Jul 14, 2022
7365a0d
handling basic union types. no support for list weights or non-null t…
shalarewicz Jul 15, 2022
c1cf473
corrected union tests
shalarewicz Jul 16, 2022
c5ea420
preliminary refactor for unions. common field names now appear in the…
shalarewicz Jul 16, 2022
5d2568d
added notes for additional union test cases
shalarewicz Jul 16, 2022
55e568a
Merge branch 'dev' of github.com:oslabs-beta/GraphQL-Gate into sh/fra…
shalarewicz Jul 16, 2022
1b08294
handled inline fragments with differing complexities
shalarewicz Jul 16, 2022
5f7b185
corrected tests for union types
shalarewicz Jul 16, 2022
1bbb262
added if block with note to handle non-null wrappers when building ty…
shalarewicz Jul 16, 2022
d3335ae
minor cleanup
shalarewicz Jul 16, 2022
54013c4
added objects to union type
shalarewicz Jul 16, 2022
4ba017b
refactored union parsing for clarity
shalarewicz Jul 18, 2022
859ba4d
udpated ast parser comments
shalarewicz Jul 18, 2022
afd8c63
renamed ASTnodefunctions to ASTParser to align with class name
shalarewicz Jul 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"type": "module",
"scripts": {
"test": "jest --passWithNoTests",
"test": "jest --passWithNoTests --coverage",
"lint": "eslint src test",
"lint:fix": "eslint --fix src test @types",
"prettier": "prettier --write .",
Expand Down
9 changes: 9 additions & 0 deletions src/@types/buildTypeWeights.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,12 @@ export interface TypeWeightSet {
type Variables = {
[index: string]: readonly unknown;
};

// Type for use when getting fields for union types
type FieldMap = {
[index: string]: {
type: GraphQLOutputType;
weight?: FieldWeight;
resolveTo?: string;
};
};
243 changes: 243 additions & 0 deletions src/analysis/ASTParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import {
DocumentNode,
FieldNode,
SelectionSetNode,
DefinitionNode,
Kind,
SelectionNode,
} from 'graphql';
import { FieldWeight, TypeWeightObject, Variables } from '../@types/buildTypeWeights';
/**
* 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
*
*/

class ASTParser {
typeWeights: TypeWeightObject;

variables: Variables;

fragmentCache: { [index: string]: number };

constructor(typeWeights: TypeWeightObject, variables: Variables) {
this.typeWeights = typeWeights;
this.variables = variables;
this.fragmentCache = {};
}

private calculateCost(
node: FieldNode,
parentName: string,
typeName: string,
typeWeight: FieldWeight
) {
let complexity = 0;
// field resolves to an object or a list with possible selections
let selectionsCost = 0;
let calculatedWeight = 0;

// call the function to handle selection set node with selectionSet property if it is not undefined
if (node.selectionSet) {
selectionsCost += this.selectionSetNode(node.selectionSet, typeName);
}
// if there are arguments and this is a list, call the 'weightFunction' to get the weight of this field. otherwise the weight is static and can be accessed through the typeWeights object
if (node.arguments && typeof typeWeight === 'function') {
// FIXME: May never happen but what if weight is a function and arguments don't exist
calculatedWeight += typeWeight([...node.arguments], this.variables, selectionsCost);
} else {
calculatedWeight += this.typeWeights[typeName].weight + selectionsCost;
}
complexity += calculatedWeight;

return complexity;
}

fieldNode(node: FieldNode, parentName: string): number {
try {
let complexity = 0;
const parentType = this.typeWeights[parentName];
if (!parentType) {
throw new Error(
`ERROR: ASTParser Failed to obtain parentType for parent: ${parentName} and node: ${node.name.value}`
);
}
let typeName: string | undefined;
let typeWeight: FieldWeight | undefined;

if (node.name.value in this.typeWeights) {
// node is an object type n the typeWeight root
typeName = node.name.value;
typeWeight = this.typeWeights[typeName].weight;
complexity += this.calculateCost(node, parentName, typeName, typeWeight);
} else if (parentType.fields[node.name.value].resolveTo) {
// field resolves to another type in type weights or a list
typeName = parentType.fields[node.name.value].resolveTo;
typeWeight = parentType.fields[node.name.value].weight;
// if this is a list typeWeight is a weight function
// otherwise the weight would be null as the weight is defined on the typeWeights root
if (typeName && typeWeight) {
// Type is a list and has a weight function
complexity += this.calculateCost(node, parentName, typeName, typeWeight);
} else if (typeName) {
// resolve type exists at root of typeWeight object and is not a list
typeWeight = this.typeWeights[typeName].weight;
complexity += this.calculateCost(node, parentName, typeName, typeWeight);
} else {
throw new Error(
`ERROR: ASTParser Failed to obtain resolved type name or weight for node: ${parentName}.${node.name.value}`
);
}
} else {
// field is a scalar
typeName = node.name.value;
if (typeName) {
typeWeight = parentType.fields[typeName].weight;
if (typeof typeWeight === 'number') {
complexity += typeWeight;
} else {
throw new Error(
`ERROR: ASTParser Failed to obtain type weight for ${parentName}.${node.name.value}`
);
}
} else {
throw new Error(
`ERROR: ASTParser Failed to obtain type name for ${parentName}.${node.name.value}`
);
}
}
return complexity;
} catch (err) {
throw new Error(
`ERROR: ASTParser.fieldNode Uncaught error handling ${parentName}.${
node.name.value
}\n
${err instanceof Error && err.stack}`
);
}
}

selectionNode(node: SelectionNode, 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
complexity += this.fieldNode(node, parentName.toLowerCase());
} else if (node.kind === Kind.FRAGMENT_SPREAD) {
complexity += this.fragmentCache[node.name.value];
// This is a leaf
// need to parse fragment definition at root and get the result here
} else if (node.kind === Kind.INLINE_FRAGMENT) {
const { typeCondition } = node;

// named type is the type from which inner fields should be take
// If the TypeCondition is omitted, an inline fragment is considered to be of the same type as the enclosing context
const namedType = typeCondition ? typeCondition.name.value.toLowerCase() : parentName;

// TODO: Handle directives like @include
complexity += this.selectionSetNode(node.selectionSet, namedType);
} else {
// FIXME: Consider removing this check. SelectionNodes cannot have any other kind in the current spec.
throw new Error(`ERROR: ASTParser.selectionNode: node type not supported`);
}
return complexity;
}

selectionSetNode(node: SelectionSetNode, parentName: string): number {
let complexity = 0;
let maxFragmentComplexity = 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
const selectionNode = node.selections[i];
const selectionCost = this.selectionNode(node.selections[i], parentName);

// we need to get the largest possible complexity so we save the largest inline fragment
// FIXME: Consider the case where 2 typed fragments are applicable
// e.g. ...UnionType and ...PartofTheUnion
// this case these complexities should be summed in order to be accurate
// However an estimation suffice
if (selectionNode.kind === Kind.INLINE_FRAGMENT) {
if (!selectionNode.typeCondition) {
// complexity is always applicable
complexity += selectionCost;
} else if (selectionCost > maxFragmentComplexity)
maxFragmentComplexity = selectionCost;
} else {
complexity += selectionCost;
}
}
return complexity + maxFragmentComplexity;
}

definitionNode(node: DefinitionNode): 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 this.typeWeights) {
// if it is, it is an object type, add it's type weight to the total
complexity += this.typeWeights[node.operation].weight;
// console.log(`the weight of ${node.operation} is ${complexity}`);
// call the function to handle selection set node with selectionSet property if it is not undefined
if (node.selectionSet) {
complexity += this.selectionSetNode(node.selectionSet, node.operation);
}
}
} else if (node.kind === Kind.FRAGMENT_DEFINITION) {
// Fragments can only be defined on the root type.
// Parse the complexity of this fragment once and store it for use when analyzing other
// nodes. The complexity of a fragment can be added to the selection cost for the query.
const namedType = node.typeCondition.name.value;
// Duplicate fragment names are not allowed by the GraphQL spec and an error is thrown if used.
const fragmentName = node.name.value;

if (this.fragmentCache[fragmentName]) return this.fragmentCache[fragmentName];

const fragmentComplexity = this.selectionSetNode(
node.selectionSet,
namedType.toLowerCase()
);

// Don't count fragment complexity in the node's complexity. Only when fragment is used.
this.fragmentCache[fragmentName] = fragmentComplexity;
} else {
// TODO: Verify that are no other type definition nodes that need to be handled (see ast.d.ts in 'graphql')
// Other types include TypeSystemDefinitionNode (Schema, Type, Directvie) and
// TypeSystemExtensionNode(Schema, Type);
throw new Error(`ERROR: ASTParser.definitionNode: ${node.kind} type not supported`);
}
return complexity;
}

documentNode(node: DocumentNode): number {
let complexity = 0;
// sort the definitions array by kind so that fragments are always parsed first.
// Fragments must be parsed first so that their complexity is available to other nodes.
const sortedDefinitions = [...node.definitions].sort((a, b) =>
a.kind.localeCompare(b.kind)
);
for (let i = 0; i < sortedDefinitions.length; i += 1) {
// call the function to handle the various types of definition nodes
complexity += this.definitionNode(sortedDefinitions[i]);
}
return complexity;
}
}

export default ASTParser;
Loading