mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
WIP
This commit is contained in:
parent
9d51c80810
commit
dd081977a0
9 changed files with 201 additions and 75 deletions
|
@ -19,7 +19,9 @@ locals_without_parens = [
|
|||
has_many: 2,
|
||||
has_many: 3,
|
||||
many_to_many: 2,
|
||||
many_to_many: 3
|
||||
many_to_many: 3,
|
||||
default_page_size: 1,
|
||||
resources: 1
|
||||
]
|
||||
|
||||
[
|
||||
|
|
|
@ -25,11 +25,10 @@
|
|||
* Validate rules at creation
|
||||
* Maybe fix the crappy parts of optimal and bring it in for opts validation?
|
||||
* The ecto internals that live on structs are going to cause problems w/ pluggability of backends, like the `%Ecto.Association.NotLoaded{}`. That backend may need to scrub the ecto specifics off of those structs.
|
||||
* Add a mixin compatibility checker framework, to allow for mixins to declare what features they do/don't support.
|
||||
* Add a mixin compatibility checker framework, to allow for mix_ins to declare what features they do/don't support.
|
||||
* Make `Ash.Type` that is a superset of things like `Ecto.Type`. If we bring in ecto database-less(looking like more and more of a good idea to me) that kind of thing gets easier and we can potentially lean on ecto for type validations well.
|
||||
* use a process to hold constructed DSL state, and then coalesce it all at the end. This can clean things up, and also allow us to potentially eliminate the registry. This will probably go hand in hand w/ the "capabilities" layer, where the DSL confirms that your data layer is capable of performing everything that your DSL declares
|
||||
* make ets dep optional
|
||||
|
||||
go through all the remaining endpoints and make them use all the goodness that get and index have now.
|
||||
start on the work that will make it “well featured” like supporting all the different field types and whatnot
|
||||
|
||||
|
||||
|
|
55
lib/ash.ex
55
lib/ash.ex
|
@ -12,8 +12,8 @@ defmodule Ash do
|
|||
@type sort :: Keyword.t()
|
||||
@type side_loads :: Keyword.t()
|
||||
|
||||
def resources() do
|
||||
Application.get_env(:ash, :resources) || []
|
||||
def resources(api) do
|
||||
api.resources()
|
||||
end
|
||||
|
||||
def primary_key(resource) do
|
||||
|
@ -33,6 +33,10 @@ defmodule Ash do
|
|||
resource.relationships()
|
||||
end
|
||||
|
||||
def side_load_config(api) do
|
||||
api.side_load_config()
|
||||
end
|
||||
|
||||
def primary_action(resource, type) do
|
||||
resource
|
||||
|> actions()
|
||||
|
@ -75,49 +79,6 @@ defmodule Ash do
|
|||
resource.data_layer()
|
||||
end
|
||||
|
||||
def get(resource, id, params \\ %{}) do
|
||||
# TODO: Figure out this interface
|
||||
params_with_filter =
|
||||
params
|
||||
|> Map.put_new(:filter, %{})
|
||||
|> Map.update!(:filter, &Map.put(&1, :id, id))
|
||||
|> Map.put(:page, %{limit: 2})
|
||||
|
||||
case read(resource, params_with_filter) do
|
||||
{:ok, %{results: [single_result]}} ->
|
||||
{:ok, single_result}
|
||||
|
||||
{:ok, %{results: []}} ->
|
||||
{:ok, nil}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{:ok, %{results: results}} when is_list(results) ->
|
||||
{:error, :too_many_results}
|
||||
end
|
||||
end
|
||||
|
||||
def read(resource, params \\ %{}) do
|
||||
case Map.get(params, :action) || primary_action(resource, :read) do
|
||||
nil ->
|
||||
{:error, "no action provided, and no primary action found"}
|
||||
|
||||
action ->
|
||||
Ash.DataLayer.Actions.run_read_action(resource, action, params)
|
||||
end
|
||||
end
|
||||
|
||||
def create(resource, params) do
|
||||
case Map.get(params, :action) || primary_action(resource, :create) do
|
||||
nil ->
|
||||
{:error, "no action provided, and no primary action found"}
|
||||
|
||||
action ->
|
||||
Ash.DataLayer.Actions.run_create_action(resource, action, params)
|
||||
end
|
||||
end
|
||||
|
||||
# # TODO: auth
|
||||
# def create(resource, attributes, relationships, params \\ %{}) do
|
||||
# action = Map.get(params, :action) || primary_action(resource, :create)
|
||||
|
@ -136,9 +97,5 @@ defmodule Ash do
|
|||
# Ash.DataLayer.Actions.run_destroy_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
|
||||
|
||||
## Datalayer shit TODO move this elsewhere
|
||||
end
|
||||
|
|
97
lib/ash/api/api.ex
Normal file
97
lib/ash/api/api.ex
Normal file
|
@ -0,0 +1,97 @@
|
|||
defmodule Ash.Api do
|
||||
defmacro __using__(opts) do
|
||||
quote bind_quoted: [opts: opts] do
|
||||
import Ash.Api, only: [api: 1]
|
||||
@before_compile Ash.Api
|
||||
|
||||
@default_page_size nil
|
||||
@no_interface !!opts[:no_interface?]
|
||||
@side_load_type :simple
|
||||
@side_load_config []
|
||||
|
||||
Module.register_attribute(__MODULE__, :mix_ins, accumulate: true)
|
||||
Module.register_attribute(__MODULE__, :resources, accumulate: true)
|
||||
Module.register_attribute(__MODULE__, :named_resources, accumulate: true)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro api(do: block) do
|
||||
quote do
|
||||
import Ash.Api,
|
||||
only: [
|
||||
default_page_size: 1,
|
||||
resources: 1,
|
||||
side_load: 2,
|
||||
side_load: 1
|
||||
]
|
||||
|
||||
unquote(block)
|
||||
|
||||
import Ash.Api, only: []
|
||||
end
|
||||
end
|
||||
|
||||
defmacro resources(resources) do
|
||||
quote do
|
||||
Enum.map(unquote(resources), fn resource ->
|
||||
case resource do
|
||||
{name, resource} ->
|
||||
@resources resource
|
||||
@named_resources {name, resource}
|
||||
|
||||
resource ->
|
||||
@resources resource
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro side_load(type, config \\ []) do
|
||||
quote bind_quoted: [type: type, config: config] do
|
||||
unless type in [:parallel, :simple] do
|
||||
raise "side_load type must be one if `:parallel` or `:simple`"
|
||||
end
|
||||
|
||||
case type do
|
||||
:simple ->
|
||||
@side_load_type :simple
|
||||
|
||||
:parallel ->
|
||||
@side_load_type :parallel
|
||||
# TODO: validate no extra keys
|
||||
raise "`:supervisor` option must be set."
|
||||
|
||||
@side_load_config [
|
||||
supervisor: config[:supervisor],
|
||||
max_concurrency: config[:max_concurrency],
|
||||
timeout: opts[:timeout],
|
||||
shutdown: opts[:shutdown]
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro default_page_size(value) do
|
||||
quote do
|
||||
@default_page_size unquote(value)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro __before_compile__(env) do
|
||||
quote do
|
||||
def default_page_size(), do: @default_page_size
|
||||
def mix_ins(), do: @mix_ins
|
||||
def resources(), do: @resources
|
||||
def side_load_config(), do: {@side_load_type, @side_load_config}
|
||||
|
||||
unless @no_interface do
|
||||
use Ash.Api.Interface
|
||||
end
|
||||
|
||||
Enum.map(@mix_ins || [], fn hook_module ->
|
||||
code = hook_module.before_compile_hook(unquote(Macro.escape(env)))
|
||||
Module.eval_quoted(__MODULE__, code)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
75
lib/ash/api/interface.ex
Normal file
75
lib/ash/api/interface.ex
Normal file
|
@ -0,0 +1,75 @@
|
|||
defmodule Ash.Api.Interface do
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
def get(resource, id, params \\ %{}) do
|
||||
Ash.Api.Interface.get(__MODULE__, resource, id, params)
|
||||
end
|
||||
|
||||
def read(resource, params) do
|
||||
Ash.Api.Interface.read(__MODULE__, resource, params)
|
||||
end
|
||||
|
||||
def create(resource, params) do
|
||||
Ash.Api.Interface.create(__MODULE__, resource, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get(api, resource, id, params \\ %{}) do
|
||||
# TODO: Figure out this interface
|
||||
params_with_filter =
|
||||
params
|
||||
|> Map.put_new(:filter, %{})
|
||||
|> Map.update!(:filter, &Map.put(&1, :id, id))
|
||||
|> Map.put(:page, %{limit: 2})
|
||||
|
||||
case read(api, resource, params_with_filter) do
|
||||
{:ok, %{results: [single_result]}} ->
|
||||
{:ok, single_result}
|
||||
|
||||
{:ok, %{results: []}} ->
|
||||
{:ok, nil}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{:ok, %{results: results}} when is_list(results) ->
|
||||
{:error, :too_many_results}
|
||||
end
|
||||
end
|
||||
|
||||
def read(api, resource, params \\ %{}) do
|
||||
params = add_default_page_size(api, params)
|
||||
|
||||
case Map.get(params, :action) || Ash.primary_action(resource, :read) do
|
||||
nil ->
|
||||
{:error, "no action provided, and no primary action found"}
|
||||
|
||||
action ->
|
||||
Ash.DataLayer.Actions.run_read_action(resource, action, api, params)
|
||||
end
|
||||
end
|
||||
|
||||
def create(api, resource, params) do
|
||||
case Map.get(params, :action) || Ash.primary_action(resource, :create) do
|
||||
nil ->
|
||||
{:error, "no action provided, and no primary action found"}
|
||||
|
||||
action ->
|
||||
Ash.DataLayer.Actions.run_create_action(resource, action, api, params)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_default_page_size(_api, %{page: %{limit: value}} = params) when is_integer(value) do
|
||||
params
|
||||
end
|
||||
|
||||
defp add_default_page_size(api, params) do
|
||||
params
|
||||
|> Map.update(
|
||||
:page,
|
||||
%{limit: api.default_page_size},
|
||||
&Map.put(&1, :limit, api.default_page_size)
|
||||
)
|
||||
end
|
||||
end
|
|
@ -23,7 +23,7 @@ defmodule Ash.DataLayer.Actions do
|
|||
# Ash.Data.delete(record, action, params)
|
||||
# end
|
||||
|
||||
def run_read_action(resource, action, params) do
|
||||
def run_read_action(resource, action, api, params) do
|
||||
auth_context = %{
|
||||
resource: resource,
|
||||
action: action,
|
||||
|
@ -49,6 +49,7 @@ defmodule Ash.DataLayer.Actions do
|
|||
resource,
|
||||
found,
|
||||
Map.get(instructions, :side_load, []),
|
||||
api,
|
||||
Map.take(params, [:authorize?, :user])
|
||||
),
|
||||
:allow <-
|
||||
|
@ -65,6 +66,7 @@ defmodule Ash.DataLayer.Actions do
|
|||
resource,
|
||||
side_loaded_for_auth,
|
||||
Map.get(params, :side_load, []),
|
||||
api,
|
||||
Map.take(params, [:authorize?, :user])
|
||||
) do
|
||||
{:ok, %{paginator | results: side_loaded}}
|
||||
|
@ -82,7 +84,7 @@ defmodule Ash.DataLayer.Actions do
|
|||
end
|
||||
end
|
||||
|
||||
def run_create_action(resource, action, params) do
|
||||
def run_create_action(resource, action, params, api) do
|
||||
auth_context = %{
|
||||
resource: resource,
|
||||
action: action,
|
||||
|
@ -113,6 +115,7 @@ defmodule Ash.DataLayer.Actions do
|
|||
resource,
|
||||
created,
|
||||
Map.get(params, :side_load, []),
|
||||
api,
|
||||
Map.take(params, [:authorize?, :user])
|
||||
) do
|
||||
{:ok, side_loaded}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
defmodule Ash.DataLayer.SideLoader do
|
||||
def side_load(resource, record, keyword, global_params \\ %{})
|
||||
def side_load(resource, record, keyword, api, global_params \\ %{})
|
||||
|
||||
def side_load(_resource, record_or_records, [], _global_params), do: {:ok, record_or_records}
|
||||
def side_load(_resource, record_or_records, [], _api, _global_params),
|
||||
do: {:ok, record_or_records}
|
||||
|
||||
def side_load(resource, record, side_loads, global_params) when not is_list(record) do
|
||||
case side_load(resource, [record], side_loads, global_params) do
|
||||
def side_load(resource, record, side_loads, api, global_params) when not is_list(record) do
|
||||
case side_load(resource, [record], side_loads, api, global_params) do
|
||||
{:ok, [side_loaded]} -> side_loaded
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def side_load(resource, records, side_loads, global_params) do
|
||||
def side_load(resource, records, side_loads, api, global_params) do
|
||||
# TODO: No global config!
|
||||
config = Application.get_env(:ash, :side_loader)
|
||||
parallel_supervisor = config[:parallel_supervisor]
|
||||
{side_load_type, config} = Ash.side_load_config(resource)
|
||||
async? = side_load_type == :parallel
|
||||
|
||||
side_loads =
|
||||
Enum.map(side_loads, fn side_load_part ->
|
||||
|
@ -26,7 +27,7 @@ defmodule Ash.DataLayer.SideLoader do
|
|||
|
||||
side_loaded =
|
||||
side_loads
|
||||
|> maybe_async_stream(config, parallel_supervisor, fn relationship_name, further ->
|
||||
|> maybe_async_stream(config, async?, fn relationship_name, further ->
|
||||
relationship = Ash.relationship(resource, relationship_name)
|
||||
|
||||
# Combining filters, and handling boolean filters is
|
||||
|
@ -41,7 +42,7 @@ defmodule Ash.DataLayer.SideLoader do
|
|||
})
|
||||
|> Map.put_new(:paginate?, false)
|
||||
|
||||
with {:ok, related_records} <- Ash.read(relationship.destination, action_params),
|
||||
with {:ok, related_records} <- api.read(relationship.destination, action_params),
|
||||
{:ok, %{results: side_loaded_related}} <-
|
||||
side_load(relationship.destination, related_records, further, global_params) do
|
||||
keyed_by_id =
|
||||
|
@ -87,13 +88,13 @@ defmodule Ash.DataLayer.SideLoader do
|
|||
first_error || {:ok, Enum.map(side_loaded, &elem(&1, 1))}
|
||||
end
|
||||
|
||||
defp maybe_async_stream(preloads, _opts, nil, function) do
|
||||
defp maybe_async_stream(preloads, _opts, false, function) do
|
||||
Enum.map(preloads, fn {association, further} ->
|
||||
function.(association, further)
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_async_stream(preloads, opts, supervisor, function) do
|
||||
defp maybe_async_stream(preloads, opts, true, function) do
|
||||
# We could theoretically do one of them outside of a task whlie we wait for the rest
|
||||
# Not worth implementing to start, IMO.
|
||||
opts = [
|
||||
|
@ -104,7 +105,7 @@ defmodule Ash.DataLayer.SideLoader do
|
|||
shutdown: opts[:shutdown] || :timer.seconds(5)
|
||||
]
|
||||
|
||||
supervisor
|
||||
opts[:supervisor]
|
||||
|> Task.Supervisor.async_stream_nolink(
|
||||
preloads,
|
||||
fn {key, further} -> function.(key, further) end,
|
||||
|
|
|
@ -40,10 +40,6 @@ defmodule Ash.Resource do
|
|||
|
||||
defmacro __before_compile__(env) do
|
||||
quote location: :keep do
|
||||
if __MODULE__ not in Ash.resources() 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)
|
||||
|
||||
|
|
|
@ -67,10 +67,6 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
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
|
||||
|
||||
through
|
||||
|
||||
# TODO: do this check at runtime. When done at compilation, it forces the modules
|
||||
|
|
Loading…
Reference in a new issue