Compare commits
4 commits
37c948c91a
...
84abe0a4b5
| Author | SHA1 | Date | |
|---|---|---|---|
| 84abe0a4b5 | |||
| e2bbe1fb40 | |||
| 21e7a46881 | |||
| bec7e705e8 |
8 changed files with 411 additions and 9 deletions
|
|
@ -60,15 +60,54 @@ defmodule Mv.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:read, :create, :destroy, :update]
|
defaults [:read, :create, :destroy]
|
||||||
|
|
||||||
|
update :update do
|
||||||
|
primary? true
|
||||||
|
require_atomic? false
|
||||||
|
end
|
||||||
|
|
||||||
create :create_user do
|
create :create_user do
|
||||||
|
# Only accept email directly - member_id is NOT in accept list
|
||||||
|
# This prevents direct foreign key manipulation, forcing use of manage_relationship
|
||||||
accept [:email]
|
accept [:email]
|
||||||
|
# Allow member to be passed as argument for relationship management
|
||||||
|
argument :member, :map, allow_nil?: true
|
||||||
upsert? true
|
upsert? true
|
||||||
|
|
||||||
|
# Manage the member relationship during user creation
|
||||||
|
change manage_relationship(:member, :member,
|
||||||
|
# Look up existing member and relate to it
|
||||||
|
on_lookup: :relate,
|
||||||
|
# Error if member doesn't exist in database
|
||||||
|
on_no_match: :error,
|
||||||
|
# If member already linked to this user, ignore (shouldn't happen in create)
|
||||||
|
on_match: :ignore,
|
||||||
|
# If no member provided, that's fine (optional relationship)
|
||||||
|
on_missing: :ignore
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_user do
|
update :update_user do
|
||||||
|
# Only accept email directly - member_id is NOT in accept list
|
||||||
|
# This prevents direct foreign key manipulation, forcing use of manage_relationship
|
||||||
accept [:email]
|
accept [:email]
|
||||||
|
# Allow member to be passed as argument for relationship management
|
||||||
|
argument :member, :map, allow_nil?: true
|
||||||
|
# Required because custom validation function cannot be done atomically
|
||||||
|
require_atomic? false
|
||||||
|
|
||||||
|
# Manage the member relationship during user update
|
||||||
|
change manage_relationship(:member, :member,
|
||||||
|
# Look up existing member and relate to it
|
||||||
|
on_lookup: :relate,
|
||||||
|
# Error if member doesn't exist in database
|
||||||
|
on_no_match: :error,
|
||||||
|
# If same member provided, that's fine (allows updates with same member)
|
||||||
|
on_match: :ignore,
|
||||||
|
# If no member provided, remove existing relationship (allows member removal)
|
||||||
|
on_missing: :unrelate
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Admin action for direct password changes in admin panel
|
# Admin action for direct password changes in admin panel
|
||||||
|
|
@ -76,6 +115,7 @@ defmodule Mv.Accounts.User do
|
||||||
update :admin_set_password do
|
update :admin_set_password do
|
||||||
accept [:email]
|
accept [:email]
|
||||||
argument :password, :string, allow_nil?: false, sensitive?: true
|
argument :password, :string, allow_nil?: false, sensitive?: true
|
||||||
|
require_atomic? false
|
||||||
|
|
||||||
# Set the strategy context that HashPasswordChange expects
|
# Set the strategy context that HashPasswordChange expects
|
||||||
change set_context(%{strategy_name: :password})
|
change set_context(%{strategy_name: :password})
|
||||||
|
|
@ -125,6 +165,28 @@ defmodule Mv.Accounts.User do
|
||||||
validate string_length(:password, min: 8) do
|
validate string_length(:password, min: 8) do
|
||||||
where action_is([:register_with_password, :admin_set_password])
|
where action_is([:register_with_password, :admin_set_password])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Prevent overwriting existing member relationship
|
||||||
|
# This validation ensures race condition safety by requiring explicit two-step process:
|
||||||
|
# 1. Remove existing member (set member to nil)
|
||||||
|
# 2. Add new member
|
||||||
|
# This prevents accidental overwrites when multiple admins work simultaneously
|
||||||
|
validate fn changeset, _context ->
|
||||||
|
member_arg = Ash.Changeset.get_argument(changeset, :member)
|
||||||
|
current_member_id = changeset.data.member_id
|
||||||
|
|
||||||
|
# Only trigger if:
|
||||||
|
# - member argument is provided AND has an ID
|
||||||
|
# - user currently has a member
|
||||||
|
# - the new member ID is different from current member ID
|
||||||
|
if member_arg && member_arg[:id] && current_member_id &&
|
||||||
|
member_arg[:id] != current_member_id do
|
||||||
|
{:error,
|
||||||
|
field: :member, message: "User already has a member. Remove existing member first."}
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_oidc_id_present(changeset, _context) do
|
def validate_oidc_id_present(changeset, _context) do
|
||||||
|
|
@ -146,12 +208,16 @@ defmodule Mv.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
|
# 1:1 relationship - User can optionally belong to one Member
|
||||||
|
# This automatically creates a `member_id` attribute in the User table
|
||||||
|
# The relationship is optional (allow_nil? true by default)
|
||||||
belongs_to :member, Mv.Membership.Member
|
belongs_to :member, Mv.Membership.Member
|
||||||
end
|
end
|
||||||
|
|
||||||
identities do
|
identities do
|
||||||
identity :unique_email, [:email]
|
identity :unique_email, [:email]
|
||||||
identity :unique_oidc_id, [:oidc_id]
|
identity :unique_oidc_id, [:oidc_id]
|
||||||
|
identity :unique_member, [:member_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
# You can customize this if you wish, but this is a safe default that
|
# You can customize this if you wish, but this is a safe default that
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ defmodule Mv.Membership.Member do
|
||||||
|
|
||||||
create :create_member do
|
create :create_member do
|
||||||
primary? true
|
primary? true
|
||||||
|
# Properties can be created along with member
|
||||||
argument :properties, {:array, :map}
|
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 [
|
accept [
|
||||||
:first_name,
|
:first_name,
|
||||||
|
|
@ -32,12 +36,29 @@ defmodule Mv.Membership.Member do
|
||||||
]
|
]
|
||||||
|
|
||||||
change manage_relationship(:properties, type: :create)
|
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
|
end
|
||||||
|
|
||||||
update :update_member do
|
update :update_member do
|
||||||
primary? true
|
primary? true
|
||||||
|
# Required because custom validation function cannot be done atomically
|
||||||
require_atomic? false
|
require_atomic? false
|
||||||
|
# Properties can be updated or created along with member
|
||||||
argument :properties, {:array, :map}
|
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 [
|
accept [
|
||||||
:first_name,
|
:first_name,
|
||||||
|
|
@ -56,6 +77,18 @@ defmodule Mv.Membership.Member do
|
||||||
]
|
]
|
||||||
|
|
||||||
change manage_relationship(:properties, on_match: :update, on_no_match: :create)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -67,6 +100,40 @@ defmodule Mv.Membership.Member do
|
||||||
validate present(:last_name)
|
validate present(:last_name)
|
||||||
validate present(:email)
|
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
|
# Birth date not in the future
|
||||||
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
|
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
|
||||||
where: [present(:birth_date)],
|
where: [present(:birth_date)],
|
||||||
|
|
@ -170,5 +237,9 @@ defmodule Mv.Membership.Member do
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
has_many :properties, Mv.Membership.Property
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,16 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
<:item title={gettext("Street")}>{@member.street}</:item>
|
<:item title={gettext("Street")}>{@member.street}</:item>
|
||||||
<:item title={gettext("House Number")}>{@member.house_number}</:item>
|
<:item title={gettext("House Number")}>{@member.house_number}</:item>
|
||||||
<:item title={gettext("Postal Code")}>{@member.postal_code}</:item>
|
<:item title={gettext("Postal Code")}>{@member.postal_code}</:item>
|
||||||
|
<:item title={gettext("Linked User")}>
|
||||||
|
<%= if @member.user do %>
|
||||||
|
<.link navigate={~p"/users/#{@member.user}"} class="text-blue-600 hover:text-blue-800 underline">
|
||||||
|
<.icon name="hero-user" class="h-4 w-4 inline mr-1" />
|
||||||
|
{@member.user.email}
|
||||||
|
</.link>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-gray-500 italic">{gettext("No user linked")}</span>
|
||||||
|
<% end %>
|
||||||
|
</:item>
|
||||||
</.list>
|
</.list>
|
||||||
|
|
||||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
||||||
|
|
@ -67,7 +77,7 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
query =
|
query =
|
||||||
Mv.Membership.Member
|
Mv.Membership.Member
|
||||||
|> filter(id == ^id)
|
|> filter(id == ^id)
|
||||||
|> load(properties: [:property_type])
|
|> load([:user, properties: [:property_type]])
|
||||||
|
|
||||||
member = Ash.read_one!(query)
|
member = Ash.read_one!(query)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,16 @@ defmodule MvWeb.UserLive.Show do
|
||||||
<:item title={gettext("Password Authentication")}>
|
<:item title={gettext("Password Authentication")}>
|
||||||
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
|
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
|
||||||
</:item>
|
</:item>
|
||||||
|
<:item title={gettext("Linked Member")}>
|
||||||
|
<%= if @user.member do %>
|
||||||
|
<.link navigate={~p"/members/#{@user.member}"} class="text-blue-600 hover:text-blue-800 underline">
|
||||||
|
<.icon name="hero-users" class="h-4 w-4 inline mr-1" />
|
||||||
|
{@user.member.first_name} {@user.member.last_name}
|
||||||
|
</.link>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-gray-500 italic">{gettext("No member linked")}</span>
|
||||||
|
<% end %>
|
||||||
|
</:item>
|
||||||
</.list>
|
</.list>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
|
|
@ -33,9 +43,11 @@ defmodule MvWeb.UserLive.Show do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"id" => id}, _session, socket) do
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
|
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member])
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, gettext("Show User"))
|
|> assign(:page_title, gettext("Show User"))
|
||||||
|> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))}
|
|> assign(:user, user)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
17
priv/repo/migrations/20250926164519_member_relation.exs
Normal file
17
priv/repo/migrations/20250926164519_member_relation.exs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule Mv.Repo.Migrations.MemberRelation do
|
||||||
|
@moduledoc """
|
||||||
|
Updates resources based on their most recent snapshots.
|
||||||
|
|
||||||
|
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
create unique_index(:users, [:member_id], name: "users_unique_member_index")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop_if_exists unique_index(:users, [:member_id], name: "users_unique_member_index")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -92,3 +92,86 @@ for member_attrs <- [
|
||||||
] do
|
] do
|
||||||
Membership.create_member!(member_attrs)
|
Membership.create_member!(member_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Create additional users for user-member linking examples
|
||||||
|
additional_users = [
|
||||||
|
%{email: "hans.mueller@example.de"},
|
||||||
|
%{email: "greta.schmidt@example.de"},
|
||||||
|
%{email: "maria.weber@example.de"},
|
||||||
|
%{email: "thomas.klein@example.de"}
|
||||||
|
]
|
||||||
|
|
||||||
|
created_users =
|
||||||
|
Enum.map(additional_users, fn user_attrs ->
|
||||||
|
Accounts.create_user!(user_attrs, upsert?: true, upsert_identity: :unique_email)
|
||||||
|
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|
||||||
|
|> Ash.update!()
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Create members with linked users to demonstrate the 1:1 relationship
|
||||||
|
# Only create if users don't already have members
|
||||||
|
linked_members = [
|
||||||
|
%{
|
||||||
|
first_name: "Maria",
|
||||||
|
last_name: "Weber",
|
||||||
|
email: "maria.weber@example.de",
|
||||||
|
birth_date: ~D[1992-07-14],
|
||||||
|
join_date: ~D[2023-03-15],
|
||||||
|
paid: true,
|
||||||
|
phone_number: "+49301357924",
|
||||||
|
city: "Frankfurt",
|
||||||
|
street: "Goetheplatz",
|
||||||
|
house_number: "5",
|
||||||
|
postal_code: "60313",
|
||||||
|
notes: "Linked to user account",
|
||||||
|
# Link to the third user (maria.weber@example.de)
|
||||||
|
user: Enum.at(created_users, 2)
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
first_name: "Thomas",
|
||||||
|
last_name: "Klein",
|
||||||
|
email: "thomas.klein@example.de",
|
||||||
|
birth_date: ~D[1988-12-03],
|
||||||
|
join_date: ~D[2023-04-01],
|
||||||
|
paid: false,
|
||||||
|
phone_number: "+49302468135",
|
||||||
|
city: "Köln",
|
||||||
|
street: "Rheinstraße",
|
||||||
|
house_number: "23",
|
||||||
|
postal_code: "50667",
|
||||||
|
notes: "Linked to user account - needs payment follow-up",
|
||||||
|
# Link to the fourth user (thomas.klein@example.de)
|
||||||
|
user: Enum.at(created_users, 3)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create the linked members only if the users don't already have members
|
||||||
|
Enum.each(linked_members, fn member_attrs ->
|
||||||
|
user = member_attrs.user
|
||||||
|
member_attrs_without_user = Map.delete(member_attrs, :user)
|
||||||
|
|
||||||
|
# Check if user already has a member
|
||||||
|
if user.member_id == nil do
|
||||||
|
# User is free, create member and link
|
||||||
|
Membership.create_member!(Map.put(member_attrs_without_user, :user, %{id: user.id}))
|
||||||
|
else
|
||||||
|
# User already has a member, just create the member without linking
|
||||||
|
Membership.create_member!(member_attrs_without_user)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
IO.puts("✅ Seeds completed successfully!")
|
||||||
|
IO.puts("📝 Created sample data:")
|
||||||
|
IO.puts(" - Property types: String, Date, Boolean, Email")
|
||||||
|
IO.puts(" - Admin user: admin@mv.local (password: testpassword)")
|
||||||
|
IO.puts(" - Sample members: Hans, Greta, Friedrich")
|
||||||
|
|
||||||
|
IO.puts(
|
||||||
|
" - Additional users: hans.mueller@example.de, greta.schmidt@example.de, maria.weber@example.de, thomas.klein@example.de"
|
||||||
|
)
|
||||||
|
|
||||||
|
IO.puts(
|
||||||
|
" - Linked members: Maria Weber ↔ maria.weber@example.de, Thomas Klein ↔ thomas.klein@example.de"
|
||||||
|
)
|
||||||
|
|
||||||
|
IO.puts("🔗 Visit the application to see user-member relationships in action!")
|
||||||
|
|
|
||||||
141
priv/resource_snapshots/repo/users/20250926164519.json
Normal file
141
priv/resource_snapshots/repo/users/20250926164519.json
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"gen_random_uuid()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "email",
|
||||||
|
"type": "citext"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "hashed_password",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "oidc_id",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"deferrable": false,
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null,
|
||||||
|
"index?": false,
|
||||||
|
"match_type": null,
|
||||||
|
"match_with": null,
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "users_member_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"table": "members"
|
||||||
|
},
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "member_id",
|
||||||
|
"type": "uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "FDEBD4840449609DDA8B50D6741C2EEDE9D81DFBC1E26D4BC77DBD9B5A8EA4DC",
|
||||||
|
"identities": [
|
||||||
|
{
|
||||||
|
"all_tenants?": false,
|
||||||
|
"base_filter": null,
|
||||||
|
"index_name": "users_unique_email_index",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"type": "atom",
|
||||||
|
"value": "email"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "unique_email",
|
||||||
|
"nils_distinct?": true,
|
||||||
|
"where": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"all_tenants?": false,
|
||||||
|
"base_filter": null,
|
||||||
|
"index_name": "users_unique_oidc_id_index",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"type": "atom",
|
||||||
|
"value": "oidc_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "unique_oidc_id",
|
||||||
|
"nils_distinct?": true,
|
||||||
|
"where": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"all_tenants?": false,
|
||||||
|
"base_filter": null,
|
||||||
|
"index_name": "users_unique_member_index",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"type": "atom",
|
||||||
|
"value": "member_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "unique_member",
|
||||||
|
"nils_distinct?": true,
|
||||||
|
"where": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mv.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "users"
|
||||||
|
}
|
||||||
|
|
@ -148,7 +148,8 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
||||||
email: "dave@example.com"
|
email: "dave@example.com"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} = Accounts.update_user(user, %{member: %{id: member2.id}})
|
assert {:error, %Ash.Error.Invalid{}} =
|
||||||
|
Accounts.update_user(user, %{member: %{id: member2.id}})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "prevents linking user to already linked member on update" do
|
test "prevents linking user to already linked member on update" do
|
||||||
|
|
@ -159,7 +160,8 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
||||||
|
|
||||||
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
|
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
|
||||||
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} = Accounts.update_user(user2, %{member: %{id: member.id}})
|
assert {:error, %Ash.Error.Invalid{}} =
|
||||||
|
Accounts.update_user(user2, %{member: %{id: member.id}})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "prevents linking member to already linked user on creation" do
|
test "prevents linking member to already linked user on creation" do
|
||||||
|
|
@ -184,10 +186,10 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
||||||
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
|
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
|
||||||
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} =
|
assert {:error, %Ash.Error.Invalid{}} =
|
||||||
Accounts.create_user(%{
|
Accounts.create_user(%{
|
||||||
email: "test5@example.com",
|
email: "test5@example.com",
|
||||||
member: %{id: member.id}
|
member: %{id: member.id}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue