lib/ash/resource/attribute.ex

defmodule Ash.Resource.Attribute do
  @moduledoc "Represents an attribute on a resource"

  defstruct [
    :name,
    :type,
    :allow_nil?,
    :generated?,
    :primary_key?,
    :private?,
    :writable?,
    :always_select?,
    :default,
    :update_default,
    :description,
    :source,
    sensitive?: false,
    filterable?: true,
    constraints: []
  ]

  @type t :: %__MODULE__{
          name: atom(),
          constraints: Keyword.t(),
          type: Ash.Type.t(),
          primary_key?: boolean(),
          private?: boolean(),
          default: (() -> term),
          update_default: (() -> term) | (Ash.Resource.record() -> term),
          sensitive?: boolean(),
          writable?: boolean()
        }

  alias Ash.OptionsHelpers

  @schema [
    name: [
      type: :atom,
      doc: "The name of the attribute."
    ],
    type: [
      type: :ash_type,
      doc: "The type of the attribute."
    ],
    constraints: [
      type: :keyword_list,
      doc:
        "Constraints to provide to the type when casting the value. See the type's documentation for more information."
    ],
    sensitive?: [
      type: :boolean,
      default: false,
      doc:
        "Whether or not the attribute value contains sensitive information, like PII. If so, it will be redacted while inspecting data."
    ],
    source: [
      type: :atom,
      doc: """
      If the field should be mapped to a different name in the data layer.
      """
    ],
    always_select?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not to always select this attribute when reading from the database.
      Useful if fields are used in read action preparations consistently.

      A primary key attribute *cannot be deselected*, so this option will have no effect.

      Generally, you should favor selecting the field that you need while running your preparation. For example:

      ```elixir
      defmodule MyApp.QueryPreparation.Thing do
        use Ash.Resource.Preparation

        def prepare(query, _, _) do
          query
          |> Ash.Query.select(:attribute_i_need)
          |> Ash.Query.after_action(fn query, results ->
            {:ok, Enum.map(results, fn result ->
              do_something_with_attribute_i_need(result)
            end)}
          end)
        end
      end
      ```

      This will prevent unnecessary fields from being selected.
      """
    ],
    primary_key?: [
      type: :boolean,
      default: false,
      doc:
        "Whether or not the attribute is part of the primary key (one or more fields that uniquely identify a resource). " <>
          "If primary_key? is true, allow_nil? must be false."
    ],
    allow_nil?: [
      type: :boolean,
      default: true,
      doc: "Whether or not the attribute can be set to nil."
    ],
    generated?: [
      type: :boolean,
      default: false,
      doc:
        "Whether or not the value may be generated by the data layer. If it is, the data layer will know to read the value back after writing."
    ],
    writable?: [
      type: :boolean,
      default: true,
      doc: "Whether or not the value can be written to."
    ],
    private?: [
      type: :boolean,
      default: false,
      doc:
        "Whether or not the attribute will appear in any interfaces created off of this resource, e.g AshJsonApi and AshGraphql."
    ],
    update_default: [
      type: {:custom, Ash.OptionsHelpers, :default, []},
      doc:
        "A zero argument function, an {mod, fun, args} triple or a value. `Ash.Changeset.for_update/4` sets the default in the changeset if a value is not provided."
    ],
    filterable?: [
      type: {:or, [:boolean, {:in, [:simple_equality]}]},
      default: true,
      doc: "Whether or not the attribute should be usable in filters."
    ],
    default: [
      type: {:custom, Ash.OptionsHelpers, :default, []},
      doc:
        "A zero argument function, an {mod, fun, args} triple or a value. `Ash.Changeset.for_create/4` sets the default in the changeset if a value is not provided."
    ],
    description: [
      type: :string,
      doc: "An optional description for the attribute."
    ]
  ]

  @create_timestamp_schema @schema
                           |> OptionsHelpers.set_default!(:writable?, false)
                           |> OptionsHelpers.set_default!(:private?, true)
                           |> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
                           |> OptionsHelpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
                           |> OptionsHelpers.set_default!(:allow_nil?, false)

  @update_timestamp_schema @schema
                           |> OptionsHelpers.set_default!(:writable?, false)
                           |> OptionsHelpers.set_default!(:private?, true)
                           |> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
                           |> OptionsHelpers.set_default!(
                             :update_default,
                             &DateTime.utc_now/0
                           )
                           |> OptionsHelpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
                           |> OptionsHelpers.set_default!(:allow_nil?, false)

  @uuid_primary_key_schema @schema
                           |> OptionsHelpers.set_default!(:writable?, false)
                           |> OptionsHelpers.set_default!(:default, &Ash.UUID.generate/0)
                           |> OptionsHelpers.set_default!(:primary_key?, true)
                           |> OptionsHelpers.set_default!(:type, :uuid)
                           |> Keyword.delete(:allow_nil?)

  @integer_primary_key_schema @schema
                              |> OptionsHelpers.set_default!(:writable?, false)
                              |> OptionsHelpers.set_default!(:primary_key?, true)
                              |> OptionsHelpers.set_default!(:generated?, true)
                              |> OptionsHelpers.set_default!(:type, :integer)
                              |> Keyword.delete(:allow_nil?)

  @doc false
  def attribute_schema, do: @schema
  def create_timestamp_schema, do: @create_timestamp_schema
  def update_timestamp_schema, do: @update_timestamp_schema
  def uuid_primary_key_schema, do: @uuid_primary_key_schema
  def integer_primary_key_schema, do: @integer_primary_key_schema
end