Skip to content

Commit 4cd7382

Browse files
authored
Merge pull request #33 from oslabs-beta/sh/build-type-weights
Type Weights Parser
2 parents 3cfbef4 + 39473b5 commit 4cd7382

File tree

7 files changed

+244
-72
lines changed

7 files changed

+244
-72
lines changed

.eslintrc.json

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

package-lock.json

Lines changed: 0 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/@types/buildTypeWeights.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
interface Fields {
2-
readonly [index: string]: number | ((arg: number, type: Type) => number);
2+
[index: string]: number | ((args: ArgumentNode[]) => number);
33
}
44

55
interface Type {
@@ -8,7 +8,7 @@ interface Type {
88
}
99

1010
interface TypeWeightObject {
11-
readonly [index: string]: Type;
11+
[index: string]: Type;
1212
}
1313

1414
interface TypeWeightConfig {

src/analysis/buildTypeWeights.ts

Lines changed: 204 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
1+
import {
2+
ArgumentNode,
3+
GraphQLArgument,
4+
GraphQLFieldMap,
5+
GraphQLNamedType,
6+
GraphQLObjectType,
7+
GraphQLOutputType,
8+
isCompositeType,
9+
isEnumType,
10+
isInterfaceType,
11+
isListType,
12+
isNonNullType,
13+
isObjectType,
14+
isScalarType,
15+
isUnionType,
16+
Kind,
17+
ValueNode,
18+
} from 'graphql';
19+
import { Maybe } from 'graphql/jsutils/Maybe';
20+
import { ObjMap } from 'graphql/jsutils/ObjMap';
121
import { GraphQLSchema } from 'graphql/type/schema';
222

23+
export const KEYWORDS = ['first', 'last', 'limit'];
24+
25+
// These variables exist to provide a default value for typescript when accessing a weight
26+
// since all props are optioal in TypeWeightConfig
27+
const DEFAULT_MUTATION_WEIGHT = 10;
28+
const DEFAULT_OBJECT_WEIGHT = 1;
29+
const DEFAULT_SCALAR_WEIGHT = 0;
30+
const DEFAULT_CONNECTION_WEIGHT = 2;
31+
const DEFAULT_QUERY_WEIGHT = 1;
32+
33+
// FIXME: What about Interface defaults
34+
335
/**
436
* Default TypeWeight Configuration:
537
* mutation: 10
@@ -8,19 +40,168 @@ import { GraphQLSchema } from 'graphql/type/schema';
840
* connection: 2
941
*/
1042
export const defaultTypeWeightsConfig: TypeWeightConfig = {
11-
mutation: 10,
12-
object: 1,
13-
scalar: 0,
14-
connection: 2,
43+
mutation: DEFAULT_MUTATION_WEIGHT,
44+
object: DEFAULT_OBJECT_WEIGHT,
45+
scalar: DEFAULT_SCALAR_WEIGHT,
46+
connection: DEFAULT_CONNECTION_WEIGHT,
1547
};
1648

49+
/**
50+
* Parses the Query type in the provided schema object and outputs a new TypeWeightObject
51+
* @param schema
52+
* @param typeWeightObject
53+
* @param typeWeights
54+
* @returns
55+
*/
56+
function parseQuery(
57+
schema: GraphQLSchema,
58+
typeWeightObject: TypeWeightObject,
59+
typeWeights: TypeWeightConfig
60+
): TypeWeightObject {
61+
// Get any Query fields (these are the queries that the API exposes)
62+
const queryType: Maybe<GraphQLObjectType> = schema.getQueryType();
63+
64+
if (!queryType) return typeWeightObject;
65+
66+
const result: TypeWeightObject = { ...typeWeightObject };
67+
68+
result.query = {
69+
weight: typeWeights.query || DEFAULT_QUERY_WEIGHT,
70+
// fields gets populated with the query fields and associated weights.
71+
fields: {},
72+
};
73+
74+
const queryFields: GraphQLFieldMap<any, any> = queryType.getFields();
75+
76+
Object.keys(queryFields).forEach((field) => {
77+
// this is the type the query resolves to
78+
const resolveType: GraphQLOutputType = queryFields[field].type;
79+
80+
// check if any of our keywords 'first', 'last', 'limit' exist in the arg list
81+
queryFields[field].args.forEach((arg: GraphQLArgument) => {
82+
// If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query
83+
// should be dependent on both the weight of the resolved type and the limiting argument.
84+
if (KEYWORDS.includes(arg.name) && isListType(resolveType)) {
85+
// Get the type that comprises the list
86+
const listType = resolveType.ofType;
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+
// FIXME: If the weight of the resolveType is 0 the weight can be set to 0 rather than a function.
92+
result.query.fields[field] = (args: ArgumentNode[]): number => {
93+
// TODO: Test this function
94+
const limitArg: ArgumentNode | undefined = args.find(
95+
(cur) => cur.name.value === arg.name
96+
);
97+
98+
if (limitArg) {
99+
const node: ValueNode = limitArg.value;
100+
101+
if (Kind.INT === node.kind) {
102+
const multiplier = Number(node.value || arg.defaultValue);
103+
const weight = isCompositeType(listType)
104+
? result[listType.name.toLowerCase()].weight
105+
: typeWeights.scalar || DEFAULT_SCALAR_WEIGHT; // Note this includes enums
106+
107+
return weight * multiplier;
108+
}
109+
110+
if (Kind.VARIABLE === node.kind) {
111+
// TODO: Get variable value and return
112+
// const multiplier: number =
113+
// return result[listType.name.toLowerCase()].weight * multiplier;
114+
throw new Error(
115+
'ERROR: buildTypeWeights Variable arge values not supported;'
116+
);
117+
}
118+
}
119+
120+
// FIXME: The list is unbounded. Return the object weight for
121+
throw new Error(
122+
`ERROR: buildTypeWeights: Unbouned list complexity not supported. Query results should be limited with ${KEYWORDS}`
123+
);
124+
};
125+
}
126+
});
127+
128+
// if the field is a scalar or an enum set weight accordingly
129+
if (isScalarType(resolveType) || isEnumType(resolveType)) {
130+
result.query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
131+
}
132+
});
133+
return result;
134+
}
135+
136+
/**
137+
* Parses all types in the provided schema object excempt for Query, Mutation
138+
* and built in types that begin with '__' and outputs a new TypeWeightObject
139+
* @param schema
140+
* @param typeWeightObject
141+
* @param typeWeights
142+
* @returns
143+
*/
144+
function parseTypes(
145+
schema: GraphQLSchema,
146+
typeWeightObject: TypeWeightObject,
147+
typeWeights: TypeWeightConfig
148+
): TypeWeightObject {
149+
const typeMap: ObjMap<GraphQLNamedType> = schema.getTypeMap();
150+
151+
const result: TypeWeightObject = { ...typeWeightObject };
152+
153+
// Handle Object, Interface, Enum and Union types
154+
Object.keys(typeMap).forEach((type) => {
155+
const typeName = type.toLowerCase();
156+
157+
const currentType: GraphQLNamedType = typeMap[type];
158+
// Get all types that aren't Query or Mutation or a built in type that starts with '__'
159+
if (type !== 'Query' && type !== 'Mutation' && !type.startsWith('__')) {
160+
if (isObjectType(currentType) || isInterfaceType(currentType)) {
161+
// Add the type and it's associated fields to the result
162+
result[typeName] = {
163+
fields: {},
164+
weight: typeWeights.object || DEFAULT_OBJECT_WEIGHT,
165+
};
166+
167+
const fields = currentType.getFields();
168+
169+
Object.keys(fields).forEach((field: string) => {
170+
const fieldType: GraphQLOutputType = fields[field].type;
171+
172+
// Only scalars are considered here any other types should be references from the top level of the type weight object.
173+
if (
174+
isScalarType(fieldType) ||
175+
(isNonNullType(fieldType) && isScalarType(fieldType.ofType))
176+
) {
177+
result[typeName].fields[field] =
178+
typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
179+
}
180+
});
181+
} else if (isEnumType(currentType)) {
182+
result[typeName] = {
183+
fields: {},
184+
weight: typeWeights.scalar || DEFAULT_SCALAR_WEIGHT,
185+
};
186+
} else if (isUnionType(currentType)) {
187+
result[typeName] = {
188+
fields: {},
189+
weight: typeWeights.object || DEFAULT_OBJECT_WEIGHT,
190+
};
191+
}
192+
}
193+
});
194+
195+
return result;
196+
}
197+
17198
/**
18199
* The default typeWeightsConfig object is based off of Shopifys implementation of query
19200
* cost analysis. Our function should input a users configuration of type weights or fall
20201
* back on shopifys settings. We can change this later.
21202
*
22203
* This function should
23-
* - itreate through the schema object and create the typeWeightObject as described in the tests
204+
* - iterate through the schema object and create the typeWeightObject as described in the tests
24205
* - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
25206
*
26207
* @param schema
@@ -30,6 +211,23 @@ function buildTypeWeightsFromSchema(
30211
schema: GraphQLSchema,
31212
typeWeightsConfig: TypeWeightConfig = defaultTypeWeightsConfig
32213
): TypeWeightObject {
33-
throw Error(`getTypeWeightsFromSchema is not implemented.`);
214+
if (!schema) throw new Error('Missing Argument: schema is required');
215+
216+
// Merge the provided type weights with the default to account for missing values
217+
const typeWeights: TypeWeightConfig = {
218+
...defaultTypeWeightsConfig,
219+
...typeWeightsConfig,
220+
};
221+
222+
// Confirm that any custom weights are positive
223+
Object.entries(typeWeights).forEach((value: [string, number]) => {
224+
if (value[1] < 0) {
225+
throw new Error(`Type weights cannot be negative. Received: ${value[0]}: ${value[1]} `);
226+
}
227+
});
228+
229+
const objectTypeWeights = parseTypes(schema, {}, typeWeights);
230+
return parseQuery(schema, objectTypeWeights, typeWeights);
34231
}
232+
35233
export default buildTypeWeightsFromSchema;

0 commit comments

Comments
 (0)