Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 14 additions & 13 deletions src/analysis/buildTypeWeights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,9 @@ function parseObjectFields(
// Iterate through the fields and add the required data to the result
Object.keys(fields).forEach((field: string) => {
// The GraphQL type that this field represents
const fieldType: GraphQLOutputType = fields[field].type;
if (
isScalarType(fieldType) ||
(isNonNullType(fieldType) && isScalarType(fieldType.ofType))
) {
let fieldType: GraphQLOutputType = fields[field].type;
if (isNonNullType(fieldType)) fieldType = fieldType.ofType;
if (isScalarType(fieldType)) {
result.fields[field] = {
weight: typeWeights.scalar,
// resolveTo: fields[field].name.toLowerCase(),
Expand All @@ -100,7 +98,8 @@ function parseObjectFields(
};
} else if (isListType(fieldType)) {
// 'listType' is the GraphQL type that the list resolves to
const listType = fieldType.ofType;
let listType = fieldType.ofType;
if (isNonNullType(listType)) listType = listType.ofType;
if (isScalarType(listType) && typeWeights.scalar === 0) {
// list won't compound if weight is zero
result.fields[field] = {
Expand All @@ -115,7 +114,6 @@ function parseObjectFields(
fields[field].args.forEach((arg: GraphQLArgument) => {
// If field has an argument matching one of the limiting keywords and resolves to a list
// then the weight of the field should be dependent on both the weight of the resolved type and the limiting argument.
// FIXME: Can nonnull wrap list types?
if (KEYWORDS.includes(arg.name)) {
// Get the type that comprises the list
result.fields[field] = {
Expand Down Expand Up @@ -183,6 +181,7 @@ function compareTypes(a: GraphQLOutputType, b: GraphQLOutputType): boolean {
return (
(isObjectType(b) && isObjectType(a) && a.name === b.name) ||
(isUnionType(b) && isUnionType(a) && a.name === b.name) ||
(isEnumType(b) && isEnumType(a) && a.name === b.name) ||
(isInterfaceType(b) && isInterfaceType(a) && a.name === b.name) ||
(isScalarType(b) && isScalarType(a) && a.name === b.name) ||
(isListType(b) && isListType(a) && compareTypes(b.ofType, a.ofType)) ||
Expand Down Expand Up @@ -289,24 +288,26 @@ function parseUnionTypes(
* c. objects have a resolveTo type.
* */

const current = commonFields[field].type;
let current = commonFields[field].type;
if (isNonNullType(current)) current = current.ofType;
if (isScalarType(current)) {
fieldTypes[field] = {
weight: commonFields[field].weight,
};
} else if (isObjectType(current) || isInterfaceType(current) || isUnionType(current)) {
} else if (
isObjectType(current) ||
isInterfaceType(current) ||
isUnionType(current) ||
isEnumType(current)
) {
fieldTypes[field] = {
resolveTo: commonFields[field].resolveTo,
weight: typeWeights.object,
};
} else if (isListType(current)) {
fieldTypes[field] = {
resolveTo: commonFields[field].resolveTo,
weight: commonFields[field].weight,
};
} else if (isNonNullType(current)) {
throw new Error('non null types not supported on unions');
// TODO: also a recursive data structure
} else {
throw new Error('Unhandled union type. Should never get here');
}
Expand Down
192 changes: 184 additions & 8 deletions test/analysis/buildTypeWeights.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,8 @@ describe('Test buildTypeWeightsFromSchema function', () => {
});
});

describe('union types', () => {
test('union types', () => {
describe('union types with ...', () => {
test('lists of union types and scalars', () => {
schema = buildSchema(`
union SearchResult = Human | Droid
type Human{
Expand Down Expand Up @@ -510,14 +510,136 @@ describe('Test buildTypeWeightsFromSchema function', () => {
});
});

xtest('additional test cases for ...', () => {
// TODO: unions with non-null types
// unions with lists of non-null types
// lists with > 2 levels of nesting (may need to add these for lists on other types as well)
test('object types', () => {
schema = buildSchema(`
union SearchResult = Human | Droid
type Human{
name: String
homePlanet: String
info: Info
search(first: Int!): [SearchResult]
}
type Droid {
name: String
primaryFunction: String
info: Info
search(first: Int!): [SearchResult]
}
type Info {
height: Int
}
`);
expect(buildTypeWeightsFromSchema(schema)).toEqual({
searchresult: {
weight: 1,
fields: {
name: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
info: { resolveTo: 'info' },
},
},
human: {
weight: 1,
fields: {
name: { weight: 0 },
homePlanet: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
info: { resolveTo: 'info' },
},
},
droid: {
weight: 1,
fields: {
name: { weight: 0 },
primaryFunction: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
info: { resolveTo: 'info' },
},
},
info: {
weight: 1,
fields: {
height: { weight: 0 },
},
},
});
});

test('enum types', () => {
schema = buildSchema(`
union SearchResult = Human | Droid
type Human{
name: String
homePlanet: String
episode: Episode
search(first: Int!): [SearchResult]
}
type Droid {
name: String
primaryFunction: String
episode: Episode
search(first: Int!): [SearchResult]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
`);
expect(buildTypeWeightsFromSchema(schema)).toEqual({
searchresult: {
weight: 1,
fields: {
episode: { resolveTo: 'episode' },
name: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
},
},
human: {
weight: 1,
fields: {
name: { weight: 0 },
homePlanet: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
episode: { resolveTo: 'episode' },
},
},
droid: {
weight: 1,
fields: {
name: { weight: 0 },
primaryFunction: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
episode: { resolveTo: 'episode' },
},
},
episode: {
weight: 0,
fields: {},
},
});
});
});

xdescribe('Not null operator (!) is used', () => {
describe('Not null operator (!) is used', () => {
test('on a scalar, enum or object type', () => {
schema = buildSchema(`
type Human{
Expand Down Expand Up @@ -612,6 +734,60 @@ describe('Test buildTypeWeightsFromSchema function', () => {
},
});
});

test('on union types', () => {
schema = buildSchema(`
union SearchResult = Human | Droid
type Human{
age: Int!
name: String
homePlanet: String
search(first: Int!): [SearchResult!]!
}
type Droid {
age: Int!
name: String
primaryFunction: String!
search(first: Int!): [SearchResult!]!
}`);
expect(buildTypeWeightsFromSchema(schema)).toEqual({
searchresult: {
weight: 1,
fields: {
name: { weight: 0 },
age: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
},
},
human: {
weight: 1,
fields: {
name: { weight: 0 },
age: { weight: 0 },
homePlanet: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
},
},
droid: {
weight: 1,
fields: {
name: { weight: 0 },
age: { weight: 0 },
primaryFunction: { weight: 0 },
search: {
resolveTo: 'searchresult',
weight: expect.any(Function),
},
},
},
});
});
});

// TODO: Tests should be written to account for the additional scenarios possible in a schema
Expand Down Expand Up @@ -669,7 +845,7 @@ describe('Test buildTypeWeightsFromSchema function', () => {
});

// this is only if we choose to have 'query' as its own property (seperate from object types) in the user configuration options
xtest('query parameter', () => {
test('query parameter', () => {
const typeWeightObject = buildTypeWeightsFromSchema(schema, {
query: 2,
});
Expand Down
34 changes: 34 additions & 0 deletions test/analysis/typeComplexityAnalysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,40 @@ describe('Test getQueryTypeComplexity function', () => {
expect(getQueryTypeComplexity(parse(query), {}, unionTypeWeights)).toBe(5);
});

test('that have greater than 2 levels of nesting', () => {
query = `
query {
hero(episode: EMPIRE) {
name
... on Droid {
primaryFunction
friends(first: 5) {
name
friends(first: 3) {
name
}
}
}
... on Human {
homePlanet
friends(first: 5) {
name
friends(first: 3) {
name
}
}
}
}
}`;
mockCharacterFriendsFunction.mockReturnValue(3);
mockDroidFriendsFunction.mockReturnValueOnce(20);
mockHumanFriendsFunction.mockReturnValueOnce(20);
// Query 1 + 1 hero + 3 friends/character
expect(getQueryTypeComplexity(parse(query), variables, unionTypeWeights)).toBe(
22
);
});

xtest('that include a directive', () => {
query = `
query {
Expand Down
4 changes: 2 additions & 2 deletions test/analysis/weightFunction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('Weight Function correctly parses Argument Nodes if', () => {
});
});

xtest('the list is defined with non-null operators (!)', () => {
test('the list is defined with non-null operators (!)', () => {
const villainsQuery = `query { villains(episode: NEWHOPE, limit: 3) { stars, episode } }`;
const willainsQueryAST: DocumentNode = parse(villainsQuery);
expect(getQueryTypeComplexity(willainsQueryAST, {}, typeWeights)).toBe(4);
Expand All @@ -93,7 +93,7 @@ describe('Weight Function correctly parses Argument Nodes if', () => {
const charQueryAST: DocumentNode = parse(charQuery);
expect(getQueryTypeComplexity(charQueryAST, {}, typeWeights)).toBe(4);

const droidsQuery = `droidsQuery { droids(episode: NEWHOPE, limit: 3) { stars, episode } }`;
const droidsQuery = `query droidsQuery { droids(episode: NEWHOPE, limit: 3) { stars, episode } }`;
const droidsQueryAST: DocumentNode = parse(droidsQuery);
expect(getQueryTypeComplexity(droidsQueryAST, {}, typeWeights)).toBe(4);
});
Expand Down