This commit is contained in:
Zach Daniel 2020-04-19 23:15:52 -04:00
parent 8170efca8a
commit 24d1bd03c4
No known key found for this signature in database
GPG key ID: C377365383138D4B
14 changed files with 91 additions and 134 deletions

View file

@ -170,3 +170,4 @@ end
- check if preparations have been done on a superset filter of a request and, if so, use it - check if preparations have been done on a superset filter of a request and, if so, use it
- without transactions, we can't ensure that all changes are rolled back in the case that relationship updates are included. Don't think there is really anything to do about that, but something worth considering. - without transactions, we can't ensure that all changes are rolled back in the case that relationship updates are included. Don't think there is really anything to do about that, but something worth considering.
- perhaps have auth steps express which fields need to be present, so we can avoid loading things unnecessarily - perhaps have auth steps express which fields need to be present, so we can avoid loading things unnecessarily
- lift `or` filters over the same field equaling a value into a single `in` filter, for performance (potentially)

View file

@ -3,21 +3,7 @@ defmodule Ash.Actions.Create do
alias Ash.Actions.{Attributes, Relationships, SideLoad} alias Ash.Actions.{Attributes, Relationships, SideLoad}
require Logger require Logger
@spec run(Ash.api(), Ash.resource(), Ash.action(), Ash.params()) ::
{:ok, Ash.record()} | {:error, Ecto.Changeset.t()} | {:error, Ash.error()}
def run(api, resource, action, params) do def run(api, resource, action, params) do
transaction_result =
Ash.DataLayer.transact(resource, fn ->
do_run(api, resource, action, params)
end)
case transaction_result do
{:ok, value} -> value
{:error, error} -> {:error, error}
end
end
defp do_run(api, resource, action, params) do
attributes = Keyword.get(params, :attributes, %{}) attributes = Keyword.get(params, :attributes, %{})
side_loads = Keyword.get(params, :side_load, []) side_loads = Keyword.get(params, :side_load, [])
side_load_filter = Keyword.get(params, :side_load_filter) side_load_filter = Keyword.get(params, :side_load_filter)

View file

@ -4,18 +4,6 @@ defmodule Ash.Actions.Destroy do
@spec run(Ash.api(), Ash.record(), Ash.action(), Ash.params()) :: @spec run(Ash.api(), Ash.record(), Ash.action(), Ash.params()) ::
{:ok, Ash.record()} | {:error, Ecto.Changeset.t()} | {:error, Ash.error()} {:ok, Ash.record()} | {:error, Ecto.Changeset.t()} | {:error, Ash.error()}
def run(api, %resource{} = record, action, params) do def run(api, %resource{} = record, action, params) do
transaction_result =
Ash.DataLayer.transact(resource, fn ->
do_authorized(api, params, action, record)
end)
case transaction_result do
{:ok, value} -> value
{:error, error} -> {:error, error}
end
end
defp do_authorized(api, params, action, %resource{} = record) do
auth_request = auth_request =
Ash.Engine.Request.new( Ash.Engine.Request.new(
resource: resource, resource: resource,
@ -24,7 +12,7 @@ defmodule Ash.Actions.Destroy do
strict_access: false, strict_access: false,
path: [:data], path: [:data],
data: data:
Ash.Engine.Request.UnresolvedField.data([], fn _request, _ -> Ash.Engine.Request.UnresolvedField.data([], fn _ ->
case Ash.data_layer(resource).destroy(record) do case Ash.data_layer(resource).destroy(record) do
:ok -> {:ok, record} :ok -> {:ok, record}
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
@ -34,15 +22,21 @@ defmodule Ash.Actions.Destroy do
resolve_when_fetch_only?: true resolve_when_fetch_only?: true
) )
if params[:authorization] do result =
Engine.run( if params[:authorization] do
[auth_request], Engine.run(
api, [auth_request],
user: params[:authorization][:user], api,
log_final_report?: params[:authorization][:log_final_report?] user: params[:authorization][:user],
) log_final_report?: params[:authorization][:log_final_report?]
else )
Engine.run([auth_request], api, fetch_only?: true) else
Engine.run([auth_request], api, fetch_only?: true)
end
case result do
%{errors: errors} when errors == %{} -> :ok
%{errors: errors} -> {:error, errors}
end end
end end
end end

View file

@ -5,18 +5,6 @@ defmodule Ash.Actions.Read do
require Logger require Logger
def run(api, resource, action, params) do def run(api, resource, action, params) do
transaction_result =
Ash.DataLayer.transact(resource, fn ->
do_run(api, resource, action, params)
end)
case transaction_result do
{:ok, value} -> value
{:error, error} -> {:error, error}
end
end
defp do_run(api, resource, action, params) do
filter = Keyword.get(params, :filter, []) filter = Keyword.get(params, :filter, [])
sort = Keyword.get(params, :sort, []) sort = Keyword.get(params, :sort, [])
side_loads = Keyword.get(params, :side_load, []) side_loads = Keyword.get(params, :side_load, [])

View file

@ -1,43 +0,0 @@
# defmodule Ash.Actions.Relationships.Create do
# alias Ash.Actions.Relationships.Change
# def changeset(changeset, api, relationships) do
# relationship_changes = relationship_changes(relationships)
# changeset
# end
# defp relationship_changes(relationships) do
# Enum.into(relationships, %{}, fn {key, value} ->
# {key, Change.from(value, :create)}
# end)
# end
# # def changeset(changeset, api, relationships) do
# # if relationships == %{} do
# # changeset
# # else
# # dependencies = Map.get(changeset, :__changes_depend_on__, [])
# # Ash.Engine.Request.UnresolvedField.field(dependencies, fn data ->
# # new_changeset =
# # data
# # |> Map.get(:relationships, %{})
# # |> Enum.reduce(changeset, fn {relationship, relationship_data}, changeset ->
# # relationship = Ash.relationship(changeset.data.__struct__, relationship)
# # relationship_data =
# # relationship_data
# # |> Enum.into(%{}, fn {key, value} ->
# # {key, value.data}
# # end)
# # |> Map.put_new(:current, [])
# # add_relationship_to_changeset(changeset, api, relationship, relationship_data)
# # end)
# # {:ok, new_changeset}
# # end)
# # end
# # end
# end

View file

@ -1,5 +0,0 @@
# defmodule Ash.Actions.Relationships do
# defmodule Change do
# defstruct [:add, :remove, :current]
# end
# end

View file

@ -91,16 +91,18 @@ defmodule Ash.Actions.SideLoad do
end) end)
|> Enum.reduce(data, fn {key, %{data: value}}, data -> |> Enum.reduce(data, fn {key, %{data: value}}, data ->
last_relationship = last_relationship!(resource, key) last_relationship = last_relationship!(resource, key)
lead_path = :lists.droplast(key)
case last_relationship do case last_relationship do
%{type: :many_to_many, name: name} -> %{type: :many_to_many, name: name} ->
# TODO: If we sort the relationships as we do them (doing the join assoc first) # TODO: If we sort the relationships as we do them (doing the join assoc first)
# then we can just use those linked assocs (maybe) # then we can just use those linked assocs (maybe)
join_association = String.to_existing_atom(to_string(name) <> "_join_assoc") join_association = String.to_existing_atom(to_string(name) <> "_join_assoc")
join_path = :lists.droplast(key) ++ [join_association]
join_path = lead_path ++ [join_association]
join_data = Map.get(includes, join_path, []) join_data = Map.get(includes, join_path, [])
map_or_update(data, fn record -> map_or_update(data, lead_path, fn record ->
source_value = Map.get(record, last_relationship.source_field) source_value = Map.get(record, last_relationship.source_field)
join_values = join_values =
@ -125,7 +127,7 @@ defmodule Ash.Actions.SideLoad do
%{cardinality: :many} -> %{cardinality: :many} ->
values = Enum.group_by(value, &Map.get(&1, last_relationship.destination_field)) values = Enum.group_by(value, &Map.get(&1, last_relationship.destination_field))
map_or_update(data, fn record -> map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_field) source_key = Map.get(record, last_relationship.source_field)
related_records = Map.get(values, source_key, []) related_records = Map.get(values, source_key, [])
Map.put(record, last_relationship.name, related_records) Map.put(record, last_relationship.name, related_records)
@ -137,7 +139,7 @@ defmodule Ash.Actions.SideLoad do
{Map.get(item, last_relationship.destination_field), item} {Map.get(item, last_relationship.destination_field), item}
end) end)
map_or_update(data, fn record -> map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_field) source_key = Map.get(record, last_relationship.source_field)
related_record = Map.get(values, source_key) related_record = Map.get(values, source_key)
Map.put(record, last_relationship.name, related_record) Map.put(record, last_relationship.name, related_record)
@ -156,12 +158,18 @@ defmodule Ash.Actions.SideLoad do
data data
end end
defp map_or_update(record, func) when not is_list(record), do: func.(record) defp map_or_update(record, [], func) when not is_list(record), do: func.(record)
defp map_or_update(records, func) do defp map_or_update(records, [], func) do
Enum.map(records, func) Enum.map(records, func)
end end
defp map_or_update(records, [path | tail], func) do
map_or_update(records, [], fn record ->
Map.update!(record, path, &map_or_update(&1, tail, func))
end)
end
defp last_relationship!(resource, [last]) do defp last_relationship!(resource, [last]) do
Ash.relationship(resource, last) || raise "Assumption Failed" Ash.relationship(resource, last) || raise "Assumption Failed"
end end
@ -177,7 +185,7 @@ defmodule Ash.Actions.SideLoad do
{:rel, Ash.relationship(resource, key)}, {:rel, Ash.relationship(resource, key)},
nested_path <- path ++ [relationship], nested_path <- path ++ [relationship],
{:ok, requests} <- {:ok, requests} <-
requests(api, relationship.destination, further, filters, nested_path) do requests(api, relationship.destination, further, filters, root_filter, nested_path) do
default_read = default_read =
Ash.primary_action(relationship.destination, :read) || Ash.primary_action(relationship.destination, :read) ||
raise "Must set default read for #{inspect(resource)}" raise "Must set default read for #{inspect(resource)}"
@ -211,7 +219,7 @@ defmodule Ash.Actions.SideLoad do
path: [:include, Enum.map(nested_path, &Map.get(&1, :name))], path: [:include, Enum.map(nested_path, &Map.get(&1, :name))],
resolve_when_fetch_only?: true, resolve_when_fetch_only?: true,
filter: filter:
side_load_filter2( side_load_filter(
relationship, relationship,
Map.get(filters || %{}, source, []), Map.get(filters || %{}, source, []),
nested_path, nested_path,
@ -237,6 +245,7 @@ defmodule Ash.Actions.SideLoad do
# or for doing many to many joins, but can be slower. # or for doing many to many joins, but can be slower.
# If the relationship is already loaded, we should consider doing an in-memory filtering # If the relationship is already loaded, we should consider doing an in-memory filtering
# Right now, we just use the original query # Right now, we just use the original query
with {:ok, filter} <- with {:ok, filter} <-
true_side_load_filter( true_side_load_filter(
relationship, relationship,
@ -275,7 +284,7 @@ defmodule Ash.Actions.SideLoad do
strict_access?: root_filter not in [:create, :update], strict_access?: root_filter not in [:create, :update],
resolve_when_fetch_only?: true, resolve_when_fetch_only?: true,
filter: filter:
side_load_filter2( side_load_filter(
Ash.relationship(resource, join_relationship.name), Ash.relationship(resource, join_relationship.name),
[], [],
nested_path, nested_path,
@ -325,7 +334,7 @@ defmodule Ash.Actions.SideLoad do
end end
end end
defp side_load_filter2( defp side_load_filter(
%{reverse_relationship: nil, type: :many_to_many} = relationship, %{reverse_relationship: nil, type: :many_to_many} = relationship,
_request_filter, _request_filter,
_prior_path, _prior_path,
@ -338,7 +347,7 @@ defmodule Ash.Actions.SideLoad do
end) end)
end end
defp side_load_filter2( defp side_load_filter(
relationship, relationship,
request_filter, request_filter,
prior_path, prior_path,
@ -372,13 +381,13 @@ defmodule Ash.Actions.SideLoad do
{:ok, reverse_path} -> {:ok, reverse_path} ->
Ash.Filter.parse( Ash.Filter.parse(
relationship.destination, relationship.destination,
put_nested_relationship(request_filter, reverse_path, root_filter) put_nested_relationship(request_filter, reverse_path, root_filter, false)
) )
end end
end) end)
end end
defp side_load_filter2( defp side_load_filter(
relationship, relationship,
request_filter, request_filter,
prior_path, prior_path,
@ -416,7 +425,7 @@ defmodule Ash.Actions.SideLoad do
{:ok, reverse_path} -> {:ok, reverse_path} ->
Ash.Filter.parse( Ash.Filter.parse(
relationship.destination, relationship.destination,
put_nested_relationship(request_filter, reverse_path, root_filter) put_nested_relationship(request_filter, reverse_path, root_filter, false)
) )
:error -> :error ->
@ -450,17 +459,22 @@ defmodule Ash.Actions.SideLoad do
Map.get(data, :data) Map.get(data, :data)
path -> path ->
Map.get(data, [:include, Enum.reverse(path)]) path_names = path |> Enum.reverse() |> Enum.map(& &1.name)
data
|> Map.get(:include, %{})
|> Map.get(path_names, %{})
end end
values = get_fields(source_data.data, pkey) related_data = Map.get(source_data || %{}, :data, [])
cond do cond do
reverse_relationship -> reverse_relationship ->
values = get_fields(related_data, pkey)
{:ok, put_nested_relationship(filter, [reverse_relationship], values)} {:ok, put_nested_relationship(filter, [reverse_relationship], values)}
true -> true ->
ids = Enum.map(source_data.data, &Map.get(&1, relationship.source_field)) ids = Enum.map(related_data, &Map.get(&1, relationship.source_field))
filter_value = filter_value =
case ids do case ids do
@ -588,30 +602,33 @@ defmodule Ash.Actions.SideLoad do
# |> get_field(name, rest) # |> get_field(name, rest)
# end # end
defp put_nested_relationship(_, _, []), do: [__impossible__: true] defp put_nested_relationship(request_filter, path, value, records? \\ true)
defp put_nested_relationship(_, _, nil), do: [__impossible__: true] defp put_nested_relationship(_, _, [], true), do: [__impossible__: true]
defp put_nested_relationship(_, _, nil, true), do: [__impossible__: true]
defp put_nested_relationship(_, _, [], false), do: []
defp put_nested_relationship(_, _, nil, false), do: []
defp put_nested_relationship(request_filter, path, value) when not is_list(value) do defp put_nested_relationship(request_filter, path, value, records?) when not is_list(value) do
put_nested_relationship(request_filter, path, [value]) put_nested_relationship(request_filter, path, [value], records?)
end end
defp put_nested_relationship(request_filter, [rel | rest], values) do defp put_nested_relationship(request_filter, [rel | rest], values, records?) do
[ [
{rel, put_nested_relationship(request_filter, rest, values)} {rel, put_nested_relationship(request_filter, rest, values, records?)}
] ]
end end
defp put_nested_relationship(request_filter, [], [[{field, _}] | _] = keys) do defp put_nested_relationship(request_filter, [], [[{field, _}] | _] = keys, _) do
add_relationship_id_filter(request_filter, field, Enum.map(keys, &elem(&1, 1))) add_relationship_id_filter(request_filter, field, Enum.map(keys, &elem(&1, 1)))
end end
defp put_nested_relationship(request_filter, [], [values]) do defp put_nested_relationship(request_filter, [], [values], _) do
Enum.reduce(values, request_filter, fn {field, value}, filter -> Enum.reduce(values, request_filter, fn {field, value}, filter ->
add_relationship_id_filter(filter, field, [value]) add_relationship_id_filter(filter, field, [value])
end) end)
end end
defp put_nested_relationship(request_filter, [], values) do defp put_nested_relationship(request_filter, [], values, _) do
Keyword.update(request_filter, :or, values, &Kernel.++(&1, values)) Keyword.update(request_filter, :or, values, &Kernel.++(&1, values))
end end

View file

@ -467,6 +467,7 @@ defmodule Ash.Api.Interface do
end end
end end
defp unwrap_or_raise!(:ok), do: :ok
defp unwrap_or_raise!({:ok, result}), do: result defp unwrap_or_raise!({:ok, result}), do: result
defp unwrap_or_raise!({:error, error}) when is_bitstring(error) do defp unwrap_or_raise!({:error, error}) when is_bitstring(error) do

View file

@ -69,6 +69,9 @@ defmodule Ash.DataLayer.Ets do
{:ok, %{query | sort: sort}} {:ok, %{query | sort: sort}}
end end
@impl true
def run_query(%Query{filter: %Ash.Filter{impossible?: true}}, _), do: {:ok, []}
@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},

View file

@ -9,7 +9,7 @@ defmodule Ash.Filter do
requests: [], requests: [],
path: [], path: [],
errors: [], errors: [],
impossible: false impossible?: false
] ]
alias Ash.Engine.Request alias Ash.Engine.Request
@ -23,7 +23,7 @@ defmodule Ash.Filter do
attributes: Keyword.t(), attributes: Keyword.t(),
relationships: Map.t(), relationships: Map.t(),
path: list(atom), path: list(atom),
impossible: boolean, impossible?: boolean,
errors: list(String.t()), errors: list(String.t()),
requests: list(Ash.Engine.Request.t()) requests: list(Ash.Engine.Request.t())
} }
@ -258,7 +258,7 @@ defmodule Ash.Filter do
# TODO: We should probably include some kind of filter that *makes* it immediately impossible # TODO: We should probably include some kind of filter that *makes* it immediately impossible
# that way, if the data layer doesn't check impossibility they will run the simpler query, # that way, if the data layer doesn't check impossibility they will run the simpler query,
# like for each pkey field say `[field: [in: []]]` # like for each pkey field say `[field: [in: []]]`
%{filter | impossible: true} %{filter | impossible?: true}
else else
filter filter
end end
@ -293,14 +293,14 @@ defmodule Ash.Filter do
defp lift_impossibility(filter) do defp lift_impossibility(filter) do
with_related_impossibility = with_related_impossibility =
if Enum.any?(filter.relationships || %{}, fn {_, val} -> Map.get(val, :impossible) end) do if Enum.any?(filter.relationships || %{}, fn {_, val} -> Map.get(val, :impossible?) end) do
Map.put(filter, :impossible, true) Map.put(filter, :impossible?, true)
else else
filter filter
end end
Map.update!(with_related_impossibility, :ors, fn ors -> Map.update!(with_related_impossibility, :ors, fn ors ->
Enum.reject(ors, &Map.get(&1, :impossible)) Enum.reject(ors, &Map.get(&1, :impossible?))
end) end)
end end
@ -407,6 +407,9 @@ defmodule Ash.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 == :__impossible__ && value == true ->
%{filter | impossible?: true}
key in [:or, :and, :not] -> key in [:or, :and, :not] ->
new_filter = add_expression_level_boolean_filter(filter, resource, key, value) new_filter = add_expression_level_boolean_filter(filter, resource, key, value)

View file

@ -70,7 +70,7 @@ defimpl Inspect, for: Ash.Filter do
ors: ors, ors: ors,
relationships: relationships, relationships: relationships,
attributes: attributes, attributes: attributes,
impossible: impossible impossible?: impossible
}, },
opts opts
) )
@ -89,7 +89,7 @@ defimpl Inspect, for: Ash.Filter do
end end
end end
def inspect(%{impossible: impossible} = filter, opts) do def inspect(%{impossible?: impossible} = filter, opts) do
rels = rels =
filter filter
|> Map.get(:relationships) |> Map.get(:relationships)

View file

@ -243,7 +243,7 @@ defmodule Ash.Resource.Relationships do
has_many_name, has_many_name,
unquote(config)[:through], unquote(config)[:through],
destination_field: unquote(config)[:source_field_on_join_table], destination_field: unquote(config)[:source_field_on_join_table],
source_field: unquote(config)[:source_field] source_field: unquote(config)[:source_field] || :id
) )
with {:many_to_many, {:ok, many_to_many}} <- {:many_to_many, many_to_many}, with {:many_to_many, {:ok, many_to_many}} <- {:many_to_many, many_to_many},

View file

@ -82,7 +82,7 @@ defmodule Ash.Test.Actions.DestroyTest do
test "allows destroying a record" do test "allows destroying a record" do
post = Api.create!(Post, attributes: %{title: "foo", contents: "bar"}) post = Api.create!(Post, attributes: %{title: "foo", contents: "bar"})
assert Api.destroy!(post) == post assert Api.destroy!(post) == :ok
refute Api.get!(Post, post.id) refute Api.get!(Post, post.id)
end end

View file

@ -12,7 +12,7 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
end end
describe "representation" do describe "representation" do
test "it creates a relationship" do test "it creates a relationship and a join relationship" do
defposts do defposts do
relationships do relationships do
many_to_many :foobars, Foobar, through: SomeResource many_to_many :foobars, Foobar, through: SomeResource
@ -20,16 +20,28 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
end end
assert [ assert [
%Ash.Resource.Relationships.HasMany{
cardinality: :many,
destination: SomeResource,
destination_field: :posts_id,
name: :foobars_join_assoc,
source: Ash.Test.Resource.Relationships.ManyToManyTest.Post,
source_field: :id,
type: :has_many,
write_rules: []
},
%Ash.Resource.Relationships.ManyToMany{ %Ash.Resource.Relationships.ManyToMany{
cardinality: :many, cardinality: :many,
destination: Foobar, destination: Foobar,
destination_field: :id, destination_field: :id,
destination_field_on_join_table: :foobars_id, destination_field_on_join_table: :foobars_id,
name: :foobars, name: :foobars,
source: Ash.Test.Resource.Relationships.ManyToManyTest.Post,
source_field: :id, source_field: :id,
source_field_on_join_table: :posts_id, source_field_on_join_table: :posts_id,
through: SomeResource, through: SomeResource,
type: :many_to_many type: :many_to_many,
write_rules: []
} }
] = Ash.relationships(Post) ] = Ash.relationships(Post)
end end