mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
docs: revamp hexdocs
fix: use current read action for counting
This commit is contained in:
parent
6c5cd551ff
commit
072486bebd
39 changed files with 583 additions and 641 deletions
|
@ -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"
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
260
lib/ash.ex
260
lib/ash.ex
|
@ -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, %{}) || %{}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__{}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
77
lib/ash/process_helpers.ex
Normal file
77
lib/ash/process_helpers.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
60
lib/ash/resource/change/debug_log.ex
Normal file
60
lib/ash/resource/change/debug_log.ex
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -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
308
mix.exs
|
@ -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
|
||||
|
|
4
mix.lock
4
mix.lock
|
@ -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"},
|
||||
|
|
Loading…
Reference in a new issue