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