refactor: fix review issues - member_count aggregate, migration down, docs, actor handling
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-01-27 17:09:07 +01:00
parent fc8306cfee
commit e92c98b559
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
3 changed files with 37 additions and 32 deletions

View file

@ -67,12 +67,12 @@ defmodule Mv.Membership.Group do
validate present(:name) validate present(:name)
# Case-insensitive name uniqueness validation # Case-insensitive name uniqueness validation
validate fn changeset, _context -> validate fn changeset, context ->
name = Ash.Changeset.get_attribute(changeset, :name) name = Ash.Changeset.get_attribute(changeset, :name)
current_id = Ash.Changeset.get_attribute(changeset, :id) current_id = Ash.Changeset.get_attribute(changeset, :id)
if name do if name do
check_name_uniqueness(name, current_id) check_name_uniqueness(name, current_id, context)
else else
:ok :ok
end end
@ -115,24 +115,8 @@ defmodule Mv.Membership.Group do
many_to_many :members, Mv.Membership.Member, through: Mv.Membership.MemberGroup many_to_many :members, Mv.Membership.Member, through: Mv.Membership.MemberGroup
end end
calculations do aggregates do
calculate :member_count, :integer do count :member_count, :member_groups
description "Number of members in this group"
calculation fn [group], _context ->
system_actor = SystemActor.get_system_actor()
opts = Helpers.ash_actor_opts(system_actor)
query =
Mv.Membership.MemberGroup
|> Ash.Query.filter(group_id == ^group.id)
case Ash.read(query, opts) do
{:ok, member_groups} -> [length(member_groups)]
{:error, _} -> [0]
end
end
end
end end
identities do identities do
@ -140,14 +124,21 @@ defmodule Mv.Membership.Group do
end end
# Private helper function for case-insensitive name uniqueness check # Private helper function for case-insensitive name uniqueness check
defp check_name_uniqueness(name, exclude_id) do # Uses context actor if available (respects policies), falls back to system actor
defp check_name_uniqueness(name, exclude_id, context) do
# Use context actor if available (respects user permissions), otherwise fall back to system actor
actor =
case context do
%{actor: actor} when not is_nil(actor) -> actor
_ -> SystemActor.get_system_actor()
end
query = query =
Mv.Membership.Group Mv.Membership.Group
|> Ash.Query.filter(fragment("LOWER(?) = LOWER(?)", name, ^name)) |> Ash.Query.filter(fragment("LOWER(?) = LOWER(?)", name, ^name))
|> maybe_exclude_id(exclude_id) |> maybe_exclude_id(exclude_id)
system_actor = SystemActor.get_system_actor() opts = Helpers.ash_actor_opts(actor)
opts = Helpers.ash_actor_opts(system_actor)
case Ash.read(query, opts) do case Ash.read(query, opts) do
{:ok, []} -> {:ok, []} ->

View file

@ -24,11 +24,18 @@ defmodule Mv.Membership.MemberGroup do
## Examples ## Examples
# Add member to group # Add member to group
MemberGroup.create!(%{member_id: member.id, group_id: group.id}) {:ok, member_group} =
Membership.create_member_group(%{member_id: member.id, group_id: group.id})
# Remove member from group # Remove member from group
member_group = MemberGroup.get_by_member_and_group!(member.id, group.id) {:ok, [member_group]} =
MemberGroup.destroy!(member_group) Ash.read(
Mv.Membership.MemberGroup
|> Ash.Query.filter(member_id == ^member.id and group_id == ^group.id),
domain: Mv.Membership
)
:ok = Membership.destroy_member_group(member_group)
""" """
use Ash.Resource, use Ash.Resource,
domain: Mv.Membership, domain: Mv.Membership,
@ -54,13 +61,13 @@ defmodule Mv.Membership.MemberGroup do
validate present(:group_id) validate present(:group_id)
# Prevent duplicate associations # Prevent duplicate associations
validate fn changeset, _context -> validate fn changeset, context ->
member_id = Ash.Changeset.get_attribute(changeset, :member_id) member_id = Ash.Changeset.get_attribute(changeset, :member_id)
group_id = Ash.Changeset.get_attribute(changeset, :group_id) group_id = Ash.Changeset.get_attribute(changeset, :group_id)
current_id = Ash.Changeset.get_attribute(changeset, :id) current_id = Ash.Changeset.get_attribute(changeset, :id)
if member_id && group_id do if member_id && group_id do
check_duplicate_association(member_id, group_id, current_id) check_duplicate_association(member_id, group_id, current_id, context)
else else
:ok :ok
end end
@ -96,17 +103,24 @@ defmodule Mv.Membership.MemberGroup do
end end
# Private helper function to check for duplicate associations # Private helper function to check for duplicate associations
defp check_duplicate_association(member_id, group_id, exclude_id) do # Uses context actor if available (respects policies), falls back to system actor
defp check_duplicate_association(member_id, group_id, exclude_id, context) do
alias Mv.Helpers alias Mv.Helpers
alias Mv.Helpers.SystemActor alias Mv.Helpers.SystemActor
# Use context actor if available (respects user permissions), otherwise fall back to system actor
actor =
case context do
%{actor: actor} when not is_nil(actor) -> actor
_ -> SystemActor.get_system_actor()
end
query = query =
Mv.Membership.MemberGroup Mv.Membership.MemberGroup
|> Ash.Query.filter(member_id == ^member_id and group_id == ^group_id) |> Ash.Query.filter(member_id == ^member_id and group_id == ^group_id)
|> maybe_exclude_id(exclude_id) |> maybe_exclude_id(exclude_id)
system_actor = SystemActor.get_system_actor() opts = Helpers.ash_actor_opts(actor)
opts = Helpers.ash_actor_opts(system_actor)
case Ash.read(query, opts) do case Ash.read(query, opts) do
{:ok, []} -> {:ok, []} ->

View file

@ -82,7 +82,7 @@ defmodule Mv.Repo.Migrations.AddGroupsAndMemberGroups do
end end
def down do def down do
execute("DROP INDEX IF EXISTS groups_unique_name_lower_index", "") execute("DROP INDEX IF EXISTS groups_unique_name_lower_index")
drop_if_exists unique_index(:groups, [:slug], name: "groups_unique_slug_index") drop_if_exists unique_index(:groups, [:slug], name: "groups_unique_slug_index")