From 78a95eb8fc4296a869bd61f5d0a0be6edab61540 Mon Sep 17 00:00:00 2001 From: hq1 Date: Thu, 7 Nov 2024 12:27:27 +0100 Subject: [PATCH] Extract schema transitions under delegated namespace (#4788) * Extract schema transitions under delegated namespace * fixup --- lib/plausible/sites.ex | 114 +---------------- lib/plausible/teams/adapter/read/billing.ex | 20 +++ lib/plausible/teams/adapter/read/ownership.ex | 71 ++++++++++ lib/plausible/teams/adapter/read/sites.ex | 121 ++++++++++++++++++ lib/plausible_web/live/sites.ex | 83 +----------- 5 files changed, 222 insertions(+), 187 deletions(-) create mode 100644 lib/plausible/teams/adapter/read/billing.ex create mode 100644 lib/plausible/teams/adapter/read/ownership.ex create mode 100644 lib/plausible/teams/adapter/read/sites.ex diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index aaf4ee5c6..33dc60e50 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -13,8 +13,6 @@ defmodule Plausible.Sites do require Plausible.Site.UserPreference - @type list_opt() :: {:filter_by_domain, String.t()} - def get_by_domain(domain) do Repo.get_by(Site, domain: domain) end @@ -71,109 +69,10 @@ defmodule Plausible.Sites do ) end - def list(user, pagination_params, opts \\ []) do - if Plausible.Teams.read_team_schemas?(user) do - Plausible.Teams.Sites.list(user, pagination_params, opts) - else - old_list(user, pagination_params, opts) - end - end + defdelegate list(user, pagination_params, opts \\ []), to: Plausible.Teams.Adapter.Read.Sites - def list_with_invitations(user, pagination_params, opts \\ []) do - if Plausible.Teams.read_team_schemas?(user) do - Plausible.Teams.Sites.list_with_invitations(user, pagination_params, opts) - else - old_list_with_invitations(user, pagination_params, opts) - end - end - - @spec old_list(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t() - def old_list(user, pagination_params, opts \\ []) do - domain_filter = Keyword.get(opts, :filter_by_domain) - - from(s in Site, - left_join: up in Site.UserPreference, - on: up.site_id == s.id and up.user_id == ^user.id, - inner_join: sm in assoc(s, :memberships), - on: sm.user_id == ^user.id, - select: %{ - s - | pinned_at: selected_as(up.pinned_at, :pinned_at), - entry_type: - selected_as( - fragment( - """ - CASE - WHEN ? IS NOT NULL THEN 'pinned_site' - ELSE 'site' - END - """, - up.pinned_at - ), - :entry_type - ) - }, - order_by: [asc: selected_as(:entry_type), desc: selected_as(:pinned_at), asc: s.domain], - preload: [memberships: sm] - ) - |> maybe_filter_by_domain(domain_filter) - |> Repo.paginate(pagination_params) - end - - @spec old_list_with_invitations(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t() - def old_list_with_invitations(user, pagination_params, opts \\ []) do - domain_filter = Keyword.get(opts, :filter_by_domain) - - result = - from(s in Site, - left_join: up in Site.UserPreference, - on: up.site_id == s.id and up.user_id == ^user.id, - left_join: i in assoc(s, :invitations), - on: i.email == ^user.email, - left_join: sm in assoc(s, :memberships), - on: sm.user_id == ^user.id, - where: not is_nil(sm.id) or not is_nil(i.id), - select: %{ - s - | pinned_at: selected_as(up.pinned_at, :pinned_at), - entry_type: - selected_as( - fragment( - """ - CASE - WHEN ? IS NOT NULL THEN 'invitation' - WHEN ? IS NOT NULL THEN 'pinned_site' - ELSE 'site' - END - """, - i.id, - up.pinned_at - ), - :entry_type - ) - }, - order_by: [asc: selected_as(:entry_type), desc: selected_as(:pinned_at), asc: s.domain], - preload: [memberships: sm, invitations: i] - ) - |> maybe_filter_by_domain(domain_filter) - |> Repo.paginate(pagination_params) - - # Populating `site` preload on `invitation` - # without requesting it from database. - # Necessary for invitation modals logic. - entries = - Enum.map(result.entries, fn - %{invitations: [invitation]} = site -> - site = %{site | invitations: [], memberships: []} - invitation = %{invitation | site: site} - %{site | invitations: [invitation]} - - site -> - site - end) - - %{result | entries: entries} - end + defdelegate list_with_invitations(user, pagination_params, opts \\ []), + to: Plausible.Teams.Adapter.Read.Sites @spec for_user_query(Auth.User.t()) :: Ecto.Query.t() def for_user_query(user) do @@ -184,13 +83,6 @@ defmodule Plausible.Sites do ) end - defp maybe_filter_by_domain(query, domain) - when byte_size(domain) >= 1 and byte_size(domain) <= 64 do - where(query, [s], ilike(s.domain, ^"%#{domain}%")) - end - - defp maybe_filter_by_domain(query, _), do: query - def create(user, params) do with :ok <- Quota.ensure_can_add_new_site(user) do Ecto.Multi.new() diff --git a/lib/plausible/teams/adapter/read/billing.ex b/lib/plausible/teams/adapter/read/billing.ex new file mode 100644 index 000000000..82d238a41 --- /dev/null +++ b/lib/plausible/teams/adapter/read/billing.ex @@ -0,0 +1,20 @@ +defmodule Plausible.Teams.Adapter.Read.Billing do + @moduledoc """ + Transition adapter for new schema reads + """ + alias Plausible.Teams + + def check_needs_to_upgrade(user) do + if Teams.read_team_schemas?(user) do + team = + case Teams.get_by_owner(user) do + {:ok, team} -> team + {:error, _} -> nil + end + + Teams.Billing.check_needs_to_upgrade(team) + else + Plausible.Billing.check_needs_to_upgrade(user) + end + end +end diff --git a/lib/plausible/teams/adapter/read/ownership.ex b/lib/plausible/teams/adapter/read/ownership.ex new file mode 100644 index 000000000..4a04ace2f --- /dev/null +++ b/lib/plausible/teams/adapter/read/ownership.ex @@ -0,0 +1,71 @@ +defmodule Plausible.Teams.Adapter.Read.Ownership do + @moduledoc """ + Transition adapter for new schema reads + """ + use Plausible + alias Plausible.Site + alias Plausible.Auth + alias Plausible.Teams + alias Plausible.Site.Memberships.Invitations + + def ensure_can_take_ownership(site, user) do + if Teams.read_team_schemas?(user) do + team = + case Teams.get_by_owner(user) do + {:ok, team} -> team + {:error, _} -> nil + end + + Teams.Invitations.ensure_can_take_ownership(site, team) + else + Invitations.ensure_can_take_ownership(site, user) + end + end + + def has_sites?(user) do + if Teams.read_team_schemas?(user) do + Teams.Users.has_sites?(user, include_pending?: true) + else + Site.Memberships.any_or_pending?(user) + end + end + + def owns_sites?(user, sites) do + if Teams.read_team_schemas?(user) do + Teams.Users.owns_sites?(user, include_pending?: true) + else + Enum.any?(sites.entries, fn site -> + length(site.invitations) > 0 && List.first(site.invitations).role == :owner + end) || + Auth.user_owns_sites?(user) + end + end + + on_ee do + def check_feature_access(site, new_owner) do + user_or_team = + if Teams.read_team_schemas?(new_owner) do + case Teams.get_by_owner(new_owner) do + {:ok, team} -> team + {:error, _} -> nil + end + else + new_owner + end + + missing_features = + Plausible.Billing.Quota.Usage.features_usage(nil, [site.id]) + |> Enum.filter(&(&1.check_availability(user_or_team) != :ok)) + + if missing_features == [] do + :ok + else + {:error, {:missing_features, missing_features}} + end + end + else + def check_feature_access(_site, _new_owner) do + :ok + end + end +end diff --git a/lib/plausible/teams/adapter/read/sites.ex b/lib/plausible/teams/adapter/read/sites.ex new file mode 100644 index 000000000..173394c20 --- /dev/null +++ b/lib/plausible/teams/adapter/read/sites.ex @@ -0,0 +1,121 @@ +defmodule Plausible.Teams.Adapter.Read.Sites do + @moduledoc """ + Transition adapter for new schema reads + """ + import Ecto.Query + alias Plausible.Repo + alias Plausible.Site + alias Plausible.Auth + + def list(user, pagination_params, opts \\ []) do + if Plausible.Teams.read_team_schemas?(user) do + Plausible.Teams.Sites.list(user, pagination_params, opts) + else + old_list(user, pagination_params, opts) + end + end + + def list_with_invitations(user, pagination_params, opts \\ []) do + if Plausible.Teams.read_team_schemas?(user) do + Plausible.Teams.Sites.list_with_invitations(user, pagination_params, opts) + else + old_list_with_invitations(user, pagination_params, opts) + end + end + + @type list_opt() :: {:filter_by_domain, String.t()} + @spec old_list(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t() + def old_list(user, pagination_params, opts \\ []) do + domain_filter = Keyword.get(opts, :filter_by_domain) + + from(s in Site, + left_join: up in Site.UserPreference, + on: up.site_id == s.id and up.user_id == ^user.id, + inner_join: sm in assoc(s, :memberships), + on: sm.user_id == ^user.id, + select: %{ + s + | pinned_at: selected_as(up.pinned_at, :pinned_at), + entry_type: + selected_as( + fragment( + """ + CASE + WHEN ? IS NOT NULL THEN 'pinned_site' + ELSE 'site' + END + """, + up.pinned_at + ), + :entry_type + ) + }, + order_by: [asc: selected_as(:entry_type), desc: selected_as(:pinned_at), asc: s.domain], + preload: [memberships: sm] + ) + |> maybe_filter_by_domain(domain_filter) + |> Repo.paginate(pagination_params) + end + + @spec old_list_with_invitations(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t() + def old_list_with_invitations(user, pagination_params, opts \\ []) do + domain_filter = Keyword.get(opts, :filter_by_domain) + + result = + from(s in Site, + left_join: up in Site.UserPreference, + on: up.site_id == s.id and up.user_id == ^user.id, + left_join: i in assoc(s, :invitations), + on: i.email == ^user.email, + left_join: sm in assoc(s, :memberships), + on: sm.user_id == ^user.id, + where: not is_nil(sm.id) or not is_nil(i.id), + select: %{ + s + | pinned_at: selected_as(up.pinned_at, :pinned_at), + entry_type: + selected_as( + fragment( + """ + CASE + WHEN ? IS NOT NULL THEN 'invitation' + WHEN ? IS NOT NULL THEN 'pinned_site' + ELSE 'site' + END + """, + i.id, + up.pinned_at + ), + :entry_type + ) + }, + order_by: [asc: selected_as(:entry_type), desc: selected_as(:pinned_at), asc: s.domain], + preload: [memberships: sm, invitations: i] + ) + |> maybe_filter_by_domain(domain_filter) + |> Repo.paginate(pagination_params) + + # Populating `site` preload on `invitation` + # without requesting it from database. + # Necessary for invitation modals logic. + entries = + Enum.map(result.entries, fn + %{invitations: [invitation]} = site -> + site = %{site | invitations: [], memberships: []} + invitation = %{invitation | site: site} + %{site | invitations: [invitation]} + + site -> + site + end) + + %{result | entries: entries} + end + + defp maybe_filter_by_domain(query, domain) + when byte_size(domain) >= 1 and byte_size(domain) <= 64 do + where(query, [s], ilike(s.domain, ^"%#{domain}%")) + end + + defp maybe_filter_by_domain(query, _), do: query +end diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index 2a7e2427b..e3b0618b5 100644 --- a/lib/plausible_web/live/sites.ex +++ b/lib/plausible_web/live/sites.ex @@ -6,11 +6,7 @@ defmodule PlausibleWeb.Live.Sites do use PlausibleWeb, :live_view import PlausibleWeb.Live.Components.Pagination - alias Plausible.Auth - alias Plausible.Site alias Plausible.Sites - alias Plausible.Site.Memberships.Invitations - alias Plausible.Teams def mount(params, _session, socket) do uri = @@ -644,38 +640,11 @@ defmodule PlausibleWeb.Live.Sites do {:noreply, socket} end - defp has_sites?(user) do - if Teams.read_team_schemas?(user) do - Teams.Users.has_sites?(user, include_pending?: true) - else - Site.Memberships.any_or_pending?(user) - end - end + defdelegate has_sites?(user), to: Plausible.Teams.Adapter.Read.Ownership - defp owns_sites?(user, sites) do - if Teams.read_team_schemas?(user) do - Teams.Users.owns_sites?(user, include_pending?: true) - else - Enum.any?(sites.entries, fn site -> - length(site.invitations) > 0 && List.first(site.invitations).role == :owner - end) || - Auth.user_owns_sites?(user) - end - end + defdelegate owns_sites?(user, sites), to: Plausible.Teams.Adapter.Read.Ownership - defp check_needs_to_upgrade(user) do - if Teams.read_team_schemas?(user) do - team = - case Teams.get_by_owner(user) do - {:ok, team} -> team - {:error, _} -> nil - end - - Teams.Billing.check_needs_to_upgrade(team) - else - Plausible.Billing.check_needs_to_upgrade(user) - end - end + defdelegate check_needs_to_upgrade(user), to: Plausible.Teams.Adapter.Read.Billing defp load_sites(%{assigns: assigns} = socket) do sites = @@ -726,32 +695,12 @@ defmodule PlausibleWeb.Live.Sites do defp check_limits(invitation, _), do: %{invitation: invitation} - defp ensure_can_take_ownership(site, user) do - if Teams.read_team_schemas?(user) do - team = - case Teams.get_by_owner(user) do - {:ok, team} -> team - {:error, _} -> nil - end + defdelegate ensure_can_take_ownership(site, user), to: Plausible.Teams.Adapter.Read.Ownership - Teams.Invitations.ensure_can_take_ownership(site, team) - else - Invitations.ensure_can_take_ownership(site, user) - end - end + defdelegate check_feature_access(site, user), to: Plausible.Teams.Adapter.Read.Ownership - defp check_features(%{role: :owner, site: site} = invitation, user) do - user_or_team = - if Teams.read_team_schemas?(user) do - case Teams.get_by_owner(user) do - {:ok, team} -> team - {:error, _} -> nil - end - else - user - end - - case check_feature_access(site, user_or_team) do + def check_features(%{role: :owner, site: site} = invitation, user) do + case check_feature_access(site, user) do :ok -> %{invitation: invitation} @@ -765,24 +714,6 @@ defmodule PlausibleWeb.Live.Sites do end end - if ce?() do - defp check_feature_access(_site, _new_owner) do - :ok - end - else - defp check_feature_access(site, new_owner) do - missing_features = - Plausible.Billing.Quota.Usage.features_usage(nil, [site.id]) - |> Enum.filter(&(&1.check_availability(new_owner) != :ok)) - - if missing_features == [] do - :ok - else - {:error, {:missing_features, missing_features}} - end - end - end - defp set_filter_text(socket, filter_text) do uri = socket.assigns.uri