Query Cost Analysis

Query cost analysis can be used to limit the complexity of the executed queries and to protect the service against various attacks (DoS etc).

Cost of the query is defined by calculating complexity value for each field and setting a maximum allowed cost. Complexity can be set by setting a default field complexity which will be used for all fields in a query and/or by using @cost directive on the schema.

Example

Example schema has few fields with and without set complexity.

        public CostFacts()
        {
            var sdl =
                @"
                schema {
                    query: Query
                }

                type Query {
                    default: Int
                    withCost: Int @cost(complexity: 1)
                    withMultiplier(count: Int!): Int @cost(complexity: 2, multipliers: [""count""])
                }
                ";

            Schema = new SchemaBuilder()
                .Include(CostAnalyzer.CostDirective)
                .Sdl(sdl)
                .Build();
        }

Default field complexity

Default field complexity will be used when @cost directive is not present in the field.


        [Fact]
        public void Cost_above_max_cost_with_defaultComplexity()
        {
            /* Given */
            var document = Parser.ParseDocument(
                @"{
                    default
                }");

            /* When */
            var result = Validate(
                document,
                CostAnalyzer.MaxCost(
                    maxCost: 0,
                    defaultFieldComplexity: 1)
                );

            /* Then */
            Assert.False(result.IsValid);
            Assert.Single(
                result.Errors,
                error => error.Code == "MAX_COST");
        }

Cost Directive

Complexity of field is set with @cost directive.


        [Fact]
        public void Cost_above_max_cost_with_costDirective()
        {
            /* Given */
            var document = Parser.ParseDocument(
                @"{
                    withCost
                }");

            /* When */
            var result = Validate(
                document,
                CostAnalyzer.MaxCost(
                    maxCost: 0, 
                    defaultFieldComplexity: 0)
                );

            /* Then */
            Assert.False(result.IsValid);
            Assert.Single(
                result.Errors,
                error => error.Code == "MAX_COST");
        }

In some cases the complexity of the field is related to its arguments. In these cases value of the argument can be used as mulpliplier for the complexity.


        [Fact]
        public void Cost_above_max_cost_with_costDirective_and_multiplier()
        {
            /* Given */
            var document = Parser.ParseDocument(
                @"{
                    withMultiplier(count: 5)
                }");

            /* When */
            var result = Validate(
                document,
                CostAnalyzer.MaxCost(
                    maxCost: 5, 
                    defaultFieldComplexity: 0)
                );

            /* Then */
            Assert.False(result.IsValid);
            Assert.Single(
                result.Errors,
                error => error.Code == "MAX_COST");
        }

Import directive

Using SDL import

        [Fact]
        public async Task Parse_Sdl()
        {
            /* Given */
            var sdl =
                  @"
                    """"""
                    tanka_import from ""tanka://cost-analysis""
                    """"""

                    type ObjectType {
	                    property: Int! @cost(complexity: 1)
                    }

                    type Query {
                        obj: ObjectType
                    }
                 ";

            /* When */
            var builder = await new SchemaBuilder()
                // BuiltIn import providers are used
                .SdlAsync(sdl);

            var schema = builder.Build();

            /* Then */
            var objectType = schema.GetNamedType<ObjectType>("ObjectType");
            var property = schema.GetField(objectType.Name, "property");
            Assert.Single(property.Directives, directive => directive.Name == "cost");
        }

Or you can include it manually using the SchemaBuilder.Include

var builder = new SchemaBuilder()
    .Include(CostAnalyzer.CostDirective);