128 lines
3.3 KiB
Elixir
128 lines
3.3 KiB
Elixir
defmodule Mv.Membership.MemberGroup do
|
|
@moduledoc """
|
|
Ash resource representing the join table for the many-to-many relationship
|
|
between Members and Groups.
|
|
|
|
## Overview
|
|
MemberGroup is a join table that links members to groups. It enables the
|
|
many-to-many relationship where:
|
|
- A member can belong to multiple groups
|
|
- A group can contain multiple members
|
|
|
|
## Attributes
|
|
- `member_id` - Foreign key to Member (required)
|
|
- `group_id` - Foreign key to Group (required)
|
|
|
|
## Relationships
|
|
- `belongs_to :member` - Relationship to Member
|
|
- `belongs_to :group` - Relationship to Group
|
|
|
|
## Constraints
|
|
- Unique constraint on `(member_id, group_id)` - prevents duplicate memberships
|
|
- CASCADE delete: Removing member removes all group associations
|
|
- CASCADE delete: Removing group removes all member associations
|
|
|
|
## Examples
|
|
# Add member to group
|
|
MemberGroup.create!(%{member_id: member.id, group_id: group.id})
|
|
|
|
# Remove member from group
|
|
member_group = MemberGroup.get_by_member_and_group!(member.id, group.id)
|
|
MemberGroup.destroy!(member_group)
|
|
"""
|
|
use Ash.Resource,
|
|
domain: Mv.Membership,
|
|
data_layer: AshPostgres.DataLayer
|
|
|
|
require Ash.Query
|
|
import Ash.Expr
|
|
|
|
postgres do
|
|
table "member_groups"
|
|
repo Mv.Repo
|
|
end
|
|
|
|
actions do
|
|
defaults [:read, :destroy]
|
|
|
|
create :create do
|
|
accept [:member_id, :group_id]
|
|
end
|
|
end
|
|
|
|
validations do
|
|
validate present(:member_id)
|
|
validate present(:group_id)
|
|
|
|
# Prevent duplicate associations
|
|
validate fn changeset, _context ->
|
|
member_id = Ash.Changeset.get_attribute(changeset, :member_id)
|
|
group_id = Ash.Changeset.get_attribute(changeset, :group_id)
|
|
current_id = Ash.Changeset.get_attribute(changeset, :id)
|
|
|
|
if member_id && group_id do
|
|
check_duplicate_association(member_id, group_id, current_id)
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
|
|
attributes do
|
|
uuid_v7_primary_key :id
|
|
|
|
attribute :member_id, :uuid do
|
|
allow_nil? false
|
|
end
|
|
|
|
attribute :group_id, :uuid do
|
|
allow_nil? false
|
|
end
|
|
|
|
timestamps()
|
|
end
|
|
|
|
relationships do
|
|
belongs_to :member, Mv.Membership.Member do
|
|
allow_nil? false
|
|
end
|
|
|
|
belongs_to :group, Mv.Membership.Group do
|
|
allow_nil? false
|
|
end
|
|
end
|
|
|
|
identities do
|
|
identity :unique_member_group, [:member_id, :group_id]
|
|
end
|
|
|
|
# Private helper function to check for duplicate associations
|
|
defp check_duplicate_association(member_id, group_id, exclude_id) do
|
|
alias Mv.Helpers
|
|
alias Mv.Helpers.SystemActor
|
|
|
|
query =
|
|
Mv.Membership.MemberGroup
|
|
|> Ash.Query.filter(member_id == ^member_id and group_id == ^group_id)
|
|
|> maybe_exclude_id(exclude_id)
|
|
|
|
system_actor = SystemActor.get_system_actor()
|
|
opts = Helpers.ash_actor_opts(system_actor)
|
|
|
|
case Ash.read(query, opts) do
|
|
{:ok, []} ->
|
|
:ok
|
|
|
|
{:ok, _} ->
|
|
{:error, field: :member_id, message: "Member is already in this group", value: member_id}
|
|
|
|
{:error, _reason} ->
|
|
# Fail-open: if query fails, allow operation to proceed
|
|
# Database constraint will catch duplicates anyway
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp maybe_exclude_id(query, nil), do: query
|
|
defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id)
|
|
end
|