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 = [ locals_without_parens = [
get: 1, get: 1,
index: 1, index: 1,
post: 1,
attribute: 2, attribute: 2,
attribute: 3, attribute: 3,
belongs_to: 2, belongs_to: 2,
belongs_to: 3, belongs_to: 3,
create: 1,
create: 2,
update: 1,
update: 2,
delete: 1,
delete: 2,
has_one: 2, has_one: 2,
has_one: 3, has_one: 3,
has_many: 2, has_many: 2,

View file

@ -13,4 +13,5 @@
* DSL level validations! Things like includes validating that their chain exists. * DSL level validations! Things like includes validating that their chain exists.
* break up the `Ash` module * break up the `Ash` module
* Wire up/formalize the error handling * 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 * 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) || [] Application.get_env(:ash, :resources) || []
end end
def primary_key(resource) do
resource.primary_key()
end
def relationship(resource, relationship_name) do def relationship(resource, relationship_name) do
resource.relationship(relationship_name) resource.relationship(relationship_name)
end end
@ -55,6 +59,18 @@ defmodule Ash do
Ash.DataLayer.Actions.run_index_action(resource, action, params) Ash.DataLayer.Actions.run_index_action(resource, action, params)
end 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 # TODO: Implement a to_resource protocol, like ecto's to query logic
def to_resource(%resource{}), do: resource def to_resource(%resource{}), do: resource
def to_resource(resource) when is_atom(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
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() @spec resource_to_query(Ash.resource()) :: Ash.query()
def resource_to_query(resource) do def resource_to_query(resource) do
Ash.data_layer(resource).resource_to_query(resource) 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) Ash.Data.get_by_id(resource, id)
end 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 def run_index_action(resource, _action, params) do
with {:ok, query} <- Ash.Data.resource_to_query(resource), with {:ok, query} <- Ash.Data.resource_to_query(resource),
{:ok, paginator} <- Ash.DataLayer.Paginator.paginate(resource, query, params), {: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()} {:ok, [Ash.record()]} | {:error, Ash.error()}
@callback side_load([Ash.record()], Ash.side_load_keyword(), Ash.resource()) :: @callback side_load([Ash.record()], Ash.side_load_keyword(), Ash.resource()) ::
{:ok, [Ash.resource()]} | {:error, Ash.error()} {: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 end

View file

@ -11,7 +11,9 @@ defmodule Ash.Resource do
Module.register_attribute(__MODULE__, :relationships, accumulate: true) Module.register_attribute(__MODULE__, :relationships, accumulate: true)
Module.register_attribute(__MODULE__, :mix_ins, 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) # Module.put_attribute(__MODULE__, :custom_threshold_for_lib, 10)
import Ash.Resource import Ash.Resource
@ -41,30 +43,46 @@ defmodule Ash.Resource do
raise "Your module (#{inspect(__MODULE__)}) must be in config, :ash, resources: [...]" raise "Your module (#{inspect(__MODULE__)}) must be in config, :ash, resources: [...]"
end 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 def type() do
@resource_type @resource_type
end end
def relationship(_name) do def create(action, attributes, parameters) do
nil 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 end
def relationships() do def relationships() do
@relationships @relationships
end end
def action(_name) do def action(name) do
nil Enum.find(actions(), &(&1.name == name))
end end
def actions() do def actions() do
@actions @sanitized_actions
end end
def attributes() do def attributes() do
@attributes @attributes
end end
def primary_key() do
@ash_primary_key
end
def name() do def name() do
@name @name
end end
@ -91,4 +109,46 @@ defmodule Ash.Resource do
end) end)
end 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 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
end end
defmacro get(name \\ :get, _opts \\ []) do # TODO: Originally I had it in my mind that you had to set up your own actions
quote do # for the basic capabilities. Instead, these capabilities will just automatically exist
action = Ash.Resource.Actions.Action.new(unquote(name), :get) # 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 # defmacro update(name \\ :update, opts \\ []) do
@current_action # quote bind_quoted: [name: name, opts: opts] do
end # action = Ash.Resource.Actions.Update.new(name, primary?: opts[:primary?] || false)
end
end
defmacro index(name \\ :index, _opts \\ []) do # @actions action
quote do # end
action = Ash.Resource.Actions.Action.new(unquote(name), :index) # 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 # defmacro get(name \\ :get, opts \\ []) do
@current_action # quote bind_quoted: [name: name, opts: opts] do
end # action = Ash.Resource.Actions.Get.new(name, primary?: opts[:primary?] || false)
end
end # @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 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 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 = ecto_type =
if type == :uuid do if type == :uuid do
:binary_id :binary_id
@ -12,7 +14,8 @@ defmodule Ash.Resource.Attributes.Attribute do
%__MODULE__{ %__MODULE__{
name: name, name: name,
type: type, type: type,
ecto_type: ecto_type ecto_type: ecto_type,
primary_key?: opts[:primary_key?] || false
} }
end end
end end

View file

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

View file

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

View file

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

View file

@ -27,14 +27,24 @@ defmodule Ash.Resource.Relationships.ManyToMany do
def new(resource_name, name, related_resource, opts \\ []) do def new(resource_name, name, related_resource, opts \\ []) do
path = opts[:path] || resource_name <> "/:id/" <> to_string(name) path = opts[:path] || resource_name <> "/:id/" <> to_string(name)
through = through!(opts)
source_field_on_join_table = 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 = destination_field_on_join_table =
opts[:destination_field_on_join_table] || atomize(
String.to_atom(Ash.name(related_resource) <> "_id") 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__{ %__MODULE__{
name: name, name: name,
@ -44,20 +54,41 @@ defmodule Ash.Resource.Relationships.ManyToMany do
through: through, through: through,
side_load: opts[:side_load], side_load: opts[:side_load],
destination: related_resource, destination: related_resource,
source_field: opts[:source_field] || :id, source_field: source_field,
destination_field: opts[:destination_field] || :id, destination_field: destination_field,
source_field_on_join_table: source_field_on_join_table, source_field_on_join_table: source_field_on_join_table,
destination_field_on_join_table: destination_field_on_join_table destination_field_on_join_table: destination_field_on_join_table
} }
end 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 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 when is_bitstring(through) ->
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 end
end end

View file

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