Skip to content

Commit a9d922d

Browse files
committed
feat: Type and Query complexity callbacks
A custom complexity callback can be provided for a type to be used when that type is resolved through a relationship. A query-level complexity callback applies when the resource has a top-level query in the schema.
1 parent a90e1b9 commit a9d922d

File tree

6 files changed

+66
-4
lines changed

6 files changed

+66
-4
lines changed

.formatter.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ spark_locals_without_parens = [
1616
create: 2,
1717
create: 3,
1818
create: 4,
19+
complexity: 1,
1920
depth_limit: 1,
2021
derive_filter?: 1,
2122
derive_sort?: 1,

lib/resource/info.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,9 @@ defmodule AshGraphql.Resource.Info do
229229
true
230230
)
231231
end
232+
233+
@doc "The complexity callback `{mod, fun}` for this type"
234+
def complexity(resource) do
235+
Extension.get_opt(resource, [:graphql], :complexity, nil)
236+
end
232237
end

lib/resource/query.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule AshGraphql.Resource.Query do
1010
:modify_resolution,
1111
:relay_id_translations,
1212
:description,
13+
:complexity,
1314
as_mutation?: false,
1415
hide_inputs: [],
1516
metadata_names: [],
@@ -74,6 +75,13 @@ defmodule AshGraphql.Resource.Query do
7475
type: {:list, :atom},
7576
doc: "A list of inputs to hide from the mutation.",
7677
default: []
78+
],
79+
complexity: [
80+
type: :mod_arg,
81+
doc: """
82+
An {module, function} that will be called with the arguments and complexity value of the child fields query. It should return the complexity of this query.
83+
""",
84+
default: {AshGraphql.Graphql.Resolver, :query_complexity}
7785
]
7886
]
7987

lib/resource/resource.ex

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ defmodule AshGraphql.Resource do
192192
describe: """
193193
Configures the behavior of a given managed_relationship for a given action.
194194
195-
If there are type conflicts (for example, if the input could create or update a record, and the
195+
If there are type conflicts (for example, if the input could create or update a record, and the
196196
create and update actions have an argument of the same name but with a different type),
197197
a warning is emitted at compile time and the first one is used. If that is insufficient, you will need to do one of the following:
198198
@@ -419,6 +419,12 @@ defmodule AshGraphql.Resource do
419419
A simple way to prevent massive queries.
420420
"""
421421
],
422+
complexity: [
423+
type: :mod_arg,
424+
doc: """
425+
An {module, function} that will be called with the arguments and complexity value of the child fields query. It should return the complexity of this query.
426+
"""
427+
],
422428
generate_object?: [
423429
type: :boolean,
424430
doc:
@@ -688,7 +694,7 @@ defmodule AshGraphql.Resource do
688694
[
689695
{{AshGraphql.Graphql.Resolver, :resolve}, {domain, resource, query, relay_ids?}}
690696
],
691-
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
697+
complexity: query.complexity || {AshGraphql.Graphql.Resolver, :query_complexity},
692698
module: schema,
693699
name: to_string(query.name),
694700
description: query.description || query_action.description,
@@ -4303,6 +4309,7 @@ defmodule AshGraphql.Resource do
43034309
end
43044310

43054311
type = AshGraphql.Resource.Info.type(relationship.destination)
4312+
type_complexity = AshGraphql.Resource.Info.complexity(relationship.destination)
43064313

43074314
pagination_strategy =
43084315
relationship_pagination_strategy(resource, relationship.name, read_action)
@@ -4314,7 +4321,7 @@ defmodule AshGraphql.Resource do
43144321
module: schema,
43154322
name: to_string(name),
43164323
description: relationship.description,
4317-
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
4324+
complexity: type_complexity || {AshGraphql.Graphql.Resolver, :query_complexity},
43184325
middleware: [
43194326
{{AshGraphql.Graphql.Resolver, :resolve_assoc_many},
43204327
{domain, relationship, pagination_strategy}}

test/read_test.exs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,37 @@ defmodule AshGraphql.ReadTest do
421421
]
422422
end
423423

424+
test "custom complexity calculation" do
425+
query = """
426+
query PostLibrary {
427+
paginatedPosts(limit: 2) {
428+
results{
429+
text
430+
sponsoredComments(limit: 5) {
431+
text
432+
}
433+
}
434+
}
435+
}
436+
"""
437+
438+
resp =
439+
query
440+
|> Absinthe.run(AshGraphql.Test.Schema,
441+
analyze_complexity: true,
442+
max_complexity: 300
443+
)
444+
445+
assert {:ok, %{errors: errors}} = resp
446+
447+
assert errors |> Enum.map(& &1.message) |> Enum.sort() == [
448+
"Field paginatedPosts is too complex: complexity is 1014 and maximum is 300",
449+
"Field results is too complex: complexity is 507 and maximum is 300",
450+
"Field sponsoredComments is too complex: complexity is 505 and maximum is 300",
451+
"Operation PostLibrary is too complex: complexity is 1014 and maximum is 300"
452+
]
453+
end
454+
424455
test "a read with a loaded field works" do
425456
AshGraphql.Test.Post
426457
|> Ash.Changeset.for_create(:create, text: "bar", published: true)

test/support/resources/sponsored_comment.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ defmodule AshGraphql.Test.SponsoredComment do
88

99
graphql do
1010
type :sponsored_comment
11+
complexity {__MODULE__, :query_complexity}
1112

1213
queries do
13-
get :get_sponsored_comment, :read
14+
get :get_sponsored_comment, :read, complexity: {__MODULE__, :query_complexity}
1415
end
1516

1617
mutations do
@@ -45,4 +46,13 @@ defmodule AshGraphql.Test.SponsoredComment do
4546
relationships do
4647
belongs_to(:post, AshGraphql.Test.Post, public?: true)
4748
end
49+
50+
@doc "Sponsored comments are complex to serve, add 100 to the cost per comment"
51+
def query_complexity(%{limit: n}, child_complexity, _resolution) do
52+
n * (child_complexity + 100)
53+
end
54+
55+
def query_complexity(_args, child_complexity, _resolution) do
56+
child_complexity + 100
57+
end
4858
end

0 commit comments

Comments
 (0)