mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
improvement: add get_by
and get_by_identity
to code interface
improvement: compile time validations for managed relationships
This commit is contained in:
parent
f1a51532b0
commit
5e6442c3e4
7 changed files with 200 additions and 12 deletions
|
@ -53,6 +53,8 @@ locals_without_parens = [
|
|||
first: 4,
|
||||
generated?: 1,
|
||||
get?: 1,
|
||||
get_by: 1,
|
||||
get_by_identity: 1,
|
||||
global?: 1,
|
||||
has_many: 2,
|
||||
has_many: 3,
|
||||
|
|
|
@ -23,9 +23,26 @@ defmodule Ash.CodeInterface do
|
|||
for interface <- Ash.Resource.Info.interfaces(resource) do
|
||||
action = Ash.CodeInterface.require_action(resource, interface)
|
||||
|
||||
args = interface.args || []
|
||||
filter_keys =
|
||||
if action.type == :read do
|
||||
if interface.get_by_identity do
|
||||
Ash.Resource.Info.identity(resource, interface.get_by_identity).keys
|
||||
else
|
||||
if interface.get_by do
|
||||
interface.get_by
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
args = List.wrap(filter_keys) ++ (interface.args || [])
|
||||
arg_vars = Enum.map(args, &{&1, [], Elixir})
|
||||
|
||||
unless Enum.uniq(args) == args do
|
||||
raise """
|
||||
Arguments #{inspect(args)} for #{interface.name} are not unique!
|
||||
"""
|
||||
end
|
||||
|
||||
doc = """
|
||||
#{action.description || "Calls the #{action.name} action on the #{inspect(resource)} resource."}
|
||||
|
||||
|
@ -62,13 +79,27 @@ defmodule Ash.CodeInterface do
|
|||
end)
|
||||
|
||||
query =
|
||||
opts[:query]
|
||||
|> Kernel.||(unquote(resource))
|
||||
|> Ash.Query.for_read(
|
||||
unquote(action.name),
|
||||
input,
|
||||
Keyword.take(opts, [:actor, :tenant])
|
||||
)
|
||||
if unquote(filter_keys) do
|
||||
require Ash.Query
|
||||
{filters, input} = Map.split(input, unquote(filter_keys))
|
||||
|
||||
opts[:query]
|
||||
|> Kernel.||(unquote(resource))
|
||||
|> Ash.Query.for_read(
|
||||
unquote(action.name),
|
||||
input,
|
||||
Keyword.take(opts, [:actor, :tenant])
|
||||
)
|
||||
|> Ash.Query.filter(filters)
|
||||
else
|
||||
opts[:query]
|
||||
|> Kernel.||(unquote(resource))
|
||||
|> Ash.Query.for_read(
|
||||
unquote(action.name),
|
||||
input,
|
||||
Keyword.take(opts, [:actor, :tenant])
|
||||
)
|
||||
end
|
||||
|
||||
if unquote(interface.get?) do
|
||||
query
|
||||
|
|
|
@ -745,6 +745,7 @@ defmodule Ash.Resource.Dsl do
|
|||
],
|
||||
target: Ash.Resource.Interface,
|
||||
schema: Ash.Resource.Interface.schema(),
|
||||
transform: {Ash.Resource.Interface, :transform, []},
|
||||
args: [:name]
|
||||
}
|
||||
|
||||
|
@ -1067,6 +1068,7 @@ defmodule Ash.Resource.Dsl do
|
|||
]
|
||||
|
||||
@transformers [
|
||||
Ash.Resource.Transformers.ValidateManagedRelationshipOpts,
|
||||
Ash.Resource.Transformers.RequireUniqueActionNames,
|
||||
Ash.Resource.Transformers.SetRelationshipSource,
|
||||
Ash.Resource.Transformers.BelongsToAttribute,
|
||||
|
|
|
@ -129,6 +129,14 @@ defmodule Ash.Resource.Info do
|
|||
Extension.get_entities(resource, [:identities])
|
||||
end
|
||||
|
||||
@doc "Get an identity by name from the resource"
|
||||
@spec identity(Ash.Resource.t(), atom) :: Ash.Resource.Identity.t() | nil
|
||||
def identity(resource, name) do
|
||||
resource
|
||||
|> identities()
|
||||
|> Enum.find(&(&1.name == name))
|
||||
end
|
||||
|
||||
@doc "A list of authorizers to be used when accessing"
|
||||
@spec authorizers(Ash.Resource.t()) :: [module]
|
||||
def authorizers(resource) do
|
||||
|
@ -268,12 +276,12 @@ defmodule Ash.Resource.Info do
|
|||
resource,
|
||||
[:multitenancy],
|
||||
:parse_attribute,
|
||||
{__MODULE__, :identity, []}
|
||||
{__MODULE__, :_identity, []}
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def identity(x), do: x
|
||||
def _identity(x), do: x
|
||||
|
||||
@spec multitenancy_global?(Ash.Resource.t()) :: atom | nil
|
||||
def multitenancy_global?(resource) do
|
||||
|
|
|
@ -2,7 +2,7 @@ defmodule Ash.Resource.Interface do
|
|||
@moduledoc """
|
||||
Represents a function in a resource's code interface
|
||||
"""
|
||||
defstruct [:name, :action, :args, :get?]
|
||||
defstruct [:name, :action, :args, :get?, :get_by, :get_by_identity]
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
|
@ -19,6 +19,14 @@ defmodule Ash.Resource.Interface do
|
|||
end
|
||||
end
|
||||
|
||||
def transform(definition) do
|
||||
if definition.get_by || definition.get_by_identity do
|
||||
{:ok, %{definition | get?: true}}
|
||||
else
|
||||
{:ok, definition}
|
||||
end
|
||||
end
|
||||
|
||||
def interface_options(action_type) do
|
||||
[
|
||||
tenant: [
|
||||
|
@ -89,7 +97,29 @@ defmodule Ash.Resource.Interface do
|
|||
doc: """
|
||||
Only relevant for read actions. Expects to only receive a single result from a read action.
|
||||
|
||||
For example, `get_user_by_email`.
|
||||
The action should return a single result based on any arguments provided. To make it so that the function
|
||||
takes a specific field, and filters on that field, use `get_by` instead.
|
||||
|
||||
Useful for creating functions like `get_user_by_email` that map to an action that has an `:email` argument.
|
||||
"""
|
||||
],
|
||||
get_by: [
|
||||
type: {:list, :atom},
|
||||
doc: """
|
||||
Only relevant for read actions. Takes a list of fields and adds those fields as arguments, which will then be used to filter.
|
||||
|
||||
Automatically sets `get?` to `true`.
|
||||
|
||||
The action should return a single result based on any arguments provided. To make it so that the function
|
||||
takes a specific field, and filters on that field, use `get_by` instead. When combined, `get_by` takes precedence.
|
||||
|
||||
Useful for creating functions like `get_user_by_id` that map to a basic read action.
|
||||
"""
|
||||
],
|
||||
get_by_identity: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
Only relevant for read actions. Takes an identity, and gets its field list, performing the same logic as `get_by` once it has the list of fields.
|
||||
"""
|
||||
]
|
||||
]
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
defmodule Ash.Resource.Transformers.ValidateManagedRelationshipOpts do
|
||||
@moduledoc """
|
||||
Confirms that all action types declared on a resource are supported by its data layer
|
||||
"""
|
||||
use Ash.Dsl.Transformer
|
||||
|
||||
alias Ash.Changeset.ManagedRelationshipHelpers
|
||||
alias Ash.Dsl.Transformer
|
||||
|
||||
def after_compile?, do: true
|
||||
|
||||
def transform(resource, dsl_state) do
|
||||
dsl_state
|
||||
|> Transformer.get_entities([:actions])
|
||||
|> Enum.reject(&(&1.type == :read))
|
||||
|> Enum.each(fn action ->
|
||||
action.changes
|
||||
|> Enum.filter(
|
||||
&match?(%Ash.Resource.Change{change: {Ash.Resource.Change.ManageRelationship, _}}, &1)
|
||||
)
|
||||
|> Enum.each(fn %Ash.Resource.Change{change: {_, opts}} ->
|
||||
unless Enum.find(action.arguments, &(&1.name == opts[:argument])) do
|
||||
raise Ash.Error.Dsl.DslError,
|
||||
module: resource,
|
||||
path:
|
||||
[
|
||||
:actions,
|
||||
action.type,
|
||||
action.name,
|
||||
:change,
|
||||
:manage_relationship
|
||||
] ++ Enum.uniq([opts[:argument], opts[:relationship]]),
|
||||
message: "Action #{action.name} has no argument `#{inspect(opts[:argument])}`."
|
||||
end
|
||||
|
||||
relationship =
|
||||
Ash.Resource.Info.relationship(resource, opts[:relationship]) ||
|
||||
raise Ash.Error.Dsl.DslError,
|
||||
module: resource,
|
||||
path:
|
||||
[
|
||||
:actions,
|
||||
action.type,
|
||||
action.name,
|
||||
:change,
|
||||
:manage_relationship
|
||||
] ++ Enum.uniq([opts[:argument], opts[:relationship]]),
|
||||
message: "No such relationship #{opts[:relationship]} exists."
|
||||
|
||||
if ensure_compiled?(relationship) do
|
||||
try do
|
||||
manage_opts =
|
||||
if opts[:opts][:type] do
|
||||
defaults = Ash.Changeset.manage_relationship_opts(opts[:opts][:type])
|
||||
|
||||
Enum.reduce(defaults, Ash.Changeset.manage_relationship_schema(), fn {key, value},
|
||||
manage_opts ->
|
||||
Ash.OptionsHelpers.set_default!(manage_opts, key, value)
|
||||
end)
|
||||
else
|
||||
Ash.Changeset.manage_relationship_schema()
|
||||
end
|
||||
|
||||
opts = Ash.OptionsHelpers.validate!(opts[:opts], manage_opts)
|
||||
|
||||
ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
|
||||
rescue
|
||||
e ->
|
||||
reraise Ash.Error.Dsl.DslError,
|
||||
[
|
||||
module: resource,
|
||||
path:
|
||||
[
|
||||
:actions,
|
||||
action.type,
|
||||
action.name,
|
||||
:change,
|
||||
:manage_relationship
|
||||
] ++ Enum.uniq([opts[:argument], opts[:relationship]]),
|
||||
message: """
|
||||
The following error was raised when validating options provided to manage_relationship.
|
||||
|
||||
#{Exception.format(:error, e, __STACKTRACE__)}
|
||||
"""
|
||||
],
|
||||
__STACKTRACE__
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
{:ok, dsl_state}
|
||||
end
|
||||
|
||||
defp ensure_compiled?(%Ash.Resource.Relationships.ManyToMany{
|
||||
through: through,
|
||||
destination: destination
|
||||
}) do
|
||||
Code.ensure_loaded?(through) && Code.ensure_loaded?(destination)
|
||||
end
|
||||
|
||||
defp ensure_compiled?(%{destination: destination}) do
|
||||
Code.ensure_loaded?(destination)
|
||||
end
|
||||
end
|
|
@ -14,6 +14,7 @@ defmodule Ash.Test.CodeInterfaceTest do
|
|||
define_for Ash.Test.CodeInterfaceTest.Api
|
||||
define :get_user, action: :read, get?: true, args: [:id]
|
||||
define :read_users, action: :read
|
||||
define :get_by_id, action: :read, get_by: [:id]
|
||||
end
|
||||
|
||||
actions do
|
||||
|
@ -58,4 +59,13 @@ defmodule Ash.Test.CodeInterfaceTest do
|
|||
User.get_user!(Ash.UUID.generate())
|
||||
end
|
||||
end
|
||||
|
||||
test "get_by! adds the proper arguments and filters" do
|
||||
user =
|
||||
User
|
||||
|> Ash.Changeset.for_create(:create, %{first_name: "ted", last_name: "Danson"})
|
||||
|> Api.create!()
|
||||
|
||||
assert User.get_by_id!(user.id).id == user.id
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue