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,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: []
  ]

  defmodule Helpers do
    @moduledoc "Helpers for building attributes"

    defmacro timestamps(opts \\ []) do
      quote do
        create_timestamp :inserted_at, unquote(opts)
        update_timestamp :updated_at, unquote(opts)
      end
    end
  end

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

  alias Spark.OptionsHelpers

  @schema [
    name: [
      type: :atom,
      doc: "The name of the attribute."
    ],
    type: [
      type: Ash.OptionsHelpers.ash_type(),
      doc: "The type of the attribute. See `Ash.Type` for more."
    ],
    constraints: [
      type: :keyword_list,
      doc: """
      Constraints to provide to the type when casting the value.
      For more information see the specific type's documentation,
      for general type information see `Ash.Type` and
      for practical example [see the constraints topic](/documentation/topics/constraints.md).
      """
    ],
    description: [
      type: :string,
      doc: "An optional description for the attribute."
    ],
    sensitive?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the attribute value contains sensitive information, like PII.
      Using this option will cause the attribute to be `** Redacted **` from the resource when logging or inspecting.
      See the [Security guide](/documentation/topics/security.md) for more.
      """
    ],
    source: [
      type: :atom,
      doc: """
      If the field should be mapped to a different name in the data layer. Support varies by data layer.
      """
    ],
    always_select?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not to ensure this attribute is always selected when reading from the database.

      When this option is true and performing a read action, the attribute will **always** be selected even if it was explicitly selected out of the query.
      For example say there is a resource with two attributes `:foo` and `:bar`.
      Say `:foo` has `always_select? true` set.
      The query `Ash.Query.select(MyResource, [:bar])` would return both `:foo` and `:bar` even though `:foo` was not selected in the query.
      """
    ],
    primary_key?: [
      type: :boolean,
      default: false,
      doc: """
      Whether the attribute is the primary key.
      Composite primary key is also possible by using `primary_key? true` in more than one attribute.
      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.
      If nil value is given error is raised.
      """
    ],
    generated?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the value may be generated by the data layer.
      """
    ],
    writable?: [
      type: :boolean,
      default: true,
      doc: """
      Whether or not the value can be written to.
      If `writable? false` then attribute is read-only and cannot be written to even when creating a record.
      This can be overridden with `Ash.Changeset.force_change_attribute/3`.
      """
    ],
    private?: [
      type: :boolean,
      default: false,
      doc: """
      If `private? true` then attribute is read-only and cannot be written to even when creating a record.
      Additionally it tells other extensions (e.g. AshJsonApi or AshGraphql) not to expose these attributes through the API.
      The value of the attribute can be overridden with `Ash.Changeset.force_change_attribute/3`.

      See the [security guide](/documentation/topics/security.md) for more.
      """
    ],
    default: [
      type: {:or, [{:mfa_or_fun, 0}, :literal]},
      doc: "A value to be set on all creates, unless a value is being provided already."
    ],
    update_default: [
      type: {:or, [{:mfa_or_fun, 0}, :literal]},
      doc: "A value to be set on all updates, unless a value is being provided already."
    ],
    filterable?: [
      type: {:or, [:boolean, {:in, [:simple_equality]}]},
      default: true,
      doc: """
      Whether or not the attribute can be referenced in filters.
      Can be used to prevent filtering on large text columns with no indexing.
      """
    ],
    match_other_defaults?: [
      type: :boolean,
      default: false,
      doc: """
      Ensures that other attributes that use the same "lazy" default (a function or an mfa), use the same default value.
      Has no effect unless `default` is a zero argument function.
      For example, create and update timestamps use this option, and have the same lazy function `&DateTime.utc_now/0`, so they
      get the same value, instead of having slightly different timestamps.
      """
    ]
  ]

  @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!(:match_other_defaults?, true)
                           |> OptionsHelpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
                           |> OptionsHelpers.set_default!(:allow_nil?, false)
                           |> Ash.OptionsHelpers.hide_all_except([:name])

  @update_timestamp_schema @schema
                           |> OptionsHelpers.set_default!(:writable?, false)
                           |> OptionsHelpers.set_default!(:private?, true)
                           |> OptionsHelpers.set_default!(:match_other_defaults?, 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)
                           |> Ash.OptionsHelpers.hide_all_except([:name])

  @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?)
                           |> Ash.OptionsHelpers.hide_all_except([:name])

  @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?)
                              |> Ash.OptionsHelpers.hide_all_except([:name])

  def transform(attribute) do
    Ash.Type.set_type_transformation(%{attribute | source: attribute.source || attribute.name})
  end

  @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