How to Extract a Relationship into a Separate Module for Reuse in `Ash`?

I’m working on a feature that lets users add custom fields to records. For this, I have three resources:

  1. FieldType with columns name and description.
  2. Field with columns name, field_type_id, description, and options.
  3. FieldResourceRecord with columns field_id, resource_name, resource_record_id, and value.

Any resource needing custom fields will have a one-to-many relationship with FieldResourceRecord where resource_name matches ResourceWithManyFields.

Currently, I’m defining the has_many :fields, MyApp.Fields.FieldResourceRecord relationship on each resource. Is there a way to centralize this relationship, similar to how we handle preparations, changes, and manual_actions?

You can centralize it by writing an extension, although for just one relationship it may be sort of a nuclear option :slight_smile:

defmodule YourApp.CustomFields do
  use Spark.Dsl.Extension, 
    transformers: [YourApp.CustomFields.Transformers.AddCustomFields]
end

defmodule YourApp.CustomFields.Transformers.AddCustomFields do
  use Spark.Dsl.Transformer

  def transform(dsl) do
    dsl
    |> Ash.Resource.Builder.add_new_relationship(:has_many, :fields, MyApp>Fields.FieldResourceRecord)
  end
end

Then you can do

use Ash.Resource, extensions: [YourApp.CustomFields]
1 Like

Thank you @zachdaniel, how can I add the following filters to that relationship in this extension. I could not determine how to do it in the documentation: Ash.Resource.Builder — ash v3.4.5

   has_many :fields, Hr.Fields.FieldResourceRecord do
      destination_attribute :resource_record_id
      
      # where `Hr.People.Person` is determined automatically  as the module running this extension 
      filter expr(resource_name == "Hr.People.Person")
   end

Here is how far I could go. Still unable to apply filters in the transformers add_new_relationship function.

defmodule MyApp.Extensions.CustomerFields.Transformers.AddCustomerFields do
  use Spark.Dsl.Transformer
  def transform(dsl) do

       Ash.Resource.Builder.add_new_relationship(
      dsl,
      :has_many,
      :fields,
      Hr.Fields.FieldResourceRecord,
      destination_attribute: :resource_record_id
     )
      |> dbg()
  end
end

Make sure to import Ash.Expr

import Ash.Expr

and then you can use it like so:

    Ash.Resource.Builder.add_new_relationship(
      dsl,
      :has_many,
      :fields,
      Hr.Fields.FieldResourceRecord,
      destination_attribute: :resource_record_id,
      filter: expr(resource_name == "Hr.People.Person")
     )

EDIT: I’m pretty sure that the filter option should “just work” like that, but let me know if not.

1 Like

It was not working. I fixed it like below:

defmodule Hr.Extensions.CustomFields.Transformers.AddCustomFields do
  use Spark.Dsl.Transformer
  import Ash.Expr

  @doc """
  Automatically add relationship to the FieldResourceRecord on any given resource
  """
  def transform(dsl) do
    Ash.Resource.Builder.add_new_relationship(
      dsl,
      :has_many,
      :fields,
      Hr.Fields.FieldResourceRecord,
      destination_attribute: :resource_record_id,
      filters: expr(resource_name == get_resource_name(dsl))
    )
  end

  defp get_resource_name(dsl) do
    dsl.persist.module
    |> Atom.to_string()
    |> String.replace("Elixir.", "")
  end
end

Also the error message says it requires filter options even if it has been passed. So, I figured out that the issues is in the error messages.

The error message should report missing destination_attribute instead of destination_field, and it should report missing filters instead of filter.

Error showing missing filter instead of filters

Error showing destination field missing nstead of destination_attribute

1 Like