Extract schema transitions under delegated namespace (#4788)

* Extract schema transitions under delegated namespace

* fixup
This commit is contained in:
hq1 2024-11-07 12:27:27 +01:00 committed by GitHub
parent 799e163eef
commit 78a95eb8fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 222 additions and 187 deletions

View File

@ -13,8 +13,6 @@ defmodule Plausible.Sites do
require Plausible.Site.UserPreference require Plausible.Site.UserPreference
@type list_opt() :: {:filter_by_domain, String.t()}
def get_by_domain(domain) do def get_by_domain(domain) do
Repo.get_by(Site, domain: domain) Repo.get_by(Site, domain: domain)
end end
@ -71,109 +69,10 @@ defmodule Plausible.Sites do
) )
end end
def list(user, pagination_params, opts \\ []) do defdelegate list(user, pagination_params, opts \\ []), to: Plausible.Teams.Adapter.Read.Sites
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 defdelegate list_with_invitations(user, pagination_params, opts \\ []),
if Plausible.Teams.read_team_schemas?(user) do to: Plausible.Teams.Adapter.Read.Sites
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
@spec for_user_query(Auth.User.t()) :: Ecto.Query.t() @spec for_user_query(Auth.User.t()) :: Ecto.Query.t()
def for_user_query(user) do def for_user_query(user) do
@ -184,13 +83,6 @@ defmodule Plausible.Sites do
) )
end 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 def create(user, params) do
with :ok <- Quota.ensure_can_add_new_site(user) do with :ok <- Quota.ensure_can_add_new_site(user) do
Ecto.Multi.new() Ecto.Multi.new()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -6,11 +6,7 @@ defmodule PlausibleWeb.Live.Sites do
use PlausibleWeb, :live_view use PlausibleWeb, :live_view
import PlausibleWeb.Live.Components.Pagination import PlausibleWeb.Live.Components.Pagination
alias Plausible.Auth
alias Plausible.Site
alias Plausible.Sites alias Plausible.Sites
alias Plausible.Site.Memberships.Invitations
alias Plausible.Teams
def mount(params, _session, socket) do def mount(params, _session, socket) do
uri = uri =
@ -644,38 +640,11 @@ defmodule PlausibleWeb.Live.Sites do
{:noreply, socket} {:noreply, socket}
end end
defp has_sites?(user) do defdelegate has_sites?(user), to: Plausible.Teams.Adapter.Read.Ownership
if Teams.read_team_schemas?(user) do
Teams.Users.has_sites?(user, include_pending?: true)
else
Site.Memberships.any_or_pending?(user)
end
end
defp owns_sites?(user, sites) do defdelegate owns_sites?(user, sites), to: Plausible.Teams.Adapter.Read.Ownership
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
defp check_needs_to_upgrade(user) do defdelegate check_needs_to_upgrade(user), to: Plausible.Teams.Adapter.Read.Billing
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
defp load_sites(%{assigns: assigns} = socket) do defp load_sites(%{assigns: assigns} = socket) do
sites = sites =
@ -726,32 +695,12 @@ defmodule PlausibleWeb.Live.Sites do
defp check_limits(invitation, _), do: %{invitation: invitation} defp check_limits(invitation, _), do: %{invitation: invitation}
defp ensure_can_take_ownership(site, user) do defdelegate ensure_can_take_ownership(site, user), to: Plausible.Teams.Adapter.Read.Ownership
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) defdelegate check_feature_access(site, user), to: Plausible.Teams.Adapter.Read.Ownership
else
Invitations.ensure_can_take_ownership(site, user)
end
end
defp check_features(%{role: :owner, site: site} = invitation, user) do def check_features(%{role: :owner, site: site} = invitation, user) do
user_or_team = case check_feature_access(site, user) do
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
:ok -> :ok ->
%{invitation: invitation} %{invitation: invitation}
@ -765,24 +714,6 @@ defmodule PlausibleWeb.Live.Sites do
end end
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 defp set_filter_text(socket, filter_text) do
uri = socket.assigns.uri uri = socket.assigns.uri