defmodule Ash.Resource do @moduledoc """ A resource is a static definition of an entity in your system. Resource DSL documentation: `Ash.Dsl` """ alias Ash.Dsl.Extension defmacro __using__(opts) do data_layer = Macro.expand(opts[:data_layer], __CALLER__) authorizers = opts[:authorizers] |> List.wrap() |> Enum.map(&Macro.expand(&1, __CALLER__)) extensions = if data_layer && Ash.implements_behaviour?(data_layer, Ash.Dsl.Extension) do [data_layer, Ash.Dsl] else [Ash.Dsl] end authorizer_extensions = Enum.filter(authorizers, &Ash.implements_behaviour?(&1, Ash.Dsl.Extension)) extensions = Enum.concat([extensions, opts[:extensions] || [], authorizer_extensions]) body = quote bind_quoted: [opts: opts] do @before_compile Ash.Resource @authorizers opts[:authorizers] || [] @data_layer opts[:data_layer] end preparations = Extension.prepare(extensions) [body | preparations] end # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defmacro __before_compile__(_env) do quote do @doc false alias Ash.Dsl.Extension Module.register_attribute(__MODULE__, :is_ash_resource, persist: true, accumulate: false) @is_ash_resource true @on_load :on_load :persistent_term.put({__MODULE__, :data_layer}, @data_layer) :persistent_term.put({__MODULE__, :authorizers}, @authorizers) @ash_dsl_config Extension.set_state() def on_load do :persistent_term.put({__MODULE__, :data_layer}, @data_layer) :persistent_term.put({__MODULE__, :authorizers}, @authorizers) Extension.load() end require Ash.Schema Ash.Schema.define_schema() end end @doc "A list of authorizers to be used when accessing" @spec authorizers(Ash.resource()) :: [module] def authorizers(resource) do {resource, :authorizers} |> :persistent_term.get([]) |> List.wrap() end @spec validations(Ash.resource(), :create | :update | :destroy) :: [Ash.validation()] def validations(resource, type) do resource |> validations() |> Enum.filter(&(type in &1.on)) end @doc "A list of all validations for the resource" @spec validations(Ash.resource()) :: [Ash.validation()] def validations(resource) do Extension.get_entities(resource, [:validations]) end @doc "Whether or not a given module is a resource module" @spec resource?(module) :: boolean def resource?(module) when is_atom(module) do module.module_info(:attributes)[:is_ash_resource] == [true] end def resource?(_), do: false @doc "A list of field names corresponding to the primary key" @spec primary_key(Ash.resource()) :: list(atom) def primary_key(resource) do :persistent_term.get({resource, :primary_key}, []) end @spec relationships(Ash.resource()) :: list(Ash.relationship()) def relationships(resource) do Extension.get_entities(resource, [:relationships]) end @doc "Get a relationship by name or path" @spec relationship(Ash.resource(), atom | [atom]) :: Ash.relationship() | nil def relationship(resource, [name]) do relationship(resource, name) end def relationship(resource, [name | rest]) do case relationship(resource, name) do nil -> nil relationship -> relationship(relationship.destination, rest) end end def relationship(resource, relationship_name) when is_bitstring(relationship_name) do resource |> relationships() |> Enum.find(&(to_string(&1.name) == relationship_name)) end def relationship(resource, relationship_name) do resource |> relationships() |> Enum.find(&(&1.name == relationship_name)) end @doc "Returns the primary action of the given type" @spec primary_action!(Ash.resource(), Ash.action_type()) :: Ash.action() | no_return def primary_action!(resource, type) do case primary_action(resource, type) do nil -> raise "Required primary #{type} action for #{inspect(resource)}" action -> action end end @doc "Returns the primary action of a given type" @spec primary_action(Ash.resource(), Ash.action_type()) :: Ash.action() | nil def primary_action(resource, type) do resource |> actions() |> Enum.filter(&(&1.type == type)) |> case do [action] -> action actions -> Enum.find(actions, & &1.primary?) end end @doc "Returns all actions of a resource" @spec actions(Ash.resource()) :: [Ash.action()] def actions(resource) do Extension.get_entities(resource, [:actions]) end @doc "Returns the action with the matching name and type on the resource" @spec action(Ash.resource(), atom(), Ash.action_type()) :: Ash.action() | nil def action(resource, name, type) do resource |> actions() |> Enum.find(&(&1.name == name && &1.type == type)) end @doc "Returns all attributes of a resource" @spec attributes(Ash.resource()) :: [Ash.attribute()] def attributes(resource) do Extension.get_entities(resource, [:attributes]) end @doc "Get an attribute name from the resource" @spec attribute(Ash.resource(), String.t() | atom) :: Ash.attribute() | nil def attribute(resource, name) when is_bitstring(name) do resource |> attributes() |> Enum.find(&(to_string(&1.name) == name)) end def attribute(resource, name) do resource |> attributes() |> Enum.find(&(&1.name == name)) end @spec related(Ash.resource(), atom() | String.t() | [atom() | String.t()]) :: Ash.resource() | nil def related(resource, relationship) when not is_list(relationship) do related(resource, [relationship]) end def related(resource, []), do: resource def related(resource, [path | rest]) do case relationship(resource, path) do %{destination: destination} -> related(destination, rest) nil -> nil end end @doc "The data layer of the resource, or nil if it does not have one" @spec data_layer(Ash.resource()) :: Ash.data_layer() def data_layer(resource) do :persistent_term.get({resource, :data_layer}) end @doc "Whether ornot the data layer supports a specific feature" @spec data_layer_can?(Ash.resource(), Ash.DataLayer.feature()) :: boolean def data_layer_can?(resource, feature) do data_layer = data_layer(resource) data_layer && Ash.DataLayer.can?(feature, resource) end @doc "Custom filters suppoted by the data layer of the resource" @spec data_layer_filters(Ash.resource()) :: map def data_layer_filters(resource) do Ash.DataLayer.custom_filters(resource) end @doc "Whether or not the data layer for the resource is currently in a transaction" @spec in_transaction?(Ash.resource()) :: boolean def in_transaction?(resource) do data_layer(resource).in_transaction?(resource) end @doc "Wraps the execution of the function in a transaction with the resource's datalayer" @spec transaction(Ash.resource(), (() -> term)) :: term def transaction(resource, func) do if data_layer_can?(resource, :transact) do data_layer(resource).transaction(resource, func) else func.() end end @doc "Rolls back the current transaction" @spec rollback(Ash.resource(), term) :: no_return def rollback(resource, term) do data_layer(resource).rollback(resource, term) end end