fix: include a missing module

fix: properly set filterability on attributes
This commit is contained in:
Zach Daniel 2022-02-07 16:36:51 -05:00
parent 079b941e90
commit 0268c06c63
6 changed files with 157 additions and 19 deletions

View file

@ -0,0 +1,24 @@
defmodule Ash.Error.Query.InvalidFilterReference do
@moduledoc "Used when an invalid reference is used in a filter"
use Ash.Error.Exception
def_ash_error([:field, :simple_equality?], class: :invalid)
defimpl Ash.ErrorKind do
def id(_), do: Ash.UUID.generate()
def code(_), do: "invalid_filter_reference"
def class(_), do: :invalid
def message(%{field: field, simple_equality?: true}) do
"#{field} cannot be referenced in filters, except by simple equality"
end
def message(%{field: field}) do
"#{field} cannot be referenced in filters"
end
def stacktrace(_), do: nil
end
end

View file

@ -302,14 +302,18 @@ defmodule Ash.Filter do
|> Enum.flat_map(fn ref ->
field = ref.attribute
case field.filterable? do
true ->
# This handles manually added calcualtions and aggregates
case Map.fetch(field, :filterable?) do
:error ->
[]
false ->
{:ok, true} ->
[]
{:ok, false} ->
[Ash.Error.Query.InvalidFilterReference.exception(field: field.name)]
:simple_equality ->
{:ok, :simple_equality} ->
if ref.simple_equality? do
[]
else
@ -326,7 +330,7 @@ defmodule Ash.Filter do
multiple_filter_errors =
refs
|> Enum.filter(fn ref ->
ref.attribute.filterable? == :simple_equality
Map.fetch(ref.attribute, :filterable?) == {:ok, :simple_equality}
end)
|> Enum.group_by(& &1.attribute.name)
|> Enum.flat_map(fn
@ -1840,7 +1844,8 @@ defmodule Ash.Filter do
aggregate.relationship_path,
aggregate_query,
aggregate.field,
aggregate.default
aggregate.default,
aggregate.filterable?
) do
case parse_predicates(nested_statement, query_aggregate, context) do
{:ok, nested_statement} ->
@ -1902,7 +1907,8 @@ defmodule Ash.Filter do
module,
opts,
resource_calculation.type,
Map.put(args, :context, context.query_context)
Map.put(args, :context, context.query_context),
resource_calculation.filterable?
) do
case parse_predicates(nested_statement, calculation, context) do
{:ok, nested_statement} ->
@ -2071,7 +2077,8 @@ defmodule Ash.Filter do
module,
opts,
resource_calculation.type,
Map.put(args, :context, context.query_context)
Map.put(args, :context, context.query_context),
resource_calculation.filterable?
) do
{:ok,
%Ref{
@ -2178,7 +2185,8 @@ defmodule Ash.Filter do
module,
opts,
resource_calculation.type,
Map.put(args, :context, context.query_context)
Map.put(args, :context, context.query_context),
resource_calculation.filterable?
) do
{:ok, %{ref | attribute: calculation, resource: related}}
else
@ -2199,7 +2207,8 @@ defmodule Ash.Filter do
aggregate.relationship_path,
aggregate_query,
aggregate.field,
aggregate.default
aggregate.default,
aggregate.filterable?
) do
{:ok, %{ref | attribute: query_aggregate, resource: related}}
else

View file

@ -10,7 +10,8 @@ defmodule Ash.Query.Aggregate do
:kind,
:type,
:authorization_filter,
:load
:load,
filterable?: true
]
@type t :: %__MODULE__{}
@ -27,7 +28,7 @@ defmodule Ash.Query.Aggregate do
@doc false
def kinds, do: @kinds
def new(resource, name, kind, relationship, query, field, default \\ nil) do
def new(resource, name, kind, relationship, query, field, default \\ nil, filterable? \\ true) do
field_type =
if field do
related = Ash.Resource.Info.related(resource, relationship)
@ -46,7 +47,8 @@ defmodule Ash.Query.Aggregate do
field: field,
kind: kind,
type: type,
query: query
query: query,
filterable?: filterable?
}}
end
end

View file

@ -10,12 +10,13 @@ defmodule Ash.Query.Calculation do
context: %{},
select: [],
sequence: 0,
allow_async?: false
allow_async?: false,
filterable?: true
]
@type t :: %__MODULE__{}
def new(name, module, opts, type, context \\ %{}) do
def new(name, module, opts, type, context \\ %{}, filterable? \\ true) do
case module.init(opts) do
{:ok, opts} ->
{:ok,
@ -24,7 +25,8 @@ defmodule Ash.Query.Calculation do
module: module,
type: type,
opts: opts,
context: context
context: context,
filterable?: filterable?
}}
{:error, error} ->

View file

@ -0,0 +1,83 @@
defmodule Ash.Query.Function.GetPath do
@moduledoc """
Gets the value at the provided path in the value, which must be a map or embed.
If you are using a datalayer that provides a `type` function (like AshPostgres), it is a good idea to
wrap your call in that function, e.g `type(author[:bio][:title], :string)`, since data layers that depend
on knowing types may not be able to infer the type from the path. Ash may eventually be able to figure out
the type, in the case that the path consists of only embedded attributes.
If an atom key is provided, access is *indiscriminate* of atoms vs strings. The atom key is checked first.
If a string key is provided, that is the only thing that is checked. If the value will or may be a struct, be sure to use atoms.
The data layer may handle this differently, for example, AshPostgres only checks
strings at the data layer (because thats all it can be in the database anyway).
Available in query expressions using bracket syntax, e.g `foo[:bar][:baz]`.
"""
use Ash.Query.Function, name: :get_path, predicate?: true, no_inspect?: true
def args,
do: [
[:map, {:array, :any}]
]
def new([%__MODULE__{arguments: [inner_left, inner_right]} = get_path, right])
when is_list(inner_right) and is_list(right) do
{:ok, %{get_path | arguments: [inner_left, inner_right ++ right]}}
end
def new([_, right]) when not (is_list(right) or is_atom(right) or is_binary(right)) do
{:error, "#{inspect(right)} is not a valid path to get"}
end
def new([left, right]) when not is_list(right) do
new([left, [right]])
end
def new([left, right]) do
super([left, right])
end
def evaluate(%{arguments: [%{} = obj, path]}) when is_list(path) do
Enum.reduce_while(path, {:known, obj}, fn key, {:known, obj} ->
if is_map(obj) do
value =
if is_atom(key) do
Map.get(obj, key) || Map.get(obj, to_string(key))
else
Map.get(obj, to_string(key))
end
case value do
nil ->
{:halt, {:known, nil}}
value ->
{:cont, {:known, value}}
end
else
{:halt, :unknown}
end
end)
end
def evaluate(_), do: :unknown
defimpl Inspect do
import Inspect.Algebra
def inspect(%{arguments: [value, path]}, opts) do
path_items =
path
|> Enum.map(fn item ->
concat(["[", to_doc(item, opts), "]"])
end)
|> concat()
value
|> to_doc(opts)
|> concat(path_items)
end
end
end

View file

@ -966,7 +966,8 @@ defmodule Ash.Query do
aggregate.relationship_path,
aggregate_query,
aggregate.field,
aggregate.default
aggregate.default,
aggregate.filterable?
) do
query_aggregate = %{query_aggregate | load: field}
new_aggregates = Map.put(query.aggregates, aggregate.name, query_aggregate)
@ -1379,7 +1380,15 @@ defmodule Ash.Query do
atom | list(atom),
Keyword.t() | nil
) :: t()
def aggregate(query, name, type, relationship, agg_query \\ nil) do
def aggregate(
query,
name,
type,
relationship,
agg_query \\ nil,
default \\ nil,
filterable? \\ true
) do
{field, agg_query} = Keyword.pop(agg_query || [], :field)
query = to_query(query)
@ -1395,7 +1404,16 @@ defmodule Ash.Query do
build(Ash.Resource.Info.related(query.resource, relationship), options)
end
case Aggregate.new(query.resource, name, type, relationship, agg_query, field) do
case Aggregate.new(
query.resource,
name,
type,
relationship,
agg_query,
field,
default,
filterable?
) do
{:ok, aggregate} ->
new_aggregates = Map.put(query.aggregates, aggregate.name, aggregate)