Skip to content
Open
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
36 changes: 27 additions & 9 deletions lib/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -984,12 +984,28 @@ defmodule AshGraphql.Resource do
resource
|> mutations(all_domains)
|> Enum.map(fn mutation ->
%{
mutation
| action:
Ash.Resource.Info.action(resource, mutation.action) ||
raise("No such action #{mutation.action} for #{inspect(resource)}")
}
action =
Ash.Resource.Info.action(resource, mutation.action) ||
raise("No such action #{mutation.action} for #{inspect(resource)}")

# Validate that generic actions are not used in mutation blocks (create/update/destroy)
if action.type == :action && mutation.type != :action do
raise """
Invalid GraphQL mutation.

`#{mutation.type}` references Ash action `#{mutation.action}`, but that action has type `:action`.

GraphQL `#{mutation.type}` mutations require an Ash action with type `:#{mutation.type}`.

Resource: #{inspect(resource)}

Fix:
• Define an Ash `:#{mutation.type}` action
• Or expose the generic action under `graphql.actions`
"""
end

%{mutation | action: action}
end)
|> Enum.flat_map(fn mutation ->
description =
Expand Down Expand Up @@ -1077,7 +1093,9 @@ defmodule AshGraphql.Resource do
end

result =
if mutation.action.type == :action && mutation.error_location == :top_level do
if mutation.action.type == :action &&
mutation.type == :action &&
Map.get(mutation, :error_location) == :top_level do
[]
else
[
Expand Down Expand Up @@ -4971,8 +4989,8 @@ defmodule AshGraphql.Resource do
Ash.Resource.Info.resource?(constraints[:instance_of]) &&
result == (Application.get_env(:ash_graphql, :json_type) || :json_string) do
IO.warn("""
Struct type with instance_of constraint falls back to JsonString for input.
Consider creating a custom type with `use AshGraphql.Type` and `graphql_input_type/1`
Struct type with instance_of constraint falls back to JsonString for input.
Consider creating a custom type with `use AshGraphql.Type` and `graphql_input_type/1`
for structured validation.

Resource: #{inspect(resource)}
Expand Down
133 changes: 133 additions & 0 deletions test/generic_action_mutation_error_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# SPDX-FileCopyrightText: 2020 ash_graphql contributors <https://github.com/ash-project/ash_graphql/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule AshGraphql.GenericActionMutationErrorTest do
use ExUnit.Case

test "raises helpful error when real generic action is used in create mutation block" do
# This should raise an error with our improved message
assert_raise RuntimeError, fn ->
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionErrorTestResource,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)
end

# Verify the error message contains helpful details
try do
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionErrorTestResource,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)
rescue
e in RuntimeError ->
error_message = Exception.message(e)

# Verify it mentions the generic action
assert error_message =~ "Invalid GraphQL mutation"
assert error_message =~ "random_action"
assert error_message =~ "create"
assert error_message =~ "has type `:action`"
assert error_message =~ "require an Ash action with type `:create`"
assert error_message =~ "Fix:"
assert error_message =~ "graphql.actions"
assert error_message =~ "GenericActionErrorTestResource"
end
end

test "raises helpful error when real generic action is used in update mutation block" do
assert_raise RuntimeError, fn ->
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionErrorTestResourceUpdate,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)
end

try do
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionErrorTestResourceUpdate,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)
rescue
e in RuntimeError ->
error_message = Exception.message(e)
assert error_message =~ "Invalid GraphQL mutation"
assert error_message =~ "count_action"
assert error_message =~ "update"
assert error_message =~ "has type `:action`"
assert error_message =~ "require an Ash action with type `:update`"
assert error_message =~ "Fix:"
assert error_message =~ "graphql.actions"
end
end

test "raises helpful error when real generic action is used in destroy mutation block" do
assert_raise RuntimeError, fn ->
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionErrorTestResourceDestroy,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)
end

try do
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionErrorTestResourceDestroy,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)
rescue
e in RuntimeError ->
error_message = Exception.message(e)
assert error_message =~ "Invalid GraphQL mutation"
assert error_message =~ "random_action"
assert error_message =~ "destroy"
assert error_message =~ "has type `:action`"
assert error_message =~ "require an Ash action with type `:destroy`"
assert error_message =~ "Fix:"
assert error_message =~ "graphql.actions"
end
end

test "correct usage of generic actions in action blocks does not raise errors" do
# This should NOT raise an error - correct usage
# Note: Generic actions with error_location: :top_level return empty list (no result object type needed)
result =
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionCorrectUsageResource,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)

# Should return mutation types successfully without raising an error
assert is_list(result)
# Empty list is valid for generic actions with top_level error_location
end

test "typed actions (create/update/destroy) work correctly in their respective blocks" do
# This should NOT raise an error - correct usage of typed actions
result =
AshGraphql.Resource.mutation_types(
AshGraphql.Test.GenericActionTypedActionsResource,
AshGraphql.Test.Domain,
[],
AshGraphql.Test.Schema
)

# Should return mutation types successfully
assert is_list(result)
assert length(result) > 0
end
end
2 changes: 2 additions & 0 deletions test/support/domain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ defmodule AshGraphql.Test.Domain do
resource(AshGraphql.Test.Product)
resource(AshGraphql.Test.ResourceWithUnion)
resource(AshGraphql.Test.ResourceWithTypedStruct)
resource(AshGraphql.Test.GenericActionCorrectUsageResource)
resource(AshGraphql.Test.GenericActionTypedActionsResource)
resource(AshGraphql.Test.AfterTransactionEts)
resource(AshGraphql.Test.AfterTransactionMnesia)
end
Expand Down
188 changes: 188 additions & 0 deletions test/support/resources/generic_action_error_test_resources.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# SPDX-FileCopyrightText: 2020 ash_graphql contributors <https://github.com/ash-project/ash_graphql/graphs.contributors>
#
# SPDX-License-Identifier: MIT

# Test resources for testing generic action mutation errors
# These resources have real generic actions that can be misused in mutation blocks

defmodule AshGraphql.Test.GenericActionErrorTestResource do
@moduledoc false
use Ash.Resource,
domain: AshGraphql.Test.Domain,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]

graphql do
type :generic_action_error_test_resource

mutations do
# These are WRONG - trying to use generic actions in typed mutation blocks
# This should trigger our error message
create :wrong_create_with_generic, :random_action
end
end

actions do
defaults([:create, :read, :update, :destroy])

# Real generic action (type: :action) - similar to :random in Post
action :random_action, :struct do
constraints(instance_of: __MODULE__)
allow_nil? true

run(fn _input, _ ->
__MODULE__
|> Ash.Query.limit(1)
|> Ash.read_one()
end)
end
end

attributes do
uuid_primary_key(:id)
attribute(:name, :string, public?: true)
end
end

defmodule AshGraphql.Test.GenericActionErrorTestResourceUpdate do
@moduledoc false
use Ash.Resource,
domain: nil,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]

graphql do
type :generic_action_error_test_resource_update

mutations do
# Wrong usage - generic action in update block
update :wrong_update_with_generic, :count_action
end
end

actions do
defaults([:create, :read, :update, :destroy])

action :count_action, :integer do
run(fn _input, _ ->
__MODULE__
|> Ash.count()
end)
end
end

attributes do
uuid_primary_key(:id)
attribute(:name, :string, public?: true)
end
end

defmodule AshGraphql.Test.GenericActionErrorTestResourceDestroy do
@moduledoc false
use Ash.Resource,
domain: nil,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]

graphql do
type :generic_action_error_test_resource_destroy

mutations do
# Wrong usage - generic action in destroy block
destroy :wrong_destroy_with_generic, :random_action
end
end

actions do
defaults([:create, :read, :update, :destroy])

action :random_action, :struct do
constraints(instance_of: __MODULE__)
allow_nil? true

run(fn _input, _ ->
__MODULE__
|> Ash.Query.limit(1)
|> Ash.read_one()
end)
end
end

attributes do
uuid_primary_key(:id)
attribute(:name, :string, public?: true)
end
end

defmodule AshGraphql.Test.GenericActionCorrectUsageResource do
@moduledoc false
use Ash.Resource,
domain: AshGraphql.Test.Domain,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]

graphql do
type :generic_action_correct_usage_resource

mutations do
# CORRECT usage - generic actions in action blocks
action(:correct_random, :random_action)
action(:correct_count, :count_action)
end
end

actions do
defaults([:create, :read, :update, :destroy])

action :random_action, :struct do
constraints(instance_of: __MODULE__)
allow_nil? true

run(fn _input, _ ->
__MODULE__
|> Ash.Query.limit(1)
|> Ash.read_one()
end)
end

action :count_action, :integer do
run(fn _input, _ ->
__MODULE__
|> Ash.count()
end)
end
end

attributes do
uuid_primary_key(:id)
attribute(:name, :string, public?: true)
end
end

defmodule AshGraphql.Test.GenericActionTypedActionsResource do
@moduledoc false
use Ash.Resource,
domain: AshGraphql.Test.Domain,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]

graphql do
type :generic_action_typed_actions_resource

mutations do
# CORRECT usage - typed actions in their respective blocks
create :correct_create, :create
update :correct_update, :update
destroy :correct_destroy, :destroy
end
end

actions do
defaults([:create, :read, :update, :destroy])
end

attributes do
uuid_primary_key(:id)
attribute(:name, :string, public?: true)
end
end