mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
WIP
This commit is contained in:
parent
2f85a1385f
commit
fc259c9012
19 changed files with 451 additions and 344 deletions
|
@ -112,4 +112,6 @@ end
|
||||||
* Factor out shared relationship options into its own schema, and merge them, for clearer docs.
|
* Factor out shared relationship options into its own schema, and merge them, for clearer docs.
|
||||||
* Consider making a "params builder" so you can say things like `Ash.Params.add_side_load(params, [:foo, :bar, :baz])` and build params up over time.
|
* Consider making a "params builder" so you can say things like `Ash.Params.add_side_load(params, [:foo, :bar, :baz])` and build params up over time.
|
||||||
* validate using composite primary keys using the `data_layer.can?(:composite_primary_key)`
|
* validate using composite primary keys using the `data_layer.can?(:composite_primary_key)`
|
||||||
* Think hard about the data_layer.can? pattern to make sure we're giving enough info, but not too much.
|
* Think hard about the data_layer.can? pattern to make sure we're giving enough info, but not too much.
|
||||||
|
* Use the sat solver at compile time to tell people when requests they've configured (and maybe all combinations of includes they've allowed?) couldn't possibly be allowed together.
|
||||||
|
* Support arbitrary "through" relationships
|
|
@ -68,13 +68,16 @@ defmodule Ash.Actions.ChangesetHelpers do
|
||||||
{:ok, filter} ->
|
{:ok, filter} ->
|
||||||
before_change(changeset, fn changeset ->
|
before_change(changeset, fn changeset ->
|
||||||
case api.get(destination, filter, authorize?: authorize?, user: user) do
|
case api.get(destination, filter, authorize?: authorize?, user: user) do
|
||||||
{:ok, record} ->
|
{:ok, record} when not is_nil(record) ->
|
||||||
changeset
|
changeset
|
||||||
|> Ecto.Changeset.put_change(source_field, Map.get(record, destination_field))
|
|> Ecto.Changeset.put_change(source_field, Map.get(record, destination_field))
|
||||||
|> after_change(fn _changeset, result ->
|
|> after_change(fn _changeset, result ->
|
||||||
{:ok, Map.put(result, relationship.name, record)}
|
{:ok, Map.put(result, relationship.name, record)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
{:ok, nil} ->
|
||||||
|
{:error, "not found"}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error, error}
|
{:error, error}
|
||||||
end
|
end
|
||||||
|
@ -178,7 +181,7 @@ defmodule Ash.Actions.ChangesetHelpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp relate_items(api, to_relate, destination_field, destination_field_value, authorize?, user) do
|
defp relate_items(api, to_relate, _destination_field, destination_field_value, authorize?, user) do
|
||||||
Enum.reduce(to_relate, {:ok, []}, fn
|
Enum.reduce(to_relate, {:ok, []}, fn
|
||||||
to_be_related, {:ok, now_related} ->
|
to_be_related, {:ok, now_related} ->
|
||||||
case api.update(to_be_related,
|
case api.update(to_be_related,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
defmodule Ash.Actions.Create do
|
defmodule Ash.Actions.Create do
|
||||||
alias Ash.Authorization.Authorizer
|
alias Ash.Authorization.Authorizer
|
||||||
alias Ash.Actions.SideLoad
|
|
||||||
alias Ash.Actions.ChangesetHelpers
|
alias Ash.Actions.ChangesetHelpers
|
||||||
|
|
||||||
@spec run(Ash.api(), Ash.resource(), Ash.action(), Ash.params()) ::
|
@spec run(Ash.api(), Ash.resource(), Ash.action(), Ash.params()) ::
|
||||||
|
@ -32,7 +31,7 @@ defmodule Ash.Actions.Create do
|
||||||
auth_request =
|
auth_request =
|
||||||
Ash.Authorization.Request.new(
|
Ash.Authorization.Request.new(
|
||||||
resource: resource,
|
resource: resource,
|
||||||
rules: action.rules,
|
authorization_steps: action.authorization_steps,
|
||||||
changeset: changeset
|
changeset: changeset
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,14 @@ defmodule Ash.Actions.Read do
|
||||||
user = Keyword.get(params, :user, [])
|
user = Keyword.get(params, :user, [])
|
||||||
side_loads = Keyword.get(params, :side_load, [])
|
side_loads = Keyword.get(params, :side_load, [])
|
||||||
|
|
||||||
|
# * We need to be able to perform relationship filters when there is no way to perform a join.
|
||||||
|
# * There will be unpredictable crossover between what data is fetched during authorization
|
||||||
|
# and the joins/relationship filters applied.
|
||||||
|
# * Thus: The first pass will be done without this cache, and as such will not have good
|
||||||
|
# performance characteristics.
|
||||||
|
# * The addition of a cache that keys on filters, and compares filters to determine if the data
|
||||||
|
# has been cached, should improve those performance characteristics significantly.
|
||||||
|
|
||||||
with %Ash.Filter{errors: []} = filter <-
|
with %Ash.Filter{errors: []} = filter <-
|
||||||
Ash.Filter.parse(resource, filter),
|
Ash.Filter.parse(resource, filter),
|
||||||
{:ok, side_load_auths} <- SideLoad.process(resource, side_loads, filter),
|
{:ok, side_load_auths} <- SideLoad.process(resource, side_loads, filter),
|
||||||
|
@ -32,7 +40,11 @@ defmodule Ash.Actions.Read do
|
||||||
defp do_authorize(params, action, user, resource, filter, side_load_auths) do
|
defp do_authorize(params, action, user, resource, filter, side_load_auths) do
|
||||||
if Keyword.get(params, :authorize?, false) do
|
if Keyword.get(params, :authorize?, false) do
|
||||||
auth_request =
|
auth_request =
|
||||||
Ash.Authorization.Request.new(resource: resource, rules: action.rules, filter: filter)
|
Ash.Authorization.Request.new(
|
||||||
|
resource: resource,
|
||||||
|
authorization_steps: action.rules,
|
||||||
|
filter: filter
|
||||||
|
)
|
||||||
|
|
||||||
Authorizer.authorize(user, [auth_request | side_load_auths])
|
Authorizer.authorize(user, [auth_request | side_load_auths])
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
defmodule Ash.Actions.Update do
|
defmodule Ash.Actions.Update do
|
||||||
alias Ash.Authorization.Authorizer
|
alias Ash.Authorization.Authorizer
|
||||||
alias Ash.Actions.SideLoad
|
|
||||||
alias Ash.Actions.ChangesetHelpers
|
alias Ash.Actions.ChangesetHelpers
|
||||||
|
|
||||||
@spec run(Ash.api(), Ash.record(), Ash.action(), Ash.params()) ::
|
@spec run(Ash.api(), Ash.record(), Ash.action(), Ash.params()) ::
|
||||||
|
@ -38,7 +37,7 @@ defmodule Ash.Actions.Update do
|
||||||
auth_request =
|
auth_request =
|
||||||
Ash.Authorization.Request.new(
|
Ash.Authorization.Request.new(
|
||||||
resource: resource,
|
resource: resource,
|
||||||
rules: action.rules,
|
authorization_steps: action.authorization_steps,
|
||||||
changeset: changeset
|
changeset: changeset
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@ defmodule Ash.Api.Interface do
|
||||||
raise Ash.Error.FrameworkError.exception(message: error)
|
raise Ash.Error.FrameworkError.exception(message: error)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unwrap_or_raise!({:error, %Ecto.Changeset{} = cs}) do
|
defp unwrap_or_raise!({:error, %Ecto.Changeset{}}) do
|
||||||
raise(Ash.Error.FrameworkError, message: "invalid changes")
|
raise(Ash.Error.FrameworkError, message: "invalid changes")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,187 +1,7 @@
|
||||||
defmodule Ash.Authorization.Authorizer do
|
defmodule Ash.Authorization.Authorizer do
|
||||||
alias Ash.Authorization.Rule
|
|
||||||
|
|
||||||
@type result :: :authorized | :forbidden | :undecided
|
@type result :: :authorized | :forbidden | :undecided
|
||||||
|
|
||||||
def authorize(user, requests) do
|
def authorize(_user, _requests) do
|
||||||
:authorized
|
:authorized
|
||||||
end
|
end
|
||||||
|
|
||||||
# def run_precheck(_, _) do
|
|
||||||
# :authorized
|
|
||||||
# end
|
|
||||||
|
|
||||||
# ######
|
|
||||||
|
|
||||||
# def authorize(user, :none, context, callback) do
|
|
||||||
# callback.(fn _user, _data, _rules, _context ->
|
|
||||||
# :authorized
|
|
||||||
# end)
|
|
||||||
# end
|
|
||||||
|
|
||||||
# def authorize(user, rules, context, callback) do
|
|
||||||
# case authorize_precheck(user, rules, context) do
|
|
||||||
# {%{prediction: :unknown}, per_check_data} ->
|
|
||||||
# callback.(fn user, data, rules, context ->
|
|
||||||
# do_authorize(user, data, rules, context, per_check_data)
|
|
||||||
# end)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp authorize_precheck(user, rules, context) do
|
|
||||||
# rules
|
|
||||||
# |> Enum.reduce({%{}, []}, fn rule, {instructions, per_check_data} ->
|
|
||||||
# {instructions, check_data} =
|
|
||||||
# rule
|
|
||||||
# |> precheck_result(user, context)
|
|
||||||
# |> List.wrap()
|
|
||||||
# |> Enum.reduce({instructions, %{}}, &handle_precheck_result/2)
|
|
||||||
|
|
||||||
# {instructions, [check_data | per_check_data]}
|
|
||||||
# end)
|
|
||||||
# |> predict_result(rules)
|
|
||||||
# end
|
|
||||||
|
|
||||||
# # Never call authorize w/o first calling authorize_precheck before
|
|
||||||
# # the operation
|
|
||||||
# defp do_authorize(user, data, rules, context, per_check_data) do
|
|
||||||
# {_decision, remaining_records} =
|
|
||||||
# rules
|
|
||||||
# |> Enum.zip(per_check_data)
|
|
||||||
# |> Enum.reduce({:undecided, data}, fn
|
|
||||||
# {rule, per_check_data}, {:undecided, data} ->
|
|
||||||
# rule_with_per_check_data =
|
|
||||||
# case per_check_data do
|
|
||||||
# %{decision: value} ->
|
|
||||||
# %{rule | check: fn _, _, _ -> value end}
|
|
||||||
|
|
||||||
# _ ->
|
|
||||||
# rule
|
|
||||||
# end
|
|
||||||
|
|
||||||
# full_context = Map.merge(context, Map.get(per_check_data, :context) || %{})
|
|
||||||
|
|
||||||
# checked_records = run_check(rule_with_per_check_data, user, data, full_context)
|
|
||||||
|
|
||||||
# if Enum.any?(checked_records, &(&1.__authorization_decision__ == :forbidden)) do
|
|
||||||
# {:forbiden, data}
|
|
||||||
# else
|
|
||||||
# remaining_records =
|
|
||||||
# Enum.reject(checked_records, &(&1.__authorization_decision__ == :authorized))
|
|
||||||
|
|
||||||
# if Enum.empty?(remaining_records) do
|
|
||||||
# {:allow, []}
|
|
||||||
# else
|
|
||||||
# {:undecided, remaining_records}
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# _, {decision, data} ->
|
|
||||||
# {decision, data}
|
|
||||||
# end)
|
|
||||||
|
|
||||||
# if Enum.empty?(remaining_records) do
|
|
||||||
# :authorized
|
|
||||||
# else
|
|
||||||
# # Return some kind of information here?
|
|
||||||
# # Maybe full auth breakdown in dev envs?
|
|
||||||
# {:forbidden, nil}
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp run_check(
|
|
||||||
# %{check: check, kind: kind},
|
|
||||||
# user,
|
|
||||||
# data,
|
|
||||||
# context
|
|
||||||
# ) do
|
|
||||||
# check_function =
|
|
||||||
# case check do
|
|
||||||
# {module, function, args} ->
|
|
||||||
# fn user, data, context ->
|
|
||||||
# apply(module, function, [user, data, context] ++ args)
|
|
||||||
# end
|
|
||||||
|
|
||||||
# function ->
|
|
||||||
# function
|
|
||||||
# end
|
|
||||||
|
|
||||||
# result = check_function.(user, data, context)
|
|
||||||
|
|
||||||
# Enum.map(data, fn item ->
|
|
||||||
# result =
|
|
||||||
# case result do
|
|
||||||
# true -> true
|
|
||||||
# false -> false
|
|
||||||
# ids -> item.id in ids
|
|
||||||
# end
|
|
||||||
|
|
||||||
# decision = Rule.result_to_decision(kind, result)
|
|
||||||
# Map.put(item, :__authorization_decision__, decision)
|
|
||||||
# end)
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp predict_result({instructions, per_check_data}, rules) do
|
|
||||||
# prediction = get_prediction(Enum.zip(rules, per_check_data))
|
|
||||||
|
|
||||||
# {Map.put(instructions, :prediction, prediction), per_check_data}
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp get_prediction([]), do: :unknown
|
|
||||||
|
|
||||||
# defp get_prediction([{rule, %{decision: value}} | rest]) do
|
|
||||||
# case Rule.result_to_decision(rule.kind, value) do
|
|
||||||
# :authorized -> :authorized
|
|
||||||
# :forbidden -> :forbidden
|
|
||||||
# :undecided -> get_prediction(rest)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp get_prediction([{rule, _} | rest]) do
|
|
||||||
# result_if_true = Rule.result_to_decision(rule.kind, true)
|
|
||||||
# result_if_false = Rule.result_to_decision(rule.kind, false)
|
|
||||||
|
|
||||||
# if result_if_true != :authorized and result_if_false != :authorized do
|
|
||||||
# :forbidden
|
|
||||||
# else
|
|
||||||
# get_prediction(rest)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp handle_precheck_result(nil, instructions_and_data), do: instructions_and_data
|
|
||||||
# defp handle_precheck_result(:ok, instructions_and_data), do: instructions_and_data
|
|
||||||
|
|
||||||
# defp handle_precheck_result({:context, context}, {instructions, data}) do
|
|
||||||
# {instructions, Map.update(data, :context, context, &Map.merge(&1, context))}
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp handle_precheck_result({:precheck, boolean}, {instructions, data})
|
|
||||||
# when is_boolean(boolean) do
|
|
||||||
# {instructions, Map.put(data, :precheck, boolean)}
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp handle_precheck_result({:side_load, relationship}, {instructions, data}) do
|
|
||||||
# new_instructions =
|
|
||||||
# instructions
|
|
||||||
# |> Map.put_new(:side_load, [])
|
|
||||||
# |> Map.update!(:side_load, &Keyword.put_new(&1, relationship, []))
|
|
||||||
|
|
||||||
# {new_instructions, data}
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp precheck_result(%{decision: nil}, _user, _context), do: nil
|
|
||||||
|
|
||||||
# defp precheck_result(%{decision: precheck}, user, context) do
|
|
||||||
# case precheck do
|
|
||||||
# {module, function, args} ->
|
|
||||||
# if function_exported?(module, function, Enum.count(args) + 2) do
|
|
||||||
# apply(module, function, [user, context] ++ args)
|
|
||||||
# else
|
|
||||||
# nil
|
|
||||||
# end
|
|
||||||
|
|
||||||
# function ->
|
|
||||||
# function.(user, context)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
defmodule Ash.Authorization.Request do
|
defmodule Ash.Authorization.Request do
|
||||||
defstruct [:resource, :rules, :filter]
|
defstruct [:resource, :authorization_steps, :filter]
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
resource: Ash.resource(),
|
||||||
|
authorization_steps: list(term),
|
||||||
|
filter: Ash.Filter.t()
|
||||||
|
}
|
||||||
|
|
||||||
def new(opts) do
|
def new(opts) do
|
||||||
struct!(__MODULE__, opts)
|
struct!(__MODULE__, opts)
|
||||||
|
|
|
@ -14,6 +14,8 @@ defmodule Ash.DataLayer.Ets do
|
||||||
|
|
||||||
@behaviour Ash.DataLayer
|
@behaviour Ash.DataLayer
|
||||||
|
|
||||||
|
alias Ash.Filter.{Eq, In, And, Or}
|
||||||
|
|
||||||
defmacro __using__(opts) do
|
defmacro __using__(opts) do
|
||||||
quote bind_quoted: [opts: opts] do
|
quote bind_quoted: [opts: opts] do
|
||||||
@data_layer Ash.DataLayer.Ets
|
@data_layer Ash.DataLayer.Ets
|
||||||
|
@ -31,7 +33,7 @@ defmodule Ash.DataLayer.Ets do
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule Query do
|
defmodule Query do
|
||||||
defstruct [:resource, :filter, :limit, :sort, joins: [], offset: 0]
|
defstruct [:resource, :filter, :limit, :sort, relationships: %{}, joins: [], offset: 0]
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -41,6 +43,7 @@ defmodule Ash.DataLayer.Ets do
|
||||||
def can?({:filter, :in}), do: true
|
def can?({:filter, :in}), do: true
|
||||||
def can?({:filter, :eq}), do: true
|
def can?({:filter, :eq}), do: true
|
||||||
def can?({:filter, :and}), do: true
|
def can?({:filter, :and}), do: true
|
||||||
|
def can?({:filter, :or}), do: true
|
||||||
def can?({:filter_related, _}), do: true
|
def can?({:filter_related, _}), do: true
|
||||||
def can?(_), do: false
|
def can?(_), do: false
|
||||||
|
|
||||||
|
@ -61,17 +64,8 @@ defmodule Ash.DataLayer.Ets do
|
||||||
def can_query_async?(_), do: false
|
def can_query_async?(_), do: false
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def filter(query, filter, resource) do
|
def filter(query, filter, _resource) do
|
||||||
# query = %{query | filter: query.filter || []}
|
{:ok, %{query | filter: filter}}
|
||||||
|
|
||||||
# Enum.reduce(filter, {:ok, query}, fn
|
|
||||||
# _, {:error, error} ->
|
|
||||||
# {:error, error}
|
|
||||||
|
|
||||||
# {key, value}, {:ok, query} ->
|
|
||||||
# do_filter(query, key, value, resource)
|
|
||||||
# end)
|
|
||||||
{:ok, query}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -79,22 +73,90 @@ defmodule Ash.DataLayer.Ets do
|
||||||
{:ok, %{query | sort: sort}}
|
{:ok, %{query | sort: sort}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_filter(query, field, id, _resource) do
|
|
||||||
{:ok, %{query | filter: [{field, id} | query.filter]}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def run_query(
|
def run_query(
|
||||||
%Query{resource: resource, filter: filter, offset: offset, limit: limit, sort: sort},
|
%Query{resource: resource, filter: filter, offset: offset, limit: limit, sort: sort},
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
with {:ok, match_spec} <- filter_to_matchspec(resource, filter),
|
query_results =
|
||||||
|
filter
|
||||||
|
|> all_top_level_queries()
|
||||||
|
|> Enum.reduce({:ok, []}, fn query, {:ok, records} ->
|
||||||
|
case do_run_query(resource, query, limit + offset) do
|
||||||
|
{:ok, results} -> {:ok, records ++ results}
|
||||||
|
{:error, error} -> {:eror, error}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
case query_results do
|
||||||
|
{:ok, results} ->
|
||||||
|
final_results =
|
||||||
|
results
|
||||||
|
|> Enum.uniq_by(&Map.take(&1, Ash.primary_key(resource)))
|
||||||
|
|> do_sort(sort)
|
||||||
|
|> Enum.drop(offset)
|
||||||
|
|> Enum.take(limit)
|
||||||
|
|
||||||
|
{:ok, final_results}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_run_query(resource, filter, limit) do
|
||||||
|
with %{errors: []} = filter <- relationships_to_attribute_filters(resource, filter),
|
||||||
|
{:ok, match_spec} <- filter_to_matchspec(resource, filter),
|
||||||
{:ok, table} <- wrap_or_create_table(resource),
|
{:ok, table} <- wrap_or_create_table(resource),
|
||||||
{:ok, results} <- match_limit(table, match_spec, limit, offset),
|
{:ok, results} <- match_limit(table, match_spec, limit) do
|
||||||
records <- Enum.map(results, &elem(&1, 1)),
|
{:ok, results}
|
||||||
sorted <- do_sort(records, sort),
|
else
|
||||||
without_offset <- Enum.drop(sorted, offset) do
|
%{errors: errors} ->
|
||||||
{:ok, without_offset}
|
{:error, errors}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp relationships_to_attribute_filters(_, %{relationships: relationships} = filter)
|
||||||
|
when relationships in [nil, %{}] do
|
||||||
|
filter
|
||||||
|
end
|
||||||
|
|
||||||
|
defp relationships_to_attribute_filters(resource, %{relationships: relationships} = filter) do
|
||||||
|
Enum.reduce(relationships, filter, fn {rel, related_filter}, filter ->
|
||||||
|
relationship = Ash.relationship(resource, rel)
|
||||||
|
|
||||||
|
{field, parsed_related_filter} = related_ids_filter(relationship, related_filter)
|
||||||
|
|
||||||
|
Ash.Filter.add_to_filter(filter, [{field, parsed_related_filter}])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp related_ids_filter(%{cardinality: :many_to_many} = rel, filter) do
|
||||||
|
with {:ok, results} <- do_run_query(rel.destination, filter, nil),
|
||||||
|
destination_values <- Enum.map(results, &Map.get(&1, rel.destination_field)),
|
||||||
|
%{errors: []} = through_query <-
|
||||||
|
Ash.Filter.parse(rel.through, [
|
||||||
|
{rel.destination_field_on_join_table, [in: destination_values]}
|
||||||
|
]),
|
||||||
|
{:ok, join_results} <- do_run_query(rel.through, through_query, nil) do
|
||||||
|
{rel.source_field,
|
||||||
|
[in: Enum.map(join_results, &Map.get(&1, rel.source_field_on_join_table))]}
|
||||||
|
else
|
||||||
|
%{errors: errors} -> {:error, errors}
|
||||||
|
{:error, error} -> {:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp related_ids_filter(rel, filter) do
|
||||||
|
case do_run_query(rel.destination, filter, nil) do
|
||||||
|
{:ok, results} ->
|
||||||
|
{rel.source_field, [in: Enum.map(results, &Map.get(&1, rel.destination_field))]}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -127,41 +189,33 @@ defmodule Ash.DataLayer.Ets do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Id matching would be great here for performance, but
|
|
||||||
# for behaviour is technically unnecessary
|
|
||||||
defp filter_to_matchspec(resource, filter) do
|
defp filter_to_matchspec(resource, filter) do
|
||||||
filter = filter || []
|
filter = filter || []
|
||||||
|
|
||||||
{pkey_match, pkey_names} =
|
pkey_match =
|
||||||
resource
|
resource
|
||||||
|> Ash.primary_key()
|
|> Ash.primary_key()
|
||||||
|> Enum.reduce({%{}, []}, fn
|
|> Enum.reduce(%{}, fn
|
||||||
_attr, {:_, pkey_names} ->
|
_attr, :_ ->
|
||||||
{:_, pkey_names}
|
:_
|
||||||
|
|
||||||
attr, {pkey_match, pkey_names} ->
|
attr, pkey_match ->
|
||||||
with {:ok, field_filter} <- Keyword.fetch(filter, attr),
|
case Map.fetch(filter.attributes, attr) do
|
||||||
{:ok, value} <- Keyword.fetch(field_filter, :equal) do
|
{:ok, %Eq{value: value}} ->
|
||||||
{Map.put(pkey_match, attr, value), [attr | pkey_names]}
|
Map.put(pkey_match, attr, value)
|
||||||
else
|
|
||||||
:error ->
|
_ ->
|
||||||
{:_, [attr | pkey_names]}
|
:_
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
starting_matchspec = {{pkey_match, %{__struct__: resource}}, [], [:"$_"]}
|
starting_matchspec = {{pkey_match, %{__struct__: resource}}, [], [:"$_"]}
|
||||||
|
|
||||||
filter
|
filter
|
||||||
|> Kernel.||([])
|
|> Map.get(:attributes)
|
||||||
|> Keyword.drop(pkey_names)
|
|
||||||
|> Enum.flat_map(fn {field, filter} ->
|
|
||||||
Enum.map(filter, fn {type, value} ->
|
|
||||||
{field, type, value}
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|> Enum.reduce({:ok, {starting_matchspec, %{}}}, fn
|
|> Enum.reduce({:ok, {starting_matchspec, %{}}}, fn
|
||||||
{key, type, value}, {:ok, {spec, bindings}} ->
|
{field, value}, {:ok, {spec, bindings}} ->
|
||||||
do_filter_to_matchspec(resource, key, type, value, spec, bindings)
|
do_filter_to_matchspec(resource, field, value, spec, bindings)
|
||||||
|
|
||||||
_, {:error, error} ->
|
_, {:error, error} ->
|
||||||
{:error, error}
|
{:error, error}
|
||||||
|
@ -172,13 +226,27 @@ defmodule Ash.DataLayer.Ets do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_filter_to_matchspec(resource, key, type, value, spec, binding) do
|
defp all_top_level_queries(filter = %{ors: ors}) when is_list(ors) do
|
||||||
cond do
|
[filter | Enum.flat_map(ors, &all_top_level_queries/1)]
|
||||||
attr = Ash.attribute(resource, key) ->
|
end
|
||||||
do_filter_to_matchspec_attribute(resource, attr.name, type, value, spec, binding)
|
|
||||||
|
|
||||||
_rel = Ash.relationship(resource, key) ->
|
defp all_top_level_queries(filter), do: [filter]
|
||||||
{:error, "relationship filtering not supported"}
|
|
||||||
|
# defp attributes_to_expression(%{ors: [first | rest]} = filter, bindings, conditions) do
|
||||||
|
# {left, bindings, conditions} = attributes_to_conditions(first, bindings, conditions)
|
||||||
|
# {right, bindings, conditions} = attributes_to_conditions(rest, bindings, conditions)
|
||||||
|
|
||||||
|
# {this_expression, bindings, conditions} = attributes_to_conditions(%{filter | ors: []})
|
||||||
|
|
||||||
|
# {{:orelse, {:orelse, left, right}, this_expression}, bindings, conditions}
|
||||||
|
# end
|
||||||
|
|
||||||
|
# defp attributes_to_conditions(%{ors: [first]})
|
||||||
|
|
||||||
|
defp do_filter_to_matchspec(resource, field, value, spec, binding) do
|
||||||
|
cond do
|
||||||
|
attr = Ash.attribute(resource, field) ->
|
||||||
|
do_filter_to_matchspec_attribute(attr.name, value, spec, binding)
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{:error, "unsupported filter"}
|
{:error, "unsupported filter"}
|
||||||
|
@ -186,9 +254,7 @@ defmodule Ash.DataLayer.Ets do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_filter_to_matchspec_attribute(
|
defp do_filter_to_matchspec_attribute(
|
||||||
_resource,
|
|
||||||
name,
|
name,
|
||||||
type,
|
|
||||||
value,
|
value,
|
||||||
{{id_match, struct_match}, conditions, matcher},
|
{{id_match, struct_match}, conditions, matcher},
|
||||||
bindings
|
bindings
|
||||||
|
@ -196,7 +262,7 @@ defmodule Ash.DataLayer.Ets do
|
||||||
case Map.get(bindings, name) do
|
case Map.get(bindings, name) do
|
||||||
nil ->
|
nil ->
|
||||||
binding = bindings |> Map.values() |> Enum.max(fn -> 0 end) |> Kernel.+(1)
|
binding = bindings |> Map.values() |> Enum.max(fn -> 0 end) |> Kernel.+(1)
|
||||||
condition = condition(type, value, binding)
|
condition = condition(value, binding)
|
||||||
|
|
||||||
new_spec =
|
new_spec =
|
||||||
{{id_match, Map.put(struct_match, name, :"$#{binding}")}, [condition | conditions],
|
{{id_match, Map.put(struct_match, name, :"$#{binding}")}, [condition | conditions],
|
||||||
|
@ -205,7 +271,7 @@ defmodule Ash.DataLayer.Ets do
|
||||||
{:ok, {new_spec, Map.put(bindings, name, binding)}}
|
{:ok, {new_spec, Map.put(bindings, name, binding)}}
|
||||||
|
|
||||||
binding ->
|
binding ->
|
||||||
condition = condition(type, value, binding)
|
condition = condition(value, binding)
|
||||||
|
|
||||||
new_spec = {{id_match, struct_match}, [condition | conditions], matcher}
|
new_spec = {{id_match, struct_match}, [condition | conditions], matcher}
|
||||||
|
|
||||||
|
@ -213,24 +279,32 @@ defmodule Ash.DataLayer.Ets do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def condition(:equal, value, binding) do
|
def condition(%Eq{value: value}, binding) do
|
||||||
{:==, :"$#{binding}", value}
|
{:==, :"$#{binding}", value}
|
||||||
end
|
end
|
||||||
|
|
||||||
def condition(:in, [value], binding) do
|
def condition(%In{values: []}, _binding) do
|
||||||
condition(:equal, value, binding)
|
{:==, true, false}
|
||||||
end
|
end
|
||||||
|
|
||||||
def condition(:in, [value1, value2], binding) do
|
def condition(%In{values: [value1, value2]}, binding) do
|
||||||
[{:orelse, {:"=:=", :"$#{binding}", value1}, {:"=:=", :"$#{binding}", value2}}]
|
{:orelse, {:"=:=", :"$#{binding}", value1}, {:"=:=", :"$#{binding}", value2}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def condition(:in, [value1 | rest], binding) do
|
def condition(%In{values: [value1 | rest]}, binding) do
|
||||||
{:orelse, condition(:equal, value1, binding), condition(:in, rest, binding)}
|
{:orelse, condition(%Eq{value: value1}, binding), condition(%In{values: rest}, binding)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def condition(%Or{left: left, right: right}, binding) do
|
||||||
|
{:orelse, condition(left, binding), condition(right, binding)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def condition(%And{left: left, right: right}, binding) do
|
||||||
|
{:andalso, condition(left, binding), condition(right, binding)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def condition(:in, [], _binding) do
|
def condition(:in, [], _binding) do
|
||||||
[{:==, false, true}]
|
{:==, false, true}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -256,10 +330,10 @@ defmodule Ash.DataLayer.Ets do
|
||||||
create(resource, changeset)
|
create(resource, changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp match_limit(table, match_spec, limit, offset) do
|
defp match_limit(table, match_spec, limit) do
|
||||||
result =
|
result =
|
||||||
if limit do
|
if limit do
|
||||||
ETS.Set.select(table, [match_spec], limit + offset)
|
ETS.Set.select(table, [match_spec], limit)
|
||||||
else
|
else
|
||||||
ETS.Set.select(table, [match_spec])
|
ETS.Set.select(table, [match_spec])
|
||||||
end
|
end
|
||||||
|
@ -267,6 +341,7 @@ defmodule Ash.DataLayer.Ets do
|
||||||
case result do
|
case result do
|
||||||
{:ok, {matches, _}} -> {:ok, matches}
|
{:ok, {matches, _}} -> {:ok, matches}
|
||||||
{:ok, :"$end_of_table"} -> {:ok, []}
|
{:ok, :"$end_of_table"} -> {:ok, []}
|
||||||
|
{:ok, matches} -> {:ok, matches}
|
||||||
{:error, error} -> {:error, error}
|
{:error, error} -> {:error, error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,28 @@
|
||||||
defmodule Ash.Filter.And do
|
defmodule Ash.Filter.And do
|
||||||
defstruct [:left, :right]
|
defstruct [:left, :right]
|
||||||
|
|
||||||
def new([first | [last | []]]), do: {:ok, %__MODULE__{left: first, right: last}}
|
def new(resource, attr_type, [first | [last | []]]) do
|
||||||
def new([first | rest]), do: {:ok, %__MODULE__{left: first, right: new(rest)}}
|
with {:ok, first} <- Ash.Filter.parse_predicates(resource, first, attr_type),
|
||||||
def new(left, right), do: {:ok, %__MODULE__{left: left, right: right}}
|
{:ok, right} <- Ash.Filter.parse_predicates(resource, last, attr_type) do
|
||||||
|
{:ok, %__MODULE__{left: first, right: right}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(resource, attr_type, [first | rest]) do
|
||||||
|
case Ash.Filter.parse_predicates(resource, first, attr_type) do
|
||||||
|
{:ok, first} ->
|
||||||
|
{:ok, %__MODULE__{left: first, right: new(resource, attr_type, rest)}}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(resource, attr_type, {left, right}) do
|
||||||
|
new(resource, attr_type, [left, right])
|
||||||
|
end
|
||||||
|
|
||||||
|
def prebuilt_new(left, right) do
|
||||||
|
%__MODULE__{left: left, right: right}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
defmodule Ash.Filter.Eq do
|
defmodule Ash.Filter.Eq do
|
||||||
defstruct [:value]
|
defstruct [:value]
|
||||||
|
|
||||||
def new(value), do: {:ok, %__MODULE__{value: value}}
|
def new(_resource, attr_type, value) do
|
||||||
|
case Ash.Type.cast_input(attr_type, value) do
|
||||||
|
{:ok, value} ->
|
||||||
|
{:ok, %__MODULE__{value: value}}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:error, "invalid value #{inspect(value)} for type #{inspect(attr_type)}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ defmodule Ash.Filter do
|
||||||
:ors,
|
:ors,
|
||||||
attributes: %{},
|
attributes: %{},
|
||||||
relationships: %{},
|
relationships: %{},
|
||||||
|
authorizations: [],
|
||||||
errors: []
|
errors: []
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -14,7 +15,8 @@ defmodule Ash.Filter do
|
||||||
ors: %__MODULE__{} | nil,
|
ors: %__MODULE__{} | nil,
|
||||||
attributes: Keyword.t(),
|
attributes: Keyword.t(),
|
||||||
relationships: Keyword.t(),
|
relationships: Keyword.t(),
|
||||||
errors: list(String.t())
|
errors: list(String.t()),
|
||||||
|
authorizations: list(Ash.Authorization.Request.t())
|
||||||
}
|
}
|
||||||
|
|
||||||
@predicates %{
|
@predicates %{
|
||||||
|
@ -24,17 +26,56 @@ defmodule Ash.Filter do
|
||||||
or: Ash.Filter.Or
|
or: Ash.Filter.Or
|
||||||
}
|
}
|
||||||
|
|
||||||
@spec parse(Ash.resource(), Keyword.t()) :: t()
|
@spec parse(Ash.resource(), Keyword.t(), rules :: list(term)) :: t()
|
||||||
def parse(resource, filter) do
|
def parse(resource, filter, authorization_steps \\ nil) do
|
||||||
do_parse(resource, filter, %Ash.Filter{resource: resource})
|
authorization_steps =
|
||||||
|
authorization_steps || Ash.primary_action(resource, :read).authorization_steps
|
||||||
|
|
||||||
|
filter
|
||||||
|
|> do_parse(%Ash.Filter{resource: resource})
|
||||||
|
|> lift_ors()
|
||||||
|
|> add_authorization(
|
||||||
|
Ash.Authorization.Request.new(
|
||||||
|
resource: resource,
|
||||||
|
authorization_steps: authorization_steps,
|
||||||
|
filter: filter
|
||||||
|
)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_parse(resource, filter_statement, filter) do
|
def add_to_filter(filter, additions) do
|
||||||
|
do_parse(additions, filter)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp lift_ors(%Ash.Filter{
|
||||||
|
ors: [or_filter | rest],
|
||||||
|
relationships: rels,
|
||||||
|
attributes: attrs,
|
||||||
|
errors: errors,
|
||||||
|
authorizations: authorizations
|
||||||
|
})
|
||||||
|
when attrs == %{} and rels == %{} do
|
||||||
|
or_filter
|
||||||
|
|> Map.put(:ors, rest)
|
||||||
|
|> add_error(errors)
|
||||||
|
|> add_authorization(authorizations)
|
||||||
|
|> lift_ors()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp lift_ors(filter), do: filter
|
||||||
|
|
||||||
|
defp do_parse(filter_statement, %{resource: resource} = filter) do
|
||||||
Enum.reduce(filter_statement, filter, fn
|
Enum.reduce(filter_statement, filter, fn
|
||||||
{key, value}, filter ->
|
{key, value}, filter ->
|
||||||
cond do
|
cond do
|
||||||
key == :or || key == :and ->
|
key == :or || key == :and ->
|
||||||
add_expression_level_boolean_filter(filter, resource, key, value)
|
new_filter = add_expression_level_boolean_filter(filter, resource, key, value)
|
||||||
|
|
||||||
|
if Ash.data_layer(resource).can?({:filter, key}) do
|
||||||
|
new_filter
|
||||||
|
else
|
||||||
|
add_error(new_filter, "data layer does not support #{inspect(key)} filters")
|
||||||
|
end
|
||||||
|
|
||||||
attr = Ash.attribute(resource, key) ->
|
attr = Ash.attribute(resource, key) ->
|
||||||
add_attribute_filter(filter, attr, value)
|
add_attribute_filter(filter, attr, value)
|
||||||
|
@ -57,9 +98,9 @@ defmodule Ash.Filter do
|
||||||
add_expression_level_boolean_filter(filter, resource, key, [left, right])
|
add_expression_level_boolean_filter(filter, resource, key, [left, right])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_expression_level_boolean_filter(filter, resource, :and, expressions) do
|
defp add_expression_level_boolean_filter(filter, _resource, :and, expressions) do
|
||||||
Enum.reduce(expressions, filter, fn expression, filter ->
|
Enum.reduce(expressions, filter, fn expression, filter ->
|
||||||
do_parse(resource, expression, filter)
|
do_parse(expression, filter)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -68,8 +109,9 @@ defmodule Ash.Filter do
|
||||||
parsed_expression = parse(resource, expression)
|
parsed_expression = parse(resource, expression)
|
||||||
|
|
||||||
filter
|
filter
|
||||||
|> Map.update!(:ors, fn ors -> [parsed_expression | ors] end)
|
|> Map.update!(:ors, fn ors -> [parsed_expression | ors || []] end)
|
||||||
|> add_error(parsed_expression.errors)
|
|> add_error(parsed_expression.errors)
|
||||||
|
|> add_authorization(parsed_expression.authorizations)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,51 +132,78 @@ defmodule Ash.Filter do
|
||||||
predicate_name,
|
predicate_name,
|
||||||
value
|
value
|
||||||
) do
|
) do
|
||||||
|
case parse_predicate(resource, predicate_name, attr_type, value) do
|
||||||
|
{:ok, predicate} ->
|
||||||
|
new_attributes =
|
||||||
|
Map.update(
|
||||||
|
attributes,
|
||||||
|
attr_name,
|
||||||
|
predicate,
|
||||||
|
&Merge.merge(&1, predicate)
|
||||||
|
)
|
||||||
|
|
||||||
|
%{filter | attributes: new_attributes}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
add_error(filter, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_predicates(resource, keyword, attr_type) do
|
||||||
|
Enum.reduce(keyword, {:ok, nil}, fn {predicate_name, value}, {:ok, existing_predicate} ->
|
||||||
|
case parse_predicate(resource, predicate_name, attr_type, value) do
|
||||||
|
{:ok, predicate} ->
|
||||||
|
if existing_predicate do
|
||||||
|
{:ok, Merge.merge(existing_predicate, predicate)}
|
||||||
|
else
|
||||||
|
{:ok, predicate}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_predicate(resource, predicate_name, attr_type, value) do
|
||||||
with {:predicate_type, {:ok, predicate_type}} <-
|
with {:predicate_type, {:ok, predicate_type}} <-
|
||||||
{:predicate_type, Map.fetch(@predicates, predicate_name)},
|
{:predicate_type, Map.fetch(@predicates, predicate_name)},
|
||||||
{:data_layer_can?, _, true} <-
|
{:data_layer_can?, _, true} <-
|
||||||
{:data_layer_can?, predicate_name,
|
{:data_layer_can?, predicate_name,
|
||||||
Ash.data_layer(resource).can?({:filter, predicate_name})},
|
Ash.data_layer(resource).can?({:filter, predicate_name})},
|
||||||
{:casted, {:ok, casted}} <- {:casted, Ash.Type.cast_input(attr_type, value)},
|
{:predicate, {:ok, predicate}} =
|
||||||
{:predicate, {:ok, predicate}} = {:predicate, predicate_type.new(casted)} do
|
{:predicate, predicate_type.new(resource, attr_type, value)} do
|
||||||
new_attributes =
|
{:ok, predicate}
|
||||||
Map.update(
|
|
||||||
attributes,
|
|
||||||
attr_name,
|
|
||||||
predicate,
|
|
||||||
&Merge.merge(&1, predicate)
|
|
||||||
)
|
|
||||||
|
|
||||||
%{filter | attributes: new_attributes}
|
|
||||||
else
|
else
|
||||||
{:predicate_type, :error} ->
|
{:predicate_type, :error} ->
|
||||||
add_error(filter, "No such filter type #{predicate_name}")
|
{:error, "No such filter type #{predicate_name}"}
|
||||||
|
|
||||||
{:casted, _} ->
|
{:casted, _} ->
|
||||||
add_error(filter, "Invalid value: #{inspect(value)} for #{inspect(attr_name)}")
|
{:error, "Invalid value: #{inspect(value)} for #{inspect(attr_type)}"}
|
||||||
|
|
||||||
{:predicate, {:error, error}} ->
|
{:predicate, {:error, error}} ->
|
||||||
add_error(filter, error)
|
{:error, error}
|
||||||
|
|
||||||
{:data_layer_can?, predicate_name, false} ->
|
{:data_layer_can?, predicate_name, false} ->
|
||||||
add_error(filter, "data layer not capable of provided filter: #{predicate_name}")
|
{:error, "data layer not capable of provided filter: #{predicate_name}"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_relationship_filter(
|
defp add_relationship_filter(
|
||||||
%{relationships: relationships} = filter,
|
%{relationships: relationships} = filter,
|
||||||
%{destination: destination, name: name} = relationship,
|
%{destination: destination, name: name} = relationship,
|
||||||
value
|
value
|
||||||
) do
|
) do
|
||||||
related_filter = parse(destination, value)
|
related_filter = parse(destination, value)
|
||||||
filter_with_errors = Enum.reduce(related_filter.errors, filter, &add_error(&2, &1))
|
|
||||||
|
|
||||||
new_relationships =
|
new_relationships =
|
||||||
Map.update(relationships, name, related_filter, &Merge.merge(&1, related_filter))
|
Map.update(relationships, name, related_filter, &Merge.merge(&1, related_filter))
|
||||||
|
|
||||||
filter_with_errors
|
filter
|
||||||
|> Map.put(:relationships, new_relationships)
|
|> Map.put(:relationships, new_relationships)
|
||||||
|> add_relationship_compatibility_error(relationship)
|
|> add_relationship_compatibility_error(relationship)
|
||||||
|
|> add_error(related_filter.errors)
|
||||||
|
|> add_authorization(related_filter.authorizations)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_relationship_compatibility_error(%{resource: resource} = filter, %{
|
defp add_relationship_compatibility_error(%{resource: resource} = filter, %{
|
||||||
|
@ -162,6 +231,13 @@ defmodule Ash.Filter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp add_authorization(%{authorizations: authorizations} = filter, authorizations)
|
||||||
|
when is_list(authorizations),
|
||||||
|
do: %{filter | authorizations: filter.authorizations ++ authorizations}
|
||||||
|
|
||||||
|
defp add_authorization(%{authorizations: authorizations} = filter, authorization),
|
||||||
|
do: %{filter | authorizations: [authorization | authorizations]}
|
||||||
|
|
||||||
defp add_error(%{errors: errors} = filter, errors) when is_list(errors),
|
defp add_error(%{errors: errors} = filter, errors) when is_list(errors),
|
||||||
do: %{filter | errors: filter.errors ++ errors}
|
do: %{filter | errors: filter.errors ++ errors}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,27 @@
|
||||||
defmodule Ash.Filter.In do
|
defmodule Ash.Filter.In do
|
||||||
defstruct [:values]
|
defstruct [:values]
|
||||||
|
|
||||||
def new([]), do: {:ok, %Ash.Filter.Impossible{cause: :empty_in}}
|
def new(_resource, attr_type, values) do
|
||||||
def new(values), do: {:ok, %__MODULE__{values: List.wrap(values)}}
|
casted =
|
||||||
|
values
|
||||||
|
|> List.wrap()
|
||||||
|
|> Enum.reduce({:ok, []}, fn
|
||||||
|
value, {:ok, casted} ->
|
||||||
|
case Ash.Type.cast_input(attr_type, value) do
|
||||||
|
{:ok, value} -> {:ok, [value | casted]}
|
||||||
|
:error -> {:error, "invalid value #{inspect(value)} for type #{inspect(attr_type)}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
_, {:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end)
|
||||||
|
|
||||||
|
case casted do
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
|
||||||
|
{:ok, values} ->
|
||||||
|
{:ok, %__MODULE__{values: values}}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,12 @@ defmodule Ash.Filter.InspectHelpers do
|
||||||
%{opts | custom_options: Keyword.put(custom, :attr, attr)}
|
%{opts | custom_options: Keyword.put(custom, :attr, attr)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def make_non_root(%{custom_options: custom} = opts) do
|
||||||
|
new_options = Keyword.put(custom, :path, custom[:path] || [])
|
||||||
|
|
||||||
|
%{opts | custom_options: new_options}
|
||||||
|
end
|
||||||
|
|
||||||
def add_to_path(%{custom_options: custom} = opts, path_item) do
|
def add_to_path(%{custom_options: custom} = opts, path_item) do
|
||||||
new_options =
|
new_options =
|
||||||
Keyword.update(custom, :path, [to_string(path_item)], fn path ->
|
Keyword.update(custom, :path, [to_string(path_item)], fn path ->
|
||||||
|
@ -40,44 +46,80 @@ defimpl Inspect, for: Ash.Filter do
|
||||||
import Inspect.Algebra
|
import Inspect.Algebra
|
||||||
import Ash.Filter.InspectHelpers
|
import Ash.Filter.InspectHelpers
|
||||||
|
|
||||||
def inspect(%{or: nil} = filter, opts) do
|
def inspect(%Ash.Filter{ors: ors, relationships: relationships, attributes: attributes}, opts)
|
||||||
|
when ors in [nil, []] and relationships in [nil, %{}] and attributes in [nil, %{}] do
|
||||||
|
if root?(opts) do
|
||||||
|
concat(["#Filter<", to_doc(nil, opts), ">"])
|
||||||
|
else
|
||||||
|
concat([to_doc(nil, opts)])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect(filter, opts) do
|
||||||
rels =
|
rels =
|
||||||
filter
|
filter
|
||||||
|> Map.get(:relationships)
|
|> Map.get(:relationships)
|
||||||
|> Enum.map(fn {key, value} ->
|
|> case do
|
||||||
to_doc(value, add_to_path(opts, key))
|
rels when rels == %{} ->
|
||||||
end)
|
[]
|
||||||
|
|
||||||
|
rels ->
|
||||||
|
Enum.map(rels, fn {key, value} ->
|
||||||
|
to_doc(value, add_to_path(opts, key))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
attrs =
|
attrs =
|
||||||
filter
|
filter
|
||||||
|> Map.get(:attributes)
|
|> Map.get(:attributes)
|
||||||
|> Enum.map(fn {key, value} ->
|
|> case do
|
||||||
to_doc(value, put_attr(opts, key))
|
attrs when attrs == %{} ->
|
||||||
end)
|
[]
|
||||||
|> Enum.concat(rels)
|
|
||||||
|> Enum.intersperse(" and ")
|
attrs ->
|
||||||
|> concat()
|
Enum.map(attrs, fn {key, value} ->
|
||||||
|
to_doc(value, put_attr(opts, key))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
and_container =
|
||||||
|
case attrs ++ rels do
|
||||||
|
[] ->
|
||||||
|
empty()
|
||||||
|
|
||||||
|
and_clauses ->
|
||||||
|
Inspect.Algebra.container_doc("(", and_clauses, ")", opts, fn term, _ -> term end,
|
||||||
|
break: :flex,
|
||||||
|
separator: " and "
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
all_container =
|
||||||
|
case Map.get(filter, :ors) do
|
||||||
|
nil ->
|
||||||
|
and_container
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
and_container
|
||||||
|
|
||||||
|
ors ->
|
||||||
|
inspected_ors = Enum.map(ors, fn filter -> to_doc(filter, make_non_root(opts)) end)
|
||||||
|
|
||||||
|
Inspect.Algebra.container_doc(
|
||||||
|
"",
|
||||||
|
[and_container | inspected_ors],
|
||||||
|
"",
|
||||||
|
opts,
|
||||||
|
fn term, _ -> term end,
|
||||||
|
break: :strict,
|
||||||
|
separator: " or "
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
if root?(opts) do
|
if root?(opts) do
|
||||||
concat(["#Filter< ", attrs, " >"])
|
concat(["#Filter<", all_container, ">"])
|
||||||
else
|
else
|
||||||
concat([attrs])
|
concat([all_container])
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def inspect(%{or: or_filter} = filter, opts) do
|
|
||||||
filter_without_or = %{filter | or: nil}
|
|
||||||
|
|
||||||
if root?(opts) do
|
|
||||||
concat([
|
|
||||||
"#Ash.Filter<(",
|
|
||||||
to_doc(filter_without_or, opts),
|
|
||||||
") or (",
|
|
||||||
to_doc(or_filter, opts),
|
|
||||||
")>"
|
|
||||||
])
|
|
||||||
else
|
|
||||||
concat(["(", to_doc(filter_without_or, opts), ") or (", to_doc(or_filter, opts), ")"])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Ash.Filter.Merge do
|
defmodule Ash.Filter.Merge do
|
||||||
alias Ash.Filter.{In, Eq, And, Impossible}
|
# alias Ash.Filter.{In, Eq, And, Impossible}
|
||||||
|
alias Ash.Filter.And
|
||||||
|
|
||||||
def merge(left, right) do
|
def merge(left, right) do
|
||||||
[left, right] = Enum.sort_by([left, right], fn %mod{} -> to_string(mod) end)
|
[left, right] = Enum.sort_by([left, right], fn %mod{} -> to_string(mod) end)
|
||||||
|
@ -45,7 +46,7 @@ defmodule Ash.Filter.Merge do
|
||||||
|
|
||||||
defp do_merge(left, right) do
|
defp do_merge(left, right) do
|
||||||
# There is no way this can reasonably fail
|
# There is no way this can reasonably fail
|
||||||
{:ok, predicate} = And.new(left, right)
|
{:ok, predicate} = And.prebuilt_new(left, right)
|
||||||
|
|
||||||
predicate
|
predicate
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,24 @@
|
||||||
defmodule Ash.Filter.Or do
|
defmodule Ash.Filter.Or do
|
||||||
defstruct [:left, :right]
|
defstruct [:left, :right]
|
||||||
|
|
||||||
def new([first | [last | []]]), do: {:ok, %__MODULE__{left: first, right: last}}
|
def new(resource, attr_type, [first | [last | []]]) do
|
||||||
def new([first | rest]), do: {:ok, %__MODULE__{left: first, right: new(rest)}}
|
with {:ok, first} <- Ash.Filter.parse_predicates(resource, first, attr_type),
|
||||||
def new(left, right), do: {:ok, %__MODULE__{left: left, right: right}}
|
{:ok, right} <- Ash.Filter.parse_predicates(resource, last, attr_type) do
|
||||||
|
{:ok, %__MODULE__{left: first, right: right}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(resource, attr_type, [first | rest]) do
|
||||||
|
case Ash.Filter.parse_predicates(resource, first, attr_type) do
|
||||||
|
{:ok, first} ->
|
||||||
|
{:ok, %__MODULE__{left: first, right: new(resource, attr_type, rest)}}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(resource, attr_type, {left, right}) do
|
||||||
|
new(resource, attr_type, [left, right])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
defmodule Ash.Resource.Actions.Create do
|
defmodule Ash.Resource.Actions.Create do
|
||||||
@moduledoc "The representation of a `create` action."
|
@moduledoc "The representation of a `create` action."
|
||||||
defstruct [:type, :name, :primary?, :authorization_steps]
|
defstruct [:type, :name, :primary?, :authorization_steps]
|
||||||
alias Ash.Authorization.Rule
|
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
type: :create,
|
type: :create,
|
||||||
|
|
|
@ -3,8 +3,6 @@ defmodule Ash.Resource.Actions.Read do
|
||||||
|
|
||||||
defstruct [:type, :name, :primary?, :authorization_steps, :paginate?]
|
defstruct [:type, :name, :primary?, :authorization_steps, :paginate?]
|
||||||
|
|
||||||
alias Ash.Authorization.Rule
|
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
type: :read,
|
type: :read,
|
||||||
name: atom,
|
name: atom,
|
||||||
|
|
|
@ -45,28 +45,36 @@ defmodule Ash.Test.Actions.SideLoadTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "side_loads" do
|
describe "side_loads" do
|
||||||
setup do
|
# setup do
|
||||||
author = Api.create!(Author, attributes: %{name: "zerg"})
|
# author = Api.create!(Author, attributes: %{name: "zerg"})
|
||||||
|
|
||||||
post1 =
|
# post1 =
|
||||||
Api.create!(Post, attributes: %{title: "post1"}, relationships: %{author: author.id})
|
# Api.create!(Post, attributes: %{title: "post1"}, relationships: %{author: author.id})
|
||||||
|
|
||||||
post2 =
|
# post2 =
|
||||||
Api.create!(Post, attributes: %{title: "post2"}, relationships: %{author: author.id})
|
# Api.create!(Post, attributes: %{title: "post2"}, relationships: %{author: author.id})
|
||||||
|
|
||||||
%{post1: post1, post2: post2}
|
# %{post1: post1, post2: post2}
|
||||||
|
# end
|
||||||
|
|
||||||
|
test "mything" do
|
||||||
|
Api.read!(Author, filter: [or: [[name: "zach"], [posts: [id: Ecto.UUID.generate()]]]])
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it allows sideloading related data", %{post1: post1, post2: post2} do
|
# test "it allows sideloading related data", %{post1: post1, post2: post2} do
|
||||||
%{results: [author]} =
|
# %{results: [author]} =
|
||||||
Api.read!(Author, side_load: [posts: [:author]], filter: [posts: [id: post1.id]])
|
# Api.read!(Author, side_load: [posts: [:author]], filter: [posts: [id: post1.id]])
|
||||||
|
|
||||||
assert Enum.sort(Enum.map(author.posts, &Map.get(&1, :id))) ==
|
# Api.read!(Author,
|
||||||
Enum.sort([post1.id, post2.id])
|
# filter: [name: "zach", posts: [id: post1.id, title: "foo", contents: "bar"]]
|
||||||
|
# )
|
||||||
|
|
||||||
for post <- author.posts do
|
# assert Enum.sort(Enum.map(author.posts, &Map.get(&1, :id))) ==
|
||||||
assert post.author.id == author.id
|
# Enum.sort([post1.id, post2.id])
|
||||||
end
|
|
||||||
end
|
# for post <- author.posts do
|
||||||
|
# assert post.author.id == author.id
|
||||||
|
# end
|
||||||
|
# end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue