refactor: email sync changes
This commit is contained in:
parent
2693f67d33
commit
001fca1d16
7 changed files with 108 additions and 121 deletions
|
|
@ -93,8 +93,11 @@ defmodule Mv.Accounts.User do
|
||||||
# complex checks that are not supported in atomic operations.
|
# complex checks that are not supported in atomic operations.
|
||||||
require_atomic? false
|
require_atomic? false
|
||||||
|
|
||||||
# Sync email changes to linked member
|
# Sync email changes to linked member (User → Member)
|
||||||
change Mv.Accounts.User.Changes.SyncEmailToMember
|
# Only runs when email is being changed
|
||||||
|
change Mv.EmailSync.Changes.SyncUserEmailToMember do
|
||||||
|
where [changing(:email)]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
create :create_user do
|
create :create_user do
|
||||||
|
|
@ -118,8 +121,8 @@ defmodule Mv.Accounts.User do
|
||||||
on_missing: :ignore
|
on_missing: :ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override member email with user email when linking
|
# Sync user email to member when linking (User → Member)
|
||||||
change Mv.Accounts.User.Changes.OverrideMemberEmailOnLink
|
change Mv.EmailSync.Changes.SyncUserEmailToMember
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_user do
|
update :update_user do
|
||||||
|
|
@ -147,10 +150,11 @@ defmodule Mv.Accounts.User do
|
||||||
on_missing: :unrelate
|
on_missing: :unrelate
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sync email changes to linked member
|
# Sync email changes and handle linking (User → Member)
|
||||||
change Mv.Accounts.User.Changes.SyncEmailToMember
|
# Runs when email OR member relationship changes
|
||||||
# Override member email with user email when linking
|
change Mv.EmailSync.Changes.SyncUserEmailToMember do
|
||||||
change Mv.Accounts.User.Changes.OverrideMemberEmailOnLink
|
where any([changing(:email), changing(:member)])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Admin action for direct password changes in admin panel
|
# Admin action for direct password changes in admin panel
|
||||||
|
|
@ -200,8 +204,8 @@ defmodule Mv.Accounts.User do
|
||||||
|> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"])
|
|> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Override member email with user email when linking (if member relationship exists)
|
# Sync user email to member when linking (User → Member)
|
||||||
change Mv.Accounts.User.Changes.OverrideMemberEmailOnLink
|
change Mv.EmailSync.Changes.SyncUserEmailToMember
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -281,9 +285,8 @@ defmodule Mv.Accounts.User do
|
||||||
# User.email is the source of truth - when a link is established, member.email
|
# User.email is the source of truth - when a link is established, member.email
|
||||||
# is overridden to match user.email. Subsequent changes to either email will
|
# is overridden to match user.email. Subsequent changes to either email will
|
||||||
# sync to the other resource.
|
# sync to the other resource.
|
||||||
# See: Mv.Accounts.User.Changes.SyncEmailToMember
|
# See: Mv.EmailSync.Changes.SyncUserEmailToMember
|
||||||
# Mv.Accounts.User.Changes.OverrideMemberEmailOnLink
|
# Mv.EmailSync.Changes.SyncMemberEmailToUser
|
||||||
# Mv.Membership.Member.Changes.SyncEmailToUser
|
|
||||||
attribute :email, :ci_string do
|
attribute :email, :ci_string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
public? true
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,11 @@ defmodule Mv.Membership.Member do
|
||||||
on_missing: :ignore
|
on_missing: :ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override member email with user email when linking
|
# Sync user email to member when linking (User → Member)
|
||||||
change Mv.Membership.Member.Changes.OverrideEmailFromUserOnLink
|
# Only runs when user relationship is being changed
|
||||||
|
change Mv.EmailSync.Changes.SyncUserEmailToMember do
|
||||||
|
where [changing(:user)]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_member do
|
update :update_member do
|
||||||
|
|
@ -93,10 +96,17 @@ defmodule Mv.Membership.Member do
|
||||||
on_missing: :unrelate
|
on_missing: :unrelate
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override member email with user email when linking
|
# Sync member email to user when email changes (Member → User)
|
||||||
change Mv.Membership.Member.Changes.OverrideEmailFromUserOnLink
|
# Only runs when email is being changed
|
||||||
# Sync email changes to linked user
|
change Mv.EmailSync.Changes.SyncMemberEmailToUser do
|
||||||
change Mv.Membership.Member.Changes.SyncEmailToUser
|
where [changing(:email)]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sync user email to member when linking (User → Member)
|
||||||
|
# Only runs when user relationship is being changed
|
||||||
|
change Mv.EmailSync.Changes.SyncUserEmailToMember do
|
||||||
|
where [changing(:user)]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -206,9 +216,8 @@ defmodule Mv.Membership.Member do
|
||||||
# User.email is the source of truth - when a link is established, member.email
|
# User.email is the source of truth - when a link is established, member.email
|
||||||
# is overridden to match user.email. Subsequent changes to either email will
|
# is overridden to match user.email. Subsequent changes to either email will
|
||||||
# sync to the other resource.
|
# sync to the other resource.
|
||||||
# See: Mv.Membership.Member.Changes.SyncEmailToUser
|
# See: Mv.EmailSync.Changes.SyncUserEmailToMember
|
||||||
# Mv.Membership.Member.Changes.OverrideEmailFromUserOnLink
|
# Mv.EmailSync.Changes.SyncMemberEmailToUser
|
||||||
# Mv.Accounts.User.Changes.SyncEmailToMember
|
|
||||||
attribute :email, :string do
|
attribute :email, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
constraints min_length: 5, max_length: 254
|
constraints min_length: 5, max_length: 254
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
defmodule Mv.Accounts.User.Changes.OverrideMemberEmailOnLink do
|
|
||||||
@moduledoc """
|
|
||||||
Overrides member email with user email when linking.
|
|
||||||
User.email is the source of truth when a link is established.
|
|
||||||
"""
|
|
||||||
use Ash.Resource.Change
|
|
||||||
alias Mv.EmailSync.{Helpers, Loader}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def change(changeset, _opts, context) do
|
|
||||||
if Map.get(context, :syncing_email, false) do
|
|
||||||
changeset
|
|
||||||
else
|
|
||||||
override_email(changeset)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp override_email(changeset) do
|
|
||||||
Ash.Changeset.around_transaction(changeset, fn cs, callback ->
|
|
||||||
result = callback.(cs)
|
|
||||||
|
|
||||||
with {:ok, user} <- Helpers.extract_record(result),
|
|
||||||
linked_member <- Loader.get_linked_member(user) do
|
|
||||||
Helpers.sync_email_to_linked_record(result, linked_member, user.email)
|
|
||||||
else
|
|
||||||
_ -> result
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
defmodule Mv.Accounts.User.Changes.SyncEmailToMember do
|
|
||||||
@moduledoc """
|
|
||||||
Synchronizes user email changes to the linked member.
|
|
||||||
Uses `around_transaction` for atomicity - both updates in the same transaction.
|
|
||||||
"""
|
|
||||||
use Ash.Resource.Change
|
|
||||||
alias Mv.EmailSync.{Helpers, Loader}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def change(changeset, _opts, context) do
|
|
||||||
cond do
|
|
||||||
Map.get(context, :syncing_email, false) -> changeset
|
|
||||||
not Ash.Changeset.changing_attribute?(changeset, :email) -> changeset
|
|
||||||
true -> sync_email(changeset)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp sync_email(changeset) do
|
|
||||||
new_email = Ash.Changeset.get_attribute(changeset, :email)
|
|
||||||
|
|
||||||
Ash.Changeset.around_transaction(changeset, fn cs, callback ->
|
|
||||||
result = callback.(cs)
|
|
||||||
|
|
||||||
with {:ok, user} <- Helpers.extract_record(result),
|
|
||||||
linked_member <- Loader.get_linked_member(user) do
|
|
||||||
Helpers.sync_email_to_linked_record(result, linked_member, new_email)
|
|
||||||
else
|
|
||||||
_ -> result
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
defmodule Mv.Membership.Member.Changes.SyncEmailToUser do
|
defmodule Mv.EmailSync.Changes.SyncMemberEmailToUser do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Synchronizes member email changes to the linked user.
|
Synchronizes Member.email → User.email
|
||||||
Uses `around_transaction` for atomicity - both updates in the same transaction.
|
|
||||||
|
Trigger conditions are configured in resources via `where` clauses:
|
||||||
|
- Member resource: Use `where: [changing(:email)]`
|
||||||
|
|
||||||
|
Used by Member resource for bidirectional email sync.
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Change
|
use Ash.Resource.Change
|
||||||
alias Mv.EmailSync.{Helpers, Loader}
|
alias Mv.EmailSync.{Helpers, Loader}
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def change(changeset, _opts, context) do
|
def change(changeset, _opts, context) do
|
||||||
cond do
|
# Only recursion protection needed - trigger logic is in `where` clauses
|
||||||
Map.get(context, :syncing_email, false) -> changeset
|
if Map.get(context, :syncing_email, false) do
|
||||||
not Ash.Changeset.changing_attribute?(changeset, :email) -> changeset
|
changeset
|
||||||
true -> sync_email(changeset)
|
else
|
||||||
|
sync_email(changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
62
lib/mv/email_sync/changes/sync_user_email_to_member.ex
Normal file
62
lib/mv/email_sync/changes/sync_user_email_to_member.ex
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
defmodule Mv.EmailSync.Changes.SyncUserEmailToMember do
|
||||||
|
@moduledoc """
|
||||||
|
Synchronizes User.email → Member.email
|
||||||
|
User.email is always the source of truth.
|
||||||
|
|
||||||
|
Trigger conditions are configured in resources via `where` clauses:
|
||||||
|
- User resource: Use `where: [changing(:email)]` or `where: any([changing(:email), changing(:member)])`
|
||||||
|
- Member resource: Use `where: [changing(:user)]`
|
||||||
|
|
||||||
|
Can be used by both User and Member resources.
|
||||||
|
"""
|
||||||
|
use Ash.Resource.Change
|
||||||
|
alias Mv.EmailSync.{Helpers, Loader}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def change(changeset, _opts, context) do
|
||||||
|
# Only recursion protection needed - trigger logic is in `where` clauses
|
||||||
|
if Map.get(context, :syncing_email, false) do
|
||||||
|
changeset
|
||||||
|
else
|
||||||
|
sync_email(changeset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sync_email(changeset) do
|
||||||
|
Ash.Changeset.around_transaction(changeset, fn cs, callback ->
|
||||||
|
result = callback.(cs)
|
||||||
|
|
||||||
|
with {:ok, record} <- Helpers.extract_record(result),
|
||||||
|
{:ok, user, member} <- get_user_and_member(record) do
|
||||||
|
# When called from Member-side, we need to update the member in the result
|
||||||
|
# When called from User-side, we update the linked member in DB only
|
||||||
|
case record do
|
||||||
|
%Mv.Membership.Member{} ->
|
||||||
|
# Member-side: Override member email in result with user email
|
||||||
|
Helpers.override_with_linked_email(result, user.email)
|
||||||
|
|
||||||
|
%Mv.Accounts.User{} ->
|
||||||
|
# User-side: Sync user email to linked member in DB
|
||||||
|
Helpers.sync_email_to_linked_record(result, member, user.email)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_ -> result
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Retrieves user and member - works for both resource types
|
||||||
|
defp get_user_and_member(%Mv.Accounts.User{} = user) do
|
||||||
|
case Loader.get_linked_member(user) do
|
||||||
|
nil -> {:error, :no_member}
|
||||||
|
member -> {:ok, user, member}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_user_and_member(%Mv.Membership.Member{} = member) do
|
||||||
|
case Loader.load_linked_user!(member) do
|
||||||
|
{:ok, user} -> {:ok, user, member}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
defmodule Mv.Membership.Member.Changes.OverrideEmailFromUserOnLink do
|
|
||||||
@moduledoc """
|
|
||||||
Overrides member email with user email when linking.
|
|
||||||
User.email is the source of truth when a link is established.
|
|
||||||
"""
|
|
||||||
use Ash.Resource.Change
|
|
||||||
alias Mv.EmailSync.{Helpers, Loader}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def change(changeset, _opts, context) do
|
|
||||||
if Map.get(context, :syncing_email, false) do
|
|
||||||
changeset
|
|
||||||
else
|
|
||||||
override_email(changeset)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp override_email(changeset) do
|
|
||||||
Ash.Changeset.around_transaction(changeset, fn cs, callback ->
|
|
||||||
result = callback.(cs)
|
|
||||||
|
|
||||||
with {:ok, member} <- Helpers.extract_record(result),
|
|
||||||
{:ok, user} <- Loader.load_linked_user!(member) do
|
|
||||||
Helpers.override_with_linked_email(result, user.email)
|
|
||||||
else
|
|
||||||
_ -> result
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue