This commit is contained in:
Zach Daniel 2019-11-02 16:36:46 -04:00
parent c6710f9381
commit 3c1ef49950
No known key found for this signature in database
GPG key ID: A57053A671EE649E
20 changed files with 328 additions and 77 deletions

View file

@ -2,11 +2,16 @@
locals_without_parens = [
get: 1,
index: 1,
post: 1,
attribute: 2,
attribute: 3,
belongs_to: 2,
belongs_to: 3,
create: 1,
create: 2,
update: 1,
update: 2,
delete: 1,
delete: 2,
has_one: 2,
has_one: 3,
has_many: 2,

View file

@ -14,3 +14,4 @@
* break up the `Ash` module
* Wire up/formalize the error handling
* Ensure that errors are properly propagated up from the data_layer behaviour, and every operation is allowed to fail
* figure out the ecto schema warning

View file

@ -14,6 +14,10 @@ defmodule Ash do
Application.get_env(:ash, :resources) || []
end
def primary_key(resource) do
resource.primary_key()
end
def relationship(resource, relationship_name) do
resource.relationship(relationship_name)
end
@ -55,6 +59,18 @@ defmodule Ash do
Ash.DataLayer.Actions.run_index_action(resource, action, params)
end
def run_create_action(resource, action, attributes, relationships, params) do
Ash.DataLayer.Actions.run_create_action(resource, action, attributes, relationships, params)
end
def run_update_action(record, action, attributes, relationships, params) do
Ash.DataLayer.Actions.run_update_action(record, action, attributes, relationships, params)
end
def run_delete_action(record, action, params) do
Ash.DataLayer.Actions.run_delete_action(record, action, params)
end
# TODO: Implement a to_resource protocol, like ecto's to query logic
def to_resource(%resource{}), do: resource
def to_resource(resource) when is_atom(resource), do: resource

View file

@ -9,6 +9,42 @@ defmodule Ash.Data do
end
end
@spec create(Ash.resource(), Ash.action(), Ash.attributes(), Ash.relationships(), Ash.params()) ::
{:ok, Ash.record()} | {:errro, Ash.error()}
def create(resource, action, attributes, relationships, params) do
Ash.data_layer(resource).create(resource, action, attributes, relationships, params)
end
@spec update(Ash.record(), Ash.action(), Ash.attributes(), Ash.relationships(), Ash.params()) ::
{:ok, Ash.record()} | {:errro, Ash.error()}
def update(%resource{} = record, action, attributes, relationships, params) do
Ash.data_layer(resource).update(record, action, attributes, relationships, params)
end
@spec delete(Ash.record(), Ash.action(), Ash.params()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
def delete(%resource{} = record, action, params) do
Ash.data_layer(resource).delete(record, action, params)
end
@spec append_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
def append_related(%resource{} = record, relationship, resource_identifiers) do
Ash.data_layer(resource).append_related(record, relationship, resource_identifiers)
end
@spec delete_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
def delete_related(%resource{} = record, relationship, resource_identifiers) do
Ash.data_layer(resource).delete_related(record, relationship, resource_identifiers)
end
@spec replace_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
def replace_related(%resource{} = record, relationship, resource_identifiers) do
Ash.data_layer(resource).replace_related(record, relationship, resource_identifiers)
end
@spec resource_to_query(Ash.resource()) :: Ash.query()
def resource_to_query(resource) do
Ash.data_layer(resource).resource_to_query(resource)

View file

@ -3,6 +3,18 @@ defmodule Ash.DataLayer.Actions do
Ash.Data.get_by_id(resource, id)
end
def run_create_action(resource, action, attributes, relationships, params) do
Ash.Data.create(resource, action, attributes, relationships, params)
end
def run_update_action(record, action, attributes, relationships, params) do
Ash.Data.update(record, action, attributes, relationships, params)
end
def run_delete_action(record, action, params) do
Ash.Data.delete(record, action, params)
end
def run_index_action(resource, _action, params) do
with {:ok, query} <- Ash.Data.resource_to_query(resource),
{:ok, paginator} <- Ash.DataLayer.Paginator.paginate(resource, query, params),

View file

@ -15,4 +15,33 @@ defmodule Ash.DataLayer do
{:ok, [Ash.record()]} | {:error, Ash.error()}
@callback side_load([Ash.record()], Ash.side_load_keyword(), Ash.resource()) ::
{:ok, [Ash.resource()]} | {:error, Ash.error()}
@callback create(
Ash.resource(),
Ash.action(),
Ash.attributes(),
Ash.relationships(),
Ash.params()
) ::
{:ok, Ash.record()} | {:error, Ash.error()}
@callback update(
Ash.record(),
Ash.action(),
Ash.attributes(),
Ash.relationships(),
Ash.params()
) ::
{:ok, Ash.record()} | {:error, Ash.error()}
@callback delete(Ash.record(), Ash.action(), Ash.params()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
@callback append_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
@callback delete_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
@callback replace_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
end

View file

@ -11,7 +11,9 @@ defmodule Ash.Resource do
Module.register_attribute(__MODULE__, :relationships, accumulate: true)
Module.register_attribute(__MODULE__, :mix_ins, accumulate: true)
@attributes Ash.Resource.Attributes.Attribute.new(:id, :uuid)
if unquote(Keyword.get(opts, :primary_key?, true)) do
@attributes Ash.Resource.Attributes.Attribute.new(:id, :uuid, primary_key?: true)
end
# Module.put_attribute(__MODULE__, :custom_threshold_for_lib, 10)
import Ash.Resource
@ -41,30 +43,46 @@ defmodule Ash.Resource do
raise "Your module (#{inspect(__MODULE__)}) must be in config, :ash, resources: [...]"
end
@sanitized_actions Ash.Resource.mark_primaries(@actions)
@ash_primary_key Ash.Resource.primary_key(@attributes)
unless @ash_primary_key do
raise "Must have a primary key for a resource: #{__MODULE__}"
end
def type() do
@resource_type
end
def relationship(_name) do
nil
def create(action, attributes, parameters) do
data_layer().create(__MODULE__, action, attributes, parameters)
end
def relationship(name) do
# TODO: Make this happen at compile time
Enum.find(relationships(), &(&1.name == name))
end
def relationships() do
@relationships
end
def action(_name) do
nil
def action(name) do
Enum.find(actions(), &(&1.name == name))
end
def actions() do
@actions
@sanitized_actions
end
def attributes() do
@attributes
end
def primary_key() do
@ash_primary_key
end
def name() do
@name
end
@ -91,4 +109,46 @@ defmodule Ash.Resource do
end)
end
end
@doc false
def primary_key(attributes) do
attributes
|> Enum.filter(& &1.primary_key?)
|> Enum.map(& &1.name)
|> case do
[] ->
nil
[single] ->
single
other ->
other
end
end
@doc false
def mark_primaries(all_actions) do
all_actions
|> Enum.group_by(& &1.type)
|> Enum.flat_map(fn {type, actions} ->
case actions do
[action] ->
[%{action | primary?: true}]
actions ->
case Enum.count(actions, & &1.primary?) do
0 ->
# TODO: Format these prettier
raise "Must declare a primary action for #{type}, as there are more than one."
1 ->
actions
_ ->
raise "Duplicate primary actions declared for #{type}, but there can only be one primary action."
end
end
end)
end
end

View file

@ -1,10 +0,0 @@
defmodule Ash.Resource.Actions.Action do
defstruct [:type, :name, :path]
def new(name, type, _opts \\ []) do
%__MODULE__{
name: name,
type: type
}
end
end

View file

@ -7,31 +7,48 @@ defmodule Ash.Resource.Actions do
end
end
defmacro get(name \\ :get, _opts \\ []) do
quote do
action = Ash.Resource.Actions.Action.new(unquote(name), :get)
# TODO: Originally I had it in my mind that you had to set up your own actions
# for the basic capabilities. Instead, these capabilities will just automatically exist
# for all resources. What you can do is create actions that are a variation of one of the
# basic kinds of resource actions, with special rules. That will be hooked up later.
@actions action
# defmacro create(name \\ :create, opts \\ []) do
# quote bind_quoted: [name: name, opts: opts] do
# action = Ash.Resource.Actions.Create.new(name, primary?: opts[:primary?] || false)
@current_action action
# @actions action
# end
# end
def action(unquote(name)) do
@current_action
end
end
end
# defmacro update(name \\ :update, opts \\ []) do
# quote bind_quoted: [name: name, opts: opts] do
# action = Ash.Resource.Actions.Update.new(name, primary?: opts[:primary?] || false)
defmacro index(name \\ :index, _opts \\ []) do
quote do
action = Ash.Resource.Actions.Action.new(unquote(name), :index)
# @actions action
# end
# end
@actions action
# defmacro delete(name \\ :delete, opts \\ []) do
# quote bind_quoted: [name: name, opts: opts] do
# action = Ash.Resource.Actions.Delete.new(name, primary?: opts[:primary?] || false)
@current_action action
# @actions action
# end
# end
def action(unquote(name)) do
@current_action
end
end
end
# defmacro get(name \\ :get, opts \\ []) do
# quote bind_quoted: [name: name, opts: opts] do
# action = Ash.Resource.Actions.Get.new(name, primary?: opts[:primary?] || false)
# @actions action
# end
# end
# defmacro index(name \\ :index, opts \\ []) do
# quote bind_quoted: [name: name, opts: opts] do
# action = Ash.Resource.Actions.Index.new(name, primary?: opts[:primary?] || false)
# @actions action
# end
# end
end

View file

@ -0,0 +1,11 @@
defmodule Ash.Resource.Actions.Create do
defstruct [:type, :name, :primary?]
def new(name, opts \\ []) do
%__MODULE__{
name: name,
type: :create,
primary?: opts[:primary?]
}
end
end

View file

@ -0,0 +1,11 @@
defmodule Ash.Resource.Actions.Delete do
defstruct [:type, :name, :primary?]
def new(name, opts \\ []) do
%__MODULE__{
name: name,
type: :delete,
primary?: opts[:primary?]
}
end
end

View file

@ -0,0 +1,11 @@
defmodule Ash.Resource.Actions.Get do
defstruct [:type, :name, :primary?]
def new(name, opts \\ []) do
%__MODULE__{
name: name,
type: :get,
primary?: opts[:primary?]
}
end
end

View file

@ -0,0 +1,11 @@
defmodule Ash.Resource.Actions.Index do
defstruct [:type, :name, :primary?]
def new(name, opts \\ []) do
%__MODULE__{
name: name,
type: :index,
primary?: opts[:primary?]
}
end
end

View file

@ -0,0 +1,11 @@
defmodule Ash.Resource.Actions.Update do
defstruct [:type, :name, :primary?]
def new(name, opts \\ []) do
%__MODULE__{
name: name,
type: :update,
primary?: opts[:primary?]
}
end
end

View file

@ -1,7 +1,9 @@
defmodule Ash.Resource.Attributes.Attribute do
defstruct [:name, :type, :ecto_type]
defstruct [:name, :type, :ecto_type, :primary_key?]
def new(name, type, _opts \\ []) do
def new(name, type, opts \\ []) do
# TODO: Remove `ecto_type` here and do that mapping in
# the database layer
ecto_type =
if type == :uuid do
:binary_id
@ -12,7 +14,8 @@ defmodule Ash.Resource.Attributes.Attribute do
%__MODULE__{
name: name,
type: type,
ecto_type: ecto_type
ecto_type: ecto_type,
primary_key?: opts[:primary_key?] || false
}
end
end

View file

@ -5,6 +5,7 @@ defmodule Ash.Resource.Relationships.BelongsTo do
:type,
:path,
:destination,
:primary_key?,
:side_load,
:destination_field,
:source_field
@ -29,10 +30,14 @@ defmodule Ash.Resource.Relationships.BelongsTo do
type: :belongs_to,
cardinality: :one,
path: path,
primary_key?: Keyword.get(opts, :primary_key, false),
destination: related_resource,
destination_field: opts[:destination_field] || "id",
source_field: opts[:source_field] || "#{name}_id",
destination_field: atomize(opts[:destination_field] || "id"),
source_field: atomize(opts[:source_field] || "#{name}_id"),
side_load: opts[:side_load]
}
end
defp atomize(value) when is_atom(value), do: value
defp atomize(value) when is_bitstring(value), do: String.to_atom(value)
end

View file

@ -21,7 +21,7 @@ defmodule Ash.Resource.Relationships.HasMany do
related_resource :: Ash.resource(),
opts :: Keyword.t()
) :: t()
def new(resource_name, name, related_resource, opts \\ []) do
def new(resource_name, resource_type, name, related_resource, opts \\ []) do
path = opts[:path] || resource_name <> "/:id/" <> to_string(name)
%__MODULE__{
@ -30,9 +30,12 @@ defmodule Ash.Resource.Relationships.HasMany do
cardinality: :many,
path: path,
destination: related_resource,
destination_field: opts[:destination_field] || "#{resource_name}_id",
source_field: opts[:source_field] || "id",
destination_field: atomize(opts[:destination_field] || "#{resource_type}_id"),
source_field: atomize(opts[:source_field] || "id"),
side_load: opts[:side_load]
}
end
defp atomize(value) when is_atom(value), do: value
defp atomize(value) when is_bitstring(value), do: String.to_atom(value)
end

View file

@ -30,9 +30,12 @@ defmodule Ash.Resource.Relationships.HasOne do
cardinality: :one,
path: path,
destination: related_resource,
destination_field: opts[:destination_field] || "#{resource_name}_id",
source_field: opts[:source_field] || "id",
destination_field: atomize(opts[:destination_field] || "#{resource_name}_id"),
source_field: atomize(opts[:source_field] || "id"),
side_load: opts[:side_load]
}
end
defp atomize(value) when is_atom(value), do: value
defp atomize(value) when is_bitstring(value), do: String.to_atom(value)
end

View file

@ -27,14 +27,24 @@ defmodule Ash.Resource.Relationships.ManyToMany do
def new(resource_name, name, related_resource, opts \\ []) do
path = opts[:path] || resource_name <> "/:id/" <> to_string(name)
through = through!(opts)
source_field_on_join_table =
opts[:source_field_on_join_table] || String.to_atom(resource_name <> "_id")
atomize(opts[:source_field_on_join_table] || String.to_atom(resource_name <> "_id"))
destination_field_on_join_table =
atomize(
opts[:destination_field_on_join_table] ||
String.to_atom(Ash.name(related_resource) <> "_id")
)
source_field = atomize(opts[:source_field] || :id)
destination_field = atomize(opts[:destination_field] || :id)
through =
through!(
opts,
source_field_on_join_table,
destination_field_on_join_table
)
%__MODULE__{
name: name,
@ -44,20 +54,41 @@ defmodule Ash.Resource.Relationships.ManyToMany do
through: through,
side_load: opts[:side_load],
destination: related_resource,
source_field: opts[:source_field] || :id,
destination_field: opts[:destination_field] || :id,
source_field: source_field,
destination_field: destination_field,
source_field_on_join_table: source_field_on_join_table,
destination_field_on_join_table: destination_field_on_join_table
}
end
defp through!(opts) do
defp atomize(value) when is_atom(value), do: value
defp atomize(value) when is_bitstring(value), do: String.to_atom(value)
defp through!(opts, source_field_on_join_table, destination_field_on_join_table) do
case opts[:through] do
through when is_atom(through) ->
unless through in Ash.resources() do
raise "Got an atom/module for `through`, but it was not a resource."
end
case Ash.primary_key(through) do
[^source_field_on_join_table, ^destination_field_on_join_table] ->
through
[^destination_field_on_join_table, ^source_field_on_join_table] ->
through
other ->
raise "The primary key of a join table must be the same as the fields that are used for joining. Needed: #{
inspect([destination_field_on_join_table, source_field_on_join_table])
} got #{other}"
end
through when is_bitstring(through) ->
through
_ ->
raise "`:through` option must be a string representing a join table"
raise "`:through` option must be a string representing a join table or a module representinga resource"
end
end
end

View file

@ -18,11 +18,6 @@ defmodule Ash.Resource.Relationships do
)
@relationships relationship
@current_relationship relationship
def relationship(unquote(relationship_name)) do
@current_relationship
end
end
end
@ -36,12 +31,11 @@ defmodule Ash.Resource.Relationships do
unquote(config)
)
@relationships relationship
@current_relationship relationship
@attributes Ash.Resource.Attributes.Attribute.new(relationship.source_field, :binary_id,
primary_key?: relationship.primary_key?
)
def relationship(unquote(relationship_name)) do
@current_relationship
end
@relationships relationship
end
end
@ -50,17 +44,13 @@ defmodule Ash.Resource.Relationships do
relationship =
Ash.Resource.Relationships.HasMany.new(
@name,
@resource_type,
unquote(relationship_name),
unquote(resource),
unquote(config)
)
@relationships relationship
@current_relationship relationship
def relationship(unquote(relationship_name)) do
@current_relationship
end
end
end
@ -75,11 +65,6 @@ defmodule Ash.Resource.Relationships do
)
@relationships relationship
@current_relationship relationship
def relationship(unquote(relationship_name)) do
@current_relationship
end
end
end
end