Skip to content

Commit 5fcd37b

Browse files
committed
Initial buildTypeWeight algorithm
1 parent 54283cc commit 5fcd37b

File tree

4 files changed

+166
-29
lines changed

4 files changed

+166
-29
lines changed

jest.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const config: Config.InitialOptions = {
66
preset: 'ts-jest',
77
testEnvironment: 'node',
88
moduleFileExtensions: ['js', 'ts'],
9-
setupFilesAfterEnv: ['./jest.setup.redis-mock.js'],
109
};
1110

1211
export default config;

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 | ((arg: number, type: Type) => 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: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,40 @@
1+
import {
2+
GraphQLArgument,
3+
GraphQLEnumType,
4+
GraphQLFieldMap,
5+
GraphQLInterfaceType,
6+
GraphQLList,
7+
GraphQLNamedType,
8+
GraphQLNonNull,
9+
GraphQLObjectType,
10+
GraphQLOutputType,
11+
GraphQLScalarType,
12+
GraphQLUnionType,
13+
isCompositeType,
14+
} from 'graphql';
15+
import { Maybe } from 'graphql/jsutils/Maybe';
116
import { GraphQLSchema } from 'graphql/type/schema';
217

18+
const KEYWORDS = ['first', 'last', 'limit'];
19+
320
/**
421
* Default TypeWeight Configuration:
522
* mutation: 10
623
* object: 1
724
* scalar: 0
825
* connection: 2
926
*/
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+
1033
export const defaultTypeWeightsConfig: TypeWeightConfig = {
11-
mutation: 10,
12-
object: 1,
13-
scalar: 0,
14-
connection: 2,
34+
mutation: DEFAULT_MUTATION_WEIGHT,
35+
object: DEFAULT_OBJECT_WEIGHT,
36+
scalar: DEFAULT_SCALAR_WEIGHT,
37+
connection: DEFAULT_CONNECTION_WEIGHT,
1538
};
1639

1740
/**
@@ -20,8 +43,8 @@ export const defaultTypeWeightsConfig: TypeWeightConfig = {
2043
* back on shopifys settings. We can change this later.
2144
*
2245
* This function should
23-
* - itreate through the schema object and create the typeWeightObject as described in the tests
24-
* - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
46+
* - TODO: iterate through the schema object and create the typeWeightObject as described in the tests
47+
* - TODO: validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
2548
*
2649
* @param schema
2750
* @param typeWeightsConfig Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
@@ -30,6 +53,120 @@ function buildTypeWeightsFromSchema(
3053
schema: GraphQLSchema,
3154
typeWeightsConfig: TypeWeightConfig = defaultTypeWeightsConfig
3255
): TypeWeightObject {
33-
throw Error(`getTypeWeightsFromSchema is not implemented.`);
56+
// Iterate each key in the schema object
57+
// this includes scalars, types, interfaces, unions, enums etc.
58+
// check the type of each add set the appropriate weight.
59+
// iterate through that types fields and set the appropriate weight
60+
// this is kind of only relevant for things like Query or Mutation
61+
// that have functions(?) as fields for which we should set the weight as a function
62+
// that take any required params.
63+
64+
if (!schema) throw new Error('Must provide schema');
65+
66+
// Merge the provided type weights with the default to account for missing values
67+
const typeWeights: TypeWeightConfig = {
68+
...defaultTypeWeightsConfig,
69+
...typeWeightsConfig,
70+
};
71+
72+
// Confirm that any custom weights are positive
73+
Object.entries(typeWeights).forEach((value: [string, number]) => {
74+
if (value[1] < 0) {
75+
throw new Error(`Type weights cannot be negative. Received: ${value[0]}: ${value[1]} `);
76+
}
77+
});
78+
79+
const result: TypeWeightObject = {};
80+
81+
// Iterate through __typeMap and set weights of all object types?
82+
83+
const typeMap = schema.getTypeMap();
84+
85+
Object.keys(typeMap).forEach((type) => {
86+
const currentType: GraphQLNamedType = typeMap[type];
87+
// Limit to object types for now
88+
// Get all types that aren't Query or Mutation and don't start with __
89+
if (
90+
currentType.name !== 'Query' &&
91+
currentType.name !== 'Mutation' &&
92+
!currentType.name.startsWith('__')
93+
) {
94+
if (
95+
currentType instanceof GraphQLObjectType ||
96+
currentType instanceof GraphQLInterfaceType
97+
) {
98+
// Add the type to the result
99+
result[type] = {
100+
fields: {},
101+
weight: typeWeights.object || DEFAULT_OBJECT_WEIGHT,
102+
};
103+
104+
const fields = currentType.getFields();
105+
Object.keys(fields).forEach((field: string) => {
106+
const fieldType: GraphQLOutputType = fields[field].type;
107+
if (
108+
fieldType instanceof GraphQLScalarType ||
109+
(fieldType instanceof GraphQLNonNull &&
110+
fieldType.ofType instanceof GraphQLScalarType)
111+
) {
112+
result[type].fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
113+
}
114+
// FIXME: Do any other types need to be included?
115+
});
116+
} else if (currentType instanceof GraphQLEnumType) {
117+
result[currentType.name] = {
118+
fields: {},
119+
weight: 0,
120+
};
121+
} else if (currentType instanceof GraphQLUnionType) {
122+
result[currentType.name] = {
123+
fields: {},
124+
weight: 1, // FIXME: Use the correct weight
125+
};
126+
}
127+
}
128+
});
129+
130+
// Get any Query fields (these are the queries that the API exposes)
131+
const queryType: Maybe<GraphQLObjectType> = schema.getQueryType();
132+
133+
if (queryType) {
134+
result.Query = {
135+
weight: typeWeights.query || DEFAULT_QUERY_WEIGHT,
136+
fields: {
137+
// This object gets populated with the query fields and associated weights.
138+
},
139+
};
140+
const queryFields: GraphQLFieldMap<any, any> = queryType.getFields();
141+
Object.keys(queryFields).forEach((field) => {
142+
const resolveType: GraphQLOutputType = queryFields[field].type;
143+
144+
queryFields[field].args.forEach((arg: GraphQLArgument) => {
145+
// check if any of our keywords 'first', 'last', 'limit' exist in the arglist
146+
if (KEYWORDS.includes(arg.name) && resolveType instanceof GraphQLList) {
147+
const defaultVal: number = <number>arg.defaultValue;
148+
// FIXME: How can we provide the complexity analysis algo with name of the argument to use?
149+
const listType = resolveType.ofType;
150+
if (isCompositeType(listType)) {
151+
result.Query.fields[field] = (multiplier: number = defaultVal) =>
152+
multiplier * result[listType.name].weight;
153+
}
154+
}
155+
});
156+
157+
// if the field is a scalars set weight accordingly
158+
// FIXME: Enums shouldn't be here???
159+
if (
160+
resolveType instanceof GraphQLScalarType ||
161+
resolveType instanceof GraphQLEnumType
162+
) {
163+
result.Query.fields[field] = typeWeights.scalar || DEFAULT_SCALAR_WEIGHT;
164+
}
165+
});
166+
}
167+
168+
// get the type of the field
169+
170+
return result;
34171
}
35172
export default buildTypeWeightsFromSchema;

test/analysis/buildTypeWeights.test.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface TestTypeWeightObject {
1616
[index: string]: TestType;
1717
}
1818

19-
xdescribe('Test buildTypeWeightsFromSchema function', () => {
19+
describe('Test buildTypeWeightsFromSchema function', () => {
2020
let schema: GraphQLSchema;
2121

2222
// this is dependant on the default type weight settings for the function
@@ -103,7 +103,6 @@ xdescribe('Test buildTypeWeightsFromSchema function', () => {
103103
weight: 1,
104104
fields: {
105105
name: 0,
106-
email: 0,
107106
},
108107
},
109108
Movie: {
@@ -212,12 +211,18 @@ xdescribe('Test buildTypeWeightsFromSchema function', () => {
212211
EMPIRE
213212
JEDI
214213
}`);
214+
215+
function reviews(multiplier: number): number {
216+
return 1;
217+
}
218+
219+
// TODO: Add tests for what the function does
215220
expect(buildTypeWeightsFromSchema(schema)).toEqual({
216221
Query: {
217222
weight: 1,
218223
fields: {
219224
// FIXME: check the best solution during implementation and update the tests here.
220-
reviews: (arg: number, type: Type) => arg * type.weight,
225+
reviews: expect.any(Function), // FIXME: Better way to test this?
221226
// code from PR review -> reviews: (type) => args[multiplierName] * typeWeightObject[type].weight
222227
},
223228
},
@@ -237,7 +242,7 @@ xdescribe('Test buildTypeWeightsFromSchema function', () => {
237242

238243
// TODO: need to figure out how to handle this situation. Skip for now.
239244
// The field friends returns a list of an unknown number of objects.
240-
xtest('fields returning lists of objects of indetermitae size', () => {
245+
xtest('fields returning lists of objects of indeterminate size', () => {
241246
schema = buildSchema(`
242247
type Human {
243248
id: ID!
@@ -299,14 +304,10 @@ xdescribe('Test buildTypeWeightsFromSchema function', () => {
299304
primaryFunction: 0,
300305
},
301306
},
302-
Episode: {
303-
weight: 0,
304-
fields: {},
305-
},
306307
});
307308
});
308309

309-
test('union tyes', () => {
310+
test('union types', () => {
310311
schema = buildSchema(`
311312
union SearchResult = Human | Droid
312313
type Human{
@@ -320,13 +321,13 @@ xdescribe('Test buildTypeWeightsFromSchema function', () => {
320321
weight: 1,
321322
fields: {},
322323
},
323-
human: {
324+
Human: {
324325
weight: 1,
325326
fields: {
326327
homePlanet: 0,
327328
},
328329
},
329-
droid: {
330+
Droid: {
330331
weight: 1,
331332
fields: {
332333
primaryFunction: 0,
@@ -389,31 +390,31 @@ xdescribe('Test buildTypeWeightsFromSchema function', () => {
389390
const typeWeightObject = buildTypeWeightsFromSchema(schema, {
390391
query: 2,
391392
});
392-
expectedOutput.query.weight = 2;
393+
expectedOutput.Query.weight = 2;
393394

394-
expect(typeWeightObject).toEqual({ expectedOutput });
395+
expect(typeWeightObject).toEqual(expectedOutput);
395396
});
396397

397398
test('object parameter', () => {
398399
const typeWeightObject = buildTypeWeightsFromSchema(schema, {
399400
object: 2,
400401
});
401402

402-
expectedOutput.user.weight = 2;
403-
expectedOutput.movie.weight = 2;
403+
expectedOutput.User.weight = 2;
404+
expectedOutput.Movie.weight = 2;
404405

405-
expect(typeWeightObject).toEqual({ expectedOutput });
406+
expect(typeWeightObject).toEqual(expectedOutput);
406407
});
407408

408409
test('scalar parameter', () => {
409410
const typeWeightObject = buildTypeWeightsFromSchema(schema, {
410411
scalar: 2,
411412
});
412413

413-
expectedOutput.user.fields.name = 2;
414-
expectedOutput.movie.fields.name = 2;
414+
expectedOutput.User.fields.name = 2;
415+
expectedOutput.Movie.fields.name = 2;
415416

416-
expect(typeWeightObject).toEqual({ expectedOutput });
417+
expect(typeWeightObject).toEqual(expectedOutput);
417418
});
418419

419420
// TODO: Tests should be written for the remaining configuration options

0 commit comments

Comments
 (0)