diff --git a/src/analysis/buildTypeWeights.ts b/src/analysis/buildTypeWeights.ts index 3b16db3..308544e 100644 --- a/src/analysis/buildTypeWeights.ts +++ b/src/analysis/buildTypeWeights.ts @@ -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(), @@ -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] = { @@ -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] = { @@ -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)) || @@ -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'); } diff --git a/test/analysis/buildTypeWeights.test.ts b/test/analysis/buildTypeWeights.test.ts index 8c306fd..de92a54 100644 --- a/test/analysis/buildTypeWeights.test.ts +++ b/test/analysis/buildTypeWeights.test.ts @@ -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{ @@ -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{ @@ -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 @@ -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, }); diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index 52e0f9c..e18af6c 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -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 { diff --git a/test/analysis/weightFunction.test.ts b/test/analysis/weightFunction.test.ts index 16eeea1..6329c79 100644 --- a/test/analysis/weightFunction.test.ts +++ b/test/analysis/weightFunction.test.ts @@ -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); @@ -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); });