Skip to content

Commit 985d3b2

Browse files
committed
modularized typeWeight algorithm
1 parent 11de437 commit 985d3b2

File tree

1 file changed

+129
-111
lines changed

1 file changed

+129
-111
lines changed

src/analysis/buildTypeWeights.ts

Lines changed: 129 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { query } from 'express';
12
import {
23
ArgumentNode,
34
GraphQLArgument,
@@ -14,6 +15,7 @@ import {
1415
isObjectType,
1516
isScalarType,
1617
isUnionType,
18+
Kind,
1719
ValueNode,
1820
} from 'graphql';
1921
import { Maybe } from 'graphql/jsutils/Maybe';
@@ -47,53 +49,111 @@ export const defaultTypeWeightsConfig: TypeWeightConfig = {
4749
connection: DEFAULT_CONNECTION_WEIGHT,
4850
};
4951

50-
/**
51-
* The default typeWeightsConfig object is based off of Shopifys implementation of query
52-
* cost analysis. Our function should input a users configuration of type weights or fall
53-
* back on shopifys settings. We can change this later.
54-
*
55-
* This function should
56-
* - iterate through the schema object and create the typeWeightObject as described in the tests
57-
* - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
58-
*
59-
* @param schema
60-
* @param typeWeightsConfig Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
61-
*/
62-
function buildTypeWeightsFromSchema(
52+
function parseQuery(
6353
schema: GraphQLSchema,
64-
typeWeightsConfig: TypeWeightConfig = defaultTypeWeightsConfig
54+
typeWeightObject: TypeWeightObject,
55+
typeWeights: TypeWeightConfig
6556
): TypeWeightObject {
66-
if (!schema) throw new Error('Must provide schema');
57+
// Get any Query fields (these are the queries that the API exposes)
58+
const queryType: Maybe<GraphQLObjectType> = schema.getQueryType();
6759

68-
// Merge the provided type weights with the default to account for missing values
69-
const typeWeights: TypeWeightConfig = {
70-
...defaultTypeWeightsConfig,
71-
...typeWeightsConfig,
60+
if (!queryType) return typeWeightObject;
61+
62+
const result: TypeWeightObject = { ...typeWeightObject };
63+
64+
result.query = {
65+
weight: typeWeights.query || DEFAULT_QUERY_WEIGHT,
66+
// fields gets populated with the query fields and associated weights.
67+
fields: {},
7268
};
7369

74-
// Confirm that any custom weights are positive
75-
Object.entries(typeWeights).forEach((value: [string, number]) => {
76-
if (value[1] < 0) {
77-
throw new Error(`Type weights cannot be negative. Received: ${value[0]}: ${value[1]} `);
70+
const queryFields: GraphQLFieldMap<any, any> = queryType.getFields();
71+
72+
Object.keys(queryFields).forEach((field) => {
73+
// this is the type the query resolves to
74+
const resolveType: GraphQLOutputType = queryFields[field].type;
75+
76+
// check if any of our keywords 'first', 'last', 'limit' exist in the arg list
77+
queryFields[field].args.forEach((arg: GraphQLArgument) => {
78+
// If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query
79+
// should be dependent on both the weight of the resolved type and the limiting argument.
80+
if (KEYWORDS.includes(arg.name) && isListType(resolveType)) {
81+
// Get the type that comprises the list
82+
const listType = resolveType.ofType;
83+
84+
// Composite Types are Objects, Interfaces and Unions.
85+
if (isCompositeType(listType)) {
86+
// Set the field weight to a function that accepts
87+
88+
// FIXME: This function can only handle integer arguments for one of the keyword params.
89+
// In order to handle variable arguments, we may need to accept a second parameter so that the complexity aglorithm
90+
// can pass in the variables as well.
91+
result.query.fields[field] = (args: ArgumentNode[]): number => {
92+
// TODO: Test this function
93+
const limitArg: ArgumentNode | undefined = args.find(
94+
(cur) => cur.name.value === arg.name
95+
);
96+
97+
if (limitArg) {
98+
const node: ValueNode = limitArg.value;
99+
100+
if (Kind.INT === node.kind) {
101+
const multiplier = Number(node.value || arg.defaultValue);
102+
103+
return result[listType.name.toLowerCase()].weight * multiplier;
104+
}
105+
106+
if (Kind.VARIABLE === node.kind) {
107+
// TODO: Get variable value and return
108+
// const multiplier: number =
109+
// return result[listType.name.toLowerCase()].weight * multiplier;
110+
throw new Error(
111+
'ERROR: buildTypeWeights Variable arge values not supported;'
112+
);
113+
}
114+
}
115+
116+
// FIXME: The list is unbounded. Return the object weight for
117+
throw new Error(
118+
`ERROR: buildTypeWeights: Unbouned list complexity not supported. Query results should be limited with ${KEYWORDS}`
119+
);
120+
};
121+
} else {
122+
// TODO: determine the type of the list and use the appropriate weight
123+
// TODO: This should multiply as well
124+
result.query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
125+
}
126+
}
127+
});
128+
129+
// if the field is a scalar set weight accordingly
130+
// TODO: Allow config for enum weights
131+
if (isScalarType(resolveType) || isEnumType(resolveType)) {
132+
result.query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
78133
}
79134
});
135+
return result;
136+
}
80137

81-
const result: TypeWeightObject = {};
82-
138+
function parseTypes(
139+
schema: GraphQLSchema,
140+
typeWeightObject: TypeWeightObject,
141+
typeWeights: TypeWeightConfig
142+
): TypeWeightObject {
83143
const typeMap: ObjMap<GraphQLNamedType> = schema.getTypeMap();
84144

145+
const result: TypeWeightObject = { ...typeWeightObject };
146+
85147
// Handle Object, Interface, Enum and Union types
86148
Object.keys(typeMap).forEach((type) => {
149+
const typeName = type.toLowerCase();
150+
87151
const currentType: GraphQLNamedType = typeMap[type];
88152
// Get all types that aren't Query or Mutation or a built in type that starts with '__'
89-
if (
90-
currentType.name !== 'Query' &&
91-
currentType.name !== 'Mutation' &&
92-
!currentType.name.startsWith('__')
93-
) {
153+
if (type !== 'Query' && type !== 'Mutation' && !type.startsWith('__')) {
94154
if (isObjectType(currentType) || isInterfaceType(currentType)) {
95-
// Add the type to the result
96-
result[type.toLowerCase()] = {
155+
// Add the type and it's associated fields to the result
156+
result[typeName] = {
97157
fields: {},
98158
weight: typeWeights.object || DEFAULT_OBJECT_WEIGHT,
99159
};
@@ -102,108 +162,66 @@ function buildTypeWeightsFromSchema(
102162

103163
Object.keys(fields).forEach((field: string) => {
104164
const fieldType: GraphQLOutputType = fields[field].type;
165+
166+
// Only scalars are considered here any other types should be references from the top level of the type weight object.
105167
if (
106168
isScalarType(fieldType) ||
107169
(isNonNullType(fieldType) && isScalarType(fieldType.ofType))
108170
) {
109-
result[type.toLowerCase()].fields[field] =
171+
result[typeName].fields[field] =
110172
typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
111173
}
112174
});
113175
} else if (isEnumType(currentType)) {
114-
result[currentType.name.toLowerCase()] = {
176+
result[typeName] = {
115177
fields: {},
116178
weight: typeWeights.scalar || DEFAULT_SCALAR_WEIGHT,
117179
};
118180
} else if (isUnionType(currentType)) {
119-
result[currentType.name.toLowerCase()] = {
181+
result[typeName] = {
120182
fields: {},
121183
weight: typeWeights.object || DEFAULT_OBJECT_WEIGHT,
122184
};
123185
}
124186
}
125187
});
126188

127-
// Get any Query fields (these are the queries that the API exposes)
128-
const queryType: Maybe<GraphQLObjectType> = schema.getQueryType();
189+
return result;
190+
}
129191

130-
if (queryType) {
131-
result.query = {
132-
weight: typeWeights.query || DEFAULT_QUERY_WEIGHT,
133-
// fields gets populated with the query fields and associated weights.
134-
fields: {},
135-
};
136-
137-
const queryFields: GraphQLFieldMap<any, any> = queryType.getFields();
138-
139-
Object.keys(queryFields).forEach((field) => {
140-
// this is the type the query resolves to
141-
const resolveType: GraphQLOutputType = queryFields[field].type;
142-
143-
// check if any of our keywords 'first', 'last', 'limit' exist in the arg list
144-
queryFields[field].args.forEach((arg: GraphQLArgument) => {
145-
// If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query
146-
// should be dependent on both the weight of the resolved type and the limiting argument.
147-
if (KEYWORDS.includes(arg.name) && isListType(resolveType)) {
148-
const defaultVal: number = <number>arg.defaultValue;
149-
150-
// Get the type that comprises the list
151-
const listType = resolveType.ofType;
152-
153-
// Composite Types are Objects, Interfaces and Unions.
154-
if (isCompositeType(listType)) {
155-
// Set the field weight to a function that accepts
156-
// TODO: Accept ArgumentNode[] and look for the arg we need.
157-
// TODO: Test this function
158-
result.query.fields[field] = (args: ArgumentNode[]): number => {
159-
// Function should receive object with arg, value as k, v pairs
160-
// function iterate on this object looking for a keyword then returns
161-
const limitArg: ArgumentNode | undefined = args.find(
162-
(cur) => cur.name.value === arg.name
163-
);
164-
165-
// FIXME: Need to use the value of this variable
166-
// const isVariable = (node: any): node is VariableNode => {
167-
// if (node as VariableNode) return true;
168-
// return false;
169-
// };
170-
171-
const isIntNode = (node: any): node is IntValueNode => {
172-
if (node as IntValueNode) return true;
173-
return false;
174-
};
175-
176-
if (limitArg) {
177-
const node: ValueNode = limitArg.value;
178-
179-
// FIXME: Is there a better way to check for the type here?
180-
if (isIntNode(node)) {
181-
const multiplier = Number(node.value || arg.defaultValue);
182-
183-
return result[listType.name.toLowerCase()].weight * multiplier;
184-
}
185-
}
192+
/**
193+
* The default typeWeightsConfig object is based off of Shopifys implementation of query
194+
* cost analysis. Our function should input a users configuration of type weights or fall
195+
* back on shopifys settings. We can change this later.
196+
*
197+
* This function should
198+
* - iterate through the schema object and create the typeWeightObject as described in the tests
199+
* - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
200+
*
201+
* @param schema
202+
* @param typeWeightsConfig Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
203+
*/
204+
function buildTypeWeightsFromSchema(
205+
schema: GraphQLSchema,
206+
typeWeightsConfig: TypeWeightConfig = defaultTypeWeightsConfig
207+
): TypeWeightObject {
208+
if (!schema) throw new Error('Missing Argument: schema is required');
186209

187-
// FIXME: The list is unbounded. Return the object weight
188-
return result[listType.name.toLowerCase()].weight;
189-
};
190-
} else {
191-
// TODO: determine the type of the list and use the appropriate weight
192-
// TODO: This should multiply as well
193-
result.query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
194-
}
195-
}
196-
});
210+
// Merge the provided type weights with the default to account for missing values
211+
const typeWeights: TypeWeightConfig = {
212+
...defaultTypeWeightsConfig,
213+
...typeWeightsConfig,
214+
};
197215

198-
// if the field is a scalar set weight accordingly
199-
// FIXME: Enums shouldn't be here???
200-
if (isScalarType(resolveType) || isEnumType(resolveType)) {
201-
result.query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
202-
}
203-
});
204-
}
216+
// Confirm that any custom weights are positive
217+
Object.entries(typeWeights).forEach((value: [string, number]) => {
218+
if (value[1] < 0) {
219+
throw new Error(`Type weights cannot be negative. Received: ${value[0]}: ${value[1]} `);
220+
}
221+
});
205222

206-
return result;
223+
const objectTypeWeights = parseTypes(schema, {}, typeWeights);
224+
return parseQuery(schema, objectTypeWeights, typeWeights);
207225
}
208226

209227
export default buildTypeWeightsFromSchema;

0 commit comments

Comments
 (0)