docs: revamp hexdocs

fix: use current read action for counting
This commit is contained in:
Zach Daniel 2024-01-12 14:07:35 -05:00
parent 6c5cd551ff
commit 072486bebd
39 changed files with 583 additions and 641 deletions

View file

@ -2,7 +2,6 @@ import Config
config :ash,
flags: [
read_uses_flow?: System.get_env("FLAG_READ_USES_FLOW", "false") == "true",
ash_three?: System.get_env("FLAG_ASH_THREE", "false") == "true"
]

View file

@ -3,13 +3,6 @@ This file was generated by Spark. Do not edit it by hand.
-->
# DSL: Ash.Api.Dsl
Apis are the entrypoints for working with your resources.
Apis may optionally include a list of resources, in which case they can be
used as an `Ash.Registry` in various places. This is for backwards compatibility,
but if at all possible you should define an `Ash.Registry` if you are using an extension
that requires a list of resources. For example, most extensions look for two application
environment variables called `:ash_apis` and `:ash_registries` to find any potential registries
## api

View file

@ -3,12 +3,6 @@ This file was generated by Spark. Do not edit it by hand.
-->
# DSL: Ash.Flow.Dsl
The built in flow DSL.
## Halting
Steps can be halted, which will stop the flow from continuing and return a halted flow. To attach a specific reason, use a `halt_reason`.
If you need more complex halting logic, then you'd want to use a custom step, and return `{:error, Ash.Error.Flow.Halted.exception(...)}`
## flow

View file

@ -33,7 +33,7 @@ See the [Aggregates guide](/documentation/topics/aggregates.md) for more informa
A method of broadly separating resources into different [bounded contexts](https://martinfowler.com/bliki/BoundedContext.html). Small apps might only have one API, in which case you can set-and-forget it, but apps with larger domains can benefit from different contexts having different views of the same resource.
See `Ash.Api.Dsl` for more information.
See `d:Ash.Api` for more information.
## Attribute

View file

@ -4,14 +4,111 @@ defmodule Ash do
"""
for {function, arity} <- Ash.Api.Functions.functions() do
if function == :load do
def load({:ok, result}, load) do
load(result, load)
end
def load({:error, error}, _), do: {:error, error}
def load([], _), do: {:ok, []}
def load(nil, _), do: {:ok, nil}
def load(%page_struct{results: []} = page, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
{:ok, page}
end
end
if function == :load! do
def load!({:ok, result}, load) do
{:ok, load!(result, load)}
end
def load!({:error, error}, _), do: raise(Ash.Error.to_error_class(error))
def load!([], _), do: []
def load!(nil, _), do: nil
def load!(%page_struct{results: []} = page, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
page
end
end
args = Macro.generate_arguments(arity, __MODULE__)
defdelegate unquote(function)(unquote_splicing(args)), to: Ash.Api.GlobalInterface
docs_arity =
if function in Ash.Api.Functions.no_opts_functions() do
arity
else
arity + 1
end
@doc "Calls `c:Ash.Api.#{function}/#{docs_arity}` on the resource's configured api. See those callback docs for more."
def unquote(function)(unquote_splicing(args)) do
resource =
Ash.Api.GlobalInterface.resource_from_args!(unquote(function), unquote(arity), [
unquote_splicing(args)
])
api = Ash.Resource.Info.api(resource)
if !api do
Ash.Api.GlobalInterface.raise_no_api_error!(resource, unquote(function), unquote(arity))
end
apply(api, unquote(function), [unquote_splicing(args)])
end
unless function in Ash.Api.Functions.no_opts_functions() do
args = Macro.generate_arguments(arity + 1, __MODULE__)
defdelegate unquote(function)(unquote_splicing(args)), to: Ash.Api.GlobalInterface
if function == :load! do
def load!({:ok, result}, load, opts) do
{:ok, load(result, load, opts)}
end
def load!({:error, error}, _, _), do: raise(Ash.Error.to_error_class(error))
def load!(nil, _, _), do: nil
def load!([], _, _), do: []
def load!(%page_struct{results: []} = page, _, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
page
end
end
if function == :load do
def load({:ok, result}, load, opts) do
load(result, load, opts)
end
def load({:error, error}, _, _), do: {:error, error}
def load([], _, _), do: {:ok, []}
def load(nil, _, _), do: {:ok, nil}
def load(%page_struct{results: []} = page, _, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
{:ok, page}
end
end
@doc "Calls `c:Ash.Api.#{function}/#{arity + 1}` on the resource's configured api. See those callback docs for more."
def unquote(function)(unquote_splicing(args)) do
resource =
Ash.Api.GlobalInterface.resource_from_args!(unquote(function), unquote(arity), [
unquote_splicing(args)
])
api = Ash.Resource.Info.api(resource)
if !api do
Ash.Api.GlobalInterface.raise_no_api_error!(resource, unquote(function), unquote(arity))
end
apply(api, unquote(function), [unquote_splicing(args)])
end
end
end
@ -32,87 +129,14 @@ defmodule Ash do
end
end
@doc """
Gets all of the ash context so it can be set into a new process.
@doc deprecated: "See `Ash.ProcessHelpers`. This alias will be removed in 3.0"
defdelegate get_context_for_transfer(opts \\ []), to: Ash.ProcessHelpers
@doc deprecated: "See `Ash.ProcessHelpers`. This alias will be removed in 3.0"
defdelegate transfer_context(term, opts \\ []), to: Ash.ProcessHelpers
Use `transfer_context/1` in the new process to set the context.
"""
@spec get_context_for_transfer(opts :: Keyword.t()) :: term
def get_context_for_transfer(opts \\ []) do
context = Ash.get_context()
actor = Process.get(:ash_actor)
authorize? = Process.get(:ash_authorize?)
tenant = Process.get(:ash_tenant)
tracer = Process.get(:ash_tracer)
tracer_context =
opts[:tracer]
|> List.wrap()
|> Enum.concat(List.wrap(tracer))
|> Map.new(fn tracer ->
{tracer, Ash.Tracer.get_span_context(tracer)}
end)
%{
context: context,
actor: actor,
tenant: tenant,
authorize?: authorize?,
tracer: tracer,
tracer_context: tracer_context
}
end
@spec transfer_context(term, opts :: Keyword.t()) :: :ok
def transfer_context(
%{
context: context,
actor: actor,
tenant: tenant,
authorize?: authorize?,
tracer: tracer,
tracer_context: tracer_context
},
_opts \\ []
) do
case actor do
{:actor, actor} ->
Ash.set_actor(actor)
_ ->
:ok
end
case tenant do
{:tenant, tenant} ->
Ash.set_tenant(tenant)
_ ->
:ok
end
case authorize? do
{:authorize?, authorize?} ->
Ash.set_authorize?(authorize?)
_ ->
:ok
end
Ash.set_tracer(tracer)
Enum.each(tracer_context || %{}, fn {tracer, tracer_context} ->
Ash.Tracer.set_span_context(tracer, tracer_context)
end)
Ash.set_context(context)
end
@doc """
Sets context into the process dictionary that is used for all changesets and queries.
In Ash 3.0, this will be updated to deep merge
"""
@doc deprecated: """
Sets context into the process dictionary that is used for all changesets and queries.
"""
@spec set_context(map) :: :ok
def set_context(map) do
Process.put(:ash_context, map)
@ -120,9 +144,9 @@ defmodule Ash do
:ok
end
@doc """
Deep merges context into the process dictionary that is used for all changesets and queries.
"""
@doc deprecated: """
Deep merges context into the process dictionary that is used for all changesets and queries.
"""
@spec merge_context(map) :: :ok
def merge_context(map) do
update_context(&Ash.Helpers.deep_merge_maps(&1, map))
@ -130,9 +154,9 @@ defmodule Ash do
:ok
end
@doc """
Updates the context into the process dictionary that is used for all changesets and queries.
"""
@doc deprecated: """
Updates the context into the process dictionary that is used for all changesets and queries.
"""
@spec update_context((map -> map)) :: :ok
def update_context(fun) do
context = Process.get(:ash_context, %{})
@ -141,9 +165,9 @@ defmodule Ash do
:ok
end
@doc """
Sets actor into the process dictionary that is used for all changesets and queries.
"""
@doc deprecated: """
Sets actor into the process dictionary that is used for all changesets and queries.
"""
@spec set_actor(map) :: :ok
def set_actor(map) do
Process.put(:ash_actor, {:actor, map})
@ -151,9 +175,9 @@ defmodule Ash do
:ok
end
@doc """
Sets authorize? into the process dictionary that is used for all changesets and queries.
"""
@doc deprecated: """
Sets authorize? into the process dictionary that is used for all changesets and queries.
"""
@spec set_authorize?(map) :: :ok
def set_authorize?(map) do
Process.put(:ash_authorize?, {:authorize?, map})
@ -161,9 +185,9 @@ defmodule Ash do
:ok
end
@doc """
Sets the tracer into the process dictionary that will be used to trace requests
"""
@doc deprecated: """
Sets the tracer into the process dictionary that will be used to trace requests
"""
@spec set_tracer(module | list(module)) :: :ok
def set_tracer(module) do
case Process.get(:ash_tracer, module) do
@ -174,9 +198,9 @@ defmodule Ash do
:ok
end
@doc """
Removes a tracer from the process dictionary.
"""
@doc deprecated: """
Removes a tracer from the process dictionary.
"""
@spec remove_tracer(module | list(module)) :: :ok
def remove_tracer(module) do
case Process.get(:ash_tracer, module) do
@ -187,9 +211,9 @@ defmodule Ash do
:ok
end
@doc """
Gets the current actor from the process dictionary
"""
@doc deprecated: """
Gets the current actor from the process dictionary
"""
@spec get_actor() :: term()
def get_actor do
case Process.get(:ash_actor) do
@ -201,9 +225,9 @@ defmodule Ash do
end
end
@doc """
Gets the current tracer
"""
@doc deprecated: """
Gets the current tracer
"""
@spec get_tracer() :: term()
def get_tracer do
case Process.get(:ash_tracer) do
@ -215,9 +239,9 @@ defmodule Ash do
end
end
@doc """
Gets the current authorize? from the process dictionary
"""
@doc deprecated: """
Gets the current authorize? from the process dictionary
"""
@spec get_authorize?() :: term()
def get_authorize? do
case Process.get(:ash_authorize?) do
@ -229,9 +253,9 @@ defmodule Ash do
end
end
@doc """
Sets tenant into the process dictionary that is used for all changesets and queries.
"""
@doc deprecated: """
Sets tenant into the process dictionary that is used for all changesets and queries.
"""
@spec set_tenant(String.t()) :: :ok
def set_tenant(tenant) do
Process.put(:ash_tenant, {:tenant, tenant})
@ -239,9 +263,9 @@ defmodule Ash do
:ok
end
@doc """
Gets the current tenant from the process dictionary
"""
@doc deprecated: """
Gets the current tenant from the process dictionary
"""
@spec get_tenant() :: term()
def get_tenant do
case Process.get(:ash_tenant) do
@ -253,9 +277,9 @@ defmodule Ash do
end
end
@doc """
Gets the current context from the process dictionary
"""
@doc deprecated: """
Gets the current context from the process dictionary
"""
@spec get_context() :: term()
def get_context do
Process.get(:ash_context, %{}) || %{}

View file

@ -1,42 +0,0 @@
defmodule Ash.Actions.Flows.Read do
@moduledoc """
Execute a read action.
"""
require Ash.Flags
use Ash.Flow
flow do
argument :query, :struct do
allow_nil? false
constraints instance_of: Ash.Query
end
argument :action, :struct do
allow_nil? false
constraints instance_of: Ash.Resource.Actions.Read
end
returns :fake_result
end
steps do
custom :fake_result, Ash.Actions.Flows.Read.FakeResult do
input %{query: arg(:query), action: arg(:action)}
end
end
@dialyzer {:no_return, [run: 3]}
def run(query, action, opts) do
Ash.Flags.assert!(:read_uses_flow?, true)
__MODULE__
|> Ash.Flow.run(%{query: query, action: action}, opts)
|> case do
result when result.errors == [] ->
{:ok, result.result}
result ->
{:error, result}
end
end
end

View file

@ -1,10 +0,0 @@
defmodule Ash.Actions.Flows.Read.FakeResult do
@moduledoc """
Generates a fake result, as the flow has to actually return something.
"""
use Ash.Flow.Step
def run(_input, _opts, _context) do
{:ok, []}
end
end

View file

@ -70,56 +70,52 @@ defmodule Ash.Actions.Read do
| {:error, term}
def run(query, action, opts \\ [])
if Ash.Flags.read_uses_flow?() do
def run(query, action, opts), do: Ash.Actions.Flows.Read.run(query, action, opts)
else
def run(query, action, opts) do
{query, opts} = Ash.Actions.Helpers.add_process_context(query.api, query, opts)
def run(query, action, opts) do
{query, opts} = Ash.Actions.Helpers.add_process_context(query.api, query, opts)
Ash.Tracer.span :action,
Ash.Api.Info.span_name(query.api, query.resource, action.name),
opts[:tracer] do
metadata = %{
api: query.api,
resource: query.resource,
resource_short_name: Ash.Resource.Info.short_name(query.resource),
actor: opts[:actor],
tenant: opts[:tenant],
action: action.name,
authorize?: opts[:authorize?]
}
Ash.Tracer.span :action,
Ash.Api.Info.span_name(query.api, query.resource, action.name),
opts[:tracer] do
metadata = %{
api: query.api,
resource: query.resource,
resource_short_name: Ash.Resource.Info.short_name(query.resource),
actor: opts[:actor],
tenant: opts[:tenant],
action: action.name,
authorize?: opts[:authorize?]
}
Ash.Tracer.telemetry_span [:ash, Ash.Api.Info.short_name(query.api), :read], metadata do
Ash.Tracer.set_metadata(opts[:tracer], :action, metadata)
Ash.Tracer.telemetry_span [:ash, Ash.Api.Info.short_name(query.api), :read], metadata do
Ash.Tracer.set_metadata(opts[:tracer], :action, metadata)
run_around_transaction_hooks(query, fn query ->
case do_run(query, action, opts) do
{:error, error} ->
if opts[:tracer] do
stacktrace =
case error do
%{stacktrace: %{stacktrace: stacktrace}} ->
stacktrace || []
run_around_transaction_hooks(query, fn query ->
case do_run(query, action, opts) do
{:error, error} ->
if opts[:tracer] do
stacktrace =
case error do
%{stacktrace: %{stacktrace: stacktrace}} ->
stacktrace || []
_ ->
{:current_stacktrace, stacktrace} =
Process.info(self(), :current_stacktrace)
_ ->
{:current_stacktrace, stacktrace} =
Process.info(self(), :current_stacktrace)
stacktrace
end
stacktrace
end
Ash.Tracer.set_handled_error(opts[:tracer], Ash.Error.to_error_class(error),
stacktrace: stacktrace
)
end
Ash.Tracer.set_handled_error(opts[:tracer], Ash.Error.to_error_class(error),
stacktrace: stacktrace
)
end
{:error, error}
{:error, error}
other ->
other
end
end)
end
other ->
other
end
end)
end
end
end
@ -3106,11 +3102,14 @@ defmodule Ash.Actions.Read do
resource: destination_resource,
context: %{
data_layer: %{lateral_join_source: {root_data, path}}
},
action: %{
name: read_action
}
},
query
) do
case Ash.Query.Aggregate.new(destination_resource, :count, :count) do
case Ash.Query.Aggregate.new(destination_resource, :count, :count, read_action: read_action) do
{:ok, aggregate} ->
Ash.DataLayer.run_aggregate_query_with_lateral_join(
query,

View file

@ -144,15 +144,7 @@ defmodule Ash.Api.Dsl do
Ash.Api.Verifiers.ValidateRelatedResourceInclusion
]
@moduledoc """
Apis are the entrypoints for working with your resources.
Apis may optionally include a list of resources, in which case they can be
used as an `Ash.Registry` in various places. This is for backwards compatibility,
but if at all possible you should define an `Ash.Registry` if you are using an extension
that requires a list of resources. For example, most extensions look for two application
environment variables called `:ash_apis` and `:ash_registries` to find any potential registries
"""
@moduledoc false
use Spark.Dsl.Extension, sections: @sections, verifiers: @verifiers
end

View file

@ -1,110 +1,8 @@
defmodule Ash.Api.GlobalInterface do
@moduledoc "The interface for calling any Ash api. Use `Ash` to call these functions."
for {function, arity} <- Ash.Api.Functions.functions() do
if function == :load do
def load({:ok, result}, load) do
load(result, load)
end
@moduledoc false
def load({:error, error}, _), do: {:error, error}
def load([], _), do: {:ok, []}
def load(nil, _), do: {:ok, nil}
def load(%page_struct{results: []} = page, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
{:ok, page}
end
end
if function == :load! do
def load!({:ok, result}, load) do
{:ok, load!(result, load)}
end
def load!({:error, error}, _), do: raise(Ash.Error.to_error_class(error))
def load!([], _), do: []
def load!(nil, _), do: nil
def load!(%page_struct{results: []} = page, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
page
end
end
args = Macro.generate_arguments(arity, __MODULE__)
docs_arity =
if function in Ash.Api.Functions.no_opts_functions() do
arity
else
arity + 1
end
@doc "Calls `c:Ash.Api.#{function}/#{docs_arity}` on the resource's configured api. See those callback docs for more."
def unquote(function)(unquote_splicing(args)) do
resource = resource_from_args!(unquote(function), unquote(arity), [unquote_splicing(args)])
api = Ash.Resource.Info.api(resource)
if !api do
raise_no_api_error!(resource, unquote(function), unquote(arity))
end
apply(api, unquote(function), [unquote_splicing(args)])
end
unless function in Ash.Api.Functions.no_opts_functions() do
args = Macro.generate_arguments(arity + 1, __MODULE__)
if function == :load! do
def load!({:ok, result}, load, opts) do
{:ok, load(result, load, opts)}
end
def load!({:error, error}, _, _), do: raise(Ash.Error.to_error_class(error))
def load!(nil, _, _), do: nil
def load!([], _, _), do: []
def load!(%page_struct{results: []} = page, _, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
page
end
end
if function == :load do
def load({:ok, result}, load, opts) do
load(result, load, opts)
end
def load({:error, error}, _, _), do: {:error, error}
def load([], _, _), do: {:ok, []}
def load(nil, _, _), do: {:ok, nil}
def load(%page_struct{results: []} = page, _, _)
when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
{:ok, page}
end
end
@doc "Calls `c:Ash.Api.#{function}/#{arity + 1}` on the resource's configured api. See those callback docs for more."
def unquote(function)(unquote_splicing(args)) do
resource =
resource_from_args!(unquote(function), unquote(arity), [unquote_splicing(args)])
api = Ash.Resource.Info.api(resource)
if !api do
raise_no_api_error!(resource, unquote(function), unquote(arity))
end
apply(api, unquote(function), [unquote_splicing(args)])
end
end
end
defp raise_no_api_error!(resource, function, arity) do
@doc false
def raise_no_api_error!(resource, function, arity) do
raise ArgumentError, """
No api configured for resource #{inspect(resource)}.
@ -114,7 +12,8 @@ defmodule Ash.Api.GlobalInterface do
"""
end
defp resource_from_args!(fun, _, [data | _]) when fun in [:load, :load!] do
@doc false
def resource_from_args!(fun, _, [data | _]) when fun in [:load, :load!] do
case data do
%struct{rerun: {%Ash.Query{resource: resource}, _}}
when struct in [Ash.Page.Keyset, Ash.Page.Offset] ->
@ -138,20 +37,20 @@ defmodule Ash.Api.GlobalInterface do
end
end
defp resource_from_args!(:reload, _, [%resource{} | _]) do
def resource_from_args!(:reload, _, [%resource{} | _]) do
resource
end
defp resource_from_args!(fun, _, [_, resource | _]) when fun in [:bulk_create, :bulk_create!] do
def resource_from_args!(fun, _, [_, resource | _]) when fun in [:bulk_create, :bulk_create!] do
resource
end
defp resource_from_args!(fun, _, [resource | _])
when fun in [:calculate, :calculate!, :get, :get!] do
def resource_from_args!(fun, _, [resource | _])
when fun in [:calculate, :calculate!, :get, :get!] do
resource
end
defp resource_from_args!(fun, _, [page | _]) when fun in [:page, :page!] do
def resource_from_args!(fun, _, [page | _]) when fun in [:page, :page!] do
case page do
%struct{rerun: {%Ash.Query{resource: resource}, _}}
when struct in [Ash.Page.Keyset, Ash.Page.Offset] ->
@ -170,8 +69,8 @@ defmodule Ash.Api.GlobalInterface do
end
end
defp resource_from_args!(fun, _, [query_or_changeset_or_action | _])
when fun in [:can, :can?] do
def resource_from_args!(fun, _, [query_or_changeset_or_action | _])
when fun in [:can, :can?] do
case query_or_changeset_or_action do
%struct{resource: resource} when struct in [Ash.Changeset, Ash.Query, Ash.ActionInput] ->
resource
@ -184,42 +83,42 @@ defmodule Ash.Api.GlobalInterface do
end
end
defp resource_from_args!(fun, _arity, [changeset_or_query | _])
when fun in [
:destroy,
:update,
:destroy!,
:update!,
:read,
:read!,
:stream,
:stream!,
:create,
:create!,
:run_action,
:run_action!,
:read_one,
:read_one!,
:get,
:count,
:count!,
:first,
:first!,
:sum,
:sum!,
:min,
:min!,
:max,
:max!,
:avg,
:avg!,
:exists,
:exists?,
:list,
:list!,
:aggregate,
:aggregate!
] do
def resource_from_args!(fun, _arity, [changeset_or_query | _])
when fun in [
:destroy,
:update,
:destroy!,
:update!,
:read,
:read!,
:stream,
:stream!,
:create,
:create!,
:run_action,
:run_action!,
:read_one,
:read_one!,
:get,
:count,
:count!,
:first,
:first!,
:sum,
:sum!,
:min,
:min!,
:max,
:max!,
:avg,
:avg!,
:exists,
:exists?,
:list,
:list!,
:aggregate,
:aggregate!
] do
case changeset_or_query do
%struct{resource: resource} when struct in [Ash.Changeset, Ash.Query, Ash.ActionInput] ->
resource
@ -236,7 +135,7 @@ defmodule Ash.Api.GlobalInterface do
end
end
defp resource_from_args!(fun, arity, _args) do
def resource_from_args!(fun, arity, _args) do
raise ArgumentError, "Could not determine resource from arguments to `Ash.#{fun}/#{arity}`"
end
end

View file

@ -2718,6 +2718,20 @@ defmodule Ash.Changeset do
end
end
def get_argument(changeset, argument) when is_binary(argument) do
changeset.arguments
|> Enum.find(fn {key, _} ->
to_string(key) == argument
end)
|> case do
{_key, value} ->
value
_ ->
nil
end
end
@doc "Fetches the value of an argument provided to the changeset or `:error`."
@spec fetch_argument(t, atom) :: {:ok, term} | :error
def fetch_argument(changeset, argument) when is_atom(argument) do
@ -2733,6 +2747,20 @@ defmodule Ash.Changeset do
end
end
def fetch_argument(changeset, argument) when is_binary(argument) do
changeset.arguments
|> Enum.find(fn {key, _} ->
to_string(key) == argument
end)
|> case do
{_key, value} ->
{:ok, value}
_ ->
:error
end
end
@doc "Gets the changing value or the original value of an attribute."
@spec get_attribute(t, atom) :: term
def get_attribute(changeset, attribute) do

View file

@ -1,49 +1,5 @@
defmodule Ash.Engine do
@moduledoc """
The Ash engine handles the parallelization/running of requests to Ash.
Much of the complexity of this doesn't come into play for simple requests.
The way it works is that it accepts a list of `Ash.Engine.Request` structs.
Some of values on those structs will be instances of `Ash.Engine.Request.UnresolvedField`.
These unresolved fields can express a dependence on the field values from other requests.
This allows the engine to wait on executing some code until it has its required inputs,
or if all of its dependencies are met, it can execute it immediately. The engine's job is
to resolve its unresolved fields in the proper order, potentially in parallel.
It also has knowledge baked in about certain special fields, like `data` which is the
field we are ultimately trying to resolve, and `query` which is the field that drives authorization
for read requests. Authorization is done on a *per engine request* basis.
As the complexity of a system grows, it becomes very difficult to write code that
is both imperative and performant. This is especially true of a framework that is
designed to be configurable. What exactly is done, as well as the order it is done in,
and whether or not is can be parallelized, varies wildly based on factors like how
the resources are configured and what capabilities the data layer has. By implementing
a generic "parallel engine", we can let the engine solve that problem. We only
have to express the various operations that must happen, and what other pieces of data
they need in order to happen, and the engine handles the rest.
There are various tradeoffs in the current design. The original version of the engine started a process
for each request. While this had the least constrained performance characteristics of all the designs,
it was problematic for various reasons. The primary reason being that it could deadlock without any
reasonable way to debug said deadlock because the various states were distributed. The second version
of the engine introduced a central `Engine` process that helped with some of these issues, but ultimately
had the same problem. The third (and current) version of the engine is reworked instead to be drastically
simpler, potentially at the expense of performance for some requests. Instead of starting a process per
request, it opts to only parallelize the `data` field resolution of fields that are marked as `async?: true`,
(unlike the previous versions which started a process for the whole request.) Although it does its best
to prioritize starting any async tasks, it is possible that if some mix of async/sync requests are passed in
a potentially long running sync task could prevent it from starting an async task, giving this potentially worse
performance characteristics. In practice, this doesn't really matter because the robust data layers support running
asynchronously, unless they are in a transaction in which case everything runs serially anyway.
The current version of the engine can be seen as an event loop that will async some events and yield them. It also
has support for a concurrency limit (per engine invocation, not globally, although that could now be added much more
easily). This limit defaults to `2 * schedulers_online`.
Check out the docs for `Ash.Engine.Request` for some more information. This is a private
interface at the moment, though, so this documentation is just here to explain how it works
it is not intended to give you enough information to use the engine directly.
"""
@moduledoc false
defstruct [
:ref,

View file

@ -1,9 +1,5 @@
defmodule Ash.Engine.Request do
@moduledoc """
Represents an individual request to be processed by the engine.
See `new/1` for more information
"""
@moduledoc false
defstruct [
:async?,
:resource,
@ -50,9 +46,7 @@ defmodule Ash.Engine.Request do
require Logger
defmodule UnresolvedField do
@moduledoc """
Represents an unresolved field to be resolved by the engine.
"""
@moduledoc false
defstruct [:resolver, deps: [], data?: false]
@type t :: %__MODULE__{}

View file

@ -1,13 +1,7 @@
defmodule Ash.Flags do
@moduledoc """
Feature flagging support for Ash internals.
These are macros so that they can be used at compile time to switch code
paths.
"""
@moduledoc false
@flags [
read_uses_flow?: false,
ash_three?: false
]
@ -19,14 +13,6 @@ defmodule Ash.Flags do
@noop {:__block__, [], []}
@doc "Should read actions use the new flow-based executor?"
@spec read_uses_flow? :: Macro.t()
defmacro read_uses_flow? do
quote do
unquote(Map.get(@flag_values, :read_uses_flow?))
end
end
@doc "Should we activate Ash 3.0 features?"
@spec ash_three? :: Macro.t()
defmacro ash_three? do

View file

@ -349,14 +349,7 @@ defmodule Ash.Flow.Dsl do
@sections [@flow, @steps]
@moduledoc """
The built in flow DSL.
## Halting
Steps can be halted, which will stop the flow from continuing and return a halted flow. To attach a specific reason, use a `halt_reason`.
If you need more complex halting logic, then you'd want to use a custom step, and return `{:error, Ash.Error.Flow.Halted.exception(...)}`
"""
@moduledoc false
use Spark.Dsl.Extension,
sections: @sections,

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.AccessingFrom do
@moduledoc false
@moduledoc "This check is true when the current action is being run \"through\" a relationship."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.Action do
@moduledoc false
@moduledoc "This check is true when the action name matches the provided action name."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.ActionType do
@moduledoc false
@moduledoc "This check is true when the action type matches the provided type"
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.ActorAttributeEquals do
@moduledoc false
@moduledoc "This check is true when the value of the specified attribute of the actor equals the specified value."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.ActorPresent do
@moduledoc false
@moduledoc "This check is true when there is an actor specified, and false when the actor is `nil`."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.Attribute do
@moduledoc false
@moduledoc "This check is true when a field on the record matches a specific filter."
use Ash.Policy.FilterCheck

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.ChangingAttributes do
@moduledoc false
@moduledoc "This check is true when attribute changes correspond to the provided options."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.ChangingRelationships do
@moduledoc false
@moduledoc "This check is true when the specified relationship is changing"
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.ContextEquals do
@moduledoc false
@moduledoc "This check is true when the value of the specified key or path in the changeset or query context equals the specified value."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.FilteringOn do
@moduledoc false
@moduledoc "This check is true when the field provided is being referenced anywhere in a filter statement."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.Loading do
@moduledoc false
@moduledoc "This check is true when the field or relationship, or path to field, is being loaded and false when it is not."
use Ash.Policy.SimpleCheck
require Logger

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.RelatesToActorVia do
@moduledoc false
@moduledoc "This check passes if the data relates to the actor via the specified relationship or path of relationships."
use Ash.Policy.FilterCheckWithContext
require Ash.Expr

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.RelatingToActor do
@moduledoc false
@moduledoc "This check is true when the specified relationship is being changed to the current actor."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.Selecting do
@moduledoc false
@moduledoc "This check is true when the field is being selected and false when it is not."
use Ash.Policy.SimpleCheck
@impl true

View file

@ -1,5 +1,5 @@
defmodule Ash.Policy.Check.Static do
@moduledoc false
@moduledoc "This check is always the result provided"
use Ash.Policy.SimpleCheck
@impl true

View file

@ -4,7 +4,6 @@ defmodule Ash.Policy.Info do
For more information, see `Ash.Policy.Authorizer`
"""
@type request :: Ash.Engine.Request.t()
alias Spark.Dsl.Extension

View file

@ -0,0 +1,77 @@
defmodule Ash.ProcessHelpers do
@doc """
Gets all of the ash context so it can be set into a new process.
Use `transfer_context/1` in the new process to set the context.
"""
@spec get_context_for_transfer(opts :: Keyword.t()) :: term
def get_context_for_transfer(opts \\ []) do
context = Ash.get_context()
actor = Process.get(:ash_actor)
authorize? = Process.get(:ash_authorize?)
tenant = Process.get(:ash_tenant)
tracer = Process.get(:ash_tracer)
tracer_context =
opts[:tracer]
|> List.wrap()
|> Enum.concat(List.wrap(tracer))
|> Map.new(fn tracer ->
{tracer, Ash.Tracer.get_span_context(tracer)}
end)
%{
context: context,
actor: actor,
tenant: tenant,
authorize?: authorize?,
tracer: tracer,
tracer_context: tracer_context
}
end
@spec transfer_context(term, opts :: Keyword.t()) :: :ok
def transfer_context(
%{
context: context,
actor: actor,
tenant: tenant,
authorize?: authorize?,
tracer: tracer,
tracer_context: tracer_context
},
_opts \\ []
) do
case actor do
{:actor, actor} ->
Ash.set_actor(actor)
_ ->
:ok
end
case tenant do
{:tenant, tenant} ->
Ash.set_tenant(tenant)
_ ->
:ok
end
case authorize? do
{:authorize?, authorize?} ->
Ash.set_authorize?(authorize?)
_ ->
:ok
end
Ash.set_tracer(tracer)
Enum.each(tracer_context || %{}, fn {tracer, tracer_context} ->
Ash.Tracer.set_span_context(tracer, tracer_context)
end)
Ash.set_context(context)
end
end

View file

@ -145,7 +145,7 @@ defmodule Ash.Query.Aggregate do
build_opts -> Ash.Query.build(related, build_opts)
end
read_action = opts[:read_action] || Ash.Resource.Info.primary_action(related, :read).name
read_action = opts[:read_action] || Ash.Resource.Info.primary_action!(related, :read).name
query =
if query.__validated_for_action__ != read_action do

View file

@ -72,6 +72,11 @@ defmodule Ash.Resource.Change.Builtins do
{Ash.Resource.Change.RelateActor, opts}
end
@spec debug_log(label :: String.t()) :: Ash.Resource.Change.ref()
def debug_log(label \\ nil) do
{Ash.Resource.Change.DebugLog, label: label}
end
@doc """
Apply an "optimistic lock" on a record being updated or destroyed.

View file

@ -0,0 +1,60 @@
defmodule Ash.Resource.Change.DebugLog do
@moduledoc false
use Ash.Resource.Change
require Logger
@doc false
@spec change(Ash.Changeset.t(), keyword, Ash.Resource.Change.context()) :: Ash.Changeset.t()
def change(changeset, opts, _) do
label = opts[:label]
debug(changeset.params, "input", label, changeset)
debug(changeset.attributes, "casted attributes", label, changeset)
debug(changeset.arguments, "casted arguments", label, changeset)
changeset
|> Ash.Changeset.after_transaction(fn changeset, result ->
debug(changeset, "final changeset", label, changeset)
debug(result, "action result", label, changeset)
result
end)
|> Ash.Changeset.around_action(fn changeset, callback ->
try do
callback.(changeset)
rescue
e ->
debug_exception(
Exception.format(:error, e, __STACKTRACE__),
"action raised error",
label,
changeset
)
reraise e, __STACKTRACE__
end
end)
end
defp debug(stuff, our_label, nil, changeset) do
Logger.debug(
"#{our_label} - #{inspect(changeset.resource)}.#{changeset.action.name}: #{inspect(stuff)}"
)
end
defp debug(stuff, our_label, their_label, changeset) do
Logger.debug(
"#{their_label} - #{our_label} - #{inspect(changeset.resource)}.#{changeset.action.name}: #{inspect(stuff)}"
)
end
defp debug_exception(stuff, our_label, nil, changeset) do
Logger.debug(
"#{our_label} - #{inspect(changeset.resource)}.#{changeset.action.name}: #{stuff}"
)
end
defp debug_exception(stuff, our_label, their_label, changeset) do
Logger.debug(
"#{their_label} - #{our_label} - #{inspect(changeset.resource)}.#{changeset.action.name}: #{stuff}"
)
end
end

View file

@ -26,6 +26,7 @@ defmodule Ash.Type.Comparable do
end
defimpl Comparable, for: unquote(lr_type) do
@moduledoc false
def compare(%unquote(lr_type){
left: unquote(left_expression),
right: unquote(right_expression)
@ -48,6 +49,7 @@ defmodule Ash.Type.Comparable do
end
defimpl Comparable, for: unquote(rl_type) do
@moduledoc false
def compare(%unquote(rl_type){
left: unquote(right_expression),
right: unquote(left_expression)

View file

@ -1,7 +1,5 @@
defmodule Mix.Mermaid do
@moduledoc """
Mermaid Diagram helper functions.
"""
@moduledoc false
@doc """
Generate the option string for a mermaid config file if it exists.

308
mix.exs
View file

@ -34,85 +34,87 @@ defmodule Ash.MixProject do
]
end
defp extras do
[
"documentation/tutorials/get-started.md",
"documentation/tutorials/why-ash.md",
"documentation/tutorials/philosophy.md",
"documentation/tutorials/using-hexdocs.md",
"documentation/tutorials/extending-resources.md",
"documentation/how_to/contribute.md",
"documentation/how_to/define-idiomatic-actions.md",
"documentation/how_to/defining-manual-relationships.md",
"documentation/how_to/handle-errors.md",
"documentation/how_to/structure-your-project.md",
"documentation/how_to/upgrade.md",
"documentation/how_to/use-without-data-layers.md",
"documentation/how_to/validate-changes.md",
"documentation/how_to/auto-format-code.md",
"documentation/topics/actions.md",
"documentation/topics/aggregates.md",
"documentation/topics/atomics.md",
"documentation/topics/attributes.md",
"documentation/topics/bulk-actions.md",
"documentation/topics/calculations.md",
"documentation/topics/code-interface.md",
"documentation/topics/constraints.md",
"documentation/topics/development-utilities.md",
"documentation/topics/embedded-resources.md",
"documentation/topics/expressions.md",
"documentation/topics/flows.md",
"documentation/topics/glossary.md",
"documentation/topics/identities.md",
"documentation/topics/managing-relationships.md",
"documentation/topics/manual-actions.md",
"documentation/topics/monitoring.md",
"documentation/topics/multitenancy.md",
"documentation/topics/notifiers.md",
"documentation/topics/pagination.md",
"documentation/topics/phoenix.md",
"documentation/topics/policies.md",
"documentation/topics/pub_sub.md",
"documentation/topics/relationships.md",
"documentation/topics/security.md",
"documentation/topics/store-context-in-process.md",
"documentation/topics/testing.md",
"documentation/topics/timeouts.md",
"documentation/topics/validations.md",
"documentation/dsls/DSL:-Ash.Api.md",
"documentation/dsls/DSL:-Ash.DataLayer.Ets.md",
"documentation/dsls/DSL:-Ash.DataLayer.Mnesia.md",
"documentation/dsls/DSL:-Ash.Flow.md",
"documentation/dsls/DSL:-Ash.Notifier.PubSub.md",
"documentation/dsls/DSL:-Ash.Policy.Authorizer.md",
"documentation/dsls/DSL:-Ash.Registry.md",
"documentation/dsls/DSL:-Ash.Resource.md"
]
end
defp groups_for_extras do
[
Tutorials: [
"documentation/tutorials/get-started.md",
"documentation/tutorials/philosophy.md",
"documentation/tutorials/why-ash.md",
~r'documentation/tutorials'
],
"How To": ~r'documentation/how_to',
Topics: ~r'documentation/topics',
DSLs: ~r'documentation/dsls'
]
end
defp docs do
# The main page in the docs
[
main: "get-started",
source_ref: "v#{@version}",
logo: "logos/small-logo.png",
extra_section: "GUIDES",
extras: extras(),
groups_for_extras: groups_for_extras(),
extras: [
"documentation/tutorials/get-started.md",
"documentation/tutorials/why-ash.md",
"documentation/tutorials/philosophy.md",
"documentation/tutorials/using-hexdocs.md",
"documentation/tutorials/extending-resources.md",
"documentation/how_to/contribute.md",
"documentation/how_to/define-idiomatic-actions.md",
"documentation/how_to/defining-manual-relationships.md",
"documentation/how_to/handle-errors.md",
"documentation/how_to/structure-your-project.md",
"documentation/how_to/upgrade.md",
"documentation/how_to/use-without-data-layers.md",
"documentation/how_to/validate-changes.md",
"documentation/how_to/auto-format-code.md",
"documentation/topics/actions.md",
"documentation/topics/aggregates.md",
"documentation/topics/atomics.md",
"documentation/topics/attributes.md",
"documentation/topics/bulk-actions.md",
"documentation/topics/calculations.md",
"documentation/topics/code-interface.md",
"documentation/topics/constraints.md",
"documentation/topics/development-utilities.md",
"documentation/topics/embedded-resources.md",
"documentation/topics/expressions.md",
"documentation/topics/flows.md",
"documentation/topics/glossary.md",
"documentation/topics/identities.md",
"documentation/topics/managing-relationships.md",
"documentation/topics/manual-actions.md",
"documentation/topics/monitoring.md",
"documentation/topics/multitenancy.md",
"documentation/topics/notifiers.md",
"documentation/topics/pagination.md",
"documentation/topics/phoenix.md",
"documentation/topics/policies.md",
"documentation/topics/pub_sub.md",
"documentation/topics/relationships.md",
"documentation/topics/security.md",
"documentation/topics/store-context-in-process.md",
"documentation/topics/testing.md",
"documentation/topics/timeouts.md",
"documentation/topics/validations.md",
"documentation/dsls/DSL:-Ash.Resource.md",
"documentation/dsls/DSL:-Ash.Api.md",
"documentation/dsls/DSL:-Ash.Notifier.PubSub.md",
"documentation/dsls/DSL:-Ash.Policy.Authorizer.md",
"documentation/dsls/DSL:-Ash.Flow.md",
"documentation/dsls/DSL:-Ash.DataLayer.Ets.md",
"documentation/dsls/DSL:-Ash.DataLayer.Mnesia.md",
"documentation/dsls/DSL:-Ash.Registry.md"
],
groups_for_extras: [
Tutorials: ~r'documentation/tutorials',
"How To": ~r'documentation/how_to',
Topics: ~r'documentation/topics',
DSLs: ~r'documentation/dsls'
],
nest_modules_by_prefix: [
Ash.Error,
Ash.Flow.Transformers,
Ash.Policy.Authorizer,
Ash.Api.Transformers,
Ash.Api.Verifiers,
Ash.Registry.Transformers,
Ash.Resource.Transformers,
Ash.Resource.Verifiers,
Ash.Registry.ResourceValidations,
Ash.Query.Function,
Ash.Query.Operator,
Ash.Resource.Change,
Ash.Resource.Validation,
Ash.Policy.Check
],
before_closing_head_tag: fn type ->
if type == :html do
"""
@ -128,86 +130,8 @@ defmodule Ash.MixProject do
"""
end
end,
spark: [
extensions: [
%{
module: Ash.Resource.Dsl,
name: "Resource",
target: "Ash.Resource",
type: "Resource",
default_for_target?: true
},
%{
module: Ash.Api.Dsl,
name: "Api",
target: "Ash.Api",
type: "Api",
default_for_target?: true
},
%{
module: Ash.DataLayer.Ets,
name: "Ets",
target: "Ash.Resource",
type: "DataLayer"
},
%{
module: Ash.DataLayer.Mnesia,
name: "Mnesia",
target: "Ash.Resource",
type: "DataLayer"
},
%{
module: Ash.Policy.Authorizer,
name: "Policy Authorizer",
target: "Ash.Resource",
type: "Authorizer"
},
%{
module: Ash.Flow.Dsl,
name: "Flow",
target: "Ash.Flow",
type: "Flow",
default_for_target?: true
},
%{
module: Ash.Notifier.PubSub,
name: "PubSub",
target: "Ash.Resource",
type: "Notifier"
},
%{
module: Ash.Registry.Dsl,
name: "Registry",
target: "Ash.Registry",
type: "Registry",
default_for_target?: true
},
%{
module: Ash.Registry.ResourceValidations,
name: "Resource Validations",
type: "Extension",
target: "Ash.Registry"
}
],
mix_tasks: [
Charts: [
Mix.Tasks.Ash.GenerateFlowCharts
]
]
],
groups_for_modules: [
Extensions: [
Ash.Api,
Ash.Resource,
Ash.DataLayer.Ets,
Ash.DataLayer.Mnesia,
Ash.DataLayer.Simple,
Ash.Notifier.PubSub,
Ash.Policy.Authorizer,
Ash.Registry
],
Resources: [
Ash.Api,
Ash.Filter.TemplateHelpers,
Ash.Calculation,
Ash.Resource.Calculation.Builtins,
@ -222,6 +146,14 @@ defmodule Ash.MixProject do
Ash.Resource.ManualRelationship,
Ash.Resource.Attribute.Helpers
],
"Action Input & Interface": [
Ash,
Ash.Api,
Ash.Query,
Ash.Changeset,
Ash.ActionInput,
Ash.BulkResult
],
Queries: [
Ash.Query,
Ash.Resource.Preparation,
@ -244,6 +176,15 @@ defmodule Ash.MixProject do
Ash.Policy.FilterCheckWithContext,
Ash.Policy.SimpleCheck
],
Extensions: [
Ash.Resource,
Ash.DataLayer.Ets,
Ash.DataLayer.Mnesia,
Ash.DataLayer.Simple,
Ash.Notifier.PubSub,
Ash.Policy.Authorizer,
Ash.Registry
],
Introspection: [
Ash.Api.Info,
Ash.Registry.Info,
@ -252,10 +193,10 @@ defmodule Ash.MixProject do
Ash.Policy.Info,
Ash.DataLayer.Ets.Info,
Ash.DataLayer.Mnesia.Info,
Ash.Notifier.PubSub.Info
Ash.Notifier.PubSub.Info,
~r/Ash.Api.Dsl.*/
],
Utilities: [
Ash,
Ash.Expr,
Ash.Page,
Ash.Page.Keyset,
@ -264,24 +205,41 @@ defmodule Ash.MixProject do
Ash.Filter.Runtime,
Ash.Sort,
Ash.CiString,
Ash.Vector,
Ash.Union,
Ash.UUID,
Ash.NotLoaded,
Ash.ForbiddenField,
Ash.NotSelected,
Ash.Changeset.ManagedRelationshipHelpers,
Ash.DataLayer.Simple,
Ash.Filter.Simple,
Ash.Filter.Simple.Not,
Ash.OptionsHelpers,
Ash.Resource.Builder,
Ash.Tracer
Ash.ProcessHelpers,
Ash.Mix.Tasks.Helpers,
Ash.PlugHelpers,
Ash.SatSolver
],
Visualizations: [
Ash.Api.Info.Diagram,
Ash.Api.Info.Livebook,
Ash.Policy.Chart.Mermaid
],
Testing: [
Ash.Generator,
Ash.Seed,
Ash.Test
],
Tracing: [
Ash.Tracer,
Ash.Tracer.Simple,
Ash.Tracer.Simple.Span
],
Flow: [
Ash.Flow,
Ash.Flow.Result,
Ash.Flow.Executor,
Ash.Flow.Step,
Ash.Flow.Chart.Mermaid,
@ -295,8 +253,46 @@ defmodule Ash.MixProject do
Ash.Error,
~r/Ash.Error\./
],
Transformers: [~r/\.Transformers\./, Ash.Registry.ResourceValidations],
Internals: ~r/.*/
"DSL Transformers": [
~r/\.Transformers\./,
~r/\.Verifiers\./,
Ash.Registry.ResourceValidations
],
Expressions: [
Ash.Filter.Predicate,
Ash.Filter.Simple,
Ash.Filter.Simple.Not,
~r/Ash.Query.Operator/,
~r/Ash.Query.Function/,
~r/Ash.Query.Ref/,
Ash.Query.Not,
Ash.Query.Call,
Ash.Query.BooleanExpression,
Ash.Query.Exists,
Ash.Query.Parent
],
Builtins: [
~r/Ash.Resource.Validation/,
~r/Ash.Resource.Change/,
~r/Ash.Policy.Check/
],
Introspection: [
~r/Ash.Resource.Relationships/,
~r/Ash.Resource.Calculation/,
~r/Ash.Resource.Interface/,
~r/Ash.Resource.Identity/,
~r/Ash.Resource.Attribute/,
~r/Ash.Resource.Attribute/,
~r/Ash.Resource.Aggregate/,
~r/Ash.Resource.Actions/,
~r/Ash.Flow.Step/,
~r/Ash.Flow/,
Ash.Mix.Tasks.Helpers,
Ash.Policy.FieldPolicy,
~r/Ash.Registry/,
Ash.Policy.Policy,
Ash.Notifier.PubSub.Publication
]
]
]
end

View file

@ -8,13 +8,13 @@
"dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"},
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
"earmark": {:hex, :earmark, "1.4.40", "ff1a0f8bf3b298113c2a257c4e7a8b29ba9db5d35f5ee6d29291cb8caa09a071", [:mix], [{:earmark_parser, "~> 1.4.35", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "5fb622d5e36046bc313a426211e8bf769ba50db7720744859a21932c6470d75c"},
"earmark_parser": {:hex, :earmark_parser, "1.4.35", "437773ca9384edf69830e26e9e7b2e0d22d2596c4a6b17094a3b29f01ea65bb8", [:mix], [], "hexpm", "8652ba3cb85608d0d7aa2d21b45c6fad4ddc9a1f9a1f1b30ca3a246f0acc33f6"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"ex_check": {:hex, :ex_check, "0.15.0", "074b94c02de11c37bba1ca82ae5cc4926e6ccee862e57a485b6ba60fca2d8dc1", [:mix], [], "hexpm", "33848031a0c7e4209c3b4369ce154019788b5219956220c35ca5474299fb6a0e"},
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "7a53ce9da286d06dd3c190a7a7104e2e47707400", []},
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "84c2037f67e07982544bfaa90b8a3471866a20aa", []},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.6.0", "e0791ee1cf5db03f2c61b7ebd70e2e95cba2bb9b9793011f26609f22c0900087", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b98fca849b18aaf490f4ac7d1dd8c6c469b0cc3e6632562d366cab095e666ffe"},