mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
fix: include a missing module
fix: properly set filterability on attributes
This commit is contained in:
parent
079b941e90
commit
0268c06c63
6 changed files with 157 additions and 19 deletions
24
lib/ash/error/query/invalid_filter_reference.ex
Normal file
24
lib/ash/error/query/invalid_filter_reference.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} ->
|
||||
|
|
83
lib/ash/query/function/get_path.ex
Normal file
83
lib/ash/query/function/get_path.ex
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue