Packages

ash

3.5.38
3.29.3 3.29.2 3.29.1 3.29.0 3.28.0 3.27.8 3.27.7 3.27.6 3.27.5 3.27.4 3.27.3 3.27.2 3.27.1 3.27.0 3.26.0 3.25.2 3.25.1 3.25.0 retired 3.24.7 3.24.6 3.24.5 3.24.4 3.24.3 3.24.2 3.24.1 3.24.0 3.23.1 3.23.0 3.22.2 3.22.1 3.22.0 3.21.3 3.21.2 3.21.1 3.21.0 3.20.0 3.19.3 3.19.2 3.19.1 3.19.0 3.18.0 3.17.1 3.17.0 3.16.0 3.15.0 3.14.1 3.14.0 retired 3.13.2 3.13.1 3.13.0 3.12.0 3.11.3 3.11.2 3.11.1 3.11.0 3.10.1 3.10.0 3.9.0 3.8.0 3.7.6 3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 retired 3.6.3 retired 3.6.2 3.6.1 3.6.0 3.5.43 3.5.42 3.5.41 3.5.40 3.5.39 3.5.38 3.5.37 3.5.36 3.5.35 3.5.34 3.5.33 3.5.32 3.5.31 3.5.30 3.5.29 3.5.28 3.5.27 3.5.26 3.5.25 3.5.24 3.5.23 3.5.22 3.5.21 3.5.20 3.5.19 3.5.18 3.5.17 3.5.16 3.5.15 3.5.14 3.5.13 3.5.12 3.5.11 3.5.10 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.74 retired 3.4.73 3.4.72 3.4.71 3.4.70 3.4.69 3.4.68 3.4.67 3.4.66 3.4.65 3.4.64 3.4.63 3.4.62 3.4.61 3.4.60 3.4.59 3.4.58 3.4.57 3.4.56 3.4.55 3.4.54 3.4.53 3.4.52 3.4.51 3.4.50 3.4.49 3.4.48 3.4.47 3.4.46 3.4.45 3.4.44 3.4.43 3.4.42 3.4.41 3.4.40 3.4.39 3.4.38 3.4.37 3.4.36 3.4.35 3.4.34 3.4.33 3.4.32 3.4.31 3.4.30 3.4.29 3.4.28 3.4.27 3.4.26 3.4.25 3.4.24 3.4.23 3.4.22 3.4.21 3.4.20 3.4.19 3.4.18 3.4.17 3.4.16 3.4.15 3.4.14 3.4.13 3.4.12 3.4.11 3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.3 3.3.2 3.3.1 3.3.0 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.16 3.0.15 3.0.14 3.0.13 3.0.12 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 3.0.0-rc.46 3.0.0-rc.45 3.0.0-rc.44 3.0.0-rc.43 3.0.0-rc.42 3.0.0-rc.41 3.0.0-rc.40 3.0.0-rc.39 3.0.0-rc.38 3.0.0-rc.37 3.0.0-rc.36 3.0.0-rc.35 3.0.0-rc.34 3.0.0-rc.33 3.0.0-rc.32 3.0.0-rc.31 3.0.0-rc.29 3.0.0-rc.28 3.0.0-rc.27 3.0.0-rc.26 3.0.0-rc.25 3.0.0-rc.24 3.0.0-rc.23 3.0.0-rc.22 3.0.0-rc.21 3.0.0-rc.20 3.0.0-rc.19 3.0.0-rc.18 3.0.0-rc.17 3.0.0-rc.16 3.0.0-rc.15 3.0.0-rc.14 3.0.0-rc.13 3.0.0-rc.12 3.0.0-rc.11 3.0.0-rc.10 3.0.0-rc.9 3.0.0-rc.8 3.0.0-rc.7 3.0.0-rc.6 3.0.0-rc.5 3.0.0-rc.4 3.0.0-rc.3 3.0.0-rc.1 3.0.0-rc.0 2.21.15 2.21.14 2.21.13 2.21.12 2.21.11 2.21.10 2.21.9 2.21.8 2.21.7 2.21.6 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.3 2.20.2 2.20.1 2.20.0 2.19.14 2.19.13 2.19.12 2.19.11 2.19.10 2.19.9 2.19.8 2.19.7 2.19.6 2.19.5 2.19.4 2.19.3 retired 2.19.2 retired 2.19.1 retired 2.19.0 retired 2.18.2 2.18.1 2.18.0 2.17.24 2.17.23 2.17.22 2.17.21 2.17.20 2.17.19 2.17.18 2.17.17 2.17.16 2.17.15 2.17.14 2.17.13 2.17.12 2.17.11 2.17.10 2.17.9 2.17.8 2.17.7 2.17.6 2.17.5 2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.1 2.16.0 2.15.20 2.15.19 2.15.18 2.15.17 2.15.16 2.15.15 2.15.14 2.15.13 2.15.12 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.2 2.15.1 2.15.0 2.14.21 2.14.20 2.14.19 2.14.18 2.14.17 2.14.16 2.14.15 2.14.14 2.14.13 2.14.12 2.14.11 2.14.10 2.14.9 2.14.8 2.14.7 2.14.6 2.14.5 2.14.4 2.14.3 2.14.2 2.14.1 2.14.0 2.13.4 retired 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.11 2.11.10 2.11.9 2.11.8 2.11.7 2.11.6 2.11.5 2.11.4 2.11.3 2.11.2 2.11.1 2.11.0 2.11.0-rc.3 2.11.0-rc.2 2.11.0-rc.1 2.11.0-rc.0 2.10.2 2.10.1 2.10.0 2.9.29 2.9.28 2.9.27 2.9.26 2.9.25 2.9.24 2.9.23 2.9.22 2.9.21 2.9.20 2.9.19 2.9.18 2.9.17 2.9.16 2.9.15 2.9.14 2.9.13 2.9.12 2.9.11 2.9.10 2.9.9 2.9.8 2.9.7 2.9.6 2.9.5 2.9.4 2.9.3 2.9.2 2.9.1 2.9.0 2.8.1 2.8.0 2.7.1 2.7.0 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.5.0-rc.6 2.5.0-rc.5 2.5.0-rc.4 2.5.0-rc.3 2.5.0-rc.2 2.5.0-rc.1 2.5.0-rc.0 2.4.30 2.4.29 2.4.28 2.4.27 2.4.26 2.4.25 2.4.24 2.4.23 2.4.22 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 2.0.0-pre.8 2.0.0-pre.7 2.0.0-pre.6 2.0.0-pre.5 2.0.0-pre.4 2.0.0-pre.3 2.0.0-pre.2 2.0.0-pre.1 2.0.0-pre.0 1.53.3 1.53.2 1.53.0 1.52.0-rc.22 1.52.0-rc.21 1.52.0-rc.20 1.52.0-rc.19 1.52.0-rc.18 1.52.0-rc.17 1.52.0-rc.16 1.52.0-rc.15 1.52.0-rc.14 1.52.0-rc.13 1.52.0-rc.12 1.52.0-rc.11 1.52.0-rc.10 1.52.0-rc.9 1.52.0-rc.8 1.52.0-rc.7 1.52.0-rc.6 1.52.0-rc.5 1.52.0-rc.4 1.52.0-rc.3 1.52.0-rc.2 1.52.0-rc.1 1.52.0-rc.0 1.51.2 1.51.1 retired 1.51.0 1.50.21 1.50.20 1.50.19 1.50.18 1.50.17 1.50.16 1.50.15 1.50.14 1.50.13 1.50.12 1.50.11 1.50.10 1.50.9 1.50.8 1.50.7 1.50.6 1.50.5 1.50.4 1.50.3 1.50.2 1.50.1 1.50.0 1.49.0 1.48.0-rc.30 1.48.0-rc.29 1.48.0-rc.28 1.48.0-rc.27 1.48.0-rc.26 1.48.0-rc.25 1.48.0-rc.24 1.48.0-rc.23 1.48.0-rc.22 1.48.0-rc.21 1.48.0-rc.20 1.48.0-rc.19 1.48.0-rc.18 1.48.0-rc.17 1.48.0-rc.16 1.48.0-rc.15 1.48.0-rc.14 1.48.0-rc.13 1.48.0-rc.12 1.48.0-rc.11 1.48.0-rc.10 1.48.0-rc.9 1.48.0-rc.8 1.48.0-rc.7 1.48.0-rc.6 1.48.0-rc.5 1.48.0-rc.4 1.48.0-rc.3 1.48.0-rc.2 1.48.0-rc.1 1.48.0-rc.0 1.47.12 1.47.11 1.47.10 1.47.9 1.47.8 1.47.7 1.47.6 1.47.5 1.47.4 1.47.3 1.47.2 1.47.1 1.47.0 1.46.13 1.46.12 1.46.11 1.46.10 1.46.9 1.46.8 1.46.7 1.46.6 1.46.5 1.46.4 1.46.3 1.46.2 1.46.1 1.46.0 1.45.0-rc9 1.45.0-rc8 1.45.0-rc7 1.45.0-rc6 1.45.0-rc5 1.45.0-rc4 1.45.0-rc3 1.45.0-rc20 1.45.0-rc2 1.45.0-rc19 1.45.0-rc18 1.45.0-rc17 1.45.0-rc16 1.45.0-rc15 1.45.0-rc14 1.45.0-rc13 1.45.0-rc12 1.45.0-rc11 1.45.0-rc10 1.45.0-rc1 1.45.0-rc0 1.44.13 1.44.12 1.44.11 1.44.10 1.44.9 1.44.8 1.44.7 1.44.6 1.44.5 1.44.4 1.44.3 1.44.2 1.44.1 1.44.0 1.43.12 1.43.11 1.43.10 1.43.9 1.43.8 1.43.7 1.43.6 1.43.5 1.43.4 1.43.3 1.43.2 1.43.1 1.43.0 1.42.0 1.41.12 1.41.11 1.41.10 1.41.9 1.41.8 1.41.7 1.41.6 1.41.5 1.41.4 1.41.3 1.41.2 1.41.1 1.41.0 1.40.0 1.39.7 1.39.6 1.39.5 1.39.4 1.39.3 1.39.2 1.39.1 1.39.0 1.38.0 1.37.2 1.37.1 1.37.0 1.36.22 1.36.21 1.36.19 1.36.18 1.36.17 1.36.16 1.36.15 1.36.14 1.36.13 1.36.12 1.36.11 1.36.10 1.36.9 1.36.8 1.36.7 1.36.6 1.36.5 1.36.4 1.36.3 1.36.2 1.36.0 1.35.1 1.35.0 1.34.9 1.34.8 1.34.7 1.34.6 1.34.5 1.34.4 1.34.3 1.34.2 1.34.1 1.34.0 1.33.0 1.32.2 1.32.1 1.32.0 1.31.1 1.31.0 1.30.2 1.30.1 1.29.0-rc1 1.29.0-rc0 1.28.1 1.28.0 1.27.1 1.27.0 1.26.13 1.26.12 1.26.11 1.26.10 1.26.9 1.26.8 1.26.7 1.26.6 1.26.5 1.26.4 1.26.2 1.26.1 1.26.0 1.25.8 1.25.7 1.25.6 1.25.5 1.25.4 1.25.3 1.25.2 1.25.1 1.25.0 1.24.2 1.24.1 1.24.0 1.23.3 1.23.2 1.23.1 1.23.0 1.22.1 1.22.0 1.20.1 1.20.0 1.19.1 1.19.0 1.18.1 1.18.0 1.17.1 1.17.0 1.16.2 1.15.1 1.15.0 1.14.0 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.12.0 1.11.1 1.11.0 1.10.0 1.9.0 1.8.0 1.7.0 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.1 1.5.0 1.4.1 1.4.0 1.3.1 1.3.0 1.2.1 1.2.0 1.1.3 1.1.2 1.1.0 1.0.3 1.0.2 1.0.1 1.0.0 0.13.1 0.13.0 0.12.0 0.10.0 0.9.1 0.9.0 0.8.0 0.7.0 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.2 0.5.1 0.5.0 0.4.0 0.3.0 0.2.0 0.1.9 0.1.8 0.1.3 0.1.1 0.1.0

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash resource.ex
Raw

lib/ash/resource.ex

defmodule Ash.Resource do
@moduledoc """
A resource is a static definition of an entity in your system.
[Resource DSL documentation](dsl-ash-resource.html)
"""
@type t :: module
@type record :: struct()
use Spark.Dsl,
single_extension_kinds: [:data_layer],
many_extension_kinds: [
:authorizers,
:notifiers
],
default_extensions: [
data_layer: Ash.DataLayer.Simple,
extensions: [Ash.Resource.Dsl]
],
extension_kind_types: [
authorizers: {:wrap_list, {:behaviour, Ash.Authorizer}},
data_layer: {:behaviour, Ash.DataLayer},
notifiers: {:wrap_list, {:behaviour, Ash.Notifier}}
],
opt_schema: [
simple_notifiers: [
type: {:list, {:behaviour, Ash.Notifier}},
doc: "Notifiers with no DSL."
],
validate_domain_inclusion?: [
type: :boolean,
doc: "Whether or not to validate that this resource is included in a domain.",
default: true
],
primary_read_warning?: [
type: :boolean,
doc:
"Set to `false` to silence warnings about arguments, preparations and filters on the primary read action.",
default: true
],
domain: [
type: {:behaviour, Ash.Domain},
doc:
"The domain to use when interacting with this resource. Also sets defaults for various options that ask for a domain."
],
embed_nil_values?: [
type: :boolean,
default: true,
doc:
"Whether or not to include keys with `nil` values in an embedded representation. Has no effect unless resource is an embedded resource."
]
]
@doc false
@impl Spark.Dsl
def init(opts) do
if opts[:data_layer] == :embedded do
{:ok,
opts
|> Keyword.put(:data_layer, Ash.DataLayer.Simple)
|> Keyword.put(:embedded?, true)}
else
{:ok, opts}
end
end
@impl true
def verify(module, opts) do
if Application.get_env(:ash, :validate_domain_resource_inclusion?, true) &&
Keyword.get(opts, :validate_domain_inclusion?, true) &&
!Ash.Resource.Info.embedded?(module) &&
Code.ensure_loaded?(Mix.Project) do
otp_app = Mix.Project.config()[:app]
domains =
Application.get_env(otp_app, :ash_domains, [])
domains =
if domain = Ash.Resource.Info.domain(module) do
[domain | domains]
else
domains
end
contained_in_domain =
if is_nil(domain) || Ash.Domain.Info.allow_unregistered?(domain) do
true
else
domains
|> Enum.flat_map(&Ash.Domain.Info.resources/1)
|> Enum.any?(&(&1 == module))
end
if !contained_in_domain do
IO.warn("""
Resource #{inspect(module)} is not present in any known Ash.Domain module.
Domain modules checked: #{inspect(domains)}
We check the following configuration for domain modules:
config :#{otp_app}, ash_domains: #{inspect(domains)}
To resolve this warning, do one of the following.
1. Add the resource to one of your configured domain modules.
2. Add the option `validate_domain_inclusion?: false` to `use Ash.Resource`
3. Configure all resources not to warn, with `config :ash, :validate_domain_resource_inclusion?, false`
""")
end
end
end
@doc false
@impl Spark.Dsl
def handle_opts(opts) do
quote bind_quoted: [
opts: opts,
embedded?: opts[:embedded?],
domain: opts[:domain],
has_domain?: Keyword.has_key?(opts, :domain),
embed_nil_values?: opts[:embed_nil_values?]
] do
@persist {:simple_notifiers, List.wrap(opts[:simple_notifiers])}
@persist {:primary_read_warning?, Keyword.get(opts, :primary_read_warning?, true)}
import Ash.Resource, only: [timestamps: 0, timestamps: 1]
require Ash.Query
if !(embedded? || has_domain?) do
IO.warn("""
Configuration Error:
`domain` option missing for #{inspect(__MODULE__)}
If you wish to make a resource compatible with multiple domains, set the domain to `nil` explicitly.
Example configuration:
use Ash.Resource, #{String.trim_trailing(String.trim_leading(inspect([{:domain, YourDomain} | opts], pretty: true), "["), "]")}
""")
end
if domain do
@persist {:domain, domain}
end
if embedded? do
@persist {:embedded?, true}
require Ash.EmbeddableType
Ash.EmbeddableType.define_embeddable_type(embed_nil_values?: embed_nil_values?)
end
end
end
@doc """
Defines create and update timestamp attributes.
Shorthand for `d:Ash.Resource.Dsl.attributes.create_timestamp` and
`d:Ash.Resource.Dsl.attributes.update_timestamp` with the attribute names
`:inserted_at` and `:updated_at` respectively. Any options passed to this
helper are passed to both timestamp macros.
"""
defmacro timestamps(opts \\ []) do
quote do
create_timestamp :inserted_at, unquote(opts)
update_timestamp :updated_at, unquote(opts)
end
end
@doc false
# sobelow_skip ["DOS.StringToAtom"]
@impl Spark.Dsl
def handle_before_compile(_opts) do
quote do
require Ash.Schema
Ash.Schema.define_schema()
if Ash.Schema.define?(__MODULE__) do
module = __MODULE__
defimpl Inspect do
import Inspect.Algebra, except: [color_doc: 3]
inspect_private_fields? = Ash.Resource.Info.inspect_private_fields?(module)
hide_inspect_fields = Ash.Resource.Info.hide_inspect_fields(module)
show_inspect_fields = Ash.Resource.Info.show_inspect_fields(module)
@show_fields module
|> Ash.Resource.Info.fields()
|> then(fn fields ->
if is_list(show_inspect_fields) do
Enum.filter(fields, &(&1.name in show_inspect_fields))
else
fields
end
end)
|> Enum.reject(&Map.get(&1, :sensitive?))
|> then(fn fields ->
if inspect_private_fields? do
fields
else
Enum.reject(
fields,
&(&1.private? ||
(is_list(show_inspect_fields) && &1.name in show_inspect_fields))
)
end
end)
|> Enum.reject(&(&1.name in hide_inspect_fields))
|> Enum.map(& &1.name)
|> Enum.concat([:__meta__, :calculations, :aggregates])
def inspect(record, opts) do
record = %{
record
| calculations:
Ash.Actions.Read.Calculations.map_without_calc_deps(record.calculations)
}
subtract_fields =
Enum.filter([:calculations, :aggregates], &(Map.get(record, &1) == %{}))
fields = @show_fields -- subtract_fields
fun = fn field, opts ->
Inspect.List.keyword({field, Map.get(record, field)}, opts)
end
open =
color_doc("%" <> inspect(record.__struct__) <> "{", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc("}", :map, opts)
Inspect.Algebra.container_doc(open, fields, close, opts, fun,
separator: sep,
break: :strict
)
end
# this was added in Elixir 1.18, so we vendored it here to be compatible w/ older elixir versions
def color_doc(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors})
when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat({:doc_color, doc, ansi(precolor)}, {:doc_color, empty(), ansi(postcolor)})
else
doc
end
end
defp ansi(color) do
color
|> IO.ANSI.format_fragment(true)
|> IO.iodata_to_binary()
end
end
end
@all_arguments __MODULE__
|> Ash.Resource.Info.actions()
|> Enum.flat_map(& &1.arguments)
|> Enum.map(& &1.name)
|> Enum.uniq()
@arguments_by_action __MODULE__
|> Ash.Resource.Info.actions()
|> Map.new(fn action ->
{action.name, Enum.map(action.arguments, & &1.name)}
end)
@all_attributes __MODULE__
|> Ash.Resource.Info.public_attributes()
|> Enum.map(& &1.name)
|> Enum.uniq()
if AshPolicyAuthorizer.Authorizer in @extensions do
raise """
AshPolicyAuthorizer has been deprecated and is now built into Ash core.
To use it, replace `authorizers: [AshPolicyAuthorizer.Authorizer]` with `authorizers: [Ash.Policy.Authorizer]`
"""
end
if Ash.Resource.Info.define_interface?(__MODULE__) do
if domain =
Ash.Resource.Info.code_interface_domain(__MODULE__) ||
Ash.Resource.Info.domain(__MODULE__) do
if domain == __MODULE__ do
raise "code_interface.domain should be set to a Domain module, not the resource."
end
require Ash.CodeInterface
Ash.CodeInterface.define_interface(domain, __MODULE__)
end
end
@default_short_name __MODULE__
|> Module.split()
|> List.last()
|> Macro.underscore()
|> String.to_atom()
def default_short_name do
@default_short_name
end
@primary_key_with_types __MODULE__
|> Ash.Resource.Info.attributes()
|> Enum.filter(& &1.primary_key?)
|> Enum.map(&{&1.name, &1.type})
@primary_key @primary_key_with_types |> Enum.map(&elem(&1, 0))
if !Enum.empty?(@primary_key) do
if Ash.Resource.Info.primary_key_simple_equality?(__MODULE__) do
def primary_key_matches?(left, right) do
left_taken = Map.take(left, @primary_key)
left_taken == Map.take(right, @primary_key) && Enum.all?(Map.values(left_taken))
end
else
case @primary_key_with_types do
[{field, type}] ->
@pkey_field field
@pkey_type type
def primary_key_matches?(left, right) when not is_nil(left) and not is_nil(right) do
Ash.Type.equal?(
@pkey_type,
Map.fetch!(left, @pkey_field),
Map.fetch!(right, @pkey_field)
)
end
def primary_key_matches?(_left, _right), do: false
_ ->
def primary_key_matches?(left, right) do
Enum.all?(@primary_key_with_types, fn {name, type} ->
with {:ok, left_value} when not is_nil(left_value) <- Map.fetch(left, name),
{:ok, right_value} when not is_nil(right_value) <- Map.fetch(right, name) do
Ash.Type.equal?(type, left_value, right_value)
else
_ ->
false
end
end)
end
end
end
end
@doc """
Validates that the keys in the provided input are valid for at least one action on the resource.
Raises a KeyError error at compile time if not. This exists because generally a struct should only ever
be created by Ash as a result of a successful action. You should not be creating records manually in code,
e.g `%MyResource{value: 1, value: 2}`. Generally that is fine, but often with embedded resources it is nice
to be able to validate the keys that are being provided, e.g
```elixir
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
```
"""
@spec input(values :: map | Keyword.t()) :: map | no_return
def input(opts) do
Map.new(opts, fn {key, value} ->
if key in @all_arguments || key in @all_attributes do
{key, value}
else
raise KeyError, key: key
end
end)
end
@doc """
Same as `input/1`, except restricts the keys to values accepted by the action provided.
"""
@spec input(values :: map | Keyword.t(), action :: atom) :: map | no_return
def input(opts, action) do
case Map.fetch(@arguments_by_action, action) do
:error ->
raise ArgumentError, message: "No such action #{inspect(action)}"
{:ok, args} ->
action = Ash.Resource.Info.action(__MODULE__, action)
Map.new(opts, fn {key, value} ->
if key in action.accept do
{key, value}
else
raise KeyError, key: key
end
end)
end
end
end
end
@spec set_metadata(Ash.Resource.record(), map) :: Ash.Resource.record()
def set_metadata(record, map) do
%{record | __metadata__: Ash.Helpers.deep_merge_maps(record.__metadata__, map)}
end
@doc false
def set_meta(%{__meta__: _} = struct, meta) do
%{struct | __meta__: meta}
end
def set_meta(struct, _), do: struct
@spec put_metadata(Ash.Resource.record(), atom, term) :: Ash.Resource.record()
def put_metadata(record, key, term) do
set_metadata(record, %{key => term})
end
@doc "Sets a list of loaded key or paths to a key back to their original unloaded stated"
@spec unload_many(
nil | list(Ash.Resource.record()) | Ash.Resource.record() | Ash.Page.page(),
list(atom) | list(list(atom))
) ::
nil | list(Ash.Resource.record()) | Ash.Resource.record() | Ash.Page.page()
def unload_many(data, paths) do
Enum.reduce(paths, data, &unload(&2, &1))
end
@doc "Sets a loaded key or path to a key back to its original unloaded stated"
@spec unload(
nil | list(Ash.Resource.record()) | Ash.Resource.record() | Ash.Page.page(),
atom | list(atom)
) ::
nil | list(Ash.Resource.record()) | Ash.Resource.record() | Ash.Page.page()
def unload(nil, _), do: nil
def unload(%struct{results: results} = page, path)
when struct in [Ash.Page.Keyset, Ash.Page.Offset] do
%{page | results: unload(results, path)}
end
def unload(records, path) when is_list(records) do
Enum.map(records, &unload(&1, path))
end
def unload(record, [path]) do
unload(record, path)
end
def unload(record, [key | rest]) do
Map.update!(record, key, &unload(&1, rest))
end
def unload(%struct{} = record, key) when is_atom(key) do
Map.put(record, key, Map.get(struct.__struct__(), key))
end
def unload(other, _), do: other
@doc """
Returns true if the load or path to load has been loaded
## Options
- `lists`: set to `:any` to have this return true if any record in a list that appears has the value loaded. Default is `:all`.
- `unknown`: set to `true` to have unknown paths (like nil values or non-resources) return true. Defaults to `false`
- `strict?`: set to `true` to return false if a calculation with arguments is being checked
"""
@spec loaded?(
nil | list(Ash.Resource.record()) | Ash.Resource.record() | Ash.Page.page(),
atom | Ash.Query.Calculation.t() | Ash.Query.Aggregate.t() | list(atom),
opts :: Keyword.t()
) ::
boolean
def loaded?(data, path, opts \\ [])
def loaded?(records, path, opts) when not is_list(path) do
loaded?(records, List.wrap(path), opts)
end
def loaded?(%Ash.NotLoaded{}, _, _opts), do: false
def loaded?(_, [], _opts), do: true
# We actually just can't tell here, so we say no
def loaded?(nil, _, opts), do: Keyword.get(opts, :unknown, false)
def loaded?(%page{results: results}, path, opts)
when page in [Ash.Page.Keyset, Ash.Page.Offset] do
loaded?(results, path, opts)
end
def loaded?(records, path, opts) when is_list(records) do
case Keyword.get(opts, :lists, :all) do
:all ->
Enum.all?(records, &loaded?(&1, path, opts))
:any ->
Enum.any?(records, &loaded?(&1, path, opts))
end
end
def loaded?(%resource{} = record, [%Ash.Query.Calculation{} = calculation | rest], opts) do
if calculation.calc_name do
ignored_via_strict? =
if opts[:strict?] do
resource_calculation = Ash.Resource.Info.calculation(resource, calculation.calc_name)
resource_calculation && Enum.any?(resource_calculation.arguments)
else
false
end
# we can't say for sure if the original arguments provided
# were the same as these, so this is always false
if ignored_via_strict? do
false
else
if calculation.load do
loaded_on_type?(
Map.get(record, calculation.load),
rest,
calculation.type,
calculation.constraints,
opts
)
else
case Map.fetch(record.calculations, calculation.name) do
{:ok, value} ->
loaded_on_type?(
value,
rest,
calculation.type,
calculation.constraints,
opts
)
:error ->
false
end
end
end
else
if calculation.load do
loaded_on_type?(
Map.get(record, calculation.load),
rest,
calculation.type,
calculation.constraints,
opts
)
else
case Map.fetch(record.calculations, calculation.name) do
{:ok, value} ->
loaded_on_type?(
value,
rest,
calculation.type,
calculation.constraints,
opts
)
:error ->
false
end
end
end
end
def loaded?(record, [%Ash.Query.Aggregate{} = aggregate | rest], opts) do
if aggregate.load do
loaded_on_type?(
Map.get(record, aggregate.load),
rest,
aggregate.type,
aggregate.constraints,
opts
)
else
case Map.fetch(record.aggregates, aggregate.name) do
{:ok, value} ->
loaded_on_type?(
value,
rest,
aggregate.type,
aggregate.constraints,
opts
)
_ ->
false
end
end
end
def loaded?(record, [%Ash.Resource.Aggregate{} = aggregate | rest], opts) do
loaded_on_type?(
Map.get(record, aggregate.name),
rest,
aggregate.type,
aggregate.constraints,
opts
)
end
def loaded?(record, [%Ash.Resource.Calculation{} = resource_calculation | rest], opts) do
if opts[:strict?] && Enum.any?(resource_calculation.arguments) do
false
else
loaded_on_type?(
Map.get(record, resource_calculation.name),
rest,
resource_calculation.type,
resource_calculation.constraints,
opts
)
end
end
def loaded?(record, [%Ash.Resource.Attribute{} = attribute | rest], opts) do
selected?(record, attribute.name) &&
loaded_on_type?(
Map.get(record, attribute.name),
rest,
attribute.type,
attribute.constraints,
opts
)
end
def loaded?(%resource{} = record, [key | rest], opts) when is_atom(key) and not is_nil(key) do
loaded?(record, [Ash.Resource.Info.field(resource, key) | rest], opts)
end
def loaded?(record, [%rel{name: name} | rest], opts)
when rel in [
Ash.Resource.Relationships.HasOne,
Ash.Resource.Relationships.HasMany,
Ash.Resource.Relationships.BelongsTo,
Ash.Resource.Relationships.ManyToMany
] do
record
|> Map.get(name)
|> loaded?(rest, opts)
end
def loaded?(_, _, opts), do: Keyword.get(opts, :unknown, false)
defp loaded_on_type?(
%Ash.NotLoaded{},
_rest,
_type,
_constraints,
_opts
) do
false
end
defp loaded_on_type?(
_,
[],
_type,
_constraints,
_opts
) do
true
end
defp loaded_on_type?(empty, _, _, _, opts) when empty in [[], nil] do
Keyword.get(opts, :unknown, false)
end
defp loaded_on_type?(value, path, type, constraints, opts) do
Ash.Type.loaded?(type, value, path, constraints, opts)
end
@spec get_metadata(Ash.Resource.record(), atom | list(atom)) :: term
def get_metadata(record, key_or_path) do
get_in(record.__metadata__ || %{}, List.wrap(key_or_path))
end
@doc """
Returns true if the given field has been selected on a record
## Options
- `forbidden_means_selected?`: set to `true` to return `true` if the field is marked as forbidden
"""
@spec selected?(Ash.Resource.record(), atom) :: boolean
def selected?(record, field, opts \\ []) do
case Map.get(record, field) do
%Ash.NotLoaded{} -> false
%Ash.ForbiddenField{} -> Keyword.get(opts, :forbidden_means_selected?, false)
_ -> true
end
end
@doc false
def reserved_names do
[
:__struct__,
:__meta__,
:__metadata__,
:__order__,
:__lateral_join_source__,
:*,
:calculations,
:aggregates,
:relationships,
:as
]
end
@impl Spark.Dsl
def explain(dsl_state, _) do
Ash.Resource.Info.description(dsl_state)
end
end