Manage_relationship in custom change not working

can someone please guide me for making it functionally working and then write it in better way?

I have a Organization resouce with users many_to_many relationship as below

actions do
  update :invite_users do
    argument :users, {:array, InviteUser},
      allow_nil?: true,
      description: "users of organization"

    require_atomic? false
    change InviteUsers
  end
...
end

relations
  many_to_many :users, Manwa.Accounts.User do
    through Manwa.Orgs.OrganizationUser
    source_attribute_on_join_resource :organization_id
    destination_attribute_on_join_resource :user_id
  end
....
end 

defmodule Manwa.Orgs.OrganizationUser do
  @moduledoc false
  use Manwa.ResourceWIthoutID,
    data_layer: AshPostgres.DataLayer,
    domain: Manwa.Orgs

  ....
  actions do
    defaults [:read, :destroy, create: :*, update: :*]
  end

  attributes do
    attribute :role, Manwa.Accounts.UserRole, primary_key?: true, allow_nil?: false, public?: true
  end

  relationships do
    belongs_to :organization, Manwa.Orgs.Organization,
      primary_key?: true,
      allow_nil?: false,
      attribute_type: :integer

    belongs_to :user, Manwa.Accounts.User,
      primary_key?: true,
      allow_nil?: false,
      attribute_type: :integer
  end
end

invite user change tries to

  • check if user has account in system, if not it creates account and then goes through manage_relationship change.

defmodule Manwa.Orgs.Changes.InviteUsers do
  use Ash.Resource.Change

  @password_length 16
  @impl true
  def change(changeset, opts, context) do
    map =
      Enum.reduce(changeset.arguments.users, [], fn invite, acc ->
        case Manwa.Accounts.get_user_by_email(invite.email) do
          {:ok, user} ->
            acc = [%{user_id: user.id, role: invite.role} | acc]

          _ ->
            random_password =
              @password_length
              |> :crypto.strong_rand_bytes()
              |> Base.encode64()
              |> binary_part(0, @password_length)

            {:ok, user} =
              Manwa.Accounts.register_with_password(
                invite.email,
                random_password,
                random_password,
                actor: context.actor
              )

            acc = [%{user_id: user.id, role: invite.role} | acc]
        end
      end)


    Ash.Changeset.manage_relationship(changeset, :users, map,
      on_lookup: :relate,
      join_keys: [:role, :user_id]
    )
  end

end

Calling the action

 Orgs.invite_users(org, %{users: [%{email: "a.com", role: :admin}, %{email: "b.com", role: :admin}]}, actor: super_user )
...

map #=> [
  %{role: "admin", user_id: 7308783368152281088},
  %{role: "admin", user_id: 7308783365732167680}
]

does not do the manage_relationship part ( does not insert data and changeset is valid)

You also need on_no_match: :create I think. Or just type: :direct_control which is

[
  on_lookup: :ignore,
  on_no_match: :create,
  on_match: :update,
  on_missing: :destroy
]  

You should also be able to move that logic into the action itself, something like
change manage_relationship(:organization_user, type: :direct_control)

well i am creating a user in changeset and I just want it to be related.

Yes. I mean instead of having

 change InviteUsers

and your custom changeset flow.
You could remove the part

    Ash.Changeset.manage_relationship(changeset, :users, map,
      on_lookup: :relate,
      join_keys: [:role, :user_id]
    )

and do that in the action:

change InviteUsers
change manage_relationship(:organization_user, type: :direct_control)

something like that. Not sure without the code, can you share your repo? That would be helpful.

thanks, ken
the reason i have a custom change is I am collecting user emails and then registering users if they are not registered already. I do this to set correct passwords and other details through register action.
I suspect Ash.Changeset.manage_relationship call is working as intended cause I can see relationships in changeset. but they are not being inserted in db

 relationships: %{
    users: [
      {[
         %{
           user: #Manwa.Accounts.User<
             user_roles: #Ash.NotLoaded<:calculation, field: :user_roles>,
             organizations: #Ash.NotLoaded<:relationship, field: :organizations>,
             organizations_join_assoc: #Ash.NotLoaded<:relationship, field: :organizations_join_assoc>,
             __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
             confirmed_at: nil,
             id: 7308783368152281088,
             email: #Ash.CiString<"b.com">,
             is_super_user?: false,
             aggregates: %{},
             calculations: %{},
             ...
           >,
           role: "admin"
         },
         %{
           user: #Manwa.Accounts.User<
             user_roles: #Ash.NotLoaded<:calculation, field: :user_roles>,
             organizations: #Ash.NotLoaded<:relationship, field: :organizations>,
             organizations_join_assoc: #Ash.NotLoaded<:relationship, field: :organizations_join_assoc>,
             __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
             confirmed_at: nil,
             id: 7308783365732167680,
             email: #Ash.CiString<"a.com">,
             is_super_user?: false,
             aggregates: %{},
             calculations: %{},
             ...
           >,
           role: "admin"
         }
       ],

Did you try adding on_no_match: :create option to your manage relationship call?

     {:ok, user} =
              Manwa.Accounts.register_with_password(
                invite.email,
                random_password,
                random_password,
                actor: context.actor
              )

            acc = [user | acc] # <- provide the user itself

and

    Ash.Changeset.manage_relationship(changeset, :users, map,
      on_lookup: :relate
#      join_keys: [:role, :user_id]  <- remove this
    )
1 Like

thanks zach but i need users role too, its an attribute on join table

defmodule Manwa.Orgs.OrganizationUser do
  @moduledoc false
  use Manwa.ResourceWIthoutID,
    data_layer: AshPostgres.DataLayer,
    domain: Manwa.Orgs

  postgres do
    table "organization_users"
    repo Manwa.Repo
  end

  actions do
    defaults [:read, :destroy, create: :*, update: :*]
  end

  attributes do
    attribute :role, Manwa.Accounts.UserRole, primary_key?: true, allow_nil?: false, public?: true
  end

  relationships do
    belongs_to :organization, Manwa.Orgs.Organization,
      primary_key?: true,
      allow_nil?: false,
      attribute_type: :integer

    belongs_to :user, Manwa.Accounts.User,
      primary_key?: true,
      allow_nil?: false,
      attribute_type: :integer
  end
end

Ah, okay. In that case, you want:

acc = [%{id: user.id, role: invite.role} | acc]

And join_keys should just be role I believe.

Thanks! that worked. so I was trying with user_id since thats the attribute on the join table.
now to second part of question. what will zachdaniel do diffrent to write it in better way ? :slight_smile:

Good question :smile: I would likely make a new action on the user resource called :register_with_random_password, and then I would do this:

argument :emails, {:array, :string}, allow_nil?: false

change manage_relationship(
  :emails, 
  :users, 
  on_no_match: {:create, :register_with_random_password}, 
  value_is_key: :email
)
1 Like

That’s a good call! thanks, i might make maybe_register_with_random_password which returns a list of newly registered users as well as existing users.
on the same topic I have this (maybe irrational) fear about manage_relationship and various on_* behaviors. perticularly on_missing: :unrelate.
let’s say
my resource has 100 tags
external api trying to add one more tag but forgetting to pass the existing tag will add a new tag but unrelate other 100’s of tags.

so I am creating two actions add_tags and remove_tags for every resource. Is that a correct way of thinking ?

The default option for all options to manage_relationship is :ignore. I think it makes a lot of sense if you are worried about bad data to prefer add_ and remove_ arguments, but in this particular example where there is no on_missing behavior, I wouldn’t worry about it.

1 Like