From e2bbe1fb4032baa8b77133c2c7832dfa983cef58 Mon Sep 17 00:00:00 2001 From: Moritz Date: Fri, 26 Sep 2025 19:55:06 +0200 Subject: [PATCH 1/6] feat: add member-user link in member view and user view --- lib/mv_web/live/member_live/show.ex | 12 +++++++++++- lib/mv_web/live/user_live/show.ex | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 304709c..b8992cf 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -37,6 +37,16 @@ defmodule MvWeb.MemberLive.Show do <:item title={gettext("Street")}>{@member.street} <:item title={gettext("House Number")}>{@member.house_number} <:item title={gettext("Postal Code")}>{@member.postal_code} + <: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} + + <% else %> + {gettext("No user linked")} + <% end %> +

{gettext("Custom Properties")}

@@ -67,7 +77,7 @@ defmodule MvWeb.MemberLive.Show do query = Mv.Membership.Member |> filter(id == ^id) - |> load(properties: [:property_type]) + |> load([:user, properties: [:property_type]]) member = Ash.read_one!(query) diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index 3bf6baf..1001b94 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -26,6 +26,16 @@ defmodule MvWeb.UserLive.Show do <:item title={gettext("Password Authentication")}> {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} + <: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} + + <% else %> + {gettext("No member linked")} + <% end %> + """ @@ -33,9 +43,11 @@ defmodule MvWeb.UserLive.Show do @impl true def mount(%{"id" => id}, _session, socket) do + user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member]) + {:ok, socket |> assign(:page_title, gettext("Show User")) - |> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))} + |> assign(:user, user)} end end From a5e2f46659af4fe888a2de0d5867bb20045c86a2 Mon Sep 17 00:00:00 2001 From: Moritz Date: Fri, 26 Sep 2025 19:57:11 +0200 Subject: [PATCH 2/6] feat: seed member user relations --- priv/repo/seeds.exs | 92 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index cb38969..04f65a8 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -48,7 +48,7 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) |> Ash.update!() -# Create sample members for testing +# Create sample members for testing - use upsert to prevent duplicates for member_attrs <- [ %{ first_name: "Hans", @@ -90,5 +90,93 @@ for member_attrs <- [ house_number: "8" } ] do - Membership.create_member!(member_attrs) + # Use upsert to prevent duplicates based on email + Membership.create_member!(member_attrs, upsert?: true, upsert_identity: :unique_email) 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 - use upsert to prevent duplicates +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 - use upsert to prevent duplicates + Membership.create_member!( + Map.put(member_attrs_without_user, :user, %{id: user.id}), + upsert?: true, + upsert_identity: :unique_email + ) + else + # User already has a member, just create the member without linking - use upsert to prevent duplicates + Membership.create_member!(member_attrs_without_user, upsert?: true, upsert_identity: :unique_email) + 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!") From cf364fc30ea38f14d150265f7d35525ec7bf231a Mon Sep 17 00:00:00 2001 From: Moritz Date: Fri, 26 Sep 2025 20:07:47 +0200 Subject: [PATCH 3/6] feat: make member emails unique --- lib/membership/member.ex | 5 + ...0926180341_add_unique_email_to_members.exs | 17 ++ .../repo/members/20250926180341.json | 202 ++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 priv/repo/migrations/20250926180341_add_unique_email_to_members.exs create mode 100644 priv/resource_snapshots/repo/members/20250926180341.json diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 5641528..7b898a8 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -242,4 +242,9 @@ defmodule Mv.Membership.Member do # The relationship is optional (allow_nil? true by default) has_one :user, Mv.Accounts.User end + + # Define identities for upsert operations + identities do + identity :unique_email, [:email] + end end diff --git a/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs new file mode 100644 index 0000000..51b874f --- /dev/null +++ b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs @@ -0,0 +1,17 @@ +defmodule Mv.Repo.Migrations.AddUniqueEmailToMembers 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(:members, [:email], name: "members_unique_email_index") + end + + def down do + drop_if_exists unique_index(:members, [:email], name: "members_unique_email_index") + end +end diff --git a/priv/resource_snapshots/repo/members/20250926180341.json b/priv/resource_snapshots/repo/members/20250926180341.json new file mode 100644 index 0000000..3582051 --- /dev/null +++ b/priv/resource_snapshots/repo/members/20250926180341.json @@ -0,0 +1,202 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v7()\")", + "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": "first_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "last_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "email", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "birth_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "paid", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "phone_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "join_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "exit_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "notes", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "street", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "house_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "postal_code", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "5F070A1E5BEE9883AE864FB5A4A5E81F487A1C57D41576C23BAC8D933005D565", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "members_unique_email_index", + "keys": [ + { + "type": "atom", + "value": "email" + } + ], + "name": "unique_email", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "members" +} \ No newline at end of file From 968013d0edfa22a0c333246068d474d7fbc78a48 Mon Sep 17 00:00:00 2001 From: Moritz Date: Fri, 26 Sep 2025 19:55:06 +0200 Subject: [PATCH 4/6] feat: add member-user link in member view and user view --- lib/mv_web/live/member_live/show.ex | 15 ++++++++++++++- lib/mv_web/live/user_live/show.ex | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 304709c..ee4a1a3 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -37,6 +37,19 @@ defmodule MvWeb.MemberLive.Show do <:item title={gettext("Street")}>{@member.street} <:item title={gettext("House Number")}>{@member.house_number} <:item title={gettext("Postal Code")}>{@member.postal_code} + <: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} + + <% else %> + {gettext("No user linked")} + <% end %> +

{gettext("Custom Properties")}

@@ -67,7 +80,7 @@ defmodule MvWeb.MemberLive.Show do query = Mv.Membership.Member |> filter(id == ^id) - |> load(properties: [:property_type]) + |> load([:user, properties: [:property_type]]) member = Ash.read_one!(query) diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index 3bf6baf..056103b 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -26,6 +26,19 @@ defmodule MvWeb.UserLive.Show do <:item title={gettext("Password Authentication")}> {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} + <: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} + + <% else %> + {gettext("No member linked")} + <% end %> + """ @@ -33,9 +46,11 @@ defmodule MvWeb.UserLive.Show do @impl true def mount(%{"id" => id}, _session, socket) do + user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member]) + {:ok, socket |> assign(:page_title, gettext("Show User")) - |> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))} + |> assign(:user, user)} end end From e943f2dc2259b455e8dafc3f5f3ee749d64ca395 Mon Sep 17 00:00:00 2001 From: Moritz Date: Fri, 26 Sep 2025 19:57:11 +0200 Subject: [PATCH 5/6] feat: seed member user relations --- priv/repo/seeds.exs | 95 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index cb38969..d850c7c 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -48,7 +48,7 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) |> Ash.update!() -# Create sample members for testing +# Create sample members for testing - use upsert to prevent duplicates for member_attrs <- [ %{ first_name: "Hans", @@ -90,5 +90,96 @@ for member_attrs <- [ house_number: "8" } ] do - Membership.create_member!(member_attrs) + # Use upsert to prevent duplicates based on email + Membership.create_member!(member_attrs, upsert?: true, upsert_identity: :unique_email) 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 - use upsert to prevent duplicates +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 - use upsert to prevent duplicates + Membership.create_member!( + Map.put(member_attrs_without_user, :user, %{id: user.id}), + upsert?: true, + upsert_identity: :unique_email + ) + else + # User already has a member, just create the member without linking - use upsert to prevent duplicates + Membership.create_member!(member_attrs_without_user, + upsert?: true, + upsert_identity: :unique_email + ) + 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!") From e0d7de471237fed4fbbb05d4e248002271498d5f Mon Sep 17 00:00:00 2001 From: Moritz Date: Fri, 26 Sep 2025 20:07:47 +0200 Subject: [PATCH 6/6] feat: make member emails unique --- lib/membership/member.ex | 5 + ...0926180341_add_unique_email_to_members.exs | 17 ++ .../repo/members/20250926180341.json | 202 ++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 priv/repo/migrations/20250926180341_add_unique_email_to_members.exs create mode 100644 priv/resource_snapshots/repo/members/20250926180341.json diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 5641528..7b898a8 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -242,4 +242,9 @@ defmodule Mv.Membership.Member do # The relationship is optional (allow_nil? true by default) has_one :user, Mv.Accounts.User end + + # Define identities for upsert operations + identities do + identity :unique_email, [:email] + end end diff --git a/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs new file mode 100644 index 0000000..51b874f --- /dev/null +++ b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs @@ -0,0 +1,17 @@ +defmodule Mv.Repo.Migrations.AddUniqueEmailToMembers 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(:members, [:email], name: "members_unique_email_index") + end + + def down do + drop_if_exists unique_index(:members, [:email], name: "members_unique_email_index") + end +end diff --git a/priv/resource_snapshots/repo/members/20250926180341.json b/priv/resource_snapshots/repo/members/20250926180341.json new file mode 100644 index 0000000..3582051 --- /dev/null +++ b/priv/resource_snapshots/repo/members/20250926180341.json @@ -0,0 +1,202 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v7()\")", + "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": "first_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "last_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "email", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "birth_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "paid", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "phone_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "join_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "exit_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "notes", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "street", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "house_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "postal_code", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "5F070A1E5BEE9883AE864FB5A4A5E81F487A1C57D41576C23BAC8D933005D565", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "members_unique_email_index", + "keys": [ + { + "type": "atom", + "value": "email" + } + ], + "name": "unique_email", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "members" +} \ No newline at end of file