if Code.ensure_loaded?(Ash) do
defmodule Pyro.Resource.Info do
@moduledoc """
Helpers to introspect the `Pyro.Resource` Ash extension. Intended for use in components that automatically build UI from resource configuration.
"""
alias Phoenix.HTML.Form, as: PHXHTML
alias Ash.Resource.Info, as: ResourceInfo
alias Ash.Policy.Info, as: PolicyInfo
alias Pyro.ResourceParams
@type ash_resource_field ::
Ash.Resource.Attribute.t()
| Ash.Resource.Aggregate.t()
| Ash.Resource.Calculation.t()
| Ash.Resource.Relationships.relationship()
@type ash_resource_field_or_name ::
ash_resource_field()
| binary()
| atom()
@type ash_action_or_name ::
Ash.Resource.Actions.action() | binary() | atom()
@type ash_actor :: map() | nil
################################################################################################
#### R E S O U R C E
################################################################################################
@doc ~S"""
The label of the resource as defined in the `Pyro.Resource` extension, defaulting to a humanized version of the module name.
## Examples
iex> resource_label(User)
"User"
"""
@spec resource_label(Ash.Resource.t()) :: binary() | nil
def resource_label(resource) do
case Spark.Dsl.Extension.get_opt(resource, [:pyro], :resource_label, nil) do
nil ->
resource
|> Module.split()
|> List.last()
|> humanize_module_name()
name ->
name
end
end
defdelegate resource_description(resource), to: ResourceInfo, as: :description
defdelegate resource?(resource), to: ResourceInfo
defdelegate multitenancy_strategy(resource), to: ResourceInfo
@doc ~S"""
The default display mode of the resource as defined in the `Pyro.Resource` extension, defaulting to `:data_table`.
## Examples
iex> default_display_mode(User)
:card_grid
"""
@spec default_display_mode(Ash.Resource.t()) :: binary()
def default_display_mode(resource),
do: Spark.Dsl.Extension.get_opt(resource, [:pyro], :default_display_mode, :data_table)
################################################################################################
#### A C T I O N S
################################################################################################
@type primary_actions_opt :: [exclude_types: [Ash.Resource.Actions.action_type()]]
@doc ~S"""
Returns the list of actions of the given resource.
Passing `exclude_types: [...]` will filter out the specified action types from the list.
## Examples
iex> r = primary_actions(User)
iex> r |> Enum.map(& &1.name)
[:update, :create, :read]
iex> r = primary_actions(User, exclude_types: [:create, :update])
iex> r |> Enum.map(& &1.name)
[:read]
"""
@spec primary_actions(Ash.Resource.t(), [PolicyInfo.can_option() | primary_actions_opt()]) ::
[Ash.Resource.Actions.action()]
def primary_actions(resource, opts \\ []) do
{exclude_types, _auth_opts} = primary_actions_opts(opts)
resource
|> ResourceInfo.actions()
|> Enum.filter(fn action ->
action.primary? == true && action.type not in exclude_types
end)
end
@doc ~S"""
The same as `primary_actions/2`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = authorized_primary_actions(User, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec authorized_primary_actions(Ash.Resource.t(), ash_actor(), [
PolicyInfo.can_option() | primary_actions_opt()
]) ::
[Ash.Resource.Actions.action()]
def authorized_primary_actions(resource, actor, opts \\ []) do
{exclude_types, auth_opts} = primary_actions_opts(opts)
resource
|> ResourceInfo.actions()
|> Enum.filter(fn action ->
action.primary? == true && action.type not in exclude_types &&
can_do?(resource, action.name, actor, auth_opts)
end)
end
defp primary_actions_opts(opts) do
exclude_types = Keyword.get(opts, :exclude_types, [])
auth_opts = Keyword.delete(opts, :exclude_types)
{List.wrap(exclude_types), auth_opts}
end
defdelegate primary_action(resource, type), to: ResourceInfo
@doc ~S"""
Returns the list of `:read` type actions intended for single-record reads of the given resource.
## Examples
iex> r = show_actions(Record)
iex> r |> Enum.map(& &1.name)
[:show]
"""
@spec show_actions(Ash.Resource.t()) :: [Ash.Resource.Actions.Read.t()]
def show_actions(resource),
do:
resource
|> ResourceInfo.actions()
|> Enum.filter(&(&1.type == :read && &1.get? == true))
@doc ~S"""
The same as `show_actions/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = show_actions(Record, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec show_actions(Ash.Resource.t(), ash_actor(), [PolicyInfo.can_option()]) :: [
Ash.Resource.Actions.Read.t()
]
def show_actions(resource, actor, opts \\ []),
do:
resource
|> ResourceInfo.actions()
|> Enum.filter(fn %{type: type} = action ->
type == :read && action.get? == true &&
can_do?(resource, action, actor, opts)
end)
@doc ~S"""
Returns the list of `:read` type actions of the given resource, excluding actions that are `get?: true`.
## Examples
iex> r = list_actions(Gage)
iex> r |> Enum.map(& &1.name)
[:calibration_alerts, :read]
"""
@spec list_actions(Ash.Resource.t()) :: [Ash.Resource.Actions.Read.t()]
def list_actions(resource),
do:
resource
|> ResourceInfo.actions()
|> Enum.filter(&(&1.type == :read && &1.get? == false))
@doc ~S"""
The same as `list_actions/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = list_actions(Gage, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec list_actions(Ash.Resource.t(), ash_actor(), [PolicyInfo.can_option()]) :: [
Ash.Resource.Actions.Read.t()
]
def list_actions(resource, actor, opts \\ []),
do:
resource
|> ResourceInfo.actions()
|> Enum.filter(fn %{type: type} = action ->
type == :read && action.get? == false &&
can_do?(resource, action, actor, opts)
end)
@doc ~S"""
Returns the list of `:create` type actions of the given resource.
## Examples
iex> r = create_actions(Gage)
iex> r |> Enum.map(& &1.name)
[:create]
"""
@spec create_actions(Ash.Resource.t()) :: [Ash.Resource.Actions.Create.t()]
def create_actions(resource), do: actions_of_type(resource, :create)
@doc ~S"""
The same as `create_actions/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = create_actions(Gage, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec create_actions(Ash.Resource.t(), ash_actor(), [PolicyInfo.can_option()]) :: [
Ash.Resource.Actions.Create.t()
]
def create_actions(resource, actor, opts \\ []),
do: actions_of_type(resource, :create, actor, opts)
@doc ~S"""
Returns the list of `:read` type actions of the given resource.
## Examples
iex> r = read_actions(Gage)
iex> r |> Enum.map(& &1.name)
[:calibration_alerts, :read]
"""
@spec read_actions(Ash.Resource.t()) :: [Ash.Resource.Actions.Read.t()]
def read_actions(resource), do: actions_of_type(resource, :read)
@doc ~S"""
The same as `read_actions/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = read_actions(Gage, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec read_actions(Ash.Resource.t(), ash_actor(), [PolicyInfo.can_option()]) :: [
Ash.Resource.Actions.Read.t()
]
def read_actions(resource, actor, opts \\ []),
do: actions_of_type(resource, :read, actor, opts)
@doc ~S"""
Returns the list of `:update` type actions of the given resource.
## Examples
iex> r = update_actions(Gage)
iex> r |> Enum.map(& &1.name)
[:update]
"""
@spec update_actions(Ash.Resource.t()) :: [Ash.Resource.Actions.Update.t()]
def update_actions(resource), do: actions_of_type(resource, :update)
@doc ~S"""
The same as `update_actions/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = update_actions(Gage, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec update_actions(Ash.Resource.t(), ash_actor(), [PolicyInfo.can_option()]) :: [
Ash.Resource.Actions.Update.t()
]
def update_actions(resource, actor, opts \\ []),
do: actions_of_type(resource, :update, actor, opts)
@doc ~S"""
Returns the list of `:destroy` type actions of the given resource.
## Examples
iex> r = destroy_actions(Gage)
iex> r |> Enum.map(& &1.name)
[:destroy]
"""
@spec destroy_actions(Ash.Resource.t()) :: [Ash.Resource.Actions.Destroy.t()]
def destroy_actions(resource), do: actions_of_type(resource, :destroy)
@doc ~S"""
The same as `destroy_actions/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = destroy_actions(Gage, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec destroy_actions(Ash.Resource.t(), ash_actor(), [PolicyInfo.can_option()]) :: [
Ash.Resource.Actions.Destroy.t()
]
def destroy_actions(resource, actor, opts \\ []),
do: actions_of_type(resource, :destroy, actor, opts)
@spec default_action_for_live_action(
Ash.Resource.t(),
ResourceParams.live_action_type(),
map() | nil
) ::
Ash.Resource.Actions.action()
def default_action_for_live_action(resource, live_action, actor) do
# TODO: To make this more effective, at some point we can add a section in the UI config to specify the default, then below is a fallback.
actions =
case live_action do
:show ->
read_actions(resource, actor)
:list ->
list_actions(resource, actor)
:create ->
create_actions(resource, actor)
:update ->
update_actions(resource, actor)
:destroy ->
destroy_actions(resource, actor)
live_action ->
raise(
"Live action #{live_action} is not implemented for &#{__MODULE__}.default_action_for_live_action/2!"
)
end
|> List.wrap()
case Enum.find(actions, &(&1.name == :show)) do
nil ->
case Enum.find(actions, &(&1.primary? == true)) do
nil -> List.first(actions)
action -> action
end
action ->
action
end
end
@doc ~S"""
The label of the action as defined in the `Pyro.Resource` extension, defaulting to a humanized version of the action name.
## Examples
iex> action_label(Gage, "calibration_alerts")
"Calibration Alerts"
iex> action_label(Gage, :update)
"Update"
iex> action_label(Gage, %{name: :read})
"Read"
"""
@spec action_label(Ash.Resource.t(), ash_action_or_name()) ::
binary() | nil
def action_label(resource, %{name: name}), do: action_label(resource, name)
def action_label(resource, name) when is_atom(name),
do: action_label(resource, Atom.to_string(name))
def action_label(_resource, name) when is_binary(name),
do:
name
|> String.split("_")
|> Enum.map_join(" ", &String.capitalize/1)
@type select_option :: [key: binary(), value: binary()]
@doc ~S"""
Convert a list of actions into a format compatible with `Phoenix.HTML.Form.select/4` `options`.
## Examples
iex> actions_to_select_options(read_actions(Gage), Gage)
[
[key: "Calibration Alerts", value: "calibration_alerts"],
[key: "Read", value: "read"]
]
"""
@spec actions_to_select_options([Ash.Resource.Actions.action()], Ash.Resource.t()) ::
[select_option()]
def actions_to_select_options(actions, resource),
do:
actions
|> Enum.map(fn %{name: name} ->
value = Atom.to_string(name)
key = action_label(resource, value)
[key: key, value: value]
end)
def action(resource, name) when is_binary(name) do
try do
action(resource, String.to_existing_atom(name))
rescue
_ -> nil
end
end
defdelegate action(resource, name), to: ResourceInfo
@doc ~S"""
The default sort of resource for the given action, falling back to the default provided by the `Pyro.Resource` extension.
This is useful for extracting default sorts from preparations.
## Examples
iex> default_sort(Gage, :calibration_alerts)
"++calibration_status_expiration,calibration_status,name"
iex> default_sort(AttendanceRecord, :read)
"-in"
iex> default_sort(AttendanceRecord, :current_attendance)
"in"
"""
@spec default_sort(Ash.Resource.t(), atom()) :: binary()
def default_sort(resource, action) do
case Ash.Query.for_read(resource, action).sort do
[] ->
with sort <- Spark.Dsl.Extension.get_opt(resource, [:pyro], :default_sort, nil),
{:ok, _} <- Ash.Sort.parse_input(resource, sort) do
sort
else
_ -> raise "Invalid default_sort for resource #{resource}!"
end
sort ->
stringify_sort(sort)
end
end
@spec resource_by_path(Ash.Resource.t(), [atom() | binary()]) :: Ash.Resource.t()
def resource_by_path(resource, []), do: resource
def resource_by_path(resource, [relationship | rest]) do
case field(resource, relationship) do
%Ash.Resource.Aggregate{} ->
resource
%Ash.Resource.Calculation{} ->
resource
%Ash.Resource.Attribute{} ->
resource
%Ash.Resource.Relationships.BelongsTo{destination: destination} ->
resource_by_path(destination, rest)
%Ash.Resource.Relationships.HasOne{destination: destination} ->
resource_by_path(destination, rest)
%Ash.Resource.Relationships.HasMany{destination: destination} ->
resource_by_path(destination, rest)
%Ash.Resource.Relationships.ManyToMany{destination: destination} ->
resource_by_path(destination, rest)
end
end
################################################################################################
#### F I E L D S
################################################################################################
@doc """
Lists all the public fields of resource.
## Examples
iex> public_fields(AttendanceType) |> Enum.map(& &1.name)
[
:label,
:order,
:shortcut,
:notes,
:inserted_at,
:updated_at,
:id,
:records_count,
:records
]
"""
@spec public_fields(Ash.Resource.t()) :: [ash_resource_field()]
def public_fields(resource),
do:
resource
|> ResourceInfo.public_attributes()
|> Enum.concat(ResourceInfo.public_aggregates(resource))
|> Enum.concat(ResourceInfo.public_calculations(resource))
|> Enum.concat(ResourceInfo.public_relationships(resource))
@doc """
The same as `public_fields/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> public_fields(AttendanceType, :read, nil) |> Enum.map(& &1.name)
[]
"""
@spec public_fields(
Ash.Resource.t(),
ash_action_or_name(),
ash_actor()
) :: [ash_resource_field()]
def public_fields(resource, action, actor),
do:
resource
|> public_fields()
|> Enum.filter(&field_authorized?(&1, resource, action, actor))
@doc """
Returns the field defined in the `Pyro.Resource` extension as a default label, falling back to the first single-key identity of the resource, further falling back to the primary key if it's single-keyed.
## Examples
iex> default_foreign_label(AttendanceType)
:label
iex> default_foreign_label(Gage)
:name
"""
@spec default_foreign_label(Ash.Resource.t()) :: atom()
def default_foreign_label(resource),
do:
resource
|> Spark.Dsl.Extension.get_opt([:pyro], :default_foreign_label, nil) ||
default_single_field_identity_key(resource)
@doc """
Returns the given field's description.
## Examples
iex> field_description(AttendanceType, :clockable)
"Clockable types are available for employees to punch in/out at kiosks and workstations."
"""
@spec field_description(
Ash.Resource.t(),
ash_resource_field_or_name()
) :: atom()
def field_description(_resource, %{__struct__: struct_name, description: description})
when struct_name in [
Ash.Resource.Relationships.HasMany,
Ash.Resource.Relationships.HasOne,
Ash.Resource.Relationships.BelongsTo,
Ash.Resource.Relationships.ManyToMany,
Ash.Resource.Aggregate,
Ash.Resource.Calculation,
Ash.Resource.Attribute
],
do: description
def field_description(resource, name),
do: field_description(resource, ResourceInfo.field(resource, name))
@doc """
WIP label for resource fields. In the future it will pull from a resource field property. This will allow different labels based on context.
"""
def field_label(resource, _action, field) do
resource
|> ResourceInfo.field(field)
|> humanize_field()
end
@doc """
Returns the form fields defined in the `Pyro.Resource` extension for the given action.
## Examples
iex> form_for(AttendanceRecord, :create) |> Enum.map(& &1.name)
[:notes, :employee_id, :type_id]
"""
@spec form_for(Ash.Resource.t(), atom()) :: [
Pyro.Resource.Form.Field.t() | Pyro.Resource.Form.FieldGroup.t()
]
def form_for(resource, action_name) do
resource
|> Spark.Dsl.Extension.get_entities([:pyro, :form])
|> Enum.find(fn action ->
action.name == action_name
end)
end
# @doc """
# Returns the specified form field defined in the `Pyro.Resource` extension for the given action.
# ## Examples
# iex> form_field(AttendanceRecord, :create, :label)
# %Pyro.Resource.Form.Field{name: :label}
# """
# @spec form_field(Ash.Resource.t(), atom(), atom(), [atom()]) :: [
# Pyro.Resource.Form.Field.t() | Pyro.Resource.Form.FieldGroup.t()
# ]
# def form_field(resource, action_name, field_name, path \\ []) do
# resource
# |> Spark.Dsl.Extension.get_entities([:pyro, :form])
# |> Enum.find(fn action ->
# action.name == action_name
# end)
# end
# defp get_field(fields, name, path), do:
@doc ~S"""
Check if a given field is authorized to be viewed by the actor.
## Examples
iex> field_authorized?("name", Gage, "read", nil)
false
iex> field_authorized?(:owner, Gage, :read, %{roles: [:Employee], active: true})
true
> #### Note: {: .warning}
>
> Currently, fields that _may_ be authorized depending on actor's attributes are authorized. We may eventually want to more intelligently deal with this, because currently a `nil` actor will return true. This particular case will handled when [#372](https://github.com/ash-project/ash/issues/372) is closed. This is mostly UI concern, because the query will still be safely validated and would fail, but we don't want to clutter the UI with fields that can't actually be loaded.
iex> field_authorized?(:owner, Gage, :read, nil)
false
> #### Note: {: .warning}
>
> We are not currently handling field-level authz for create/update types. This can probably be done by leveraging `Ash.Generator` to seed changeset data for the given field. This should allow determine e.g. if a relationship is editable by the actor, allow forms to hide relationships that an actor can't edit.
"""
@spec field_authorized?(
ash_resource_field_or_name(),
Ash.Resource.t(),
ash_action_or_name(),
ash_actor(),
keyword()
) :: boolean()
def field_authorized?(field, resource, action, actor, opts \\ [])
def field_authorized?(field, resource, action, actor, opts)
when is_atom(action) or is_binary(action),
do: field_authorized?(field, resource, action(resource, action), actor, opts)
def field_authorized?(field, resource, action, actor, opts)
when is_atom(field) or is_binary(field),
do: field_authorized?(field(resource, field), resource, action, actor, opts)
def field_authorized?(_field, _resource, _action, _actor, _opts) do
# api = Keyword.get(opts, :api)
# maybe_is = Keyword.get(opts, :maybe_is, :maybe)
# TODO: Actually deal with this
true
# case action.type do
# :update ->
# query =
# struct(resource)
# |> Ash.Changeset.new(%{})
# |> Ash.Changeset.for_update(action.name)
# run_check(actor, query, api: api, maybe_is: maybe_is)
# :create ->
# query =
# resource
# |> Ash.Changeset.new()
# |> Ash.Changeset.for_create(action.name)
# run_check(actor, query, api: api, maybe_is: maybe_is)
# :read ->
# query =
# Ash.Query.for_read(resource, action.name)
# |> select_or_load_field(field)
# run_check(actor, query, api: api, maybe_is: maybe_is)
# :destroy ->
# query =
# struct(resource)
# |> Ash.Changeset.new()
# |> Ash.Changeset.for_destroy(action.name)
# run_check(actor, query, api: api, maybe_is: maybe_is)
# action_type ->
# raise ArgumentError, message: "Invalid action type \"#{action_type}\""
# end
end
defdelegate field(resource, field), to: ResourceInfo
def field(resource, field, action, actor, opts \\ []) do
case field(resource, field) do
nil -> nil
field -> if field_authorized?(field, resource, action, actor, opts), do: field, else: nil
end
end
defdelegate public_attributes(resource), to: ResourceInfo
defdelegate attribute(resource, name), to: ResourceInfo
defdelegate public_aggregates(resource), to: ResourceInfo
defdelegate aggregate(resource, name), to: ResourceInfo
defdelegate public_calculations(resource), to: ResourceInfo
defdelegate calculation(resource, name), to: ResourceInfo
@doc ~S"""
Check if a field is sortable.
## Examples
iex> sortable_field?(User, field(User, :email))
true
iex> sortable_field?(User, :name)
true
"""
@spec sortable_field?(
Ash.Resource.t(),
ash_resource_field_or_name()
) :: boolean()
def sortable_field?(resource, %{name: name}), do: sortable_field?(resource, name)
defdelegate sortable_field?(resource, field), to: ResourceInfo, as: :sortable?
@doc ~S"""
Filter the list of fields to those which can be loaded (as opposed to those which can be selected). Basically just filters out attributes from a list of fields.
## Examples
Explicitly configured
iex> default_table_columns(User) |> loadable_fields(User)
[:best_friend]
"""
@spec loadable_fields([ash_resource_field()], Ash.Resource.t()) :: [atom()]
def loadable_fields(fields, resource),
do:
fields
|> Enum.filter(&(!ResourceInfo.attribute(resource, &1.name)))
|> Enum.map(& &1.name)
################################################################################################
#### P A G E
################################################################################################
@doc ~S"""
The custom page module of the action as defined in the `Pyro.Resource` extension, if the resource is configured as a custom page.
## Examples
iex> page_module(User)
nil
"""
@spec page_module(Ash.Resource.t()) :: module() | nil
def page_module(resource),
do:
Spark.Dsl.Extension.get_opt(
resource,
[:pyro, :page],
:module,
nil
)
@doc ~S"""
The page routing path of the resource as defined in the `Pyro.Resource` extension, if the resource is configured as a page.
## Examples
iex> page_route_path(AttendanceType)
"attendance-types"
"""
@spec page_route_path(Ash.Resource.t()) :: binary() | nil
def page_route_path(resource),
do:
Spark.Dsl.Extension.get_opt(
resource,
[:pyro, :page],
:route_path,
nil
)
@doc ~S"""
Return a boolean indicating if resource is a page as configured in the `Pyro.Resource` extension.
## Examples
iex> is_page?(AttendanceType)
true
"""
@spec is_page?(Ash.Resource.t()) :: boolean()
def is_page?(resource) do
!!Spark.Dsl.Extension.get_opt(
resource,
[:pyro, :page],
:route_path,
false
)
end
# @doc ~S"""
# Return a of resources configured as a page in the `Pyro.Resource` extension.
# ## Examples
# iex> pages(api) |> length() > 1
# true
# """
# @spec pages(Ash.Api.t()) :: [Ash.Resource.t()]
# def pages(api) do
# Ash.Api.Info.resources(api) |> Enum.filter(&is_page?/1)
# end
@doc ~S"""
Return the page limit options for the given resource/action if pagination is configured.
## Examples
iex> page_limit_options(AttendanceType, :read)
[10, 25, 50, 100, 250]
"""
@spec page_limit_options(Ash.Resource.t(), atom() | binary()) :: [pos_integer()]
def page_limit_options(resource, action_name) do
%{pagination: %{max_page_size: max_page_size}} = ResourceInfo.action(resource, action_name)
[10, 25, 50, 100, 250, 500]
|> Enum.filter(fn value -> value <= max_page_size end)
end
@doc ~S"""
Return the default page for the given resource/action.
## Examples
iex> default_page_limit(AttendanceType, :read)
25
"""
@spec default_page_limit(Ash.Resource.t(), atom() | binary()) :: pos_integer()
def default_page_limit(resource, action_name) do
# TODO: Add ability to define in UI so it can be a default without forcing a default for every call.
case ResourceInfo.action(resource, action_name) do
%{pagination: %{default_limit: default_limit}} when is_integer(default_limit) ->
default_limit
_ ->
25
end
end
################################################################################################
#### T A B L E
################################################################################################
@doc ~S"""
The default table columns to display as defined in the `Pyro.Resource` extension, with fallback to all public fields.
## Examples
Explicitly configured
iex> r = default_table_columns(User)
iex> r |> Enum.map(& &1.name)
[:name, :email, :notes]
"""
@spec default_table_columns(Ash.Resource.t()) :: [ash_resource_field()]
def default_table_columns(resource) do
case Spark.Dsl.Extension.get_opt(
resource,
[:pyro],
:default_table_columns,
:undefined
) do
:undefined ->
resource
|> public_fields()
columns ->
columns
|> Enum.map(fn name ->
case ResourceInfo.field(resource, name) do
nil -> raise "Column #{name} is not a public field on #{resource}!"
column -> column
end
end)
end
end
@doc ~S"""
Same as `default_table_columns/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = default_table_columns(Gage, :calibration_alerts, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec default_table_columns(Ash.Resource.t(), Ash.Resource.Action.t(), ash_actor()) :: [
ash_resource_field()
]
def default_table_columns(resource, action, actor),
do:
resource
|> default_table_columns()
|> Enum.filter(&field_authorized?(&1, resource, action, actor))
################################################################################################
#### C A R D
################################################################################################
@doc ~S"""
The default card fields to display as defined in the `Pyro.Resource` extension, with fallback to all public fields.
## Examples
Explicitly configured
iex> r = default_card_fields(AttendanceRecord)
iex> r |> Enum.map(& &1.name)
[
:inserted_at,
:updated_at,
:in,
:out,
:duration_hours,
:type_label,
:employee_code,
:employee_name_formal,
:notes
]
"""
@spec default_card_fields(Ash.Resource.t()) :: [ash_resource_field()]
def default_card_fields(resource) do
case Spark.Dsl.Extension.get_opt(
resource,
[:pyro],
:default_card_fields,
:undefined
) do
:undefined ->
resource
|> public_fields()
fields ->
fields
|> Enum.map(fn name ->
case ResourceInfo.field(resource, name) do
nil -> raise "Field #{name} is not a public field on #{resource}!"
field -> field
end
end)
end
end
@doc ~S"""
Same as `default_card_fields/1`, but filtered by those authorized for access for the specified action/actor.
## Examples
iex> r = default_card_fields(Gage, :calibration_alerts, nil)
iex> r |> Enum.map(& &1.name)
[]
"""
@spec default_card_fields(Ash.Resource.t(), Ash.Resource.Action.t(), ash_actor()) :: [
ash_resource_field()
]
def default_card_fields(resource, action, actor),
do:
resource
|> default_card_fields()
|> Enum.filter(&field_authorized?(&1.name, resource, action, actor))
################################################################################################
#### F O R M S
################################################################################################
@doc ~S"""
This is only used for the primary user input. This provides consistency between the input component and other parts of the UI that may want ot provide a link to that user input e.g. to click an error and focus the related input.
## Examples
iex> form = AshPhoenix.Form.for_create(AttendanceType, :create)
iex> input_id(form, :label)
"form_label"
"""
@spec input_id(AshPhoenix.Form.t(), atom()) :: binary()
def input_id(form, field) do
# resource = form.source.resource
# form_field_type = form_field(resource, field).type
# TODO: This needs to be revisited!
# append =
# case form_field_type do
# :autocomplete -> "_autocomplete_input"
# _ -> ""
# end
# <> append
PHXHTML.input_id(form, field)
end
################################################################################################
#### R E L A T I O N S H I P S
################################################################################################
defdelegate reverse_relationship(resource, path), to: ResourceInfo
defdelegate relationship(resource, path), to: ResourceInfo
defdelegate related(resource, path), to: ResourceInfo
################################################################################################
#### I D E N T I T I E S
################################################################################################
@doc ~S"""
List the single-keyed identities of the given resource.
## Examples
iex> single_field_identities(User)
[
%Ash.Resource.Identity{
description: nil,
eager_check_with: nil,
keys: [:email],
name: :name,
pre_check_with: nil
}
]
"""
@spec single_field_identities(Ash.Resource.t()) :: [Ash.Resource.Identity.t()]
def single_field_identities(resource),
do:
resource
|> ResourceInfo.identities()
|> Enum.filter(&(length(&1.keys) == 1))
@doc ~S"""
The first single-keyed identity, or the primary ID of the given resource.
## Examples
iex> default_single_field_identity_key(User)
:email
"""
@spec default_single_field_identity_key(Ash.Resource.t()) :: :atom
def default_single_field_identity_key(resource) do
case single_field_identities(resource) do
[] ->
[key] = ResourceInfo.primary_key(resource)
key
[%{keys: [key]} | _rest] ->
key
end
end
################################################################################################
#### P O L I C I E S
################################################################################################
defdelegate policies(resource), to: PolicyInfo
defdelegate can_do?(resource, action_or_query, actor, opts), to: PolicyInfo, as: :can?
# @doc ~S"""
# The description of the resource as defined in the resource DSL.
# ## Examples
# iex> resource_description(Gage)
# "An inventoried gage, subject to calibration."
# """
# @spec can_do?(
# Ash.Resource.t(),
# Ash.Resource.Action.t() | atom(),
# ash_actor(),
# [PolicyInfo.can_option()]
# ) :: boolean()
# def can_do?(resource, action_or_query, actor, opts \\ []) do
# opts = [api: Keyword.get(opts, :api), maybe_is: Keyword.get(opts, :maybe_is, true)]
# prepare_can_do?(resource, action_or_query, actor, opts)
# end
# defp prepare_can_do?(resource, %Ash.Query{} = action_or_query, actor, opts),
# do: PolicyInfo.can(resource, action_or_query, actor, opts)
# defp prepare_can_do?(resource, action_name, actor, opts) when is_atom(action_name),
# do: prepare_can_do?(resource, action(resource, action_name), actor, opts)
# defp prepare_can_do?(
# _resource,
# %{type: :read, arguments: arguments, name: _name},
# _actor,
# _opts
# )
# when is_list(arguments) and arguments != [] do
# # args =
# # arguments
# # |> Enum.filter(&(&1.allow_nil? == false))
# # |> Enum.map(fn argument ->
# # case argument do
# # %{name: name, type: Ash.Type.UtcDatetimeUsec} -> {name, DateTime.now!("Etc/UTC")}
# # %{name: name, type: {:array, _}} -> {name, []}
# # %{name: name} -> {name, "a"}
# # end
# # end)
# # query = Ash.Query.for_read(resource, name, args)
# # PolicyInfo.can(resource, query, actor, opts)
# # NOTE: Temporarily disabled for future enablement.
# false
# end
# defp prepare_can_do?(resource, action_or_query, actor, opts),
# do: PolicyInfo.can(resource, action_or_query, actor, opts)
################################################################################################
#### I N T E R N A L T O O L I N G
################################################################################################
defp stringify_sort(sort), do: sort |> Enum.map(&stringify_sort_field/1) |> Enum.join(",")
defp stringify_sort_field({%Ash.Query.Aggregate{name: name}, dir}),
do: stringify_sort_dir(dir) <> Atom.to_string(name)
defp stringify_sort_field({%Ash.Query.Calculation{name: name}, dir}),
do: stringify_sort_dir(dir) <> Atom.to_string(name)
defp stringify_sort_field({name, dir}) when is_atom(name),
do: stringify_sort_dir(dir) <> Atom.to_string(name)
defp stringify_sort_dir(:asc), do: ""
defp stringify_sort_dir(:asc_nils_first), do: "++"
defp stringify_sort_dir(:desc), do: "-"
defp stringify_sort_dir(:desc_nils_last), do: "--"
defp actions_of_type(resource, type),
do:
resource
|> ResourceInfo.actions()
|> Enum.filter(&(&1.type == type))
defp actions_of_type(resource, type, actor, opts),
do:
resource
|> ResourceInfo.actions()
|> Enum.filter(fn %{type: t} = action ->
t == type && can_do?(resource, action, actor, opts)
end)
# defp select_or_load_field(query, %{__struct__: struct_name, name: field_name}) do
# case struct_name do
# sn
# when sn in [
# Ash.Resource.Relationships.HasMany,
# Ash.Resource.Relationships.HasOne,
# Ash.Resource.Relationships.BelongsTo,
# Ash.Resource.Relationships.ManyToMany,
# Ash.Resource.Aggregate,
# Ash.Resource.Calculation
# ] ->
# query
# |> Ash.Query.select([], replace?: true)
# |> Ash.Query.load([field_name])
# _ ->
# query |> Ash.Query.select([field_name], replace?: true)
# end
# end
defp humanize_field(%{name: name}), do: humanize_field(name)
defp humanize_field(name) when is_atom(name), do: name |> Atom.to_string() |> humanize_field()
defp humanize_field(name) when is_binary(name),
do:
name
|> String.split("_")
|> Enum.map_join(" ", &String.capitalize/1)
defp humanize_module_name(atom) when is_atom(atom) do
humanize_module_name(Atom.to_string(atom))
end
defp humanize_module_name(<<h, t::binary>>) do
<<h>> <> do_humanize_module_name(t, h)
end
defp humanize_module_name("") do
""
end
defp do_humanize_module_name(<<h, t, rest::binary>>, _)
when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and not (t >= ?0 and t <= ?9) and
t != ?. and t != " " do
<<" ", h, t>> <> do_humanize_module_name(rest, t)
end
defp do_humanize_module_name(<<h, t::binary>>, prev)
when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != " " do
<<" ", h>> <> do_humanize_module_name(t, h)
end
defp do_humanize_module_name(<<?., t::binary>>, _) do
<<?/>> <> humanize_module_name(t)
end
defp do_humanize_module_name(<<h, t::binary>>, _) do
<<h>> <> do_humanize_module_name(t, h)
end
defp do_humanize_module_name(<<>>, _) do
<<>>
end
end
end