Skip to content

Commit

Permalink
feat: refactor filters
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Jun 18, 2020
1 parent d0a38bc commit 4a45a6b
Show file tree
Hide file tree
Showing 29 changed files with 1,774 additions and 1,536 deletions.
File renamed without changes.
File renamed without changes.
1,123 changes: 1,123 additions & 0 deletions filter/filter.ex

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions lib/ash.ex
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ defmodule Ash do
|> Enum.find(&(&1.name == name))
end

def related(resource, []), do: resource

def related(resource, [path | rest]) do
case relationship(resource, path) do
%{destination: destination} -> related(destination, rest)
nil -> nil
end
end

@doc "The data layer of the resource, or nil if it does not have one"
@spec data_layer(resource()) :: data_layer()
def data_layer(resource) do
Expand Down
25 changes: 16 additions & 9 deletions lib/ash/actions/read.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Ash.Actions.Read do
alias Ash.Actions.SideLoad
alias Ash.Engine
alias Ash.Engine.Request
alias Ash.Filter
require Logger

def run(query, _action, opts \\ []) do
Expand Down Expand Up @@ -40,31 +41,37 @@ defmodule Ash.Actions.Read do
end

defp requests(query, action, opts) do
filter_requests =
if Keyword.has_key?(opts, :actor) || opts[:authorize?] do
Filter.read_requests(query.filter)
else
[]
end

request =
Request.new(
resource: query.resource,
api: query.api,
query: query,
action: action,
data: data_field(opts, query.filter, query.resource, query.data_layer_query),
data: data_field(opts, filter_requests, query.resource, query.data_layer_query),
path: [:data],
name: "#{action.type} - `#{action.name}`"
)

[request | Map.get(query.filter || %{}, :requests, [])]
[request | filter_requests]
end

defp data_field(params, filter, resource, query) do
defp data_field(params, filter_requests, resource, query) do
if params[:initial_data] do
List.wrap(params[:initial_data])
else
Request.resolve(
[[:data, :query]],
Ash.Filter.optional_paths(filter),
fn %{data: %{query: ash_query}} = data ->
fetch_filter = Ash.Filter.request_filter_for_fetch(ash_query.filter, data)
relationship_filter_paths = Enum.map(filter_requests, &[&1.path, :authorization_filter])

with {:ok, query} <- Ash.DataLayer.filter(query, fetch_filter, resource),
Request.resolve(
[[:data, :query] | relationship_filter_paths],
fn %{data: %{query: ash_query}} ->
with {:ok, query} <- Ash.DataLayer.filter(query, ash_query.filter, resource),
{:ok, query} <- Ash.DataLayer.limit(query, ash_query.limit, resource),
{:ok, query} <- Ash.DataLayer.offset(query, ash_query.offset, resource) do
Ash.DataLayer.run_query(query, resource)
Expand Down
4 changes: 2 additions & 2 deletions lib/ash/actions/relationships.ex
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ defmodule Ash.Actions.Relationships do
{:ok, %{replace: identifier}}

{:error, _} ->
{:error, "Relationship change invalid for #{relationship.name} 2"}
{:error, "Relationship change invalid map for #{relationship.name}"}
end
end

Expand All @@ -283,7 +283,7 @@ defmodule Ash.Actions.Relationships do
{:ok, %{replace: identifiers}}

{:error, _} ->
{:error, "Relationship change invalid for #{relationship.name} 2"}
{:error, "Relationship change invalid list for #{relationship.name}"}
end
end

Expand Down
179 changes: 84 additions & 95 deletions lib/ash/data_layer/ets/ets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ defmodule Ash.DataLayer.Ets do
This is used for testing. *Do not use this data layer in production*
"""

alias Ash.Filter.{And, Eq, In, NotEq, NotIn, Or}
alias Ash.Filter.{Predicate, Expression, Not}
alias Ash.Filter.Predicate.{Eq, In}

@behaviour Ash.DataLayer

Expand Down Expand Up @@ -80,21 +81,13 @@ defmodule Ash.DataLayer.Ets do
{:ok, %{query | sort: sort}}
end

@impl true
def run_query(%Query{filter: %Ash.Filter{impossible?: true}}, _), do: {:ok, []}

@impl true
def run_query(
%Query{resource: resource, filter: filter, offset: offset, limit: limit, sort: sort},
_resource
) do
with {:ok, table} <- wrap_or_create_table(resource),
{:ok, records} <- ETS.Set.to_list(table) do
filtered_records =
records
|> Enum.map(&elem(&1, 1))
|> filter_matches(filter)

with {:ok, records} <- get_records(resource),
filtered_records <- filter_matches(records, filter) do
offset_records =
filtered_records
|> do_sort(sort)
Expand All @@ -113,110 +106,106 @@ defmodule Ash.DataLayer.Ets do
end
end

defp filter_matches(records, filter) do
Enum.filter(records, &matches_filter?(&1, filter))
end

defp matches_filter?(record, %{ands: [first | rest]} = filter) do
matches_filter?(record, first) and matches_filter?(record, %{filter | ands: rest})
end

defp matches_filter?(record, %{not: not_filter} = filter) when not is_nil(not_filter) do
not matches_filter?(record, not_filter) and matches_filter?(record, %{filter | not: nil})
end

defp matches_filter?(record, %{ors: [first | rest]} = filter) do
matches_filter?(record, first) or matches_filter?(record, %{filter | ors: rest})
end

defp matches_filter?(record, %{resource: resource} = filter) do
resource
|> relationships_to_attribute_filters(filter)
|> Map.get(:attributes)
|> Enum.all?(fn {key, predicate} ->
matches_predicate?(Map.get(record, key), predicate)
end)
defp get_records(resource) do
with {:ok, table} <- wrap_or_create_table(resource),
{:ok, record_tuples} <- ETS.Set.to_list(table) do
{:ok, Enum.map(record_tuples, &elem(&1, 1))}
end
end

# alias Ash.Filter.{In, NotIn}

defp matches_predicate?(value, %Eq{value: predicate_value}) do
value == predicate_value
end
defp filter_matches(records, nil), do: records

defp matches_predicate?(value, %NotEq{value: predicate_value}) do
value != predicate_value
defp filter_matches(records, filter) do
Enum.filter(records, &matches_filter?(&1, filter.expression))
end

defp matches_predicate?(value, %In{values: predicate_value}) do
value in predicate_value
defp matches_filter?(
record,
%Predicate{
predicate: predicate,
attribute: %{name: name},
relationship_path: []
}
) do
matches_predicate?(record, name, predicate)
end

defp matches_predicate?(value, %NotIn{values: predicate_value}) do
value not in predicate_value
defp matches_filter?(
record,
%Predicate{
predicate: predicate,
attribute: %{name: name},
relationship_path: path
}
) do
record
|> get_related(path)
|> Enum.any?(&matches_predicate?(&1, name, predicate))
end

defp matches_predicate?(value, %And{left: left, right: right}) do
matches_predicate?(value, left) and matches_predicate?(value, right)
defp matches_filter?(record, %Expression{op: :and, left: left, right: right}) do
matches_filter?(record, left) && matches_filter?(record, right)
end

defp matches_predicate?(value, %Or{left: left, right: right}) do
matches_predicate?(value, left) or matches_predicate?(value, right)
defp matches_filter?(record, %Expression{op: :or, left: left, right: right}) do
matches_filter?(record, left) || matches_filter?(record, right)
end

defp relationships_to_attribute_filters(_, %{relationships: relationships} = filter)
when relationships in [nil, %{}] do
filter
defp matches_filter?(record, %Not{expression: expression}) do
not matches_filter?(record, expression)
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)
defp get_related(record_or_records, []), do: List.wrap(record_or_records)

defp get_related(%resource{} = record, [first | rest]) do
relationship = Ash.relationship(resource, first)
source_value = Map.get(record, relationship.source_field)

related =
if is_nil(Map.get(record, relationship.source_field)) do
[]
else
case Ash.relationship(resource, first) do
%{type: :many_to_many} = relationship ->
{:ok, through_records} = get_records(relationship.through)
{:ok, destination_records} = get_records(relationship.destination)

through_records
|> Enum.reject(&is_nil(Map.get(&1, relationship.destination_field_on_join_table)))
|> Enum.flat_map(fn through_record ->
if Map.get(through_record, relationship.source_field_on_join_table) ==
source_value do
Enum.filter(destination_records, fn destination_record ->
Map.get(through_record, relationship.destination_field_on_join_table) ==
Map.get(destination_record, relationship.destination_field)
end)
else
[]
end
end)

relationship ->
{:ok, destination_records} = get_records(relationship.destination)

Enum.filter(destination_records, fn destination_record ->
Map.get(destination_record, relationship.destination_field) == source_value
end)
end
end

Ash.Filter.add_to_filter(filter, [{field, parsed_related_filter}])
end)
related
|> List.wrap()
|> Enum.flat_map(&get_related(&1, rest))
end

defp related_ids_filter(%{type: :many_to_many} = rel, filter) do
destination_query = %Query{
resource: rel.destination,
filter: filter
}

with {:ok, results} <- run_query(destination_query, rel.destination),
destination_values <- Enum.map(results, &Map.get(&1, rel.destination_field)),
%{errors: []} = through_filter <-
Ash.Filter.parse(
rel.through,
[
{rel.destination_field_on_join_table, [in: destination_values]}
],
filter.api
),
{:ok, join_results} <-
run_query(%Query{resource: rel.through, filter: through_filter}, rel.through) 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
defp matches_predicate?(record, field, %Eq{value: predicate_value}) do
Map.fetch(record, field) == {:ok, predicate_value}
end

defp related_ids_filter(rel, filter) do
query = %Query{
resource: rel.destination,
filter: filter
}

case run_query(query, rel.destination) do
{:ok, results} ->
{rel.source_field, [in: Enum.map(results, &Map.get(&1, rel.destination_field))]}

{:error, error} ->
{:error, error}
defp matches_predicate?(record, field, %In{values: predicate_values}) do
case Map.fetch(record, field) do
{:ok, value} -> value in predicate_values
:error -> false
end
end

Expand Down
7 changes: 0 additions & 7 deletions lib/ash/engine/engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ defmodule Ash.Engine do
authorize? = opts[:authorize?] || Keyword.has_key?(opts, :actor)
actor = opts[:actor]

requests =
if authorize? do
requests
else
Enum.reject(requests, & &1.skip_unless_authorize?)
end

case Request.validate_requests(requests) do
:ok ->
requests =
Expand Down

0 comments on commit 4a45a6b

Please sign in to comment.