Skip to content

Commit 8c94c74

Browse files
committed
Merge branch 'dev' into sh/sliding-window-log
2 parents b419a88 + 5fe7133 commit 8c94c74

File tree

7 files changed

+1099
-326
lines changed

7 files changed

+1099
-326
lines changed

src/@types/buildTypeWeights.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@ export interface TypeWeightSet {
3131
type Variables = {
3232
[index: string]: readonly unknown;
3333
};
34+
35+
// Type for use when getting fields for union types
36+
type FieldMap = {
37+
[index: string]: {
38+
type: GraphQLOutputType;
39+
weight?: FieldWeight;
40+
resolveTo?: string;
41+
};
42+
};

src/analysis/ASTParser.ts

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import {
2+
DocumentNode,
3+
FieldNode,
4+
SelectionSetNode,
5+
DefinitionNode,
6+
Kind,
7+
SelectionNode,
8+
} from 'graphql';
9+
import { FieldWeight, TypeWeightObject, Variables } from '../@types/buildTypeWeights';
10+
/**
11+
* The AST node functions call each other following the nested structure below
12+
* Each function handles a specific GraphQL AST node type
13+
*
14+
* AST nodes call each other in the following way
15+
*
16+
* Document Node
17+
* |
18+
* Definiton Node
19+
* (operation and fragment definitons)
20+
* / \
21+
* |-----> Selection Set Node not done
22+
* | /
23+
* | Selection Node
24+
* | (Field, Inline fragment and fragment spread)
25+
* | | \ \
26+
* |--Field Node not done not done
27+
*
28+
*/
29+
30+
class ASTParser {
31+
typeWeights: TypeWeightObject;
32+
33+
variables: Variables;
34+
35+
fragmentCache: { [index: string]: number };
36+
37+
constructor(typeWeights: TypeWeightObject, variables: Variables) {
38+
this.typeWeights = typeWeights;
39+
this.variables = variables;
40+
this.fragmentCache = {};
41+
}
42+
43+
private calculateCost(
44+
node: FieldNode,
45+
parentName: string,
46+
typeName: string,
47+
typeWeight: FieldWeight
48+
) {
49+
let complexity = 0;
50+
// field resolves to an object or a list with possible selections
51+
let selectionsCost = 0;
52+
let calculatedWeight = 0;
53+
54+
// call the function to handle selection set node with selectionSet property if it is not undefined
55+
if (node.selectionSet) {
56+
selectionsCost += this.selectionSetNode(node.selectionSet, typeName);
57+
}
58+
// 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
59+
if (node.arguments && typeof typeWeight === 'function') {
60+
// FIXME: May never happen but what if weight is a function and arguments don't exist
61+
calculatedWeight += typeWeight([...node.arguments], this.variables, selectionsCost);
62+
} else {
63+
calculatedWeight += this.typeWeights[typeName].weight + selectionsCost;
64+
}
65+
complexity += calculatedWeight;
66+
67+
return complexity;
68+
}
69+
70+
fieldNode(node: FieldNode, parentName: string): number {
71+
try {
72+
let complexity = 0;
73+
const parentType = this.typeWeights[parentName];
74+
if (!parentType) {
75+
throw new Error(
76+
`ERROR: ASTParser Failed to obtain parentType for parent: ${parentName} and node: ${node.name.value}`
77+
);
78+
}
79+
let typeName: string | undefined;
80+
let typeWeight: FieldWeight | undefined;
81+
82+
if (node.name.value in this.typeWeights) {
83+
// node is an object type n the typeWeight root
84+
typeName = node.name.value;
85+
typeWeight = this.typeWeights[typeName].weight;
86+
complexity += this.calculateCost(node, parentName, typeName, typeWeight);
87+
} else if (parentType.fields[node.name.value].resolveTo) {
88+
// field resolves to another type in type weights or a list
89+
typeName = parentType.fields[node.name.value].resolveTo;
90+
typeWeight = parentType.fields[node.name.value].weight;
91+
// if this is a list typeWeight is a weight function
92+
// otherwise the weight would be null as the weight is defined on the typeWeights root
93+
if (typeName && typeWeight) {
94+
// Type is a list and has a weight function
95+
complexity += this.calculateCost(node, parentName, typeName, typeWeight);
96+
} else if (typeName) {
97+
// resolve type exists at root of typeWeight object and is not a list
98+
typeWeight = this.typeWeights[typeName].weight;
99+
complexity += this.calculateCost(node, parentName, typeName, typeWeight);
100+
} else {
101+
throw new Error(
102+
`ERROR: ASTParser Failed to obtain resolved type name or weight for node: ${parentName}.${node.name.value}`
103+
);
104+
}
105+
} else {
106+
// field is a scalar
107+
typeName = node.name.value;
108+
if (typeName) {
109+
typeWeight = parentType.fields[typeName].weight;
110+
if (typeof typeWeight === 'number') {
111+
complexity += typeWeight;
112+
} else {
113+
throw new Error(
114+
`ERROR: ASTParser Failed to obtain type weight for ${parentName}.${node.name.value}`
115+
);
116+
}
117+
} else {
118+
throw new Error(
119+
`ERROR: ASTParser Failed to obtain type name for ${parentName}.${node.name.value}`
120+
);
121+
}
122+
}
123+
return complexity;
124+
} catch (err) {
125+
throw new Error(
126+
`ERROR: ASTParser.fieldNode Uncaught error handling ${parentName}.${
127+
node.name.value
128+
}\n
129+
${err instanceof Error && err.stack}`
130+
);
131+
}
132+
}
133+
134+
selectionNode(node: SelectionNode, parentName: string): number {
135+
let complexity = 0;
136+
// check the kind property against the set of selection nodes that are possible
137+
if (node.kind === Kind.FIELD) {
138+
// call the function that handle field nodes
139+
complexity += this.fieldNode(node, parentName.toLowerCase());
140+
} else if (node.kind === Kind.FRAGMENT_SPREAD) {
141+
complexity += this.fragmentCache[node.name.value];
142+
// This is a leaf
143+
// need to parse fragment definition at root and get the result here
144+
} else if (node.kind === Kind.INLINE_FRAGMENT) {
145+
const { typeCondition } = node;
146+
147+
// named type is the type from which inner fields should be take
148+
// If the TypeCondition is omitted, an inline fragment is considered to be of the same type as the enclosing context
149+
const namedType = typeCondition ? typeCondition.name.value.toLowerCase() : parentName;
150+
151+
// TODO: Handle directives like @include
152+
complexity += this.selectionSetNode(node.selectionSet, namedType);
153+
} else {
154+
// FIXME: Consider removing this check. SelectionNodes cannot have any other kind in the current spec.
155+
throw new Error(`ERROR: ASTParser.selectionNode: node type not supported`);
156+
}
157+
return complexity;
158+
}
159+
160+
selectionSetNode(node: SelectionSetNode, parentName: string): number {
161+
let complexity = 0;
162+
let maxFragmentComplexity = 0;
163+
// iterate shrough the 'selections' array on the seletion set node
164+
for (let i = 0; i < node.selections.length; i += 1) {
165+
// call the function to handle seletion nodes
166+
// pass the current parent through because selection sets act only as intermediaries
167+
const selectionNode = node.selections[i];
168+
const selectionCost = this.selectionNode(node.selections[i], parentName);
169+
170+
// we need to get the largest possible complexity so we save the largest inline fragment
171+
// FIXME: Consider the case where 2 typed fragments are applicable
172+
// e.g. ...UnionType and ...PartofTheUnion
173+
// this case these complexities should be summed in order to be accurate
174+
// However an estimation suffice
175+
if (selectionNode.kind === Kind.INLINE_FRAGMENT) {
176+
if (!selectionNode.typeCondition) {
177+
// complexity is always applicable
178+
complexity += selectionCost;
179+
} else if (selectionCost > maxFragmentComplexity)
180+
maxFragmentComplexity = selectionCost;
181+
} else {
182+
complexity += selectionCost;
183+
}
184+
}
185+
return complexity + maxFragmentComplexity;
186+
}
187+
188+
definitionNode(node: DefinitionNode): number {
189+
let complexity = 0;
190+
// check the kind property against the set of definiton nodes that are possible
191+
if (node.kind === Kind.OPERATION_DEFINITION) {
192+
// check if the operation is in the type weights object.
193+
if (node.operation.toLocaleLowerCase() in this.typeWeights) {
194+
// if it is, it is an object type, add it's type weight to the total
195+
complexity += this.typeWeights[node.operation].weight;
196+
// console.log(`the weight of ${node.operation} is ${complexity}`);
197+
// call the function to handle selection set node with selectionSet property if it is not undefined
198+
if (node.selectionSet) {
199+
complexity += this.selectionSetNode(node.selectionSet, node.operation);
200+
}
201+
}
202+
} else if (node.kind === Kind.FRAGMENT_DEFINITION) {
203+
// Fragments can only be defined on the root type.
204+
// Parse the complexity of this fragment once and store it for use when analyzing other
205+
// nodes. The complexity of a fragment can be added to the selection cost for the query.
206+
const namedType = node.typeCondition.name.value;
207+
// Duplicate fragment names are not allowed by the GraphQL spec and an error is thrown if used.
208+
const fragmentName = node.name.value;
209+
210+
if (this.fragmentCache[fragmentName]) return this.fragmentCache[fragmentName];
211+
212+
const fragmentComplexity = this.selectionSetNode(
213+
node.selectionSet,
214+
namedType.toLowerCase()
215+
);
216+
217+
// Don't count fragment complexity in the node's complexity. Only when fragment is used.
218+
this.fragmentCache[fragmentName] = fragmentComplexity;
219+
} else {
220+
// TODO: Verify that are no other type definition nodes that need to be handled (see ast.d.ts in 'graphql')
221+
// Other types include TypeSystemDefinitionNode (Schema, Type, Directvie) and
222+
// TypeSystemExtensionNode(Schema, Type);
223+
throw new Error(`ERROR: ASTParser.definitionNode: ${node.kind} type not supported`);
224+
}
225+
return complexity;
226+
}
227+
228+
documentNode(node: DocumentNode): number {
229+
let complexity = 0;
230+
// sort the definitions array by kind so that fragments are always parsed first.
231+
// Fragments must be parsed first so that their complexity is available to other nodes.
232+
const sortedDefinitions = [...node.definitions].sort((a, b) =>
233+
a.kind.localeCompare(b.kind)
234+
);
235+
for (let i = 0; i < sortedDefinitions.length; i += 1) {
236+
// call the function to handle the various types of definition nodes
237+
complexity += this.definitionNode(sortedDefinitions[i]);
238+
}
239+
return complexity;
240+
}
241+
}
242+
243+
export default ASTParser;

0 commit comments

Comments
 (0)