mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
WIP
This commit is contained in:
parent
82cd7c00b1
commit
d29afca057
21 changed files with 372 additions and 161 deletions
|
@ -53,7 +53,6 @@ end
|
|||
|
||||
* Make our router cabaple of describing its routes in `mix phx.routes` Chris McCord says that we could probably power that, seeing as phoenix controls both APIs, and that capability could be added to `Plug.Router`
|
||||
* Finish the serializer
|
||||
* Make primary key type configurable, and support composite primary keys
|
||||
* Make a DSL for join tables to support complex validation/hooks into how they work, support more than just table names in `join_through`
|
||||
* DSL level validations! Things like includes validating that their chain exists. All DSL structs should be strictly validated when they are created.
|
||||
* Especially at compile time, we should *never* ignore or skip invalid options. If an option is present and invalid, an error is raised.
|
||||
|
@ -99,4 +98,8 @@ end
|
|||
* Add `can?(:bulk_update)` to data layers, so we can more efficiently update relationships
|
||||
* Figure out under what circumstances we can bulk fetch when reading before updating many_to_many and to_many relationships, and do so.
|
||||
* most relationship stuff can't be done w/o primary keys
|
||||
* includer errors are super obscure because you can't tell what action they are about
|
||||
* includer errors are super obscure because you can't tell what action they are about
|
||||
* Allow encoding database-level constraints into the resource, like "nullable: false" or something. This will let us validate things like not leaving orphans when bulk updating a many to many
|
||||
* Validate filters, now that there can be duplicates. Doesn't make sense to provide two "exact equals" filters
|
||||
* Eventually data_layers should state what raw types they support, and the filters they support on those raw types
|
||||
* Raise on composite primary key if data layer can't do it
|
|
@ -55,16 +55,15 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
%{
|
||||
destination: destination,
|
||||
destination_field: destination_field,
|
||||
source_field: source_field,
|
||||
name: rel_name
|
||||
},
|
||||
source_field: source_field
|
||||
} = relationship,
|
||||
identifier,
|
||||
authorize?,
|
||||
user
|
||||
) do
|
||||
case Filter.value_to_primary_key_filter(destination, identifier) do
|
||||
{:error, _error} ->
|
||||
Ecto.Changeset.add_error(changeset, rel_name, "Invalid primary key supplied")
|
||||
Ecto.Changeset.add_error(changeset, relationship.name, "Invalid primary key supplied")
|
||||
|
||||
{:ok, filter} ->
|
||||
before_change(changeset, fn changeset ->
|
||||
|
@ -73,7 +72,7 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
changeset
|
||||
|> Ecto.Changeset.put_change(source_field, Map.get(record, destination_field))
|
||||
|> after_change(fn _changeset, result ->
|
||||
{:ok, Map.put(result, rel_name, record)}
|
||||
{:ok, Map.put(result, relationship.name, record)}
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
|
@ -88,16 +87,15 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
%{
|
||||
destination: destination,
|
||||
destination_field: destination_field,
|
||||
source_field: source_field,
|
||||
name: rel_name
|
||||
},
|
||||
source_field: source_field
|
||||
} = relationship,
|
||||
identifier,
|
||||
authorize?,
|
||||
user
|
||||
) do
|
||||
case Filter.value_to_primary_key_filter(destination, identifier) do
|
||||
{:error, _error} ->
|
||||
Ecto.Changeset.add_error(changeset, rel_name, "Invalid primary key supplied")
|
||||
Ecto.Changeset.add_error(changeset, relationship.name, "Invalid primary key supplied")
|
||||
|
||||
{:ok, filter} ->
|
||||
after_change(changeset, fn _changeset, result ->
|
||||
|
@ -107,12 +105,29 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
api.get(destination, filter, %{authorize?: authorize?, user: user}),
|
||||
{:ok, updated_record} <-
|
||||
api.update(record, %{attributes: %{destination_field => value}}) do
|
||||
{:ok, Map.put(result, rel_name, updated_record)}
|
||||
{:ok, Map.put(result, relationship.name, updated_record)}
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
# ChangesetHelpers.many_to_many_assoc_on_create(changeset, rel, value, authorize?, user)
|
||||
def many_to_many_assoc_update(changeset, %{name: rel_name}, identifier, _, _)
|
||||
when not is_list(identifier) do
|
||||
Ecto.Changeset.add_error(changeset, rel_name, "Invalid value")
|
||||
end
|
||||
|
||||
# def many_to_many_assoc_update(
|
||||
# changeset,
|
||||
# %{through: through, name: rel_name},
|
||||
# identifiers,
|
||||
# authorize?,
|
||||
# user
|
||||
# ) do
|
||||
# # case Filter.value_to_primary_key_filter
|
||||
# # case(values_to_primary_key_filters(destination, identifiers))
|
||||
# end
|
||||
|
||||
def has_many_assoc_update(changeset, %{name: rel_name}, identifier, _, _)
|
||||
when not is_list(identifier) do
|
||||
Ecto.Changeset.add_error(changeset, rel_name, "Invalid value")
|
||||
|
@ -123,35 +138,36 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
%{
|
||||
destination: destination,
|
||||
destination_field: destination_field,
|
||||
source_field: source_field,
|
||||
name: rel_name
|
||||
},
|
||||
source_field: source_field
|
||||
} = relationship,
|
||||
identifiers,
|
||||
authorize?,
|
||||
user
|
||||
) do
|
||||
case values_to_primary_key_filters(destination, identifiers) do
|
||||
{:error, _error} ->
|
||||
Ecto.Changeset.add_error(changeset, rel_name, "Invalid primary key supplied")
|
||||
Ecto.Changeset.add_error(changeset, relationship.name, "Invalid primary key supplied")
|
||||
|
||||
{:ok, filters} ->
|
||||
after_change(changeset, fn _changeset, %resource{} = result ->
|
||||
value = Map.get(result, source_field)
|
||||
|
||||
params = %{
|
||||
filter: [from_related: {result, relationship}],
|
||||
paginate?: false,
|
||||
authorize?: authorize?,
|
||||
user: user
|
||||
}
|
||||
|
||||
with {:ok, %{results: related}} <-
|
||||
api.read(destination, %{
|
||||
filter: %{from_related: {result, rel_name}},
|
||||
paginate?: false,
|
||||
authorize?: authorize?,
|
||||
user: user
|
||||
}),
|
||||
api.read(destination, params),
|
||||
{:ok, to_relate} <-
|
||||
get_to_relate(api, filters, destination, authorize?, user),
|
||||
to_clear <- get_no_longer_present(resource, related, to_relate),
|
||||
:ok <- clear_related(api, resource, to_clear, destination_field, authorize?, user),
|
||||
{:ok, now_related} <-
|
||||
relate_items(api, to_relate, destination_field, value, authorize?, user) do
|
||||
Map.put(result, rel_name, now_related)
|
||||
{:ok, Map.put(result, relationship.name, now_related)}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
@ -159,13 +175,13 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
|
||||
defp relate_items(api, to_relate, destination_field, destination_field_value, authorize?, user) do
|
||||
Enum.reduce(to_relate, {:ok, []}, fn
|
||||
to_be_related, {:ok, now_related} ->
|
||||
{to_be_related, updates}, {:ok, now_related} ->
|
||||
case api.update(to_be_related, %{
|
||||
attributes: %{destination_field => destination_field_value},
|
||||
attributes: Map.put(updates, destination_field, destination_field_value),
|
||||
authorize?: authorize?,
|
||||
user: user
|
||||
}) do
|
||||
{:ok, newly_related} -> [newly_related | now_related]
|
||||
{:ok, newly_related} -> {:ok, [newly_related | now_related]}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
|
||||
|
@ -196,7 +212,9 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
|
||||
to_relate_pkeys =
|
||||
to_relate
|
||||
|> Enum.map(&Map.take(&1, primary_key))
|
||||
|> Enum.map(fn {to_relate_item, _updates} ->
|
||||
Map.take(to_relate_item, primary_key)
|
||||
end)
|
||||
|> MapSet.new()
|
||||
|
||||
Enum.reject(currently_related, fn related_item ->
|
||||
|
@ -205,9 +223,12 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
end
|
||||
|
||||
defp get_to_relate(api, filters, destination, authorize?, user) do
|
||||
Enum.reduce(filters, {:ok, nil}, fn
|
||||
filter, {:ok, _} ->
|
||||
api.get(destination, filter, %{authorize?: authorize?, user: user})
|
||||
Enum.reduce(filters, {:ok, []}, fn
|
||||
{filter, updates}, {:ok, to_relate} ->
|
||||
case api.get(destination, filter, %{authorize?: authorize?, user: user}) do
|
||||
{:ok, to_relate_item} -> {:ok, [{to_relate_item, updates} | to_relate]}
|
||||
{:error, errors} -> {:error, errors}
|
||||
end
|
||||
|
||||
_, {:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -218,8 +239,18 @@ defmodule Ash.Actions.ChangesetHelpers do
|
|||
Enum.reduce(identifiers, {:ok, []}, fn
|
||||
identifier, {:ok, filters} ->
|
||||
case Filter.value_to_primary_key_filter(destination, identifier) do
|
||||
{:ok, filter} -> [filter | filters]
|
||||
{:error, error} -> {:error, error}
|
||||
{:ok, filter} ->
|
||||
updates =
|
||||
if is_map(identifier) do
|
||||
Map.drop(identifier, Ash.primary_key(destination))
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
{:ok, [{filter, updates} | filters]}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
|
||||
_, {:error, error} ->
|
||||
|
|
|
@ -126,8 +126,8 @@ defmodule Ash.Actions.Create do
|
|||
%{type: :has_many} = rel ->
|
||||
ChangesetHelpers.has_many_assoc_update(changeset, rel, value, authorize?, user)
|
||||
|
||||
# %{type: :many_to_many} = rel ->
|
||||
# many_to_many_assoc_update(changeset, rel, value, repo)
|
||||
%{type: :many_to_many} = rel ->
|
||||
ChangesetHelpers.many_to_many_assoc_on_create(changeset, rel, value, authorize?, user)
|
||||
|
||||
_ ->
|
||||
Ecto.Changeset.add_error(changeset, relationship, "No such relationship")
|
||||
|
|
|
@ -5,6 +5,33 @@ defmodule Ash.Actions.Filter do
|
|||
|
||||
@type filter_type :: :equal
|
||||
|
||||
def is_filter_subset?(resource, source_filter, candidate) do
|
||||
candidate = process(resource, candidate)
|
||||
|
||||
Enum.reduce(candidate, true, fn
|
||||
{candidate_key, candidate_value}, true ->
|
||||
case source_filter[candidate_key] do
|
||||
{:in, list} ->
|
||||
list_subset_of?(list, candidate_value)
|
||||
|
||||
filter_value ->
|
||||
filter_value == candidate_value
|
||||
end
|
||||
|
||||
_, false ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
defp list_subset_of?(source_list, candidate_list) do
|
||||
candidate_set =
|
||||
candidate_list
|
||||
|> List.wrap()
|
||||
|> MapSet.new()
|
||||
|
||||
Enum.all?(source_list, &MapSet.member?(candidate_set, &1))
|
||||
end
|
||||
|
||||
@spec filter_types() :: list(filter_type())
|
||||
def filter_types() do
|
||||
@filter_types
|
||||
|
@ -41,88 +68,153 @@ defmodule Ash.Actions.Filter do
|
|||
do_value_to_primary_key_filter(resource, [field], %{field => value})
|
||||
end
|
||||
|
||||
defp do_value_to_primary_key_filter(_, _, _), do: {:error, "Invalid primary key"}
|
||||
defp do_value_to_primary_key_filter(_, _, _), do: {:error, ["Invalid primary key"]}
|
||||
|
||||
# This logic will need to get more complex as the ability to customize filter handling arises
|
||||
# as well as when complex filter types are added
|
||||
def process(resource, filter) do
|
||||
state = %{errors: [], authorization: [], filter: []}
|
||||
|
||||
filter
|
||||
|> Enum.reduce({%{}, []}, fn {name, value}, {acc, errors} ->
|
||||
process_filter(resource, name, value, {acc, errors})
|
||||
|> Enum.reduce(state, fn {name, value}, state ->
|
||||
process_filter(resource, name, value, state)
|
||||
end)
|
||||
|> case do
|
||||
{filter, []} -> {:ok, filter}
|
||||
{_, errors} -> {:error, errors}
|
||||
%{filter: filter, errors: [], authorization: authorization} -> {:ok, filter, authorization}
|
||||
%{errors: errors} -> {:error, errors}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Look into making `from_related` accept a full filter statement for the source entity,
|
||||
# so you can say `%{filter: %{from_related: %{owner: %{name: "zach"}}}}. This would let us optimize
|
||||
# so you can say `%{filter: [from_related: [owner: [name: "zach"]]]}. This would let us optimize
|
||||
# and predict query results better, as well as represent the request to "get" those entities we
|
||||
# are filtering against as an ash request, so that authorization happens for free :D
|
||||
defp process_filter(_resource, :from_related, {[], relationship}, {filter, errors})
|
||||
when is_list(relationship) do
|
||||
{Map.put(filter, :__impossible__, true), errors}
|
||||
defp process_filter(resource, :from_related, {item, relationship}, state)
|
||||
when not is_list(item) do
|
||||
process_filter(resource, :from_related, {[item], relationship}, state)
|
||||
end
|
||||
|
||||
defp process_filter(resource, :from_related, {related, relationship_name}, {filter, errors})
|
||||
when is_atom(relationship_name) do
|
||||
case Ash.relationship(resource, relationship_name) do
|
||||
nil ->
|
||||
{filter, ["no such relationship: #{relationship_name}" | errors]}
|
||||
defp process_filter(_resource, :from_related, {_, relationship}, state)
|
||||
when is_atom(relationship) do
|
||||
add_error(state, "Must provide relationship struct, not relationship name")
|
||||
end
|
||||
|
||||
relationship ->
|
||||
{Map.put(filter, :from_related, {related, relationship}), errors}
|
||||
defp process_filter(
|
||||
_resource,
|
||||
:from_related,
|
||||
{%_source_resource{}, %{type: :many_to_many} = _rel},
|
||||
state
|
||||
) do
|
||||
add_error(state, "We don't support many to many filters yet")
|
||||
end
|
||||
|
||||
defp process_filter(
|
||||
resource,
|
||||
:from_related,
|
||||
{related, rel},
|
||||
state
|
||||
) do
|
||||
case related do
|
||||
[] ->
|
||||
process_filter(resource, rel.destination_field, [in: []], state)
|
||||
|
||||
[related] ->
|
||||
process_filter(resource, rel.destination_field, Map.get(related, rel.source_field), state)
|
||||
|
||||
[_ | _] = related ->
|
||||
values = Enum.map(related, &Map.get(&1, rel.source_field))
|
||||
|
||||
process_filter(resource, rel.destination_field, [in: values], state)
|
||||
end
|
||||
end
|
||||
|
||||
defp process_filter(_resource, :from_related, {_related, relationship}, {filter, errors})
|
||||
when is_atom(relationship) do
|
||||
{filter,
|
||||
[
|
||||
"Must provide structs, or a relationship struct. Cannot pass ids and an atom relationship. #{
|
||||
relationship
|
||||
}"
|
||||
| errors
|
||||
]}
|
||||
defp process_filter(resource, field, value, state) when not is_list(value) do
|
||||
process_filter(resource, field, [equal: value], state)
|
||||
end
|
||||
|
||||
defp process_filter(_resource, :from_related, {related, relationship}, {filter, errors}) do
|
||||
{Map.put(filter, :from_related, {related, relationship}), errors}
|
||||
end
|
||||
|
||||
# {:from_related, {[_ | _] = related, %{} = relationship}} ->
|
||||
# {Map.put(filter, :from_related, {related, relationship}), errors}
|
||||
|
||||
defp process_filter(resource, field, value, {filter, errors}) do
|
||||
defp process_filter(resource, field, value, state) do
|
||||
cond do
|
||||
attr = Ash.attribute(resource, field) ->
|
||||
process_attribute_filter(resource, attr, value, {filter, errors})
|
||||
Enum.reduce(value, state, fn {key, val}, state ->
|
||||
do_process_filter(resource, attr.name, attr.type, key, val, state)
|
||||
end)
|
||||
|
||||
rel = Ash.relationship(resource, field) ->
|
||||
process_relationship_filter(resource, rel, value, {filter, errors})
|
||||
case rel do
|
||||
%{type: :many_to_many} ->
|
||||
add_error(state, "no filtering on many to many")
|
||||
|
||||
%{source_field: source_field} ->
|
||||
process_filter(resource, source_field, [equal: value], state)
|
||||
end
|
||||
|
||||
true ->
|
||||
{filter, ["Unsupported filter: #{inspect(field)}" | errors]}
|
||||
add_error(state, "unknown filter #{field}")
|
||||
end
|
||||
end
|
||||
|
||||
defp process_attribute_filter(resource, %{name: name, type: type}, value, {filter, errors}) do
|
||||
with {:ok, casted} <- Ash.Type.cast_input(type, value),
|
||||
filters <- Ash.Type.supported_filter_types(type, Ash.data_layer(resource)),
|
||||
{:supported, true} <- {:supported, :equal in filters} do
|
||||
{Map.put(filter, name, casted), errors}
|
||||
defp do_process_filter(resource, field, field_type, filter_type, value, state) do
|
||||
with {:ok, casted} <- Ash.Type.cast_input(field_type, value),
|
||||
{:supported?, true} <- {:supported?, supports_filter?(resource, field_type, filter_type)} do
|
||||
%{state | filter: add_filter(state.filter, field, filter_type, casted)}
|
||||
else
|
||||
:error ->
|
||||
{filter, ["Invalid value: #{inspect(value)} for #{inspect(name)}" | errors]}
|
||||
add_error(state, "Invalid value: #{inspect(value)} for #{inspect(field)}")
|
||||
|
||||
{:supported, false} ->
|
||||
{filter, ["Cannot filter #{inspect(name)} for equality." | errors]}
|
||||
{:supported?, false} ->
|
||||
add_error(state, "Cannot use filter type #{filter_type} on #{inspect(field)}.")
|
||||
end
|
||||
end
|
||||
|
||||
defp process_relationship_filter(_resource, %{name: name}, value, {filter, errors}) do
|
||||
# TODO: type validate, potentially expand list of ids into a boolean filter statement
|
||||
{Map.put(filter, name, value), errors}
|
||||
defp supports_filter?(resource, type, filter_type) do
|
||||
Ash.Type.supports_filter?(type, filter_type, Ash.data_layer(resource))
|
||||
end
|
||||
|
||||
defp add_filter(filter, field, :equal, value) do
|
||||
cond do
|
||||
colliding_equal_filter?(filter, field, value) ->
|
||||
filter
|
||||
|> Keyword.put(:__impossible__, true)
|
||||
|> Keyword.put(field, [{:equal, value} | filter[field]])
|
||||
|
||||
colliding_in_filter?(filter, field, value) ->
|
||||
filter
|
||||
|> Keyword.put(:__impossible__, true)
|
||||
|> Keyword.put(field, [{:equal, value} | filter[field]])
|
||||
|
||||
true ->
|
||||
Keyword.put(filter, field, equal: value)
|
||||
end
|
||||
end
|
||||
|
||||
# defp process_relationship_filter(_resource, %{name: name}, value, {filter, errors}) do
|
||||
# # TODO: type validate, potentially expand list of ids into a boolean filter statement
|
||||
# {filter, ["no relationship filters" | errors]}
|
||||
# end
|
||||
|
||||
defp colliding_equal_filter?(filter, name, casted) do
|
||||
case Keyword.fetch(filter, name) do
|
||||
:error ->
|
||||
false
|
||||
|
||||
{:ok, filter} ->
|
||||
Enum.any?(filter, fn {key, value} ->
|
||||
key == :equal and value != casted
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp colliding_in_filter?(filter, name, casted) do
|
||||
case Keyword.fetch(filter, name) do
|
||||
:error ->
|
||||
false
|
||||
|
||||
{:ok, filter} ->
|
||||
Enum.any?(filter, fn {key, value} ->
|
||||
key == :in and casted not in value
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_error(state, error), do: %{state | errors: [error | state.errors]}
|
||||
end
|
||||
|
|
|
@ -47,12 +47,13 @@ defmodule Ash.Actions.Read do
|
|||
|
||||
defp do_run(resource, action, api, params) do
|
||||
with query <- Ash.DataLayer.resource_to_query(resource),
|
||||
{:ok, filter} <- Ash.Actions.Filter.process(resource, Map.get(params, :filter, %{})),
|
||||
{:ok, sort} <- Ash.Actions.Sort.process(resource, Map.get(params, :sort, [])),
|
||||
{:ok, filtered_query} <- Ash.DataLayer.filter(query, filter, resource),
|
||||
{:ok, sorted_query} <- Ash.DataLayer.sort(filtered_query, sort, resource),
|
||||
{:ok, filter, authorization} <-
|
||||
Ash.Actions.Filter.process(resource, Map.get(params, :filter, %{})),
|
||||
{:ok, sorted_query} <- Ash.DataLayer.sort(query, sort, resource),
|
||||
{:ok, filtered_query} <- Ash.DataLayer.filter(sorted_query, filter, resource),
|
||||
{:ok, paginator} <-
|
||||
Ash.Actions.Paginator.paginate(api, resource, action, sorted_query, params),
|
||||
Ash.Actions.Paginator.paginate(api, resource, action, filtered_query, params),
|
||||
{:ok, found} <- Ash.DataLayer.run_query(paginator.query, resource) do
|
||||
{:ok, %{paginator | results: found}}
|
||||
else
|
||||
|
|
|
@ -108,7 +108,7 @@ defmodule Ash.Actions.Update do
|
|||
ChangesetHelpers.has_many_assoc_update(changeset, rel, value, authorize?, user)
|
||||
|
||||
# %{type: :many_to_many} = rel ->
|
||||
# many_to_many_assoc_update(changeset, rel, value, repo)
|
||||
# ChangesetHelpers.many_to_many_assoc_update(changeset, rel, value, authorize?, user)
|
||||
|
||||
_ ->
|
||||
Ecto.Changeset.add_error(changeset, relationship, "No such relationship")
|
||||
|
|
|
@ -42,7 +42,7 @@ defmodule Ash.Api do
|
|||
|
||||
Then you can interact through that Api with the actions that those resources expose.
|
||||
For example: `MyApp.Api.create(OneResource, %{attributes: %{name: "thing"}})`, or
|
||||
`MyApp.Api.read(OneResource, %{filter: %{name: "thing"}})`. Corresponding actions must
|
||||
`MyApp.Api.read(OneResource, %{filter: [name: "thing"]})`. Corresponding actions must
|
||||
be defined in your resources in order to call them through the Api.
|
||||
"""
|
||||
defmacro __using__(opts) do
|
||||
|
|
|
@ -9,6 +9,8 @@ defmodule Ash.Authorization.Check.RelationshipAccess do
|
|||
"""
|
||||
use Ash.Authorization.Check
|
||||
|
||||
alias Ash.Actions.Filter
|
||||
|
||||
def init(opts) do
|
||||
with {:key, {:ok, relationship}} <- {:key, Keyword.fetch(opts, :relationship)},
|
||||
{:is_nil, false} <- {:is_nil, is_nil(relationship)},
|
||||
|
@ -93,16 +95,12 @@ defmodule Ash.Authorization.Check.RelationshipAccess do
|
|||
|
||||
def precheck(user, %{resource: resource, params: params}, opts) do
|
||||
relationship_name = opts[:relationship]
|
||||
relationship = Ash.relationship(resource, relationship_name)
|
||||
user_id = user.id
|
||||
source_field = relationship.source_field
|
||||
|
||||
filter = Map.get(params, :filter, [])
|
||||
|
||||
cond do
|
||||
match?(%{filter: %{^relationship_name => ^user_id}}, params) ->
|
||||
{:precheck, true}
|
||||
|
||||
relationship.type != :many_to_many &&
|
||||
match?(%{filter: %{^source_field => ^user_id}}, params) ->
|
||||
Filter.is_filter_subset?(resource, filter, [{relationship_name, user_id}]) ->
|
||||
{:precheck, true}
|
||||
|
||||
opts[:enforce_access?] ->
|
||||
|
|
|
@ -42,10 +42,12 @@ defmodule Ash.Authorization.Check.UserField do
|
|||
{:precheck, value_will_equal_field?}
|
||||
end
|
||||
|
||||
def precheck(user, context, opts) do
|
||||
def precheck(user, context = %{resource: resource}, opts) do
|
||||
user_value = Map.get(user, opts[:user_field])
|
||||
record_field = opts[:record_field]
|
||||
filter = Map.get(context, :filter, [])
|
||||
|
||||
{:precheck, match?(%{params: %{filter: %{^record_field => ^user_value}}}, context)}
|
||||
{:precheck,
|
||||
Ash.Actions.Filter.is_filter_subset?(resource, filter, [{record_field, user_value}])}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Ash.DataLayer do
|
||||
@type feature() :: :transact | :query_async
|
||||
@type filter_type :: :equal | :in
|
||||
@type feature() :: :transact | :query_async | {:filter, filter_type}
|
||||
|
||||
@callback filter(Ash.query(), Ash.filter(), resource :: Ash.resource()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
|
|
|
@ -37,6 +37,10 @@ defmodule Ash.DataLayer.Ets do
|
|||
@impl true
|
||||
def can?(:query_async), do: false
|
||||
def can?(:transact), do: false
|
||||
def can?({:filter, :equal}), do: true
|
||||
def can?({:filter, :in}), do: true
|
||||
def can?(:composite_primary_key), do: true
|
||||
def can?(_), do: false
|
||||
|
||||
@impl true
|
||||
def resource_to_query(resource) do
|
||||
|
@ -56,6 +60,8 @@ defmodule Ash.DataLayer.Ets do
|
|||
|
||||
@impl true
|
||||
def filter(query, filter, resource) do
|
||||
query = %{query | filter: query.filter || []}
|
||||
|
||||
Enum.reduce(filter, {:ok, query}, fn
|
||||
_, {:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -71,7 +77,7 @@ defmodule Ash.DataLayer.Ets do
|
|||
end
|
||||
|
||||
defp do_filter(query, field, id, _resource) do
|
||||
{:ok, %{query | filter: Map.put(query.filter || %{}, field, id)}}
|
||||
{:ok, %{query | filter: [{field, id} | query.filter]}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -118,11 +124,10 @@ defmodule Ash.DataLayer.Ets do
|
|||
end)
|
||||
end
|
||||
|
||||
# Id matching will have to be smarter when new filter types
|
||||
# are added. Id matching naturally only supports equality
|
||||
# filters
|
||||
# Id matching would be great here for performance, but
|
||||
# for behaviour is technically unnecessary
|
||||
defp filter_to_matchspec(resource, filter) do
|
||||
filter = filter || %{}
|
||||
filter = filter || []
|
||||
|
||||
{pkey_match, pkey_names} =
|
||||
resource
|
||||
|
@ -132,20 +137,28 @@ defmodule Ash.DataLayer.Ets do
|
|||
{:_, pkey_names}
|
||||
|
||||
attr, {pkey_match, pkey_names} ->
|
||||
case Map.fetch(filter, attr) do
|
||||
{:ok, value} -> {Map.put(pkey_match, attr, value), [attr | pkey_names]}
|
||||
:error -> {:_, [attr | pkey_names]}
|
||||
with {:ok, field_filter} <- Keyword.fetch(filter, attr),
|
||||
{:ok, value} <- Keyword.fetch(field_filter, :equal) do
|
||||
{Map.put(pkey_match, attr, value), [attr | pkey_names]}
|
||||
else
|
||||
:error ->
|
||||
{:_, [attr | pkey_names]}
|
||||
end
|
||||
end)
|
||||
|
||||
starting_matchspec = {{pkey_match, %{__struct__: resource}}, [], [:"$_"]}
|
||||
|
||||
filter
|
||||
|> Kernel.||(%{})
|
||||
|> Map.drop(pkey_names)
|
||||
|> Enum.reduce({:ok, {starting_matchspec, 1}}, fn
|
||||
{key, value}, {:ok, {spec, binding}} ->
|
||||
do_filter_to_matchspec(resource, key, value, spec, binding)
|
||||
|> Kernel.||([])
|
||||
|> 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
|
||||
{key, type, value}, {:ok, {spec, bindings}} ->
|
||||
do_filter_to_matchspec(resource, key, type, value, spec, bindings)
|
||||
|
||||
_, {:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -156,10 +169,10 @@ defmodule Ash.DataLayer.Ets do
|
|||
end
|
||||
end
|
||||
|
||||
defp do_filter_to_matchspec(resource, key, value, spec, binding) do
|
||||
defp do_filter_to_matchspec(resource, key, type, value, spec, binding) do
|
||||
cond do
|
||||
attr = Ash.attribute(resource, key) ->
|
||||
do_filter_to_matchspec_attribute(resource, attr, value, spec, binding)
|
||||
do_filter_to_matchspec_attribute(resource, attr.name, type, value, spec, binding)
|
||||
|
||||
_rel = Ash.relationship(resource, key) ->
|
||||
{:error, "relationship filtering not supported"}
|
||||
|
@ -171,17 +184,38 @@ defmodule Ash.DataLayer.Ets do
|
|||
|
||||
defp do_filter_to_matchspec_attribute(
|
||||
_resource,
|
||||
%{name: name},
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
{{id_match, struct_match}, conditions, matcher},
|
||||
binding
|
||||
bindings
|
||||
) do
|
||||
condition = {:==, :"$#{binding}", value}
|
||||
case Map.get(bindings, name) do
|
||||
nil ->
|
||||
binding = bindings |> Map.values() |> Enum.max(fn -> 0 end) |> Kernel.+(1)
|
||||
condition = condition(type, value, binding)
|
||||
|
||||
new_spec =
|
||||
{{id_match, Map.put(struct_match, name, :"$#{binding}")}, [condition | conditions], matcher}
|
||||
new_spec =
|
||||
{{id_match, Map.put(struct_match, name, :"$#{binding}")}, [condition | conditions],
|
||||
matcher}
|
||||
|
||||
{:ok, {new_spec, binding + 1}}
|
||||
{:ok, {new_spec, Map.put(bindings, name, binding)}}
|
||||
|
||||
binding ->
|
||||
condition = condition(type, value, binding)
|
||||
|
||||
new_spec = {{id_match, struct_match}, [condition | conditions], matcher}
|
||||
|
||||
{:ok, new_spec, bindings}
|
||||
end
|
||||
end
|
||||
|
||||
def condition(:equal, value, binding) do
|
||||
{:==, :"$#{binding}", value}
|
||||
end
|
||||
|
||||
def condition(:in, value, binding) do
|
||||
{:in, value, :"$#{binding}"}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -9,13 +9,15 @@ defmodule Ash.Resource.Relationships.BelongsTo do
|
|||
:define_field?,
|
||||
:field_type,
|
||||
:destination_field,
|
||||
:source_field
|
||||
:source_field,
|
||||
:source
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :belongs_to,
|
||||
cardinality: :one,
|
||||
name: atom,
|
||||
source: Ash.resource(),
|
||||
destination: Ash.resource(),
|
||||
primary_key?: boolean,
|
||||
define_field?: boolean,
|
||||
|
@ -55,16 +57,19 @@ defmodule Ash.Resource.Relationships.BelongsTo do
|
|||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(
|
||||
resource :: Ash.resource(),
|
||||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
def new(name, related_resource, opts \\ []) do
|
||||
def new(resource, name, related_resource, opts \\ []) do
|
||||
# Don't call functions on the resource! We don't want it to compile here
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
source: resource,
|
||||
type: :belongs_to,
|
||||
cardinality: :one,
|
||||
field_type: opts[:field_type],
|
||||
|
|
|
@ -5,12 +5,14 @@ defmodule Ash.Resource.Relationships.HasMany do
|
|||
:cardinality,
|
||||
:destination,
|
||||
:destination_field,
|
||||
:source_field
|
||||
:source_field,
|
||||
:source
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :has_many,
|
||||
cardinality: :many,
|
||||
source: Ash.resource(),
|
||||
name: atom,
|
||||
type: Ash.Type.t(),
|
||||
destination: Ash.resource(),
|
||||
|
@ -38,16 +40,19 @@ defmodule Ash.Resource.Relationships.HasMany do
|
|||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(
|
||||
resource :: Ash.resource(),
|
||||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
def new(resource_type, name, related_resource, opts \\ []) do
|
||||
def new(resource, resource_type, name, related_resource, opts \\ []) do
|
||||
# Don't call functions on the resource! We don't want it to compile here
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
source: resource,
|
||||
type: :has_many,
|
||||
cardinality: :many,
|
||||
destination: related_resource,
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Ash.Resource.Relationships.HasOne do
|
|||
defstruct [
|
||||
:name,
|
||||
:type,
|
||||
:source,
|
||||
:cardinality,
|
||||
:destination,
|
||||
:destination_field,
|
||||
|
@ -12,6 +13,7 @@ defmodule Ash.Resource.Relationships.HasOne do
|
|||
@type t :: %__MODULE__{
|
||||
type: :has_one,
|
||||
cardinality: :one,
|
||||
source: Ash.resource(),
|
||||
name: atom,
|
||||
type: Ash.Type.t(),
|
||||
destination: Ash.resource(),
|
||||
|
@ -39,18 +41,21 @@ defmodule Ash.Resource.Relationships.HasOne do
|
|||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(
|
||||
resource :: Ash.resource(),
|
||||
resource_type :: String.t(),
|
||||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
@doc false
|
||||
def new(resource_type, name, related_resource, opts \\ []) do
|
||||
def new(resource, resource_type, name, related_resource, opts \\ []) do
|
||||
# Don't call functions on the resource! We don't want it to compile here
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
source: resource,
|
||||
type: :has_one,
|
||||
cardinality: :one,
|
||||
destination: related_resource,
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
defstruct [
|
||||
:name,
|
||||
:type,
|
||||
:source,
|
||||
:through,
|
||||
:cardinality,
|
||||
:destination,
|
||||
|
@ -14,8 +15,9 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
@type t :: %__MODULE__{
|
||||
type: :many_to_many,
|
||||
cardinality: :many,
|
||||
source: Ash.resource(),
|
||||
name: atom,
|
||||
through: Ash.resource() | String.t(),
|
||||
through: Ash.resource(),
|
||||
destination: Ash.resource(),
|
||||
source_field: atom,
|
||||
destination_field: atom,
|
||||
|
@ -29,7 +31,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
destination_field_on_join_table: :atom,
|
||||
source_field: :atom,
|
||||
destination_field: :atom,
|
||||
through: [:atom, :string]
|
||||
through: :atom
|
||||
],
|
||||
defaults: [
|
||||
source_field: :id,
|
||||
|
@ -39,8 +41,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
:through
|
||||
],
|
||||
describe: [
|
||||
through:
|
||||
"Either a string representing a table/generic name for the join table or a module name of a resource.",
|
||||
through: "The resource to use as the join table.",
|
||||
source_field_on_join_table:
|
||||
"The field on the join table that should line up with `source_field` on this resource. Default: [resource_name]_id",
|
||||
destination_field_on_join_table:
|
||||
|
@ -56,18 +57,21 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(
|
||||
resource :: Ash.resource(),
|
||||
resource_name :: String.t(),
|
||||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
def new(resource_name, name, related_resource, opts \\ []) do
|
||||
def new(resource, resource_name, name, related_resource, opts \\ []) do
|
||||
# Don't call functions on the resource! We don't want it to compile here
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :many_to_many,
|
||||
source: resource,
|
||||
cardinality: :many,
|
||||
through: opts[:through],
|
||||
destination: related_resource,
|
||||
|
|
|
@ -55,6 +55,7 @@ defmodule Ash.Resource.Relationships do
|
|||
|
||||
relationship =
|
||||
Ash.Resource.Relationships.HasOne.new(
|
||||
__MODULE__,
|
||||
@resource_type,
|
||||
relationship_name,
|
||||
destination,
|
||||
|
@ -65,7 +66,7 @@ defmodule Ash.Resource.Relationships do
|
|||
{:ok, relationship} ->
|
||||
@relationships relationship
|
||||
|
||||
{:error, [{key, message}]} ->
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
|
@ -111,7 +112,12 @@ defmodule Ash.Resource.Relationships do
|
|||
end
|
||||
|
||||
relationship =
|
||||
Ash.Resource.Relationships.BelongsTo.new(relationship_name, destination, config)
|
||||
Ash.Resource.Relationships.BelongsTo.new(
|
||||
__MODULE__,
|
||||
relationship_name,
|
||||
destination,
|
||||
config
|
||||
)
|
||||
|
||||
case relationship do
|
||||
{:ok, relationship} ->
|
||||
|
@ -128,7 +134,7 @@ defmodule Ash.Resource.Relationships do
|
|||
|
||||
@relationships relationship
|
||||
|
||||
{:error, [{key, message}]} ->
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
|
@ -170,6 +176,7 @@ defmodule Ash.Resource.Relationships do
|
|||
|
||||
relationship =
|
||||
Ash.Resource.Relationships.HasMany.new(
|
||||
__MODULE__,
|
||||
@resource_type,
|
||||
relationship_name,
|
||||
destination,
|
||||
|
@ -180,7 +187,7 @@ defmodule Ash.Resource.Relationships do
|
|||
{:ok, relationship} ->
|
||||
@relationships relationship
|
||||
|
||||
{:error, [{key, message}]} ->
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
|
@ -213,6 +220,7 @@ defmodule Ash.Resource.Relationships do
|
|||
quote do
|
||||
relationship =
|
||||
Ash.Resource.Relationships.ManyToMany.new(
|
||||
__MODULE__,
|
||||
@name,
|
||||
unquote(relationship_name),
|
||||
unquote(resource),
|
||||
|
@ -223,7 +231,7 @@ defmodule Ash.Resource.Relationships do
|
|||
{:ok, relationship} ->
|
||||
@relationships relationship
|
||||
|
||||
{:error, [{key, message}]} ->
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
|
|
|
@ -27,16 +27,15 @@ defmodule Ash.Type do
|
|||
|
||||
@type t :: module | atom
|
||||
|
||||
@doc """
|
||||
Returns a list of filter types supported by this type. By default, a type supports only the `:equal` filter
|
||||
"""
|
||||
@spec supported_filter_types(t, Ash.data_layer()) ::
|
||||
list(Ash.Actions.Filter.filter_type())
|
||||
def supported_filter_types(type, _data_layer) when type in @builtin_names do
|
||||
@builtins[type][:filters]
|
||||
@spec supports_filter?(t(), Ash.DataLayer.filter_type(), Ash.data_layer()) :: boolean
|
||||
def supports_filter?(type, filter_type, data_layer) when type in @builtin_names do
|
||||
data_layer.can?({:filter, filter_type}) and filter_type in @builtins[type][:filters]
|
||||
end
|
||||
|
||||
def supported_filter_types(type, data_layer), do: type.supported_filter_types(data_layer)
|
||||
def supports_filter?(type, filter_type, data_layer) do
|
||||
data_layer.can?({:filter, filter_type}) and
|
||||
filter_type in type.supported_filter_types(data_layer)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines whether or not this value can be sorted.
|
||||
|
@ -171,7 +170,7 @@ defmodule Ash.Type do
|
|||
def ecto_type(), do: EctoType
|
||||
|
||||
@impl true
|
||||
def supported_filter_types(_data_layer), do: [:equal]
|
||||
def supported_filter_types(_data_layer), do: [:equal, :in]
|
||||
|
||||
@impl true
|
||||
def sortable?(_data_layer), do: true
|
||||
|
|
|
@ -16,7 +16,7 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :author, __MODULE__.Author
|
||||
belongs_to :author, Ash.Test.Actions.CreateTest.Author
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -27,6 +27,7 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
actions do
|
||||
read :default
|
||||
create :default
|
||||
update :default
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
@ -35,6 +36,8 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
|
||||
relationships do
|
||||
has_one :profile, Profile
|
||||
|
||||
has_many :posts, Ash.Test.Actions.CreateTest.Post
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -50,6 +53,7 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
actions do
|
||||
read :default
|
||||
create :default
|
||||
update :default
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
@ -125,6 +129,19 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "creating with a has_many relationship" do
|
||||
test "allows creating with a has_many relationship" do
|
||||
post = Api.create!(Post, %{attributes: %{title: "sup"}})
|
||||
|
||||
Api.create!(Author, %{
|
||||
attributes: %{title: "foobar"},
|
||||
relationships: %{
|
||||
posts: [post.id]
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe "creating with belongs_to relationships" do
|
||||
test "allows creating with belongs_to relationship" do
|
||||
author = Api.create!(Author, %{attributes: %{bio: "best dude"}})
|
||||
|
@ -162,7 +179,7 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
}).author_id == author.id
|
||||
end
|
||||
|
||||
test "it responds with the relationshi filled in" do
|
||||
test "it responds with the relationship filled in" do
|
||||
author = Api.create!(Author, %{attributes: %{bio: "best dude"}})
|
||||
|
||||
assert Api.create!(Post, %{
|
||||
|
|
|
@ -103,7 +103,7 @@ defmodule Ash.Test.Actions.ReadTest do
|
|||
|
||||
test "it raises on an error" do
|
||||
assert_raise(Ash.Error.FrameworkError, "Invalid value: 10 for :title", fn ->
|
||||
Api.read!(Post, %{filter: %{title: 10}})
|
||||
Api.read!(Post, %{filter: [title: 10]})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
@ -117,15 +117,15 @@ defmodule Ash.Test.Actions.ReadTest do
|
|||
end
|
||||
|
||||
test "a filter that matches nothing returns no results" do
|
||||
assert {:ok, %{results: []}} = Api.read(Post, %{filter: %{contents: "not_yeet"}})
|
||||
assert {:ok, %{results: []}} = Api.read(Post, %{filter: [contents: "not_yeet"]})
|
||||
end
|
||||
|
||||
test "a filter returns only matching records", %{post1: post1} do
|
||||
assert {:ok, %{results: [^post1]}} = Api.read(Post, %{filter: %{title: post1.title}})
|
||||
assert {:ok, %{results: [^post1]}} = Api.read(Post, %{filter: [title: post1.title]})
|
||||
end
|
||||
|
||||
test "a filter returns multiple records if they match", %{post1: post1, post2: post2} do
|
||||
assert {:ok, %{results: [_, _] = results}} = Api.read(Post, %{filter: %{contents: "yeet"}})
|
||||
assert {:ok, %{results: [_, _] = results}} = Api.read(Post, %{filter: [contents: "yeet"]})
|
||||
|
||||
assert post1 in results
|
||||
assert post2 in results
|
||||
|
|
|
@ -15,7 +15,7 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
|
|||
test "it creates a relationship" do
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: "some_table"
|
||||
many_to_many :foobars, Foobar, through: SomeResource
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -28,7 +28,7 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
|
|||
name: :foobars,
|
||||
source_field: :id,
|
||||
source_field_on_join_table: :posts_id,
|
||||
through: "some_table",
|
||||
through: SomeResource,
|
||||
type: :many_to_many
|
||||
}
|
||||
] = Ash.relationships(Post)
|
||||
|
@ -36,12 +36,18 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
|
|||
end
|
||||
|
||||
describe "validation" do
|
||||
test "you can pass a string to `through`" do
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: "some_table"
|
||||
test "it fails if you pass a string to `through`" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option through at relationships -> many_to_many -> foobars must be atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: "some_table"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "you can pass a module to `through`" do
|
||||
|
@ -59,7 +65,7 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
|
|||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: "table", source_field_on_join_table: "what"
|
||||
many_to_many :foobars, Foobar, through: FooBars, source_field_on_join_table: "what"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -74,7 +80,7 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
|
|||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar,
|
||||
through: "table",
|
||||
through: FooBar,
|
||||
destination_field_on_join_table: "what"
|
||||
end
|
||||
end
|
||||
|
@ -90,7 +96,7 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
|
|||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar,
|
||||
through: "table",
|
||||
through: FooBar,
|
||||
source_field: "what"
|
||||
end
|
||||
end
|
||||
|
@ -106,7 +112,7 @@ defmodule Ash.Test.Resource.Relationships.ManyToManyTest do
|
|||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar,
|
||||
through: "table",
|
||||
through: FooBars,
|
||||
destination_field: "what"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,8 +68,8 @@ defmodule Ash.Test.Type.TypeTest do
|
|||
# As we add more filter types, we may want to test their multiplicity here
|
||||
post = Api.create!(Post, %{attributes: %{title: "foobar"}})
|
||||
|
||||
assert_raise(Ash.Error.FrameworkError, "Cannot filter :title for equality.", fn ->
|
||||
Api.read!(Post, %{filter: %{title: post.title}})
|
||||
assert_raise(Ash.Error.FrameworkError, "Cannot use filter type equal on :title.", fn ->
|
||||
Api.read!(Post, %{filter: [title: post.title]})
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue