diff --git a/package.json b/package.json index 7d22897..5a60f1e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "jest --passWithNoTests", + "test": "jest --passWithNoTests --coverage", "lint": "eslint src test", "lint:fix": "eslint --fix src test @types", "prettier": "prettier --write .", diff --git a/src/@types/buildTypeWeights.d.ts b/src/@types/buildTypeWeights.d.ts index 4b9f063..4c65c8f 100644 --- a/src/@types/buildTypeWeights.d.ts +++ b/src/@types/buildTypeWeights.d.ts @@ -31,3 +31,12 @@ export interface TypeWeightSet { type Variables = { [index: string]: readonly unknown; }; + +// Type for use when getting fields for union types +type FieldMap = { + [index: string]: { + type: GraphQLOutputType; + weight?: FieldWeight; + resolveTo?: string; + }; +}; diff --git a/src/analysis/ASTParser.ts b/src/analysis/ASTParser.ts new file mode 100644 index 0000000..436e607 --- /dev/null +++ b/src/analysis/ASTParser.ts @@ -0,0 +1,243 @@ +import { + DocumentNode, + FieldNode, + SelectionSetNode, + DefinitionNode, + Kind, + SelectionNode, +} from 'graphql'; +import { FieldWeight, TypeWeightObject, Variables } from '../@types/buildTypeWeights'; +/** + * The AST node functions call each other following the nested structure below + * Each function handles a specific GraphQL AST node type + * + * AST nodes call each other in the following way + * + * Document Node + * | + * Definiton Node + * (operation and fragment definitons) + * / \ + * |-----> Selection Set Node not done + * | / + * | Selection Node + * | (Field, Inline fragment and fragment spread) + * | | \ \ + * |--Field Node not done not done + * + */ + +class ASTParser { + typeWeights: TypeWeightObject; + + variables: Variables; + + fragmentCache: { [index: string]: number }; + + constructor(typeWeights: TypeWeightObject, variables: Variables) { + this.typeWeights = typeWeights; + this.variables = variables; + this.fragmentCache = {}; + } + + private calculateCost( + node: FieldNode, + parentName: string, + typeName: string, + typeWeight: FieldWeight + ) { + let complexity = 0; + // field resolves to an object or a list with possible selections + let selectionsCost = 0; + let calculatedWeight = 0; + + // call the function to handle selection set node with selectionSet property if it is not undefined + if (node.selectionSet) { + selectionsCost += this.selectionSetNode(node.selectionSet, typeName); + } + // if there are arguments and this is a list, call the 'weightFunction' to get the weight of this field. otherwise the weight is static and can be accessed through the typeWeights object + if (node.arguments && typeof typeWeight === 'function') { + // FIXME: May never happen but what if weight is a function and arguments don't exist + calculatedWeight += typeWeight([...node.arguments], this.variables, selectionsCost); + } else { + calculatedWeight += this.typeWeights[typeName].weight + selectionsCost; + } + complexity += calculatedWeight; + + return complexity; + } + + fieldNode(node: FieldNode, parentName: string): number { + try { + let complexity = 0; + const parentType = this.typeWeights[parentName]; + if (!parentType) { + throw new Error( + `ERROR: ASTParser Failed to obtain parentType for parent: ${parentName} and node: ${node.name.value}` + ); + } + let typeName: string | undefined; + let typeWeight: FieldWeight | undefined; + + if (node.name.value in this.typeWeights) { + // node is an object type n the typeWeight root + typeName = node.name.value; + typeWeight = this.typeWeights[typeName].weight; + complexity += this.calculateCost(node, parentName, typeName, typeWeight); + } else if (parentType.fields[node.name.value].resolveTo) { + // field resolves to another type in type weights or a list + typeName = parentType.fields[node.name.value].resolveTo; + typeWeight = parentType.fields[node.name.value].weight; + // if this is a list typeWeight is a weight function + // otherwise the weight would be null as the weight is defined on the typeWeights root + if (typeName && typeWeight) { + // Type is a list and has a weight function + complexity += this.calculateCost(node, parentName, typeName, typeWeight); + } else if (typeName) { + // resolve type exists at root of typeWeight object and is not a list + typeWeight = this.typeWeights[typeName].weight; + complexity += this.calculateCost(node, parentName, typeName, typeWeight); + } else { + throw new Error( + `ERROR: ASTParser Failed to obtain resolved type name or weight for node: ${parentName}.${node.name.value}` + ); + } + } else { + // field is a scalar + typeName = node.name.value; + if (typeName) { + typeWeight = parentType.fields[typeName].weight; + if (typeof typeWeight === 'number') { + complexity += typeWeight; + } else { + throw new Error( + `ERROR: ASTParser Failed to obtain type weight for ${parentName}.${node.name.value}` + ); + } + } else { + throw new Error( + `ERROR: ASTParser Failed to obtain type name for ${parentName}.${node.name.value}` + ); + } + } + return complexity; + } catch (err) { + throw new Error( + `ERROR: ASTParser.fieldNode Uncaught error handling ${parentName}.${ + node.name.value + }\n + ${err instanceof Error && err.stack}` + ); + } + } + + selectionNode(node: SelectionNode, parentName: string): number { + let complexity = 0; + // check the kind property against the set of selection nodes that are possible + if (node.kind === Kind.FIELD) { + // call the function that handle field nodes + complexity += this.fieldNode(node, parentName.toLowerCase()); + } else if (node.kind === Kind.FRAGMENT_SPREAD) { + complexity += this.fragmentCache[node.name.value]; + // This is a leaf + // need to parse fragment definition at root and get the result here + } else if (node.kind === Kind.INLINE_FRAGMENT) { + const { typeCondition } = node; + + // named type is the type from which inner fields should be take + // If the TypeCondition is omitted, an inline fragment is considered to be of the same type as the enclosing context + const namedType = typeCondition ? typeCondition.name.value.toLowerCase() : parentName; + + // TODO: Handle directives like @include + complexity += this.selectionSetNode(node.selectionSet, namedType); + } else { + // FIXME: Consider removing this check. SelectionNodes cannot have any other kind in the current spec. + throw new Error(`ERROR: ASTParser.selectionNode: node type not supported`); + } + return complexity; + } + + selectionSetNode(node: SelectionSetNode, parentName: string): number { + let complexity = 0; + let maxFragmentComplexity = 0; + // iterate shrough the 'selections' array on the seletion set node + for (let i = 0; i < node.selections.length; i += 1) { + // call the function to handle seletion nodes + // pass the current parent through because selection sets act only as intermediaries + const selectionNode = node.selections[i]; + const selectionCost = this.selectionNode(node.selections[i], parentName); + + // we need to get the largest possible complexity so we save the largest inline fragment + // FIXME: Consider the case where 2 typed fragments are applicable + // e.g. ...UnionType and ...PartofTheUnion + // this case these complexities should be summed in order to be accurate + // However an estimation suffice + if (selectionNode.kind === Kind.INLINE_FRAGMENT) { + if (!selectionNode.typeCondition) { + // complexity is always applicable + complexity += selectionCost; + } else if (selectionCost > maxFragmentComplexity) + maxFragmentComplexity = selectionCost; + } else { + complexity += selectionCost; + } + } + return complexity + maxFragmentComplexity; + } + + definitionNode(node: DefinitionNode): number { + let complexity = 0; + // check the kind property against the set of definiton nodes that are possible + if (node.kind === Kind.OPERATION_DEFINITION) { + // check if the operation is in the type weights object. + if (node.operation.toLocaleLowerCase() in this.typeWeights) { + // if it is, it is an object type, add it's type weight to the total + complexity += this.typeWeights[node.operation].weight; + // console.log(`the weight of ${node.operation} is ${complexity}`); + // call the function to handle selection set node with selectionSet property if it is not undefined + if (node.selectionSet) { + complexity += this.selectionSetNode(node.selectionSet, node.operation); + } + } + } else if (node.kind === Kind.FRAGMENT_DEFINITION) { + // Fragments can only be defined on the root type. + // Parse the complexity of this fragment once and store it for use when analyzing other + // nodes. The complexity of a fragment can be added to the selection cost for the query. + const namedType = node.typeCondition.name.value; + // Duplicate fragment names are not allowed by the GraphQL spec and an error is thrown if used. + const fragmentName = node.name.value; + + if (this.fragmentCache[fragmentName]) return this.fragmentCache[fragmentName]; + + const fragmentComplexity = this.selectionSetNode( + node.selectionSet, + namedType.toLowerCase() + ); + + // Don't count fragment complexity in the node's complexity. Only when fragment is used. + this.fragmentCache[fragmentName] = fragmentComplexity; + } else { + // TODO: Verify that are no other type definition nodes that need to be handled (see ast.d.ts in 'graphql') + // Other types include TypeSystemDefinitionNode (Schema, Type, Directvie) and + // TypeSystemExtensionNode(Schema, Type); + throw new Error(`ERROR: ASTParser.definitionNode: ${node.kind} type not supported`); + } + return complexity; + } + + documentNode(node: DocumentNode): number { + let complexity = 0; + // sort the definitions array by kind so that fragments are always parsed first. + // Fragments must be parsed first so that their complexity is available to other nodes. + const sortedDefinitions = [...node.definitions].sort((a, b) => + a.kind.localeCompare(b.kind) + ); + for (let i = 0; i < sortedDefinitions.length; i += 1) { + // call the function to handle the various types of definition nodes + complexity += this.definitionNode(sortedDefinitions[i]); + } + return complexity; + } +} + +export default ASTParser; diff --git a/src/analysis/ASTnodefunctions.ts b/src/analysis/ASTnodefunctions.ts deleted file mode 100644 index 00655d9..0000000 --- a/src/analysis/ASTnodefunctions.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -import { - DocumentNode, - FieldNode, - SelectionSetNode, - DefinitionNode, - Kind, - SelectionNode, - isConstValueNode, -} from 'graphql'; -import { FieldWeight, TypeWeightObject, Variables } from '../@types/buildTypeWeights'; -/** - * The AST node functions call each other following the nested structure below - * Each function handles a specific GraphQL AST node type - * - * AST nodes call each other in the following way - * - * Document Node - * | - * Definiton Node - * (operation and fragment definitons) - * / \ - * |-----> Selection Set Node not done - * | / - * | Selection Node - * | (Field, Inline fragment and fragment spread) - * | | \ \ - * |--Field Node not done not done - * - */ - -export function fieldNode( - node: FieldNode, - typeWeights: TypeWeightObject, - variables: Variables, - parentName: string -): number { - let complexity = 0; - // 'resolvedTypeName' is the name of the Schema Type that this field resolves to - const resolvedTypeName = - node.name.value in typeWeights - ? node.name.value - : typeWeights[parentName].fields[node.name.value]?.resolveTo || null; - - if (resolvedTypeName) { - // field resolves to an object or a list with possible selections - let selectionsCost = 0; - let calculatedWeight = 0; - const weightFunction = typeWeights[parentName]?.fields[node.name.value]?.weight; - - // call the function to handle selection set node with selectionSet property if it is not undefined - if (node.selectionSet) { - selectionsCost += selectionSetNode( - node.selectionSet, - typeWeights, - variables, - resolvedTypeName - ); - } - // if there are arguments and this is a list, call the 'weightFunction' to get the weight of this field. otherwise the weight is static and can be accessed through the typeWeights object - if (node.arguments && typeof weightFunction === 'function') { - calculatedWeight += weightFunction([...node.arguments], variables, selectionsCost); - } else { - calculatedWeight += typeWeights[resolvedTypeName].weight + selectionsCost; - } - complexity += calculatedWeight; - } else { - // field is a scalar and 'weight' is a number - const { weight } = typeWeights[parentName].fields[node.name.value]; - if (typeof weight === 'number') { - complexity += weight; - } - } - return complexity; -} - -export function selectionNode( - node: SelectionNode, - typeWeights: TypeWeightObject, - variables: Variables, - parentName: string -): number { - let complexity = 0; - // check the kind property against the set of selection nodes that are possible - if (node.kind === Kind.FIELD) { - // call the function that handle field nodes - complexity += fieldNode(node, typeWeights, variables, parentName); - } - // TODO: add checks for Kind.FRAGMENT_SPREAD and Kind.INLINE_FRAGMENT here - return complexity; -} -export function selectionSetNode( - node: SelectionSetNode, - typeWeights: TypeWeightObject, - variables: Variables, - parentName: string -): number { - let complexity = 0; - // iterate shrough the 'selections' array on the seletion set node - for (let i = 0; i < node.selections.length; i += 1) { - // call the function to handle seletion nodes - // pass the current parent through because selection sets act only as intermediaries - complexity += selectionNode(node.selections[i], typeWeights, variables, parentName); - } - return complexity; -} - -export function definitionNode( - node: DefinitionNode, - typeWeights: TypeWeightObject, - variables: Variables -): number { - let complexity = 0; - // check the kind property against the set of definiton nodes that are possible - if (node.kind === Kind.OPERATION_DEFINITION) { - // check if the operation is in the type weights object. - if (node.operation.toLocaleLowerCase() in typeWeights) { - // if it is, it is an object type, add it's type weight to the total - complexity += typeWeights[node.operation].weight; - // console.log(`the weight of ${node.operation} is ${complexity}`); - // call the function to handle selection set node with selectionSet property if it is not undefined - if (node.selectionSet) { - complexity += selectionSetNode( - node.selectionSet, - typeWeights, - variables, - node.operation - ); - } - } - } - // TODO: add checks for Kind.FRAGMENT_DEFINITION here (there are other type definition nodes that i think we can ignore. see ast.d.ts in 'graphql') - return complexity; -} - -export function documentNode( - node: DocumentNode, - typeWeights: TypeWeightObject, - variables: Variables -): number { - let complexity = 0; - // iterate through 'definitions' array on the document node - for (let i = 0; i < node.definitions.length; i += 1) { - // call the function to handle the various types of definition nodes - complexity += definitionNode(node.definitions[i], typeWeights, variables); - } - return complexity; -} diff --git a/src/analysis/buildTypeWeights.ts b/src/analysis/buildTypeWeights.ts index c5dcea9..3b16db3 100644 --- a/src/analysis/buildTypeWeights.ts +++ b/src/analysis/buildTypeWeights.ts @@ -3,9 +3,7 @@ import { GraphQLArgument, GraphQLNamedType, GraphQLObjectType, - GraphQLScalarType, GraphQLInterfaceType, - GraphQLList, GraphQLOutputType, isCompositeType, isEnumType, @@ -17,8 +15,9 @@ import { isUnionType, Kind, ValueNode, + GraphQLUnionType, + GraphQLFieldMap, } from 'graphql'; -import { Maybe } from 'graphql/jsutils/Maybe'; import { ObjMap } from 'graphql/jsutils/ObjMap'; import { GraphQLSchema } from 'graphql/type/schema'; import { @@ -27,6 +26,8 @@ import { TypeWeightObject, Variables, Type, + Fields, + FieldMap, } from '../@types/buildTypeWeights'; export const KEYWORDS = ['first', 'last', 'limit']; @@ -86,12 +87,13 @@ function parseObjectFields( ) { result.fields[field] = { weight: typeWeights.scalar, + // resolveTo: fields[field].name.toLowerCase(), }; } else if ( isInterfaceType(fieldType) || - isUnionType(fieldType) || isEnumType(fieldType) || - isObjectType(fieldType) + isObjectType(fieldType) || + isUnionType(fieldType) ) { result.fields[field] = { resolveTo: fieldType.name.toLocaleLowerCase(), @@ -157,14 +159,166 @@ function parseObjectFields( } }); } + } else if (isNonNullType(fieldType)) { + // TODO: Implment non-null types + // not throwing and error since it causes typeWeight tests to break } else { // ? what else can get through here + throw new Error(`ERROR: buildTypeWeight: Unsupported field type: ${fieldType}`); } }); return result; } +/** + * Recursively compares two types for type equality based on type name + * @param a + * @param b + * @returns true if the types are recursively equal. + */ +function compareTypes(a: GraphQLOutputType, b: GraphQLOutputType): boolean { + // Base Case: Object or Scalar => compare type names + // Recursive Case(List / NonNull): compare ofType + return ( + (isObjectType(b) && isObjectType(a) && a.name === b.name) || + (isUnionType(b) && isUnionType(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)) || + (isNonNullType(b) && isNonNullType(a) && compareTypes(a.ofType, b.ofType)) + ); +} + +/** + * + * @param unionType union type to be parsed + * @param typeWeightObject type weight mapping object that must already contain all of the types in the schema. + * @returns object mapping field names for each union type to their respective weights, resolve type names and resolve type object + */ +function getFieldsForUnionType( + unionType: GraphQLUnionType, + typeWeightObject: TypeWeightObject +): FieldMap[] { + return unionType.getTypes().map((objectType: GraphQLObjectType) => { + // Get the field data for this type + const fields: GraphQLFieldMap = objectType.getFields(); + + const fieldMap: FieldMap = {}; + Object.keys(fields).forEach((field: string) => { + // Get the weight of this field on from parent type on the root typeWeight object. + // this only exists for scalars and lists (which resolve to a function); + const { weight, resolveTo } = + typeWeightObject[objectType.name.toLowerCase()].fields[field]; + + fieldMap[field] = { + type: fields[field].type, + weight, // will only be undefined for object types + resolveTo, + }; + }); + return fieldMap; + }); +} + +/** + * + * @param typesInUnion + * @returns a single field map containg information for fields common to the union + */ +function getSharedFieldsFromUnionTypes(typesInUnion: FieldMap[]): FieldMap { + return typesInUnion.reduce((prev: FieldMap, fieldMap: FieldMap): FieldMap => { + // iterate through the field map checking the types for any common field names + const sharedFields: FieldMap = {}; + Object.keys(prev).forEach((field: string) => { + if (fieldMap[field]) { + if (compareTypes(prev[field].type, fieldMap[field].type)) { + // they match add the type to the next set + sharedFields[field] = prev[field]; + } + } + }); + return sharedFields; + }); +} + +/** + * Parses the provided union types and returns a type weight object with any fields common to all types + * in a union added to the union type + * @param unionTypes union types to be parsed. + * @param typeWeights object specifying generic type weights. + * @param typeWeightObject original type weight object + * @returns + */ +function parseUnionTypes( + unionTypes: GraphQLUnionType[], + typeWeights: TypeWeightSet, + typeWeightObject: TypeWeightObject +) { + const typeWeightsWithUnions: TypeWeightObject = { ...typeWeightObject }; + + unionTypes.forEach((unionType: GraphQLUnionType) => { + /** + * 1. For each provided union type. We first obtain the fields for each object that + * is part of the union and store these in an object + * When obtaining types, save: + * - field name + * - type object to which the field resolves. This holds any information for recursive types (lists / not null / unions) + * - weight - for easy lookup later + * - resolveTo type - for easy lookup later + * 2. We then reduce the array of objects from step 1 a single object only containing fields + * common to each type in the union. To determine field "equality" we compare the field names and + * recursively compare the field types: + * */ + + // types is an array mapping each field name to it's respective output type + // const typesInUnion = getFieldsForUnionType(unionType, typeWeightObject); + const typesInUnion: FieldMap[] = getFieldsForUnionType(unionType, typeWeightObject); + + // reduce the data for all the types in the union + const commonFields: FieldMap = getSharedFieldsFromUnionTypes(typesInUnion); + + // transform commonFields into the correct format for the type weight object + const fieldTypes: Fields = {}; + + Object.keys(commonFields).forEach((field: string) => { + /** + * The type weight object requires that: + * a. scalars have a weight + * b. lists have a resolveTo and weight property + * c. objects have a resolveTo type. + * */ + + const current = commonFields[field].type; + if (isScalarType(current)) { + fieldTypes[field] = { + weight: commonFields[field].weight, + }; + } else if (isObjectType(current) || isInterfaceType(current) || isUnionType(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'); + } + }); + typeWeightsWithUnions[unionType.name.toLowerCase()] = { + fields: fieldTypes, + weight: typeWeights.object, + }; + }); + + return typeWeightsWithUnions; +} /** * Parses all types in the provided schema object excempt for Query, Mutation * and built in types that begin with '__' and outputs a new TypeWeightObject @@ -177,6 +331,8 @@ function parseTypes(schema: GraphQLSchema, typeWeights: TypeWeightSet): TypeWeig const result: TypeWeightObject = {}; + const unions: GraphQLUnionType[] = []; + // Handle Object, Interface, Enum and Union types Object.keys(typeMap).forEach((type) => { const typeName: string = type.toLowerCase(); @@ -193,19 +349,14 @@ function parseTypes(schema: GraphQLSchema, typeWeights: TypeWeightSet): TypeWeig weight: typeWeights.scalar, }; } else if (isUnionType(currentType)) { - // FIXME: will need information on fields inorder calculate comlpextiy - result[typeName] = { - fields: {}, - weight: typeWeights.object, - }; - } else { - // ? what else can get through here - // ? inputTypes? + unions.push(currentType); + } else if (!isScalarType(currentType)) { + throw new Error(`ERROR: buildTypeWeight: Unsupported type: ${currentType}`); } } }); - return result; + return parseUnionTypes(unions, typeWeights, result); } /** diff --git a/src/analysis/typeComplexityAnalysis.ts b/src/analysis/typeComplexityAnalysis.ts index 6db00d7..9855cef 100644 --- a/src/analysis/typeComplexityAnalysis.ts +++ b/src/analysis/typeComplexityAnalysis.ts @@ -1,6 +1,6 @@ import { DocumentNode } from 'graphql'; import { TypeWeightObject, Variables } from '../@types/buildTypeWeights'; -import { documentNode } from './ASTnodefunctions'; +import ASTParser from './ASTParser'; /** * Calculate the complexity for the query by recursivly traversing through the query AST, @@ -18,7 +18,8 @@ function getQueryTypeComplexity( typeWeights: TypeWeightObject ): number { let complexity = 0; - complexity += documentNode(queryAST, typeWeights, variables); + const parser = new ASTParser(typeWeights, variables); + complexity += parser.documentNode(queryAST); return complexity; } diff --git a/test/analysis/buildTypeWeights.test.ts b/test/analysis/buildTypeWeights.test.ts index 0f31137..8c306fd 100644 --- a/test/analysis/buildTypeWeights.test.ts +++ b/test/analysis/buildTypeWeights.test.ts @@ -460,32 +460,60 @@ describe('Test buildTypeWeightsFromSchema function', () => { }); }); - test('union types', () => { - schema = buildSchema(` - union SearchResult = Human | Droid - type Human{ - homePlanet: String - } - type Droid { - primaryFunction: String - }`); - expect(buildTypeWeightsFromSchema(schema)).toEqual({ - searchresult: { - weight: 1, - fields: {}, - }, - human: { - weight: 1, - fields: { - homePlanet: { weight: 0 }, + describe('union types', () => { + test('union types', () => { + schema = buildSchema(` + union SearchResult = Human | Droid + type Human{ + name: String + homePlanet: String + search(first: Int!): [SearchResult] + } + type Droid { + name: String + primaryFunction: String + search(first: Int!): [SearchResult] + }`); + expect(buildTypeWeightsFromSchema(schema)).toEqual({ + searchresult: { + weight: 1, + fields: { + name: { weight: 0 }, + search: { + resolveTo: 'searchresult', + weight: expect.any(Function), + }, + }, }, - }, - droid: { - weight: 1, - fields: { - primaryFunction: { weight: 0 }, + human: { + weight: 1, + fields: { + name: { weight: 0 }, + homePlanet: { weight: 0 }, + search: { + resolveTo: 'searchresult', + weight: expect.any(Function), + }, + }, }, - }, + droid: { + weight: 1, + fields: { + name: { weight: 0 }, + primaryFunction: { weight: 0 }, + search: { + resolveTo: 'searchresult', + weight: expect.any(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) }); }); diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index da1d512..52e0f9c 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -301,7 +301,7 @@ describe('Test getQueryTypeComplexity function', () => { expect(getQueryTypeComplexity(parse(query), variables, typeWeights)).toBe(2); // Query 1 + hero/character 1 }); - xdescribe('with fragments', () => { + describe('with fragments', () => { test('that have a complexity of zero', () => { query = ` query { @@ -414,10 +414,11 @@ describe('Test getQueryTypeComplexity function', () => { }); }); - xdescribe('with inline fragments', () => { + describe('with inline fragments', () => { describe('on union types', () => { let unionTypeWeights: TypeWeightObject; - beforeAll(() => { + let mockHumanCharacterFriendsFunction: jest.Mock; + beforeEach(() => { // type Query { // hero(episode: Episode): Character // } @@ -435,6 +436,7 @@ describe('Test getQueryTypeComplexity function', () => { // primaryFunction: String // friends(first: Int): [Character] // } + mockHumanCharacterFriendsFunction = jest.fn(); unionTypeWeights = { query: { weight: 1, @@ -446,7 +448,15 @@ describe('Test getQueryTypeComplexity function', () => { }, character: { weight: 1, - fields: {}, + fields: { + name: { + weight: 0, + }, + friends: { + resolveTo: 'character', + weight: mockCharacterFriendsFunction, + }, + }, }, human: { weight: 1, @@ -455,7 +465,7 @@ describe('Test getQueryTypeComplexity function', () => { homePlanet: { weight: 0 }, friends: { resolveTo: 'character', - weight: mockCharacterFriendsFunction, + weight: mockHumanCharacterFriendsFunction, }, humanFriends: { resolveTo: 'human', @@ -515,7 +525,7 @@ describe('Test getQueryTypeComplexity function', () => { } }`; // Query 1 + 1 hero + max(Droid 2, Human 3) = 5 - mockCharacterFriendsFunction.mockReturnValueOnce(3); + mockHumanCharacterFriendsFunction.mockReturnValueOnce(3); mockDroidFriendsFunction.mockReturnValueOnce(1); expect(getQueryTypeComplexity(parse(query), variables, unionTypeWeights)).toBe( 5