diff --git a/src/analysis/QueryParser.ts b/src/analysis/QueryParser.ts index d2b7010..3ecf5fc 100644 --- a/src/analysis/QueryParser.ts +++ b/src/analysis/QueryParser.ts @@ -22,10 +22,10 @@ import { FieldWeight, TypeWeightObject, Variables } from '../@types/buildTypeWei * |-----> Selection Set Node <-------| * | / * | Selection Node - * | (Field, Inline fragment and fragment spread) - * | | | \ - * | Field Node | fragmentCache - * | | | + * | (Field, Inline fragment, directives and fragment spread) + * | | | \ \ + * | Field Node | \ \ + * | | | directiveExcludeField fragmentCache * |<--calculateCast | * | | * |<------------------| @@ -148,79 +148,85 @@ class QueryParser { /** * Return true if: - * 1. there is no directive - * 2. there is a directive named inlcude and the value is true - * 3. there is a directive named skip and the value is false + * 2. there is a directive named inlcude and the value is false + * 3. there is a directive named skip and the value is true */ - // THIS IS NOT CALLED ANYWEHERE. IN PROGRESS - private directiveCheck(directive: DirectiveNode): boolean { - if (directive?.arguments) { - // get the first argument - const argument = directive.arguments[0]; - // ensure the argument name is 'if' - const argumentHasVariables = - argument.value.kind === Kind.VARIABLE && argument.name.value === 'if'; - // access the value of the argument depending on whether it is passed as a variable or not - let directiveArgumentValue; - if (argument.value.kind === Kind.BOOLEAN) { - directiveArgumentValue = Boolean(argument.value.value); - } else if (argumentHasVariables) { - directiveArgumentValue = Boolean(this.variables[argument.value.name.value]); + private directiveExcludeField(directives: DirectiveNode[]): boolean { + let skipField = false; + + directives.forEach((directive) => { + if ( + directive?.arguments && + (directive.name.value === 'include' || directive.name.value === 'skip') && + directive.arguments[0].name.value === 'if' + ) { + // only consider the first argument + const argument = directive.arguments[0]; + + const argumentHasVariables = argument.value.kind === Kind.VARIABLE; + let directiveArgumentValue; + if (argument.value.kind === Kind.BOOLEAN) { + directiveArgumentValue = Boolean(argument.value.value); + } else if (argumentHasVariables) { + directiveArgumentValue = Boolean(this.variables[argument.value.name.value]); + } + + if ( + (directive.name.value === 'include' && directiveArgumentValue === false) || + (directive.name.value === 'skip' && directiveArgumentValue === true) + ) { + skipField = true; + } } + }); - return ( - (directive.name.value === 'include' && directiveArgumentValue === true) || - (directive.name.value === 'skip' && directiveArgumentValue === false) - ); - } - return true; + return skipField; } private selectionNode(node: SelectionNode, parentName: string): number { let complexity = 0; - // TODO: complete implementation of directives include and skip /** * process this node only if: - * 1. there is no directive + * 1. there is no include or skip directive * 2. there is a directive named inlcude and the value is true * 3. there is a directive named skip and the value is false */ - // const directive = node.directives; - // if (directive && this.directiveCheck(directive[0])) { - this.depth += 1; - if (this.depth > this.maxDepth) this.maxDepth = this.depth; - // the kind of a field node will either be field, fragment spread or inline fragment - if (node.kind === Kind.FIELD) { - complexity += this.fieldNode(node, parentName.toLowerCase()); - } else if (node.kind === Kind.FRAGMENT_SPREAD) { - // add complexity and depth from fragment cache - const { complexity: fragComplexity, depth: fragDepth } = - this.fragmentCache[node.name.value]; - complexity += fragComplexity; - this.depth += fragDepth; + if (node.directives && !this.directiveExcludeField([...node.directives])) { + this.depth += 1; if (this.depth > this.maxDepth) this.maxDepth = this.depth; - this.depth -= fragDepth; + // the kind of a field node will either be field, fragment spread or inline fragment + if (node.kind === Kind.FIELD) { + complexity += this.fieldNode(node, parentName.toLowerCase()); + } else if (node.kind === Kind.FRAGMENT_SPREAD) { + // add complexity and depth from fragment cache + const { complexity: fragComplexity, depth: fragDepth } = + this.fragmentCache[node.name.value]; + complexity += fragComplexity; + this.depth += fragDepth; + if (this.depth > this.maxDepth) this.maxDepth = this.depth; + this.depth -= fragDepth; + + // 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; - // 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; - // 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; + // subtract 1 before, and add one after, entering the fragment selection to negate the additional level of depth added + this.depth -= 1; + complexity += this.selectionSetNode(node.selectionSet, namedType); + this.depth += 1; + } else { + throw new Error(`ERROR: QueryParser.selectionNode: node type not supported`); + } - // TODO: Handle directives like @include and @skip - // subtract 1 before, and add one after, entering the fragment selection to negate the additional level of depth added this.depth -= 1; - complexity += this.selectionSetNode(node.selectionSet, namedType); - this.depth += 1; - } else { - throw new Error(`ERROR: QueryParser.selectionNode: node type not supported`); } - - this.depth -= 1; - //* } return complexity; } diff --git a/test/analysis/typeComplexityAnalysis.test.ts b/test/analysis/typeComplexityAnalysis.test.ts index 4eff8ac..6ae4ae7 100644 --- a/test/analysis/typeComplexityAnalysis.test.ts +++ b/test/analysis/typeComplexityAnalysis.test.ts @@ -855,8 +855,8 @@ describe('Test getQueryTypeComplexity function', () => { }); // TODO: refine complexity analysis to consider directives includes and skip - xdescribe('with directives @includes and @skip', () => { - test('@includes on interfaces', () => { + describe('with directives @includes and @skip', () => { + test('@include on interfaces', () => { query = ` query { hero(episode: EMPIRE) { @@ -871,10 +871,10 @@ describe('Test getQueryTypeComplexity function', () => { } } }`; - expect(mockCharacterFriendsFunction).toHaveBeenCalledTimes(0); + mockCharacterFriendsFunction.mockReturnValueOnce(3); // Query 1 + 1 hero + max(...Character 3, ...Human 0) = 5 expect(queryParser.processQuery(parse(query))).toBe(5); - + mockCharacterFriendsFunction.mockReset(); query = ` query { hero(episode: EMPIRE) { @@ -889,7 +889,7 @@ describe('Test getQueryTypeComplexity function', () => { } } }`; - mockCharacterFriendsFunction.mockReturnValueOnce(3); + expect(mockCharacterFriendsFunction).toHaveBeenCalledTimes(0); // Query 1 + 1 hero = 2 expect(queryParser.processQuery(parse(query))).toBe(2); }); @@ -912,7 +912,7 @@ describe('Test getQueryTypeComplexity function', () => { expect(mockCharacterFriendsFunction).toHaveBeenCalledTimes(0); // Query 1 + 1 hero = 2 expect(queryParser.processQuery(parse(query))).toBe(2); - + mockCharacterFriendsFunction.mockReset(); query = ` query { hero(episode: EMPIRE) { @@ -932,10 +932,14 @@ describe('Test getQueryTypeComplexity function', () => { expect(queryParser.processQuery(parse(query))).toBe(5); }); - test('@includes on object types', () => { + test('@include on object types', () => { query = `query { hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } human(id: 1) @include(if: true) { id, @@ -943,12 +947,17 @@ describe('Test getQueryTypeComplexity function', () => { homePlanet } }`; - // 1 query + 1 hero + 1 human - expect(queryParser.processQuery(parse(query))).toBe(3); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + 1 human + expect(queryParser.processQuery(parse(query))).toBe(6); query = `query { hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } human(id: 1) @include(if: false) { id, @@ -956,14 +965,19 @@ describe('Test getQueryTypeComplexity function', () => { homePlanet } }`; - // 1 query + 1 hero - expect(queryParser.processQuery(parse(query))).toBe(2); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + expect(queryParser.processQuery(parse(query))).toBe(5); }); test('@skip on object types', () => { query = `query { hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } human(id: 1) @skip(if: true) { id, @@ -971,12 +985,17 @@ describe('Test getQueryTypeComplexity function', () => { homePlanet } }`; - // 1 query + 1 hero - expect(queryParser.processQuery(parse(query))).toBe(2); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + expect(queryParser.processQuery(parse(query))).toBe(5); query = `query { hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } human(id: 1) @skip(if: false) { id, @@ -984,15 +1003,21 @@ describe('Test getQueryTypeComplexity function', () => { homePlanet } }`; - // 1 query + 1 hero + 1 human - expect(queryParser.processQuery(parse(query))).toBe(3); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + 1 human + expect(queryParser.processQuery(parse(query))).toBe(6); }); - test('with arguments and varibales', () => { + + test('@skip with arguments and varibales', () => { variables = { directive: false }; queryParser = new QueryParser(typeWeights, variables); query = `query ($directive: Boolean!){ hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } human(id: 1) @skip(if: $directive) { id, @@ -1000,28 +1025,79 @@ describe('Test getQueryTypeComplexity function', () => { homePlanet } }`; - // 1 query + 1 hero + 1 human - expect(queryParser.processQuery(parse(query))).toBe(3); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + 1 human + expect(queryParser.processQuery(parse(query))).toBe(6); variables = { directive: true }; queryParser = new QueryParser(typeWeights, variables); query = `query ($directive: Boolean!){ hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } - human(id: 1) @includes(if: $directive) { + human(id: 1) @skip(if: $directive) { id, name, homePlanet } }`; - // 1 query + 1 hero - expect(queryParser.processQuery(parse(query))).toBe(2); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + expect(queryParser.processQuery(parse(query))).toBe(5); + }); + + test('@include with arguments and varibales', () => { + variables = { directive: false }; + queryParser = new QueryParser(typeWeights, variables); + query = `query ($directive: Boolean!){ + hero(episode: EMPIRE) { + id, + name + friends(first: 3) { + name + } + } + human(id: 1) @include(if: $directive) { + id, + name, + homePlanet + } + }`; + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + expect(queryParser.processQuery(parse(query))).toBe(5); + variables = { directive: true }; + queryParser = new QueryParser(typeWeights, variables); + query = `query ($directive: Boolean!){ + hero(episode: EMPIRE) { + id, + name + friends(first: 3) { + name + } + } + human(id: 1) @include(if: $directive) { + id, + name, + homePlanet + } + }`; + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + 1 human + expect(queryParser.processQuery(parse(query))).toBe(6); }); - xtest('and other directive are ignored', () => { + test('and other directives or arguments are ignored', () => { query = `query { hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } human(id: 1) @ignore(if: true) { id, @@ -1029,20 +1105,100 @@ describe('Test getQueryTypeComplexity function', () => { homePlanet } }`; - // 1 query + 1 hero + 1 human - expect(queryParser.processQuery(parse(query))).toBe(3); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3friends + 1 human + expect(queryParser.processQuery(parse(query))).toBe(6); query = `query { hero(episode: EMPIRE) { - id, name + id, + name + friends(first: 3) { + name + } } - human(id: 1) @includes(when: false) { + human(id: 1) @include(when: false) { id, name, homePlanet } }`; - // 1 query + 1 hero - expect(queryParser.processQuery(parse(query))).toBe(3); + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 3 friends + 1 hero 1 human + expect(queryParser.processQuery(parse(query))).toBe(6); + }); + + test('@include with other directives', () => { + query = `query { + hero(episode: EMPIRE) { + id, + name + friends(first: 3) { + name + } + } + human(id: 1) @ignore(if: true) @include(if: false) { + id, + name, + homePlanet + } + }`; + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + expect(queryParser.processQuery(parse(query))).toBe(5); + query = `query { + hero(episode: EMPIRE) { + id, + name + friends(first: 3) { + name + } + } + human(id: 1) @ignore(if: true) @include(if: true) { + id, + name, + homePlanet + } + }`; + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + 1 human + expect(queryParser.processQuery(parse(query))).toBe(6); + }); + + test('@skip with other directives', () => { + query = `query { + hero(episode: EMPIRE) { + id, + name + friends(first: 3) { + name + } + } + human(id: 1) @ignore(if: true) @skip(if: false) { + id, + name, + homePlanet + } + }`; + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + expect(queryParser.processQuery(parse(query))).toBe(6); + query = `query { + hero(episode: EMPIRE) { + id, + name + friends(first: 3) { + name + } + } + human(id: 1) @ignore(if: true) @skip(if: true) { + id, + name, + homePlanet + } + }`; + mockCharacterFriendsFunction.mockReturnValueOnce(3); + // 1 query + 1 hero + 3 friends + 1 human + expect(queryParser.processQuery(parse(query))).toBe(5); }); });