feat: member user relation

This commit is contained in:
Moritz 2025-09-26 16:09:49 +02:00
parent ccac004fa4
commit b4fe4e4858
Signed by: moritz
GPG key ID: 1020A035E5DD0824
4 changed files with 296 additions and 1 deletions

View file

@ -13,7 +13,11 @@ defmodule Mv.Membership.Member do
create :create_member do
primary? true
# Properties can be created along with member
argument :properties, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
accept [
:first_name,
@ -32,12 +36,29 @@ defmodule Mv.Membership.Member do
]
change manage_relationship(:properties, type: :create)
# Manage the user relationship during member creation
change manage_relationship(:user, :user,
# Look up existing user and relate to it
on_lookup: :relate,
# Error if user doesn't exist in database
on_no_match: :error,
# Error if user is already linked to another member (prevents "stealing")
on_match: :error,
# If no user provided, that's fine (optional relationship)
on_missing: :ignore
)
end
update :update_member do
primary? true
# Required because custom validation function cannot be done atomically
require_atomic? false
# Properties can be updated or created along with member
argument :properties, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
accept [
:first_name,
@ -56,6 +77,18 @@ defmodule Mv.Membership.Member do
]
change manage_relationship(:properties, on_match: :update, on_no_match: :create)
# Manage the user relationship during member update
change manage_relationship(:user, :user,
# Look up existing user and relate to it
on_lookup: :relate,
# Error if user doesn't exist in database
on_no_match: :error,
# Error if user is already linked to another member (prevents "stealing")
on_match: :error,
# If no user provided, remove existing relationship (allows user removal)
on_missing: :unrelate
)
end
end
@ -67,6 +100,40 @@ defmodule Mv.Membership.Member do
validate present(:last_name)
validate present(:email)
# Prevent linking to a user that already has a member
# This validation prevents "stealing" users from other members by checking
# if the target user is already linked to a different member
# This is necessary because manage_relationship's on_match: :error only checks
# if the user is already linked to THIS specific member, not ANY member
validate fn changeset, _context ->
user_arg = Ash.Changeset.get_argument(changeset, :user)
if user_arg && user_arg[:id] do
user_id = user_arg[:id]
current_member_id = changeset.data.id
# Check the current state of the user in the database
case Ash.get(Mv.Accounts.User, user_id) do
# User is free to be linked
{:ok, %{member_id: nil}} ->
:ok
# User already linked to this member (update scenario)
{:ok, %{member_id: ^current_member_id}} ->
:ok
{:ok, %{member_id: _other_member_id}} ->
# User is linked to a different member - prevent "stealing"
{:error, field: :user, message: "User is already linked to another member"}
{:error, _} ->
{:error, field: :user, message: "User not found"}
end
else
:ok
end
end
# Birth date not in the future
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
where: [present(:birth_date)],
@ -170,5 +237,9 @@ defmodule Mv.Membership.Member do
relationships do
has_many :properties, Mv.Membership.Property
# 1:1 relationship - Member can optionally have one User
# This references the User's member_id attribute
# The relationship is optional (allow_nil? true by default)
has_one :user, Mv.Accounts.User
end
end