Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ spark_locals_without_parens = [
attribute_types: 1,
authorize?: 1,
auto?: 1,
complexity: 1,
create: 2,
create: 3,
create: 4,
Expand Down
3 changes: 3 additions & 0 deletions documentation/dsls/DSL-AshGraphql.Domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ get :get_post, :read
| [`as_mutation?`](#graphql-queries-get-as_mutation?){: #graphql-queries-get-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-get-relay_id_translations){: #graphql-queries-get-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |
| [`hide_inputs`](#graphql-queries-get-hide_inputs){: #graphql-queries-get-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the mutation. |
| [`complexity`](#graphql-queries-get-complexity){: #graphql-queries-get-complexity } | `{module, list(any)}` | `{AshGraphql.Graphql.Resolver, :query_complexity}` | 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. |



Expand Down Expand Up @@ -156,6 +157,7 @@ read_one :current_user, :current_user
| [`as_mutation?`](#graphql-queries-read_one-as_mutation?){: #graphql-queries-read_one-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-read_one-relay_id_translations){: #graphql-queries-read_one-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |
| [`hide_inputs`](#graphql-queries-read_one-hide_inputs){: #graphql-queries-read_one-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the mutation. |
| [`complexity`](#graphql-queries-read_one-complexity){: #graphql-queries-read_one-complexity } | `{module, list(any)}` | `{AshGraphql.Graphql.Resolver, :query_complexity}` | 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. |



Expand Down Expand Up @@ -207,6 +209,7 @@ list :list_posts_paginated, :read, relay?: true
| [`as_mutation?`](#graphql-queries-list-as_mutation?){: #graphql-queries-list-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-list-relay_id_translations){: #graphql-queries-list-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |
| [`hide_inputs`](#graphql-queries-list-hide_inputs){: #graphql-queries-list-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the mutation. |
| [`complexity`](#graphql-queries-list-complexity){: #graphql-queries-list-complexity } | `{module, list(any)}` | `{AshGraphql.Graphql.Resolver, :query_complexity}` | 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. |



Expand Down
6 changes: 5 additions & 1 deletion documentation/dsls/DSL-AshGraphql.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ end
| [`argument_input_types`](#graphql-argument_input_types){: #graphql-argument_input_types } | `keyword` | | A keyword list of actions and their input type overrides for arguments. The type overrides should refer to types available in the graphql (absinthe) schema. `list_of/1` and `non_null/1` helpers can be used. |
| [`primary_key_delimiter`](#graphql-primary_key_delimiter){: #graphql-primary_key_delimiter } | `String.t` | `"~"` | If a composite primary key exists, this can be set to determine delimiter used in the `id` field value. |
| [`depth_limit`](#graphql-depth_limit){: #graphql-depth_limit } | `integer` | | A simple way to prevent massive queries. |
| [`complexity`](#graphql-complexity){: #graphql-complexity } | `{module, list(any)}` | | 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. |
| [`generate_object?`](#graphql-generate_object?){: #graphql-generate_object? } | `boolean` | `true` | Whether or not to create the GraphQL object, this allows you to manually create the GraphQL object. |
| [`filterable_fields`](#graphql-filterable_fields){: #graphql-filterable_fields } | `list(atom)` | | A list of fields that are allowed to be filtered on. Defaults to all filterable fields for which a GraphQL type can be created. |
| [`nullable_fields`](#graphql-nullable_fields){: #graphql-nullable_fields } | `atom \| list(atom)` | | Mark fields as nullable even if they are required. This is useful when using field policies. See the authorization guide for more. |
Expand Down Expand Up @@ -137,6 +138,7 @@ get :get_post, :read
| [`as_mutation?`](#graphql-queries-get-as_mutation?){: #graphql-queries-get-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-get-relay_id_translations){: #graphql-queries-get-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |
| [`hide_inputs`](#graphql-queries-get-hide_inputs){: #graphql-queries-get-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the mutation. |
| [`complexity`](#graphql-queries-get-complexity){: #graphql-queries-get-complexity } | `{module, list(any)}` | `{AshGraphql.Graphql.Resolver, :query_complexity}` | 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. |



Expand Down Expand Up @@ -182,6 +184,7 @@ read_one :current_user, :current_user
| [`as_mutation?`](#graphql-queries-read_one-as_mutation?){: #graphql-queries-read_one-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-read_one-relay_id_translations){: #graphql-queries-read_one-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |
| [`hide_inputs`](#graphql-queries-read_one-hide_inputs){: #graphql-queries-read_one-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the mutation. |
| [`complexity`](#graphql-queries-read_one-complexity){: #graphql-queries-read_one-complexity } | `{module, list(any)}` | `{AshGraphql.Graphql.Resolver, :query_complexity}` | 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. |



Expand Down Expand Up @@ -232,6 +235,7 @@ list :list_posts_paginated, :read, relay?: true
| [`as_mutation?`](#graphql-queries-list-as_mutation?){: #graphql-queries-list-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-list-relay_id_translations){: #graphql-queries-list-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |
| [`hide_inputs`](#graphql-queries-list-hide_inputs){: #graphql-queries-list-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the mutation. |
| [`complexity`](#graphql-queries-list-complexity){: #graphql-queries-list-complexity } | `{module, list(any)}` | `{AshGraphql.Graphql.Resolver, :query_complexity}` | 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. |



Expand Down Expand Up @@ -582,7 +586,7 @@ managed_relationship action, argument

Configures the behavior of a given managed_relationship for a given action.

If there are type conflicts (for example, if the input could create or update a record, and the
If there are type conflicts (for example, if the input could create or update a record, and the
create and update actions have an argument of the same name but with a different type),
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:

Expand Down
5 changes: 5 additions & 0 deletions lib/resource/info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,9 @@ defmodule AshGraphql.Resource.Info do
true
)
end

@doc "The complexity callback `{mod, fun}` for this type"
def complexity(resource) do
Extension.get_opt(resource, [:graphql], :complexity, nil)
end
end
8 changes: 8 additions & 0 deletions lib/resource/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule AshGraphql.Resource.Query do
:modify_resolution,
:relay_id_translations,
:description,
:complexity,
as_mutation?: false,
hide_inputs: [],
metadata_names: [],
Expand Down Expand Up @@ -74,6 +75,13 @@ defmodule AshGraphql.Resource.Query do
type: {:list, :atom},
doc: "A list of inputs to hide from the mutation.",
default: []
],
complexity: [
type: :mod_arg,
Copy link
Copy Markdown
Contributor

@zachdaniel zachdaniel Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type: :mod_arg,
type: :mfa,

I think we should make these MFAs.

doc: """
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.
""",
default: {AshGraphql.Graphql.Resolver, :query_complexity}
]
]

Expand Down
13 changes: 10 additions & 3 deletions lib/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ defmodule AshGraphql.Resource do
describe: """
Configures the behavior of a given managed_relationship for a given action.

If there are type conflicts (for example, if the input could create or update a record, and the
If there are type conflicts (for example, if the input could create or update a record, and the
create and update actions have an argument of the same name but with a different type),
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:

Expand Down Expand Up @@ -419,6 +419,12 @@ defmodule AshGraphql.Resource do
A simple way to prevent massive queries.
"""
],
complexity: [
type: :mod_arg,
doc: """
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.
"""
],
generate_object?: [
type: :boolean,
doc:
Expand Down Expand Up @@ -688,7 +694,7 @@ defmodule AshGraphql.Resource do
[
{{AshGraphql.Graphql.Resolver, :resolve}, {domain, resource, query, relay_ids?}}
],
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
complexity: query.complexity || {AshGraphql.Graphql.Resolver, :query_complexity},
module: schema,
name: to_string(query.name),
description: query.description || query_action.description,
Expand Down Expand Up @@ -4303,6 +4309,7 @@ defmodule AshGraphql.Resource do
end

type = AshGraphql.Resource.Info.type(relationship.destination)
type_complexity = AshGraphql.Resource.Info.complexity(relationship.destination)

pagination_strategy =
relationship_pagination_strategy(resource, relationship.name, read_action)
Expand All @@ -4314,7 +4321,7 @@ defmodule AshGraphql.Resource do
module: schema,
name: to_string(name),
description: relationship.description,
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
complexity: type_complexity || {AshGraphql.Graphql.Resolver, :query_complexity},
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc_many},
{domain, relationship, pagination_strategy}}
Expand Down
63 changes: 63 additions & 0 deletions test/read_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,69 @@ defmodule AshGraphql.ReadTest do
]
end

test "custom complexity calculation" do
query = """
query PostLibrary {
paginatedPosts(limit: 2) {
results{
text
sponsoredComments(limit: 5) {
text
}
}
}
}
"""

resp =
query
|> Absinthe.run(AshGraphql.Test.Schema,
analyze_complexity: true,
max_complexity: 300
)

assert {:ok, %{errors: errors}} = resp

assert errors |> Enum.map(& &1.message) |> Enum.sort() == [
"Field paginatedPosts is too complex: complexity is 1014 and maximum is 300",
"Field results is too complex: complexity is 507 and maximum is 300",
"Field sponsoredComments is too complex: complexity is 505 and maximum is 300",
"Operation PostLibrary is too complex: complexity is 1014 and maximum is 300"
]
end

test "complexity calculation for aliased top-level queries" do
query = """
query SponsoredComments {
c1: getSponsoredComment(id: "abc-123") {
text
}
c2: getSponsoredComment(id: "def-456") {
text
}
c3: getSponsoredComment(id: "fed-789") {
text
}
c4: getSponsoredComment(id: "cba-012") {
text
}
}
"""

resp =
query
|> Absinthe.run(AshGraphql.Test.Schema,
analyze_complexity: true,
max_complexity: 300
)

assert {:ok, %{errors: errors}} = resp

assert errors |> Enum.map(& &1.message) |> Enum.sort() == [
"Operation SponsoredComments is too complex: complexity is 404 and maximum is 300"
]
end

test "a read with a loaded field works" do
AshGraphql.Test.Post
|> Ash.Changeset.for_create(:create, text: "bar", published: true)
Expand Down
12 changes: 11 additions & 1 deletion test/support/resources/sponsored_comment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ defmodule AshGraphql.Test.SponsoredComment do

graphql do
type :sponsored_comment
complexity {__MODULE__, :query_complexity}

queries do
get :get_sponsored_comment, :read
get :get_sponsored_comment, :read, complexity: {__MODULE__, :query_complexity}
end

mutations do
Expand Down Expand Up @@ -45,4 +46,13 @@ defmodule AshGraphql.Test.SponsoredComment do
relationships do
belongs_to(:post, AshGraphql.Test.Post, public?: true)
end

@doc "Sponsored comments are complex to serve, add 100 to the cost per comment"
def query_complexity(%{limit: n}, child_complexity, _resolution) do
n * (child_complexity + 100)
end

def query_complexity(_args, child_complexity, _resolution) do
child_complexity + 100
end
end