feat: add accept option to create/update actions (#105)

This commit is contained in:
Zach Daniel 2020-08-28 02:58:03 -04:00 committed by GitHub
parent 3ff4415c77
commit a58367414d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 160 additions and 11 deletions

View file

@ -1,6 +1,7 @@
# THIS FILE IS AUTOGENERATED USING `mix ash.formatter`
# DONT MODIFY IT BY HAND
locals_without_parens = [
accept: 1,
allow_nil?: 1,
argument: 2,
argument: 3,

View file

@ -5,6 +5,8 @@ defmodule Ash.Actions.Create do
alias Ash.Engine.Request
require Logger
alias Ash.Error.Changes.{InvalidAttribute, InvalidRelationship}
@spec run(Ash.api(), Ash.changeset(), Ash.action(), Keyword.t()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
def run(api, changeset, action, opts) do
@ -16,7 +18,7 @@ defmodule Ash.Actions.Create do
|> Keyword.take([:verbose?, :actor, :authorize?])
|> Keyword.put(:transaction?, true)
with %{valid?: true} = changeset <- changeset(changeset, api),
with %{valid?: true} = changeset <- changeset(changeset, api, action),
:ok <- check_upsert_support(changeset.resource, upsert?),
%{
data: %{commit: %^resource{} = created},
@ -43,13 +45,48 @@ defmodule Ash.Actions.Create do
end
end
defp changeset(changeset, api) do
defp changeset(changeset, api, action) do
%{changeset | api: api}
|> Relationships.handle_relationship_changes()
|> validate_attributes_accepted(action)
|> validate_relationships_accepted(action)
|> set_defaults()
|> add_validations()
end
defp validate_attributes_accepted(changeset, %{accept: nil}), do: changeset
defp validate_attributes_accepted(changeset, %{accept: accepted_attributes}) do
changeset.attributes
|> Enum.reject(fn {key, _value} ->
key in accepted_attributes
end)
|> Enum.reduce(changeset, fn {key, _}, changeset ->
Ash.Changeset.add_error(
changeset,
InvalidAttribute.exception(field: key, message: "Cannot be changed")
)
end)
end
defp validate_relationships_accepted(changeset, %{accept: nil}), do: changeset
defp validate_relationships_accepted(changeset, %{accept: accepted_relationships}) do
changeset.relationships
|> Enum.reject(fn {key, _value} ->
key in accepted_relationships
end)
|> Enum.reduce(changeset, fn {key, _}, changeset ->
Ash.Changeset.add_error(
changeset,
InvalidRelationship.exception(
relationship: key,
message: "Cannot be changed"
)
)
end)
end
defp add_validations(changeset) do
Ash.Changeset.before_action(changeset, fn changeset ->
changeset.resource

View file

@ -5,6 +5,8 @@ defmodule Ash.Actions.Update do
alias Ash.Engine.Request
require Logger
alias Ash.Error.Changes.{InvalidAttribute, InvalidRelationship}
@spec run(Ash.api(), Ash.record(), Ash.action(), Keyword.t()) ::
{:ok, Ash.record()} | {:error, Ash.Changeset.t()} | {:error, Ash.error()}
def run(api, changeset, action, opts) do
@ -15,7 +17,7 @@ defmodule Ash.Actions.Update do
resource = changeset.resource
with %{valid?: true} = changeset <- changeset(changeset, api),
with %{valid?: true} = changeset <- changeset(changeset, api, action),
%{data: %{commit: %^resource{} = updated}, errors: []} <-
do_run_requests(
changeset,
@ -37,13 +39,48 @@ defmodule Ash.Actions.Update do
end
end
defp changeset(changeset, api) do
defp changeset(changeset, api, action) do
%{changeset | api: api}
|> Relationships.handle_relationship_changes()
|> validate_attributes_accepted(action)
|> validate_relationships_accepted(action)
|> set_defaults()
|> add_validations()
end
defp validate_attributes_accepted(changeset, %{accept: nil}), do: changeset
defp validate_attributes_accepted(changeset, %{accept: accepted_attributes}) do
changeset.attributes
|> Enum.reject(fn {key, _value} ->
key in accepted_attributes
end)
|> Enum.reduce(changeset, fn {key, _}, changeset ->
Ash.Changeset.add_error(
changeset,
InvalidAttribute.exception(field: key, message: "Cannot be changed")
)
end)
end
defp validate_relationships_accepted(changeset, %{accept: nil}), do: changeset
defp validate_relationships_accepted(changeset, %{accept: accepted_relationships}) do
changeset.relationships
|> Enum.reject(fn {key, _value} ->
key in accepted_relationships
end)
|> Enum.reduce(changeset, fn {key, _}, changeset ->
Ash.Changeset.add_error(
changeset,
InvalidRelationship.exception(
relationship: key,
message: "Cannot be changed"
)
)
end)
end
defp add_validations(changeset) do
changeset.resource()
|> Ash.Resource.validations(:update)

View file

@ -2,7 +2,7 @@ defmodule Ash.Error.Changes.InvalidAttribute do
@moduledoc "Used when an invalid value is provided for an attribute change"
use Ash.Error
def_ash_error([:field, :type, :message, :validation], class: :invalid)
def_ash_error([:field, :message, :validation], class: :invalid)
defimpl Ash.ErrorKind do
def id(_), do: Ecto.UUID.generate()

View file

@ -22,6 +22,14 @@ defmodule Ash.OptionsHelpers do
end
end
def list_of_atoms(value) do
if is_list(value) and Enum.all?(value, &is_atom/1) do
{:ok, value}
else
{:error, "Expected a list of atoms"}
end
end
def default(value) when is_function(value, 0), do: {:ok, value}
def default({module, function, args})

View file

@ -1,10 +1,11 @@
defmodule Ash.Resource.Actions.Create do
@moduledoc "Represents a create action on a resource."
defstruct [:name, :primary?, type: :create]
defstruct [:name, :primary?, :accept, type: :create]
@type t :: %__MODULE__{
type: :create,
name: atom,
accept: [atom],
primary?: boolean
}
@ -14,6 +15,11 @@ defmodule Ash.Resource.Actions.Create do
required: true,
doc: "The name of the action"
],
accept: [
type: {:custom, Ash.OptionsHelpers, :list_of_atoms, []},
doc:
"The list of attributes and relationships to accept. Defaults to all attributes on the resource"
],
primary?: [
type: :boolean,
default: false,

View file

@ -1,11 +1,12 @@
defmodule Ash.Resource.Actions.Update do
@moduledoc "Represents a update action on a resource."
defstruct [:name, :primary?, type: :update]
defstruct [:name, :primary?, :accept, type: :update]
@type t :: %__MODULE__{
type: :update,
name: atom,
accept: [atom],
primary?: boolean
}
@ -14,6 +15,11 @@ defmodule Ash.Resource.Actions.Update do
type: :atom,
doc: "The name of the action"
],
accept: [
type: {:custom, Ash.OptionsHelpers, :list_of_atoms, []},
doc:
"The list of attributes and relationships to accept. Defaults to all attributes on the resource"
],
primary?: [
type: :boolean,
default: false,

View file

@ -58,9 +58,14 @@ defmodule Ash.Test.Actions.CreateTest do
end
actions do
read(:default)
create(:default)
update(:default)
read :default
create :default, primary?: true
create :only_allow_name do
accept([:name])
end
update :default
end
attributes do
@ -201,6 +206,24 @@ defmodule Ash.Test.Actions.CreateTest do
end
end
describe "accept" do
test "allows using attributes in the list" do
Author
|> new()
|> change_attribute(:name, "fred")
|> Api.create!(action: :only_allow_name)
end
test "it prevents using attributes not in the list" do
assert_raise Ash.Error.Invalid, ~r/Invalid value provided for bio: Cannot be changed/, fn ->
Author
|> new()
|> change_attribute(:bio, "foo")
|> Api.create!(action: :only_allow_name)
end
end
end
describe "creating many to many relationships" do
test "allows creating with a many_to_many relationship" do
post2 =

View file

@ -58,12 +58,17 @@ defmodule Ash.Test.Actions.UpdateTest do
actions do
read :default
create :default
update :default
update :default, primary?: true
update :only_allow_name do
accept([:name])
end
end
attributes do
attribute :id, :uuid, primary_key?: true, default: &Ecto.UUID.generate/0
attribute :name, :string
attribute :bio, :string
end
relationships do
@ -156,6 +161,32 @@ defmodule Ash.Test.Actions.UpdateTest do
end
end
describe "allow" do
test "allows attributes in the list" do
author =
Author
|> new(%{name: "fred"})
|> Api.create!()
author
|> new(%{name: "joe"})
|> Api.update!(action: :only_allow_name)
end
test "does not allow attributes in the list" do
author =
Author
|> new(%{name: "fred"})
|> Api.create!()
assert_raise Ash.Error.Invalid, ~r/Invalid value provided for bio: Cannot be changed/, fn ->
author
|> new(%{bio: "bio"})
|> Api.update!(action: :only_allow_name)
end
end
end
describe "updating many to many relationships" do
test "allows updating with a many_to_many relationship" do
post =