Skip to content

Commit

Permalink
feat: refactor changes into changesets
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Jul 12, 2020
1 parent 06d960d commit 2cf41b9
Show file tree
Hide file tree
Showing 54 changed files with 1,850 additions and 1,435 deletions.
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ locals_without_parens = [
has_many: 3,
has_one: 2,
has_one: 3,
join_relationship: 1,
many_to_many: 2,
many_to_many: 3,
primary?: 1,
Expand Down
2 changes: 2 additions & 0 deletions lib/ash.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Ash do
- [Resource Documentation](Ash.Resource.html)
- [DSL Documentation](Ash.Dsl.html)
- [Code API documentation](Ash.Api.Interface.html)
- [Getting Started Guide](getting_started.html)
## Introduction
Expand Down Expand Up @@ -75,6 +76,7 @@ defmodule Ash do
@type action :: Create.t() | Read.t() | Update.t() | Destroy.t()
@type query :: Ash.Query.t()
@type actor :: Ash.record()
@type changeset :: Ash.Changeset.t()

require Ash.Dsl.Extension
alias Ash.Dsl.Extension
Expand Down
161 changes: 13 additions & 148 deletions lib/ash/actions/create.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,21 @@ defmodule Ash.Actions.Create do
alias Ash.Actions.{Relationships, SideLoad}
require Logger

def run(api, resource, action, opts) do
attributes = Keyword.get(opts, :attributes, %{})
relationships = Keyword.get(opts, :relationships, %{})
def run(api, changeset, action, opts) do
side_load = opts[:side_load] || []
upsert? = opts[:upsert?] || false
resource = changeset.resource

engine_opts =
opts
|> Keyword.take([:verbose?, :actor, :authorize?])
|> Keyword.put(:transaction?, true)

action =
if is_atom(action) and not is_nil(action) do
Ash.action(resource, action, :read)
else
action
end

with :ok <- check_upsert_support(resource, upsert?),
{:ok, side_load_query} <- side_loads_as_query(api, resource, side_load),
{:ok, relationships} <-
Relationships.validate_not_changing_relationship_and_source_field(
relationships,
attributes,
resource
),
{:ok, attributes, relationships} <-
Relationships.field_changes_into_relationship_changes(
relationships,
attributes,
resource
),
%{valid?: true} = changeset <-
changeset(api, resource, attributes, relationships),
with %{valid?: true} = changeset <-
Relationships.handle_relationship_changes(%{changeset | api: api}),
:ok <- check_upsert_support(changeset.resource, upsert?),
{:ok, side_load_query} <-
side_loads_as_query(changeset.api, changeset.resource, side_load),
side_load_requests <-
SideLoad.requests(side_load_query),
%{
Expand All @@ -48,7 +29,6 @@ defmodule Ash.Actions.Create do
do_run_requests(
changeset,
upsert?,
relationships,
engine_opts,
action,
resource,
Expand All @@ -57,8 +37,8 @@ defmodule Ash.Actions.Create do
) do
{:ok, SideLoad.attach_side_loads(created, state)}
else
%Ecto.Changeset{} = changeset ->
{:error, Ash.Error.Changeset.changeset_to_errors(resource, changeset)}
%Ash.Changeset{errors: errors} ->
{:error, Ash.Error.to_ash_error(errors)}

%{errors: errors} ->
{:error, Ash.Error.to_ash_error(errors)}
Expand All @@ -68,16 +48,9 @@ defmodule Ash.Actions.Create do
end
end

def changeset(api, resource, attributes, relationships) do
resource
|> prepare_create_attributes(attributes)
|> Relationships.handle_relationship_changes(api, relationships, :create)
end

defp do_run_requests(
changeset,
upsert?,
relationships,
engine_opts,
action,
resource,
Expand All @@ -88,19 +61,14 @@ defmodule Ash.Actions.Create do
Request.new(
api: api,
resource: resource,
changeset:
Relationships.changeset(
changeset,
api,
relationships
),
changeset: Relationships.changeset(changeset),
action: action,
data: nil,
path: [:data],
name: "#{action.type} - `#{action.name}`: prepare"
)

relationship_read_requests = Map.get(changeset, :__requests__, [])
relationship_read_requests = changeset.requests

commit_request =
Request.new(
Expand All @@ -115,27 +83,13 @@ defmodule Ash.Actions.Create do
Request.resolve(
[[:commit, :changeset]],
fn %{commit: %{changeset: changeset}} ->
result =
Ash.Changeset.with_hooks(changeset, fn changeset ->
if upsert? do
Ash.DataLayer.upsert(resource, changeset)
else
Ash.DataLayer.create(resource, changeset)
end

case result do
{:ok, result} ->
changeset
|> Map.get(:__after_changes__, [])
|> Enum.reduce_while({:ok, result}, fn func, {:ok, result} ->
case func.(changeset, result) do
{:ok, result} -> {:cont, {:ok, result}}
{:error, error} -> {:halt, {:error, error}}
end
end)

{:error, error} ->
{:error, error}
end
end)
end
),
path: [:commit],
Expand Down Expand Up @@ -173,93 +127,4 @@ defmodule Ash.Actions.Create do
%{errors: errors} -> {:error, errors}
end
end

defp prepare_create_attributes(resource, attributes) do
allowed_keys =
resource
|> Ash.attributes()
|> Enum.map(& &1.name)

{attributes_with_defaults, unwritable_attributes} =
resource
|> Ash.attributes()
|> Enum.reduce({%{}, []}, fn attribute, {new_attributes, unwritable_attributes} ->
provided_value = fetch_attr(attributes, attribute.name)
provided? = match?({:ok, _}, provided_value)

cond do
provided? && !attribute.writable? ->
{new_attributes, [attribute | unwritable_attributes]}

provided? ->
{:ok, value} = provided_value
{Map.put(new_attributes, attribute.name, value), unwritable_attributes}

is_nil(attribute.default) ->
{new_attributes, unwritable_attributes}

true ->
{Map.put(new_attributes, attribute.name, default(attribute)), unwritable_attributes}
end
end)

changeset =
resource
|> struct()
|> Ecto.Changeset.cast(attributes_with_defaults, allowed_keys, empty_values: [])
|> Map.put(:action, :create)
|> Map.put(:__ash_relationships__, %{})

changeset =
Enum.reduce(
unwritable_attributes,
changeset,
&Ecto.Changeset.add_error(&2, &1.name, "attribute is not writable")
)

resource
|> Ash.attributes()
|> Enum.reject(&Map.get(&1, :allow_nil?))
|> Enum.reject(&Map.get(&1, :default))
|> Enum.reduce(changeset, fn attr, changeset ->
case Ecto.Changeset.get_field(changeset, attr.name) do
nil -> Ecto.Changeset.add_error(changeset, attr.name, "must not be nil")
_value -> changeset
end
end)
|> validate_constraints(resource)
end

defp validate_constraints(changeset, resource) do
resource
|> Ash.attributes()
|> Enum.reduce(changeset, fn attribute, changeset ->
with {:ok, value} <- Map.fetch(changeset.changes, attribute.name),
{:error, error} <-
Ash.Type.apply_constraints(attribute.type, value, attribute.constraints) do
error
|> List.wrap()
|> Enum.reduce(changeset, fn error, changeset ->
Ecto.Changeset.add_error(changeset, attribute.name, error)
end)
else
_ ->
changeset
end
end)
end

defp default(%{default: {:constant, value}}), do: value
defp default(%{default: {mod, func, args}}), do: apply(mod, func, args)
defp default(%{default: function}), do: function.()

defp fetch_attr(map, name) do
case Map.fetch(map, name) do
{:ok, value} ->
{:ok, value}

:error ->
Map.fetch(map, to_string(name))
end
end
end
7 changes: 0 additions & 7 deletions lib/ash/actions/destroy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,6 @@ defmodule Ash.Actions.Destroy do
|> Keyword.take([:verbose?, :actor, :authorize?])
|> Keyword.put(:transaction?, true)

action =
if is_atom(action) and not is_nil(action) do
Ash.action(resource, action, :read)
else
action
end

authorization_request =
Request.new(
resource: resource,
Expand Down

0 comments on commit 2cf41b9

Please sign in to comment.