GraphQL Cost Directives
This first draft of this document is © Copyright IBM Corp. 2021.
1Goals
This specification defines a set of GraphQL SDL directives that captures information useful for expressing the cost of executing GraphQL queries.
The spec proposes to standardize these directives, with the following goals:
- Servers can express what is costly for them in a standard way.
- Clients can know in advance not only the schema supported, but also which parts of it are more costly.
- Middleware can uniformly enforce threat protection, rate limiting and monetization features across disparate GraphQL servers and while serving different GraphQL Clients.
- Cost analysis tools across programming languages and frameworks can use the same standardized directives.
- GraphQL schemas, both SDL documents and introspection results, can work with a variety of tools by using these directives.
- Cost analysis can be applied as a static analysis on GraphQL queries or their responses, or as a dynamic calculation during query execution.
2Collaboration
The specification landing page has some short videos explaining parts of this specification, including motivation for why this work is important.
For communication, we have created a channel #cost-analysis on the GraphQL Foundation’s Slack workspace to invite anyone to openly discuss these ideas.
Alternatively, you can directly message us on Slack. This spec is largely written by Morris Matsa based on work he did for years with spec co-author Erik Wittern and through several iterations with our customers. We also have significant contributions from Igor Belyi and Andy Chang, and collaboration with Jim Laredo and Alan Cha. We hope others will join us.
3Example
An example of an SDL document containing the proposed directives is:
Example № 1type User {
name: String
age: Int @cost(weight: "2.0")
}
type Query {
users(max: Int): [User] @listSize(slicingArguments: ["max"])
}
How expensive or costly are various fields? Really this depends on how costly it is to run the resolver functions on the GraphQL server. With the ‘raw’ SDL, or GraphQL schema, we don’t know, but with the added directives we know that:
- There is a
@cost
directive on theUser.age
field with a weight of2.0
, indicating that the total cost of running all resolvers on this query should be increased by2.0
for every time this resolver is run. - There is no
@cost
directive on theQuery.users
field, so the weight is assumed to be1.0
. - The
@listSize
directive is used to define that themax
argument of the fieldQuery.users
is a slicing argument - therefore cost analysis can rely on that argument’s value in a query to know an upper bound of how manyUser
types are returned by theQuery.users
field.
We can consider how to apply this enhanced schema to the following example query:
Example № 2query Example {
users (max: 5) {
age
}
}
The field cost of this query can be statically analyzed (see Static Query Analysis) as 11.0
, as the query returns at most:
- One
Query
object type, which then runs the resolverQuery.users
once. That resolver has a cost of1.0
. - Five
User
object types which each run the resolverUser.age
which has a cost of2.0
.
When this query is actually executed using the GraphQL engine, it might not have five users, so as an example suppose that it has three users and each one has an age. Then the actual cost of executing this query can be dynamically analyzed (see Dynamic Query Analysis) to be 7.0
:
- One
Query
object type runs the resolverQuery.users
once with a cost of1.0
. - Three
User
object types which each run the resolverUser.age
with cost2.0
.
Besides dynamically summing up the field cost to 7.0
as we run the execution engine, we could also analyze the resulting example JSON data (see Query Response Analysis):
Example № 3{ "users" : [
{ "age" : 33 },
{ "age" : 45 },
{ "age" : 27 },
]
}
In this case, we can “reverse engineer” the cost to have been 7.0
:
- The
Query.users
resolver must have run once with a cost of1.0
. - The
User.age
resolver must have run three times with each weight being2.0
.
While a static analysis of the query calculates a maximum (upper-bound) cost of 11.0
unlike both a dynamic cost calculation and a post-facto static analysis of the resulting data which calculates a cost of 7.0
, there is no contradiction and both numbers are interesting:
- The static analysis of the Query gives us an upper bound on how expensive or costly it might be to execute this query on our GraphQL server.
- The dynamic or post-facto cost calculation tells us how expensive it actually was to execute this query on our GraphQL server.
Here are a few examples of how these cost calculations can be used:
- Static Query Analysis can help:
- An API Consumer hold back from sending a possibly expensive query.
- GraphQL Middleware and Servers with threat protection by refusing possibly expensive queries.
- Dynamic Query Analysis or Query Response Analysis can help:
- GraphQL Middleware with rate limiting certain API consumers over time, or charging them per use (aka monetization).
- GraphQL Servers with analytics calculations about how expensive transactions were.
All of these advantages can be achieved across the three methods of cost analysis by adding the same simple @cost
and @listSize
directives to the GraphQL schema.
4Reserved Directives
A GraphQL server which conforms to this spec must reserve certain GraphQL directives and GraphQL directive names. In particular, this spec creates guidelines for the following directives:
- A directive named cost.
- A directive named listSize.
5Cost Analysis
Cost Analysis is the process of analyzing a GraphQL transaction with the goal of quantifying the cost or expense of its execution. The process of cost analysis results in calculated counts and costs of running that query.
5.1Methods of Cost Analysis
There are different Methods of Cost Analysis:
- When conducted before executing the query, it is called Static Query Analysis.
- When conducted during query execution, it is referred to as Dynamic Query Analysis.
- When conducted after query execution, it is Query Response Analysis.
Different methods of Cost Analysis offer different semantics of precisely what their results mean, and thus do not always produce the same count and cost values. For example, Static Query Analysis could say that ten User
s might be returned, where Query Response Analysis would show that only three User
s were actually returned. Alternatively, Static Query Analysis could say that an Address
with a Street
and City
might be returned, where Dynamic Query Analysis would show that the entire Address
was returned as null
and the Street
and City
resolvers were never executed.
The three types of Cost Analysis have inherent tradeoffs, and the relative strength of their advantages depends on the environment in which GraphQL is used. Therefore, different methods will be appropriate at different times, and sometimes it can be appropriate to run multiple methods of cost analysis. One environment could run Static Cost Analysis to protect against unsafe queries and Dynamic Query Analysis to charge an API Consumer via monetization. Another environment might prefer Query Response Analysis to get a tight bound on the real cost without adding any load to the backend server. Many combinations are possible.
5.1.1Static Query Analysis
Static Query Analysis is the process of conducting a static analysis on the input GraphQL Query, with full knowledge of the GraphQL schema for the GraphQL server.
The results of Static Query Analysis should always produce an upper bound on the results calculated by Dynamic Query Analysis and Query Response Analysis, as they are based on calculating what might happen and picking the most conservative values. This is a very useful property for a-priori decisions in security-sensitive settings.
Static Query Analysis is the least accurate of the three types of Cost Analysis, but it also has the large advantage of being the only one to happen before spending compute and other resources while running the execution engine.
Static Query Analysis can be used, for example, to allow or disallow queries to be executed based on configured rate limit or threat prevention policies. It can be applied in either GraphQL backends or dedicated middleware components.
5.1.2Dynamic Query Analysis
Dynamic Query Analysis is the process of counting dynamically as the execution engine produces parts of the result data. The results are exact counts of how much data is produced. Errors do not produce data.
Dynamic Query Analysis can be used on a GraphQL server to abort query execution when certain cost thresholds are reached for Threat Protection, or for Rate Limiting.
5.1.3Query Response Analysis
Query Response Analysis is the process of Cost Analysis running as an analysis of the result from the execution engine. This analysis uses three inputs:
- The GraphQL schema for the GraphQL server
- The GraphQL query that produced this result data
- The result data itself
By combining these three input sources, the Query Response Analysis can compute a much tighter estimate of the actual counts and cost than would have been computed in Static Query Analysis.
Query Response Analysis knows exact sizes of lists returned, which is the largest unknown quantity in Static Query Analysis, however it is not always possible to know exactly which types are returned without control of the execution engine, therefore counts and costs are still an upper bound for Query Response Analysis, although in practice they are usually exact, and at least a much tighter bound.
Query Response Analysis can be used in GraphQL backends or dedicated middleware components to accurately update remaining rates towards an enforced rate limit.
5.2Inputs to Cost Analysis
All three Methods of Cost Analysis include an input of the GraphQL schema. For all of these methods, the unenhanced schema would not be sufficient to calculate the cost. Therefore, the input schema includes both the schema constructs which are part of the GraphQL specification, and additional information in the form of directives on that schema which provide:
- Numerical Weights on various schema constructs. These are used to calculate weighted sums for the result Costs.
- List Size configuration to aid in bounding the sizes of returned lists.
The Static Query Analysis method also uses an input of the transaction’s GraphQL query, while the Query Response Analysis uses as inputs both the GraphQL query and the GraphQL response. Dynamic Query Analysis can be embedded into the GraphQL execution engine with access to its state during execution.
5.3Results of Cost Analysis
Each of the three Methods of Cost Analysis yields two kinds of results: Counts and Costs. These two kinds of results from Cost Analysis MAY be provided as outputs of the whole Cost Analysis, or MAY be maintained only as internal data used as part of Cost Analysis. Furthermore, they may be skipped if it does not change the overall results of Cost Analysis, and may be stored in any form or calculated in any order. The purpose of describing them here is to explain a straightforward algorithm that is easy to understand and verify, without eliminating any opportunities for optimization.
For example, some users of Cost Analysis might use the Counts as output to achieve rate-limiting based on a subset of the counts, where other users of Cost Analysis might leave the Counts as an internal step towards calculating the Costs.
5.3.1Counts
Cost analysis computes Counts, a set of count values, where each value is a non-negative integer and corresponds to a particular part of the GraphQL schema.
When a given key refers to a specific parts of a GraphQL schema, we use a “GraphQL Path” notation. This notation is the same as the notation used in many parts of the industry today, and should evolve with the RFC: Schema Coordinates by Mark Larah.
5.3.1.1Type Counts
A type count corresponds to a specific type in the schema. In this case types includes all types which can appear in GraphQL responses: scalar types, object types, interface types, union types, and enum types. The count gives the number of times that a query produces something of that specific type. The various type counts MAY be combined into any hash map, such as a JSON object.
Example № 4{
"Query" : 1,
"User" : 3
}
5.3.1.2Input Type Counts
An input type counts corresponds to a specific input object type in the schema. Input object types are not in Type Counts because they do not appear in the GraphQL responses. The count gives the number of times that a query returns a field which uses that input type, which is the number of times that a resolver function was run for a field with this input type.
Example № 5{
"UserInput" : 1
}
5.3.1.3Field Counts
A field count corresponds to a specific field which is defined on an object type or interface type in the schema. The count gives the number of times that a query returns that specific field on that type, which is the number of times that a resolver was run on that field.
Example № 6{
"Query.user" : 3,
"User.name" : 3,
"User.friends" : 1
}
5.3.1.4Input Field Counts
An input field count corresponds to a specific field which is defined on an input object type in the schema. The count gives the number of times that a query returns a field which uses this input field, which is the number of times that a resolver function was run for a field using an input type using this input field.
Example № 7{
"UserInput.name" : 1,
"ProduceInput.category" : 2,
"Filter.age" : 1
}
5.3.1.5Argument Counts
An argument count corresponds to a specific argument to either a field or a directive on the schema. The count gives the number of times that a specific argument is used in a query, on either a field or a directive, which is the number of times that a resolver function was run for a field which uses this argument.
Example № 8{
"Query.users.limit" : 1,
"@skip.if" : 2
}
5.3.1.6Directive Counts
A directive count corresponds to a specific directive in the schema. The count gives the number of times a specific directive is used, which is the number of times that a resolver function was run for a field which uses this directive.
Example № 9{
"@defer" : 1,
"@include" : 2
}
An implementation MAY store all key-value pairs of count values in a single map or it MAY store them in a number of maps, one for type counts, one for field counts, etc.
5.3.2Costs
Cost Analysis can result in various Costs, which are numeric values characterizing the net effective expense of executing the query on the GraphQL server.
5.3.2.1Type Cost
The Type Cost is the weighted sum of the Type Counts.
5.3.2.2Field Cost
The overall Field Cost can be calculated by adding together the cost of every object field. The cost of each object field can be calculated by recursively applying these algorithms to calculate the cost of all arguments, directives, and fields, including object fields and input fields.
Arguments: For every argument a, calculate its cost:
- Add up:
- The weight of argument a
- The sum of the cost of all of the input fields which are used on argument a
- This sum may be negative
Directives: For every directive d calculate its cost:
- Add up:
- The weight of directive d
- The sum of the cost of all of the arguments on directive d
- This sum may be negative
Fields: For every field f calculate its cost:
- First, add up the raw cost of field f by calculating the sum of:
- The weight of field f
- The sum of the cost of all directives on field f
- The sum of the cost of all arguments on field f
- Second, if this sum is negative then round it up to zero
Example: Argument Weights
Example № 10type Query {
topProducts(filter: Filter @cost(weight: "15.0")): [String] @cost(weight: "5.0") @listSize(assumedSize: 10)
}
In this example, a query that asks for topProducts
has a Field Cost of 5.0
unless it uses a filter which results in much more work in the same single resolver function and therefore increases the Field Cost on this field to 20.0
, as the sum of 5.0
for the field itself and 15.0
for the argument.
Example: Negative Weights
Example № 11type Query {
mostPopularProduct(approx: Approximate @cost(weight: "-3.0")): Product @cost(weight: "5.0")
}
In this example, a query that asks for the most popular product has a Field Cost of 5.0
unless it uses an argument to indicate that only an approximate answer is needed and not an exact transactional answer, in which case the cost would reduce to 2.0
as 5.0 - 3.0 = 2.0
. This is an example of where negative weights can be used even though a given field cannot add a negative value to the overall cost. Conceptually, we can understand that certain arguments might make it less expensive to run a given resolver function.
Example: Input Field Weights
Example № 12input Filter {
approx: Approximate @cost(weight: "-12.0")
}
type Query {
topProducts(filter: Filter @cost(weight: "15.0")): [String] @cost(weight: "5.0") @listSize(assumedSize: 10)
}
This example builds on example above where the filter
contributed an extra 15.0
to the cost of the topProducts
field. Now we qualify that: If the filter is only approximate then the weight is decreased by adding -12.0
which means that the filter
argument overall contributes a weight of 15.0 - 12.0 = 3.0
to the base field cost of 5.0
for an overall field cost of 8.0
.
Example: Directive Arguments
Example № 13directive @approx (tolerance: Float! @cost(weight: "-1.0")) on FIELD
In this example, we modify the @approx
directive’s required tolerance
argument to have a weight of -1.0
, which therefore adds -1.0
to the cost of every use of the @approx
directive, and in turn to every field which uses the @approx
directive. This slightly decreases the cost of every field in a query which only needs approximate results.
Weights used in Response
Query Response Analysis has access to the original query in addition to the response, so it knows the directives and arguments on a field and can calculate the same costs as the other methods.
6Introspection
The GraphQL specification defines introspection using GraphQL itself to create “a powerful platform for tool-building”. For any GraphQL server or middleware implementing this specification, it can optionally support extensions to introspection to further enhance this powerful platform and enable tools that assist the client with understanding the cost of their queries.
6.1Introspection of Cost Analysis Results
The results of cost analysis can be added into the introspection. As with any introspection, some servers might disable it for security or enable it to support powerful tooling. Each system MAY extend introspection with cost analysis results, and if they do then they MUST extend introspection in these ways to be compatible.
6.1.1Naming
The GraphQL specification requires that: “Types and fields required by the GraphQL introspection system that are used in the same context as user-defined types and fields are prefixed with "__" two underscores.”
This extension to introspection maintains that rule.
6.1.2__cost
This extension to the schema introspection system is accessible from the meta-field __cost
which is accessible from the type of the root of a query operation.
__cost : __Cost!
Like all meta-fields, __cost
is implicit and does not appear in the fields list in the root type of the query operation.
The schema of the GraphQL cost introspection system:
type __Cost {
requestCosts : __CostMetrics
responseCosts : __CostMetrics
}
type __CostMetrics {
fieldCounts(regexName: String) : [__CostCountType!]!
typeCounts(regexName: String) : [__CostCountType!]!
inputTypeCounts(regexName: String) : [__CostCountType!]!
inputFieldCounts(regexName: String) : [__CostCountType!]!
argumentCounts(regexName: String) : [__CostCountType!]!
directiveCounts(regexName: String) : [__CostCountType!]!
fieldCost : Float!
typeCost : Float!
fieldCostByLocation(regexPath: String) : [__CostByLocation!]!
typeCostByLocation(regexPath: String) : [__CostByLocation!]!
}
type __CostByLocation {
path: String!
cost: Float!
}
type __CostCountType {
name: String!
value: Int!
}
The counts fieldCounts
, typeCounts
, inputTypeCounts
, inputFieldCounts
, argumentCounts
, and directiveCounts
returned correspond to the result counts calculated for the overall query.
The returned typeCost
and fieldCost
correspond to the overall type cost and field cost of the query, respectively.
The costs by location returned by typeCostByLocation
and fieldCostByLocation
are the subset of the overall typeCost
and fieldCost
calculated for the given subsets of the overall query.
6.1.3regexName Arguments
All regular expression name arguments regexName
on fields in the __Cost
type are regular expressions on the name of the relevant part of the GraphQL schema provided by the GraphQL schema type system, and that format should evolve with the RFC: Schema Coordinates by Mark Larah.
Each of these regular expressions limits the scope of responses in the returned list: When returning a list of __CostCountType
objects, each __CostCountType
‘s name
must match the regular expression.
This regular expression always applies to the full name, as if the start string and end string qualifiers are always specified. This implies that whenever no wildcards are used, the returned list much have at most one item in it.
6.1.4regexPath Arguments
All regular expression path arguments regexPath
are regular expressions on the string representing the path into a particular part of the GraphQL query. This path is a dot-separated list of fields from the root of the query, with references to directives, fragments, and arguments expressed as they would be using Schema Coordinates. There are a number of considerations necessary to make a proper “Query Coordinates” spec to match/extend the existing “Schema Coordinates” RFC. We’ve made some choices here, but if Query Coordinates get standardized as part of the GraphQL spec, then that standardization should be used here instead of these choices.
In the absence of a proper GraphQL Query Coordinates RFC, we are including some examples to illustrate the idea. We feel that an actual spec for Query Coordinates should be independent and not part of this document.
Consider this example GraphQL query:
Example № 14query exampleQuery ($VarDef1: Int @custom(first: 1)) {
a1(arg1: 1, arg2: {f1: 3, f2: 4}) {
x @dir1(f3: 3, f4: 4)
...b @dir2
... on A @dir3 {
c
}
}
a2 {
...b
... on A {
nest {
y
}
}
... on A {
nest {
z
}
}
}
alias1:a2 {
...b
}
a3(arg1: [{d1: 3, d2: "f"}, 2, "foo", false], arg2: $VarDef1) {
g
}
a3(arg1: [{d1: 3, d2: "f"}, 2, "foo", false], arg2: $VarDef1) {
g
}
}
fragment b on A @dir4 {
h
}
In this query, legal paths include:
exampleQuery($VarDef1:)
- This is the path to the variable definition$VarDef1
exampleQuery($VarDef1:).@custom
- This is the path to the directivecustom
within variable definition$VarDef1
exampleQuery($VarDef1:).@custom(first:)
- This is the path to the argumentfirst
of the directivecustom
exampleQuery.a1
- This is the path to the first field of the queryexampleQuery.a1(arg1:)
- This is the path to thea1
field’sarg1
argumentexampleQuery.a1(arg2:).f2
- This is the path to thea1
field’sarg1
argument’sf2
input fieldexampleQuery.a1.x
- This is the path to thea1
field’s selection set’sx
fieldexampleQuery.a1.~b
- This is the path to theb
fragment spread within thea1
fieldexampleQuery.a1.on~A
- This is the path to theon A
fragment spreadexampleQuery.a1.on~A.@dir3
- This is the path to that fragment spread’s directiveexampleQuery.a1.on~A.c
- This is the path to that fragment spread’sc
fieldexampleQuery.a2.on~A[0]
- This is the path to the first... on A
withina2
exampleQuery.a2.on~A[1]
- This is the path to the second... on A
withina2
exampleQuery.alias1
- This is the path to the fielda2
aliasedalias1
exampleQuery.alias1.~b
- This is the path to the fragment spreadb
within the fielda2
aliasedalias1
exampleQuery.a3[0](arg1:)[2]
- This is the path to the third value in list argumentarg1
exampleQuery.a3[0](arg1:)[0].d1
- This is the path to thed1
field in the first value in list argumentarg1
exampleQuery.a3[1].g
- This is the path to theg
field within the seconda3
field~~b
- This is the path to the fragmentb
, ie. the definition of theb
fragment~~b.@dir4
- This is the path to the directive on that fragment definition~~b.h
- This is the path to the fieldh
of the fragment definitionb
Take note that there are two ... on A
inlined fragments with the a2
field. It makes sense for end-user tooling to want to display the contribution of each of these two to the overall query cost independently. Therefore, we need to be able to have unique paths that distinguish between them. In this case we add [0]
and [1]
to the end of the path. There could be later path elements as well, for example:
a2.on~A[0].nest
- The firstnest
fielda2.on~A[1].nest
- The secondnest
field
To keep the path expressions simple:
- For any part of the path with only a single occurance in the query, the
[0]
must be ommitted. - For any regular expression, it should take into account that any path element might or might not have an index.
We hope that a proper GraphQL Query Coordinates RFC will improve this situation.
Here is another example:
Example № 15
query A {
a {
y: x { i j }
x: y
z: x { j k }
...B
}
}
fragment B {
z
}
query B {
a {
a
b
...A
}
}
fragment A {
c
}
In this query, legal paths include:
A
- This is the path to theA
operationA.a
- This is the path to that operation’sa
fieldA.a.y
- This refers to they: x
field, using the aliasA.a.x
- This refers to thex: y
field, using the alias~~B
- This is a path to the fragment definiton ofB
B
- This is a path to the operation definition ofB
When returning a list of __CostByLocation
objects, each __CostByLocation
‘s path
must match the regular expression.
Just like the regexName arguments, this regular expression always applies to the full path, as if the start string and end string qualifiers are always specified. This implies that whenever no wildcards are used, the returned list much have at most one item in it.
6.1.5requestCosts and responseCosts
Implementations which extend introspection with cost results must return at least one of requestCosts
and responseCosts
as non-null. If returning a non-null requestCosts
it should have been computed by Static Query Analysis. If returning a non-null responseCosts
it should have been computed by either Dynamic Query Analysis or Query Response Analysis.
6.2introspectionOnly
Implementations may support an HTTP Header named introspectionOnly
. If this header is passed, then that HTTP transaction MUST only execute the introspection parts of the query, including __schema
, __type
, __cost
, and any other extensions to introspection.
This header gives the client tooling the ability:
- to pass this header and a query which will be analyzed and yield the results of cost analysis, without actually executing the non-meta part of the query.
- to not pass this header, thus defaulting to executing all parts of the query including the meta and non-meta parts.
6.3NoSchemaIntrospectionCustomRule
Since __cost
is an extension of introspection, any custom rule that turns off all introspection such as the graphql-js v15.2.0 feature, must also forbid use of __cost
.
Implementations may make finer-grained setting to only forbid parts of introspection, however a generic toggle or validation rule must apply to all parts of introspection equally.
6.4Introspection of Cost Directives
This document specifies adding cost directives to a GraphQL schema. It would be useful for client tooling to automatically introspect these directives from a GraphQL server, allowing the server to advertise how expensive various GraphQL constructs are in that GraphQL schema. It would help if GraphQL introspection results had an option to include directives in the GraphQL schema, as is being discussed as a possible change to the GraphQL specification.
7The Cost Directive
The purpose of the cost
directive is to define a weight
for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response.
Formally, the definition of the cost Directive is:
directive @cost(weight: String!) on
| ARGUMENT_DEFINITION
| ENUM
| FIELD_DEFINITION
| INPUT_FIELD_DEFINITION
| OBJECT
| SCALAR
7.1weight
The weight
argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc. Being of type non-null String
, the weight
argument has to be set for every use of the @cost
directive. In other words, it is the sole purpose of the cost
directive to set a weight
. While weight
is required on @cost
, it is not required to include the @cost
directive on any schema construct.
While the type of weight
is a String!
, the value of the String
may be a serialized Float
. The reason that the definition is String
is to allow extensibility of using a formula which computes a Float
. All implementations must accept a literal serialized Float
.
When a @cost
directive is not provided, Cost Analysis relies on default weights:
- For scalar and enum types the weight defaults to
"0.0"
. - Fields returning scalar and enum types, arguments of scalar and enum types, as well as input fields of scalar and enum types all default to
"0.0"
. - Weights for all composite input and output types default to
"1.0"
.
These default weights reflect the fact that scalar and enum values are often contained in their composite parents, like a “name” being contained in a “user” object, so while they require an additional GraphQL resolver function to be called, it usually does not result in an additional call outside of the GraphQL execution engine.
Weights can be negative. The overall calculated weight of a field or directive can never be negative, and if found to be negative must be rounded up to zero, but negative weights on arguments can be useful when combined with positive weights on the field that uses those arguments. See Negative Weights.
7.2Locations
The @cost
directive can be used in a variety of locations, namely on all elements relevant for calculating different kinds of cost.
Deliberately, the @cost
directive cannot be used on abstract types, neither interfaces nor unions. For such types, Cost Analysis should consider as weight
the maximum weight of any of the member types. This constraint is required to ensure that Static Cost Analysis on a query produces cost values that are upper bounds of the values produced by either Dynamic Query Analysis or Query Response Analysis, which run after a concrete type was determined during execution.
For the same reason, the cost
directive cannot be used on fields of an interface.
Additonally, the @cost
directive cannot be used on an input object, although it can be used on the fields within an input object. The rationale is that the input object itself adds to the size of the query which can be calculated exactly without any directives, whereas the fields in the input type might correspond to additonal work needed in the resolver function for an object’s field, and should affect the weight of that other field.
8The List Size Directive
A main challenge when statically analyzing queries is how to produce counts and cost for fields that return lists of values. During query execution, those fields’ resolver functions return lists of finite size, limited either by the sparsity of data, by limits hard-coded in the GraphQL server implementation (e.g., how many entries to select in a database query), or relying on the values of slicing arguments used for pagination and sent in queries.
Without access to the implementation of a GraphQL server or its underlying data, a static query analysis cannot know the size of lists. To be conservative and not under-estimate cost and counts, it must initially assume the length of lists to be infinite. This approach does guarantee that the analysis produces cost and count upper bounds, but it renders the resulting cost and counts to be meaningless.
The purpose of the @listSize
directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information.
Formally, the definition of the listSize Directive is:
directive @listSize(
assumedSize: Int,
slicingArguments: [String!],
sizedFields: [String!],
requireOneSlicingArgument: Boolean = true
) on FIELD_DEFINITION
8.1assumedSize
The assumedSize
argument can be used to statically define the maximum length of a list returned by a field.
For example, in the following schema document, the assumedSize
argument states that the list of strings returned by field Query.topProducts
will always be 10:
Example № 16type Query {
topProducts: [String] @listSize(assumedSize: 10)
}
For static analysis to produce accurate counts and cost, the GraphQL server must ensure that the list returned by a field with an assumedSize
directive does in fact return lists of (at most) the defined size.
8.2slicingArguments
The slicingArguments
argument can be used to define which of the field’s arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments.
For example, in the following schema document, the slicingArguments
argument states that the max
argument of field Query.users
is a slicing argument:
Example № 17type Query {
users (max: Int!): [String] @listSize(slicingArguments: ["max"])
}
When analyzing a query that includes that field, the static analysis can use the value of the max
argument to determine how long the list of users is going to be (at most).
For any argument with a default value in the schema but no value given in the query, static analysis should treat it as if that value was given in the query.
When multiple slicing arguments are defined and a query contains more than one, static analysis should consider their largest value to ensure producing upper bounds.
8.3sizedFields
The sizedFields
argument can be used to define that the value of the assumedSize
argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields.
This option is relevant primarily for schemas that implement the GraphQL Cursor Connections Specification. In it, fields with slicing arguments first
and last
return a single Connection with a field edges
that itself returns the sliced list (in which each element is contained in an Edge).
For example, in the following schema document, the field Query.films
has two slicing arguments first
and last
. The field returns a single FilmConnection
, which itself provides access to multiple FilmEdge
s via its FilmConnection.edges
field. The sizedFields
option states the the size of the list returned by FilmConnection.edges
is determined by the value of one of the slicing arguments of the parent field:
Example № 18type FilmEdge {
cursor: ID
node: Film # Definition of this type left out in this example
}
type FilmConnection {
edges: [FilmEdge]
# See https://relay.dev/graphql/connections.htm#sec-Connection-Types.Fields.PageInfo
pageInfo: PageInfo
}
type Query {
films(first: Int, after: ID, last: Int, before: ID): FilmConnection @listSize(
slicingArguments: ["first", "last"],
sizedFields: ["edges"]
)
}
8.4requireOneSlicingArgument
The requireOneSlicingArgument
argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error.
Requiring a single slicing argument is useful once more for schemas that implement the GraphQL Cursor Connections Specification. They define two slicing arguments, first
and last
, for paginated fields. GraphQL does not support stating that exactly one or the other is required. In consequence, GraphQL API providers like GitHub implement custom validation rules to make sure exactly one slicing argument is used in a query. The requireOneSlicingArgument
argument allows to configure this behavior.
If no slicingArguments
are defined, then the value of requireOneSlicingArgument
is ignored.
Per default, requireOneSlicingArgument
is enabled, and has to be explicitly disabled if not desired for a field.
8.5Locations
The listSize
directive can be used solely on field definitions.
9Validation
The directives defined by this spec can be validated on two dimensions: First, that their definitions are present in an SDL, and are adhering to this spec. Second, that the use of directives within the SDL is correct.
9.1Cost Directive
An SDL document that implements this spec should contain a directive definition for the cost directive. Specifically:
- The
cost
directive should define a single argument namedweight
of type non-nullString
. - The
cost
should be non-repeatable. - The
cost
directive should define the locationsARGUMENT_DEFINITION
,ENUM
,FIELD_DEFINITION
,INPUT_FIELD_DEFINITION
,OBJECT
, andSCALAR
.
The correct usage of the cost
directive is partly covered by GraphQL’s specified validation rules. Specifically:
- The
cost
directive can only be used in the locations defined in its definition (covered by the “Directives Are In Valid Locations” validation rule), and can be used only once per location (covered by the “Directives Are Unique Per Location” validation rule). - When used, the
cost
directive must set a value for its definedweight
argument (covered by the “Required Arguments” validation rule) and must only use that argument (covered by the “Argument Names” validation rule) and its value must be of the correct type (covered by the “Values of Correct Type” validation rule).
In addition, further rules for using the cost
directive should be validated.
9.1.1No Cost on Interface Fields
The cost
directive is not allowed on fields in an interface. The cost of a field on an interface can be calculated based on the costs of the corresponding field on each concrete type implementing that interface, either directly or indirectly through other interfaces.
9.2List Size Directive
An SDL document that implements this spec should contain a directive definition for the listSize directive. Specifically:
- The
listSize
directive should define the following arguments:- An argument named
assumedSize
with typeInt
. - An argument named
slicingArguments
with type list ofString
. - An argument named
sizedFields
with type list ofString
. - An argument named
requireOneSlicingArgument
with typeBoolean
and a default value oftrue
.
- An argument named
- The
listSize
should be non-repeatable. - The
listSize
directive should define the single locationFIELD_DEFINITION
.
The correct usage of the listSize
directive is partly covered by GraphQL’s specified validation rules. Specifically:
- The
listSize
directive can only be used in the locations defined in its definition (covered by the “Directives Are In Valid Locations” validation rule), and can be used only once per location (covered by the “Directives Are Unique Per Location” validation rule). - When used, the
listSize
directive may set values for any of its defined arguments (covered by the “Argument Names” validation rule) and their values must be of the correct type (covered by the “Values of Correct Type” validation rule).
In addition, further rules for using the listSize
directive should be validated.
9.2.1Valid List Size Target
The listSize
directive should only be used on fields that return lists, or it uses the sizedFields
argument.
9.2.2Valid Sized Fields Target
Using the sizedFields
argument in a listSize
directive requires that both:
- The named sized fields are defined in the return type of the annotated field.
- The named sized field return lists.
9.2.3Valid Slicing Arguments Target
Using the slicingArguments
argument in a listSize
directive requires that both:
- The named slicing arguments are defined in the annotated field.
- The named slicing arguments have a type non-null
Int
or simplyInt
.
9.2.4Valid Assumed Size
Using the assumedSize
argument in a listSize
directive requires that:
- Either the same directive does not also use the
slicingArguments
argument, or: - The same directive does use the
slicingArguments
argument, but none of the slicing arguments has a default value, and therequireOneSlicingArgument
is set tofalse
.
The rationale behind this rule is to avoid cases where it is ambiguous for static analysis whether to use the value of the assumedSize
argument or that of a slicing argument. The second condition renders the value of the assumedSize
argument a backup for queries that do not use a slicing argument, and where no slicing argument has a default value. In cases where a slicing argument is provided on a field that also has an assumedSize
defined, static analysis should prioritize the value of the slicing argument.