This commit is contained in:
Zach Daniel 2019-12-01 16:58:29 -05:00
parent 9d51c80810
commit dd081977a0
No known key found for this signature in database
GPG key ID: A57053A671EE649E
9 changed files with 201 additions and 75 deletions

View file

@ -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
]
[

View file

@ -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

View file

@ -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
View 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
View 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

View file

@ -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}

View file

@ -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,

View file

@ -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)

View file

@ -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