mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
fix: properly support calc expansion in policies
fix: support `nil` cast for resource type
This commit is contained in:
parent
2a67a32874
commit
2a6fe49348
11 changed files with 118 additions and 67 deletions
|
@ -303,7 +303,7 @@ defmodule Ash.Actions.Sort do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, expression} ->
|
||||
case Ash.Expr.eval_hydrated(expression, record: record) do
|
||||
case Ash.Expr.eval_hydrated(expression, record: record, resource: resource) do
|
||||
{:ok, value} ->
|
||||
{:ok, value}
|
||||
|
||||
|
|
|
@ -383,6 +383,12 @@ defmodule Ash.Api do
|
|||
Values for references used by the calculation.
|
||||
""",
|
||||
default: %{}
|
||||
],
|
||||
actor: [
|
||||
type: :any,
|
||||
doc: """
|
||||
The actor for handling `^actor/1` templates.
|
||||
"""
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -423,6 +429,7 @@ defmodule Ash.Api do
|
|||
end
|
||||
end
|
||||
end)
|
||||
|> Map.put(:actor, opts[:actor])
|
||||
|
||||
if function_exported?(module, :expression, 2) do
|
||||
expr =
|
||||
|
|
|
@ -215,7 +215,8 @@ defmodule Ash.CodeInterface do
|
|||
|
||||
unquote(api).calculate!(unquote(resource), unquote(interface.calculation),
|
||||
refs: refs,
|
||||
args: arguments
|
||||
args: arguments,
|
||||
actor: opts[:actor]
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -243,7 +244,8 @@ defmodule Ash.CodeInterface do
|
|||
|
||||
unquote(api).calculate(unquote(resource), unquote(interface.calculation),
|
||||
refs: refs,
|
||||
args: arguments
|
||||
args: arguments,
|
||||
actor: opts[:actor]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -364,7 +364,7 @@ defmodule Ash.DataLayer.Ets do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, expression} ->
|
||||
case Ash.Expr.eval_hydrated(expression, record: record) do
|
||||
case Ash.Expr.eval_hydrated(expression, record: record, resource: resource) do
|
||||
{:ok, value} ->
|
||||
if calculation.load do
|
||||
{:cont, {:ok, Map.put(record, calculation.load, value)}}
|
||||
|
|
|
@ -35,7 +35,7 @@ defmodule Ash.Expr do
|
|||
|
||||
@doc false
|
||||
def eval_hydrated(expression, opts \\ []) do
|
||||
Ash.Filter.Runtime.do_match(opts[:record], expression, opts[:parent])
|
||||
Ash.Filter.Runtime.do_match(opts[:record], expression, opts[:parent], opts[:resource])
|
||||
end
|
||||
|
||||
defmacro where(left, right) do
|
||||
|
|
|
@ -203,7 +203,7 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def do_match(record, expression, parent \\ nil) do
|
||||
def do_match(record, expression, parent \\ nil, resource \\ nil) do
|
||||
hydrated =
|
||||
case record do
|
||||
%resource{} ->
|
||||
|
@ -213,18 +213,25 @@ defmodule Ash.Filter.Runtime do
|
|||
})
|
||||
|
||||
_ ->
|
||||
{:ok, expression}
|
||||
if resource do
|
||||
Ash.Filter.hydrate_refs(expression, %{
|
||||
resource: resource,
|
||||
public?: false
|
||||
})
|
||||
else
|
||||
{:ok, expression}
|
||||
end
|
||||
end
|
||||
|
||||
case hydrated do
|
||||
{:ok, expression} ->
|
||||
case expression do
|
||||
%Ash.Filter{expression: expression} ->
|
||||
do_match(record, expression, parent)
|
||||
do_match(record, expression, parent, resource)
|
||||
|
||||
%op{__operator__?: true, left: left, right: right} ->
|
||||
with {:ok, [left, right]} <-
|
||||
resolve_exprs([left, right], record, parent),
|
||||
resolve_exprs([left, right], record, parent, resource),
|
||||
{:op, {:ok, %op{} = new_operator}} <-
|
||||
{:op, Ash.Query.Operator.try_cast_with_ref(op, left, right)},
|
||||
{:known, val} <-
|
||||
|
@ -235,7 +242,7 @@ defmodule Ash.Filter.Runtime do
|
|||
{:error, error}
|
||||
|
||||
{:op, {:ok, expr}} ->
|
||||
do_match(record, expr, parent)
|
||||
do_match(record, expr, parent, resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -248,7 +255,7 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
|
||||
%func{__function__?: true, arguments: arguments} = function ->
|
||||
with {:ok, args} <- resolve_exprs(arguments, record, parent),
|
||||
with {:ok, args} <- resolve_exprs(arguments, record, parent, resource),
|
||||
{:args, args} when not is_nil(args) <-
|
||||
{:args, try_cast_arguments(func.args(), args)},
|
||||
{:known, val} <- func.evaluate(%{function | arguments: args}) do
|
||||
|
@ -269,7 +276,7 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
|
||||
%Not{expression: expression} ->
|
||||
case do_match(record, expression, parent) do
|
||||
case do_match(record, expression, parent, resource) do
|
||||
:unknown ->
|
||||
:unknown
|
||||
|
||||
|
@ -281,10 +288,10 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
|
||||
%Ash.Query.Exists{} = expr ->
|
||||
resolve_expr(expr, record, parent)
|
||||
resolve_expr(expr, record, parent, resource)
|
||||
|
||||
%Ash.Query.Parent{} = expr ->
|
||||
resolve_expr(expr, parent, nil)
|
||||
resolve_expr(expr, parent, nil, resource)
|
||||
|
||||
%BooleanExpression{op: op, left: left, right: right} ->
|
||||
expression_matches(op, left, right, record, parent)
|
||||
|
@ -293,12 +300,12 @@ defmodule Ash.Filter.Runtime do
|
|||
raise "Unresolvable filter component: #{inspect(call)}"
|
||||
|
||||
%Ref{} = ref ->
|
||||
resolve_expr(ref, record, parent)
|
||||
resolve_expr(ref, record, parent, resource)
|
||||
|
||||
value when is_list(value) ->
|
||||
value
|
||||
|> Enum.reduce_while({:ok, []}, fn value, {:ok, list} ->
|
||||
case do_match(record, value, parent) do
|
||||
case do_match(record, value, parent, resource) do
|
||||
{:ok, result} ->
|
||||
{:cont, {:ok, [result | list]}}
|
||||
|
||||
|
@ -338,10 +345,10 @@ defmodule Ash.Filter.Runtime do
|
|||
)
|
||||
end
|
||||
|
||||
defp resolve_exprs(exprs, record, parent) do
|
||||
defp resolve_exprs(exprs, record, parent, resource) do
|
||||
exprs
|
||||
|> Enum.reduce_while({:ok, []}, fn expr, {:ok, exprs} ->
|
||||
case resolve_expr(expr, record, parent) do
|
||||
case resolve_expr(expr, record, parent, resource) do
|
||||
{:ok, resolved} -> {:cont, {:ok, [resolved | exprs]}}
|
||||
{:error, error} -> {:halt, {:error, error}}
|
||||
:unknown -> {:halt, :unknown}
|
||||
|
@ -354,8 +361,8 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr({key, value}, record, parent) when is_atom(key) do
|
||||
case resolve_expr(value, record, parent) do
|
||||
defp resolve_expr({key, value}, record, parent, resource) when is_atom(key) do
|
||||
case resolve_expr(value, record, parent, resource) do
|
||||
{:ok, resolved} ->
|
||||
{:ok, {key, resolved}}
|
||||
|
||||
|
@ -364,31 +371,36 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ref{} = ref, record, parent) do
|
||||
resolve_ref(ref, record, parent)
|
||||
defp resolve_expr(%Ref{} = ref, record, parent, resource) do
|
||||
resolve_ref(ref, record, parent, resource)
|
||||
end
|
||||
|
||||
defp resolve_expr(%BooleanExpression{left: left, right: right}, record, parent) do
|
||||
with {:ok, left_resolved} <- resolve_expr(left, record, parent),
|
||||
{:ok, right_resolved} <- resolve_expr(right, record, parent) do
|
||||
defp resolve_expr(%BooleanExpression{left: left, right: right}, record, parent, resource) do
|
||||
with {:ok, left_resolved} <- resolve_expr(left, record, parent, resource),
|
||||
{:ok, right_resolved} <- resolve_expr(right, record, parent, resource) do
|
||||
{:ok, left_resolved && right_resolved}
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Not{expression: expression}, record, parent) do
|
||||
case resolve_expr(expression, record, parent) do
|
||||
defp resolve_expr(%Not{expression: expression}, record, parent, resource) do
|
||||
case resolve_expr(expression, record, parent, resource) do
|
||||
{:ok, resolved} -> {:ok, !resolved}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ash.Query.Parent{expr: expr}, _, parent) do
|
||||
resolve_expr(expr, parent, nil)
|
||||
defp resolve_expr(%Ash.Query.Parent{expr: expr}, _, parent, resource) do
|
||||
resolve_expr(expr, parent, nil, resource)
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ash.Query.Exists{}, nil, _parent), do: :unknown
|
||||
defp resolve_expr(%Ash.Query.Exists{}, nil, _parent, _resource), do: :unknown
|
||||
|
||||
defp resolve_expr(%Ash.Query.Exists{at_path: [], path: path, expr: expr}, record, _parent) do
|
||||
defp resolve_expr(
|
||||
%Ash.Query.Exists{at_path: [], path: path, expr: expr},
|
||||
record,
|
||||
_parent,
|
||||
resource
|
||||
) do
|
||||
record
|
||||
|> load_unflattened(path)
|
||||
|> get_related(path)
|
||||
|
@ -400,7 +412,7 @@ defmodule Ash.Filter.Runtime do
|
|||
related
|
||||
|> List.wrap()
|
||||
|> Enum.reduce_while({:ok, false}, fn related, {:ok, false} ->
|
||||
case resolve_expr(expr, related, record) do
|
||||
case resolve_expr(expr, related, record, resource) do
|
||||
{:ok, falsy} when falsy in [nil, false] ->
|
||||
{:cont, {:ok, false}}
|
||||
|
||||
|
@ -414,7 +426,7 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ash.Query.Exists{at_path: at_path} = exists, record, parent) do
|
||||
defp resolve_expr(%Ash.Query.Exists{at_path: at_path} = exists, record, parent, resource) do
|
||||
record
|
||||
|> get_related(at_path)
|
||||
|> case do
|
||||
|
@ -424,7 +436,7 @@ defmodule Ash.Filter.Runtime do
|
|||
related ->
|
||||
related
|
||||
|> Enum.reduce_while({:ok, false}, fn related, {:ok, false} ->
|
||||
case resolve_expr(%{exists | at_path: []}, related, parent) do
|
||||
case resolve_expr(%{exists | at_path: []}, related, parent, resource) do
|
||||
{:ok, true} ->
|
||||
{:halt, {:ok, true}}
|
||||
|
||||
|
@ -438,8 +450,8 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%mod{__predicate__?: _, left: left, right: right}, record, parent) do
|
||||
with {:ok, [left, right]} <- resolve_exprs([left, right], record, parent),
|
||||
defp resolve_expr(%mod{__predicate__?: _, left: left, right: right}, record, parent, resource) do
|
||||
with {:ok, [left, right]} <- resolve_exprs([left, right], record, parent, resource),
|
||||
{:op, {:ok, %mod{} = new_pred}} <-
|
||||
{:op, Ash.Query.Operator.try_cast_with_ref(mod, left, right)},
|
||||
{:known, val} <- mod.evaluate(new_pred) do
|
||||
|
@ -449,7 +461,7 @@ defmodule Ash.Filter.Runtime do
|
|||
{:error, error}
|
||||
|
||||
{:op, {:ok, expr}} ->
|
||||
resolve_expr(expr, record, parent)
|
||||
resolve_expr(expr, record, parent, resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -462,8 +474,8 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%mod{__predicate__?: _, arguments: args} = pred, record, parent) do
|
||||
with {:ok, args} <- resolve_exprs(args, record, parent),
|
||||
defp resolve_expr(%mod{__predicate__?: _, arguments: args} = pred, record, parent, resource) do
|
||||
with {:ok, args} <- resolve_exprs(args, record, parent, resource),
|
||||
{:args, args} when not is_nil(args) <-
|
||||
{:args, try_cast_arguments(mod.args(), args)},
|
||||
{:known, val} <- mod.evaluate(%{pred | arguments: args}) do
|
||||
|
@ -483,10 +495,10 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(list, record, parent) when is_list(list) do
|
||||
defp resolve_expr(list, record, parent, resource) when is_list(list) do
|
||||
list
|
||||
|> Enum.reduce_while({:ok, []}, fn item, {:ok, acc} ->
|
||||
case resolve_expr(item, record, parent) do
|
||||
case resolve_expr(item, record, parent, resource) do
|
||||
{:ok, result} ->
|
||||
{:cont, {:ok, [result | acc]}}
|
||||
|
||||
|
@ -503,7 +515,7 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(other, _, _), do: {:ok, other}
|
||||
defp resolve_expr(other, _, _, _), do: {:ok, other}
|
||||
|
||||
defp try_cast_arguments(:var_args, args) do
|
||||
Enum.map(args, fn _ -> :any end)
|
||||
|
@ -519,13 +531,6 @@ defmodule Ash.Filter.Runtime do
|
|||
|> Enum.find_value(&Ash.Query.Function.try_cast_arguments(&1, args))
|
||||
end
|
||||
|
||||
defp resolve_ref(%Ash.Query.Ref{attribute: attribute}, nil, _),
|
||||
do: :unknown |> or_default(attribute)
|
||||
|
||||
defp resolve_ref(_ref, nil, _) do
|
||||
:unknown
|
||||
end
|
||||
|
||||
defp resolve_ref(
|
||||
%Ash.Query.Ref{
|
||||
relationship_path: relationship_path,
|
||||
|
@ -537,7 +542,8 @@ defmodule Ash.Filter.Runtime do
|
|||
}
|
||||
} = ref,
|
||||
record,
|
||||
parent
|
||||
parent,
|
||||
resource
|
||||
) do
|
||||
if function_exported?(module, :expression, 2) do
|
||||
expression = module.expression(opts, context)
|
||||
|
@ -551,20 +557,45 @@ defmodule Ash.Filter.Runtime do
|
|||
})
|
||||
|
||||
_ ->
|
||||
{:ok, expression}
|
||||
if resource do
|
||||
Ash.Filter.hydrate_refs(expression, %{
|
||||
resource: resource,
|
||||
public?: false
|
||||
})
|
||||
else
|
||||
{:ok, expression}
|
||||
end
|
||||
end
|
||||
|
||||
with {:ok, hydrated} <- hydrated do
|
||||
hydrated
|
||||
|> Ash.Filter.prefix_refs(relationship_path)
|
||||
|> resolve_expr(record, parent)
|
||||
|> resolve_expr(record, parent, resource)
|
||||
end
|
||||
else
|
||||
resolve_ref(%{ref | attribute: %Ash.Resource.Attribute{name: name}}, record, parent)
|
||||
# This is problematic with variadic loads
|
||||
resolve_ref(
|
||||
%{ref | attribute: %Ash.Resource.Attribute{name: name}},
|
||||
record,
|
||||
parent,
|
||||
resource
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_ref(%Ref{attribute: attribute, relationship_path: path}, record, _parent) do
|
||||
defp resolve_ref(%Ash.Query.Ref{attribute: attribute}, nil, _, _resource),
|
||||
do: :unknown |> or_default(attribute)
|
||||
|
||||
defp resolve_ref(_ref, nil, _, _resource) do
|
||||
:unknown
|
||||
end
|
||||
|
||||
defp resolve_ref(
|
||||
%Ref{attribute: attribute, relationship_path: path},
|
||||
record,
|
||||
_parent,
|
||||
_resource
|
||||
) do
|
||||
name =
|
||||
case attribute do
|
||||
%{name: name} -> name
|
||||
|
@ -627,7 +658,7 @@ defmodule Ash.Filter.Runtime do
|
|||
|> or_default(attribute)
|
||||
end
|
||||
|
||||
defp resolve_ref(_value, _record, _), do: :unknown
|
||||
defp resolve_ref(_value, _record, _, _), do: :unknown
|
||||
|
||||
defp or_default(:unknown, %Ash.Resource.Aggregate{default: default}) when not is_nil(default) do
|
||||
if is_function(default) do
|
||||
|
|
|
@ -89,7 +89,7 @@ defmodule Ash.Policy.FilterCheck do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: query.resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
|
@ -107,7 +107,7 @@ defmodule Ash.Policy.FilterCheck do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -135,7 +135,7 @@ defmodule Ash.Policy.FilterCheck do
|
|||
nil
|
||||
end
|
||||
|
||||
Ash.Expr.eval_hydrated(hydrated, record: data)
|
||||
Ash.Expr.eval_hydrated(hydrated, record: data, resource: resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
|
@ -150,7 +150,7 @@ defmodule Ash.Policy.FilterCheck do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource, resource: resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
|
|
|
@ -73,7 +73,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -91,7 +91,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -119,7 +119,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
nil
|
||||
end
|
||||
|
||||
Ash.Expr.eval_hydrated(hydrated, record: data)
|
||||
Ash.Expr.eval_hydrated(hydrated, record: data, resource: resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -134,7 +134,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
|
|
@ -51,7 +51,7 @@ defmodule Ash.Resource do
|
|||
def storage_type, do: :map
|
||||
|
||||
@impl Ash.Type
|
||||
def cast_input(nil, _), do: nil
|
||||
def cast_input(nil, _), do: {:ok, nil}
|
||||
def cast_input(%struct{} = value, _) when struct == __MODULE__, do: {:ok, value}
|
||||
|
||||
@impl Ash.Type
|
||||
|
|
|
@ -4,7 +4,12 @@ defmodule Ash.Resource.Calculation.Expression do
|
|||
|
||||
def expression(opts, context) do
|
||||
expr =
|
||||
Ash.Filter.build_filter_from_template(opts[:expr], nil, context, context[:context] || %{})
|
||||
Ash.Filter.build_filter_from_template(
|
||||
opts[:expr],
|
||||
context[:actor],
|
||||
context,
|
||||
context[:context] || %{}
|
||||
)
|
||||
|
||||
if context[:ash][:type] do
|
||||
{:ok, expr} =
|
||||
|
@ -34,7 +39,7 @@ defmodule Ash.Resource.Calculation.Expression do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, expression} ->
|
||||
case Ash.Expr.eval_hydrated(expression, record: record) do
|
||||
case Ash.Expr.eval_hydrated(expression, record: record, resource: resource) do
|
||||
{:ok, value} ->
|
||||
{:cont, {:ok, [value | values]}}
|
||||
|
||||
|
|
|
@ -34,7 +34,13 @@ defmodule Ash.Test.Support.PolicySimple.Tweet do
|
|||
end
|
||||
|
||||
policy action(:create_foo) do
|
||||
authorize_if expr(^arg(:foo) == "foo")
|
||||
authorize_if expr(is_foo(foo: arg(:foo)))
|
||||
end
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate :is_foo, :boolean, expr(^arg(:foo) == "foo") do
|
||||
argument :foo, :string, allow_nil?: false
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue