zac
I have two resources, Teams and Users, and these are related using the relationship resource Members. I’m trying to use manage_relationship rather than write a bunch of code.
Adding team members to a team worked “out of the box,” as expected with no problems. I’ve been stuck on deleting team members from a team for over a day… I’ve simplified the case a good bit, and am completely stuck. (I’ve been through the Ash docs, examples, and Ash Framework book looking for the solution… no luck).
Here’s the Members resource:
defmodule WasteWalk.Teams.Members do
use Ash.Resource,
otp_app: :waste_walk,
domain: WasteWalk.Teams,
data_layer: AshPostgres.DataLayer
postgres do
table "team_members"
repo WasteWalk.Repo
references do
reference :user, on_delete: :delete, index?: true
reference :team, on_delete: :delete
end
end
actions do
create :create, accept: [:user_id, :team_id], primary?: true
destroy :destroy, accept: [:user_id, :team_id], primary?: true
end
relationships do
belongs_to :user, WasteWalk.Accounts.User, primary_key?: true, allow_nil?: false
belongs_to :team, WasteWalk.Teams.Team, primary_key?: true, allow_nil?: false
actions do
defaults [:read, update: :*]
end
end
end
On the Team resource, here’s the relevant functions for adding and deleting team members:
defmodule WasteWalk.Teams.Team do
...
actions do
defaults [:read]
create :create do
accept [:name]
end
update :update do
accept [:name]
end
update :add_team_member do
argument :user_id, :uuid, allow_nil?: false
require_atomic? false
change manage_relationship(:user_id, :team_memberships, value_is_key: :user_id, type: :create)
end
destroy :remove_team_member do
argument :user_id, :uuid, allow_nil?: false
require_atomic? false
# change manage_relationship(:user_id, :team_memberships, value_is_key: :user_id, type: :remove)
# change manage_relationship(:user_id, :team_memberships, type: :append_and_remove)
change manage_relationship(:user_id, :team_memberships, type: :remove)
end
end
relationships do
has_many :team_memberships, WasteWalk.Teams.Members
has_many :sprints, WasteWalk.Sprints.Sprint
many_to_many :team_members, WasteWalk.Accounts.User do
join_relationship :team_memberships
source_attribute_on_join_resource :team_id
destination_attribute_on_join_resource :user_id
end
end
Note that in my first attempt, instead of destroy :remove_team_member I started with update :remove_team_member. This seemed to make sense since I’m not destroying the team, I’m just updating one of its relationships. In that case the first commented-out line (with value_is_key:) compiled, but ultimately resulted in:
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Unknown{path: [:user_id, 0], errors: [%Ash.Error.Unknown.UnknownError{error: "** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column \"team_id\" of relation \"team_members\" violates not-null constraint...
Which, in turn quickly led me down the path of using destroy (to delete the row defining the Members relationship). But that doesn’t work either… (see below).
I’ve written a a suite of tests – everything passing (creating teams, adding members, queries, etc). The only thing that’s failing is removing team members from a team.
In the :remove_team_member function you can see a few variations that I’ve tried (I’ve tried a lot of things here). The one that seems most straight-forward and obvious is currently uncommented, change manage_relationship(:user_id, :team_memberships, type: :remove).
Here’s the relevant test:
test "only allow team leads to manage membership", context do
{:ok, team} = WasteWalk.Teams.new_team("A team", actor: context.admin)
{:ok, team} = WasteWalk.Teams.add_team_member(team.id, context.new_team_member.id, actor: context.admin)
{:ok, team} = WasteWalk.Teams.add_team_member(team.id, context.new_team_lead.id, actor: context.admin)
team = Ash.load!(team, :team_memberships, actor: context.new_team_lead)
team |> IO.inspect(label: "team after adding new_team_member")
assert length(team.team_memberships) == 2
{:ok, _} = WasteWalk.Teams.remove_team_member(team.id, context.new_team_member.id, actor: context.admin)
|> IO.inspect(label: "remove_team_member call")
end
The test fails, returning:
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidRelationship{relationship: :team_memberships, message: "changes would create a new related record", splode: Ash.Error, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid}]}}
Oddly, if I change the manage_relationship call to use :append_and_remove I get this error:
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{primary_key: "1758e517-8a4e-40d8-89ac-2a3e9df9f860", resource: WasteWalk.Teams.Members, splode: Ash.Error, bread_crumbs: [], vars: [], path: [:user_id, 0], stacktrace: #Splode.Stacktrace<>, class: :invalid}]}}
Here’s the full test output:
team after adding new_team_member: %WasteWalk.Teams.Team{
id: "f9ab360a-94c4-41fb-a4fd-971a50ea458a",
name: "Team with Lead",
inserted_at: ~U[2025-08-12 07:50:38.774100Z],
updated_at: ~U[2025-08-12 07:50:38.774100Z],
is_member: #Ash.NotLoaded<:calculation, field: :is_member>,
team_memberships: [
%WasteWalk.Teams.Members{
user_id: "f3464150-0cb5-424e-a2a8-5bf822b60d16",
team_id: "f9ab360a-94c4-41fb-a4fd-971a50ea458a",
user: #Ash.NotLoaded<:relationship, field: :user>,
team: #Ash.NotLoaded<:relationship, field: :team>,
__meta__: #Ecto.Schema.Metadata<:loaded, "team_members">
},
%WasteWalk.Teams.Members{
user_id: "1758e517-8a4e-40d8-89ac-2a3e9df9f860",
team_id: "f9ab360a-94c4-41fb-a4fd-971a50ea458a",
user: #Ash.NotLoaded<:relationship, field: :user>,
team: #Ash.NotLoaded<:relationship, field: :team>,
__meta__: #Ecto.Schema.Metadata<:loaded, "team_members">
}
],
sprints: #Ash.NotLoaded<:relationship, field: :sprints>,
team_members: #Ash.NotLoaded<:relationship, field: :team_members>,
__meta__: #Ecto.Schema.Metadata<:loaded, "teams">
}
remove_team_member call: {:error,
%Ash.Error.Invalid{
errors: [
%Ash.Error.Query.NotFound{
primary_key: "1758e517-8a4e-40d8-89ac-2a3e9df9f860",
resource: WasteWalk.Teams.Members,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [:user_id, 0],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
}}
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixirconf-us
- #elixir-ls
- #ai
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming











Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
zachdaniel
A destroy action on team would destroy the team itself. I assume that isn’t what you want to do?
If you are managing
team_memberships, then you actually want to destroy that team membership, not just unrelate it from this row. So two options are:This would be my preferred.
zac
Well, I agree with you, and that’s what I initially tried.
But any attempt to add a
destroyaction toMembershasn’t worked. (See below)./edit/
If I make the change you indicated to
Team(usingteam_membersin my case, notmembers):That results in:
If I misunderstood, and you mean implement it on
Members, then… Above, you pasted in exactly the code I have onTeam. I’d have to change it to reference the correctbelongs_torelationship for it to work onMembers, like so:If I move that over to
MembersI get this:And in
iexwhen I poke around,Membersdoesn’t offer me much to work with:As close as I can tell, I’ve implemented a pretty simple case that exactly mirrors what’s documented in the Many to Many section of the Ash docs. The docs don’t show a
destroybut extrapolating, that (above) is exactly what I tried./edit/
Also… if it’s on
Teamthe API ofremove_team_member(user.id)makes sense. But moving it toMembersmeans I’ll need to change the API (I think?) todestroy(user.id, team.id). I feel like I tried that early on – but, it’s been a while… maybe I should go back and work on that…Still very confused.
zachdaniel
Sorry, its my bad, it should not be a destroy action.
zachdaniel
What you may need is to configure Postgres references block on the join table to make sure that they are removed properly when related things are destroyed.
Also: keep in mind that if managed relationships are causing you problems, you can always write some manual code to get around it.
zac
Yep. This is what I had, although I did it as a
before_action. Guess it makes more sense as anafter_action. So, with that change it would be:But this was so much code just to implement a
Team.remove_team_member()function, I thought there would be an easier way… hence diving down getting it working using the DSL alone.Also… yes, Postgres references are there on
Members, so that should work… I should probably build a test just to verify tho:zac
@zachdaniel, tried to take another crack at it. No go… they both generate errors.
In each case, I’m replacing the
:remove_team_memberaction on theTeamresource.Case #1: (had to edit to use
:team_membersin my code):I get this runtime error (note the pecular “changes would create a new related record”), which makes no sense to me (this is one of the problems I originally reported, above, in trying to do this with the DSL).
Case #2:
Causes a compiler error:
For now I’m leaving in my original non-DSL
change()call, which works fine (see previous reply, above). But like I said, it’s just so much code and I thought there would be an easy, DSL-oriented way to achieve the sameTeam.remove_team_member()API. (But, maybe not… which is fine, just a little surprising).Leaving this in:
And the
RemoveTeamMemberfunction (above reply) works./edit/
Just for clarity… that weird error that’s coming back on the first case (“changes would create a new related record”)… I said it makes no sense because it’s a
:destroyaction on themanage_relationships()call. That would be a really weird side effect. Maybe the error message is wrong, or maybe something is off internally? It seems like this should work…zachdaniel
Try adding
debug?: trueto your manage relationship opts. What you’re trying to do should absolutely work, there is something simple missing but it’s not jumping out at me. The “would create new records” is a horrible error message that I will look into. I believe it has to do with when your input doesn’t match an existing record?zac
Adding
debug?: truedidn’t add anything to the output…zachdaniel
Hmm…it was added recently but not that recently. It uses the
Logger.debug, is your log level set to debug?zac
Sorry for the slow response @zachdaniel … went in a changed
test.devto includelevel: :debugand now I’m getting this:I added a few
IO.inspect()messages to help clarify.First thing that jumps out at me is the “skipped query run due to filter being false,” which is a bit odd… keep in mind that the custom function (above) works just fine and as far as I can tell, they should be doing very nearly the same thing.
Just to see what would happen, I added:
At the top of the
policiesblock on bothTeamandMembersand it made no difference. (Well, it broke a bunch of other tests… but this test still fails in exactly the same way).So I don’t think it’s actually authorization related. Maybe.
(Actually, now I’m really curious if you would write the custom function in the same way…)