refactor: extract per-schema Query modules for organization_id filters
Adds 7 new composable Query modules following the existing DeviceQuery/AlertQuery pattern. Each exposes base/0 and for_organization/1,2 plus filters used by 2+ callers. New modules: - Towerops.OnCall.ScheduleQuery - Towerops.OnCall.EscalationPolicyQuery - Towerops.Maintenance.MaintenanceWindowQuery - Towerops.Organizations.MembershipQuery - Towerops.Organizations.InvitationQuery - Towerops.Agents.AgentTokenQuery - Towerops.Gaiia.DeviceSubscriberLinkQuery Refactors callsites in alerts, agents, devices, gaiia, maintenance, on_call, organizations, search, and subscription_limits to use existing or new Query modules instead of inline 'where: x.organization_id == ^...' clauses.
This commit is contained in:
parent
a680d5ea94
commit
2528e78ff9
19 changed files with 315 additions and 81 deletions
|
|
@ -11,6 +11,7 @@ defmodule Towerops.Agents do
|
|||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Agents.AgentCache
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Agents.AgentTokenQuery
|
||||
alias Towerops.Agents.ReleaseChecker
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Repo
|
||||
|
|
@ -91,8 +92,9 @@ defmodule Towerops.Agents do
|
|||
"""
|
||||
@spec list_organization_agent_tokens(String.t()) :: [AgentToken.t()]
|
||||
def list_organization_agent_tokens(organization_id) do
|
||||
AgentToken
|
||||
|> where([t], t.organization_id == ^organization_id and t.is_cloud_poller == false)
|
||||
organization_id
|
||||
|> AgentTokenQuery.for_organization()
|
||||
|> AgentTokenQuery.excluding_cloud_pollers()
|
||||
|> order_by([t], desc: t.inserted_at)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
|
|
|||
36
lib/towerops/agents/agent_token_query.ex
Normal file
36
lib/towerops/agents/agent_token_query.ex
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
defmodule Towerops.Agents.AgentTokenQuery do
|
||||
@moduledoc """
|
||||
Composable query fragments for `Towerops.Agents.AgentToken`.
|
||||
|
||||
Lets context modules build queries by piping filters together instead of
|
||||
repeating `where: t.organization_id == ^...` clauses inline.
|
||||
|
||||
## Example
|
||||
|
||||
AgentTokenQuery.base()
|
||||
|> AgentTokenQuery.for_organization(org_id)
|
||||
|> AgentTokenQuery.enabled()
|
||||
|> Repo.all()
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Agents.AgentToken
|
||||
|
||||
@doc "Starting point for an AgentToken query."
|
||||
def base, do: AgentToken
|
||||
|
||||
@doc "Filter to a single organization."
|
||||
def for_organization(query \\ base(), organization_id) do
|
||||
where(query, [t], t.organization_id == ^organization_id)
|
||||
end
|
||||
|
||||
@doc "Filter to organization-scoped tokens (excludes cloud pollers)."
|
||||
def excluding_cloud_pollers(query \\ base()) do
|
||||
where(query, [t], t.is_cloud_poller == false)
|
||||
end
|
||||
|
||||
@doc "Filter to enabled tokens only."
|
||||
def enabled(query \\ base()) do
|
||||
where(query, [t], t.enabled == true)
|
||||
end
|
||||
end
|
||||
|
|
@ -9,6 +9,7 @@ defmodule Towerops.Agents.Stats do
|
|||
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Agents.AgentTokenQuery
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Monitoring.MonitoringCheck
|
||||
alias Towerops.Repo
|
||||
|
|
@ -44,8 +45,8 @@ defmodule Towerops.Agents.Stats do
|
|||
five_minutes_ago = DateTime.add(DateTime.utc_now(), -5, :minute)
|
||||
|
||||
query =
|
||||
from(a in AgentToken,
|
||||
where: a.organization_id == ^organization_id and a.enabled == true,
|
||||
from(a in AgentTokenQuery.for_organization(organization_id),
|
||||
where: a.enabled == true,
|
||||
select: %{
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
|
|
@ -234,9 +235,9 @@ defmodule Towerops.Agents.Stats do
|
|||
five_minutes_ago = DateTime.add(DateTime.utc_now(), -5, :minute)
|
||||
|
||||
Repo.all(
|
||||
from(a in AgentToken,
|
||||
from(a in AgentTokenQuery.for_organization(organization_id),
|
||||
where:
|
||||
a.organization_id == ^organization_id and a.enabled == true and
|
||||
a.enabled == true and
|
||||
(is_nil(a.last_seen_at) or a.last_seen_at < ^five_minutes_ago),
|
||||
select: %{id: a.id, name: a.name, last_seen_at: a.last_seen_at},
|
||||
order_by: [asc: a.last_seen_at]
|
||||
|
|
@ -259,8 +260,8 @@ defmodule Towerops.Agents.Stats do
|
|||
@spec get_high_load_agents(Ecto.UUID.t(), non_neg_integer()) :: [map()]
|
||||
def get_high_load_agents(organization_id, threshold \\ 50) do
|
||||
query =
|
||||
from(a in AgentToken,
|
||||
where: a.organization_id == ^organization_id and a.enabled == true,
|
||||
from(a in AgentTokenQuery.for_organization(organization_id),
|
||||
where: a.enabled == true,
|
||||
select: %{id: a.id, name: a.name}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -129,8 +129,7 @@ defmodule Towerops.Alerts do
|
|||
@spec list_organization_active_alerts(String.t(), keyword()) :: [Alert.t()]
|
||||
def list_organization_active_alerts(organization_id, opts \\ []) do
|
||||
query =
|
||||
from(a in Alert,
|
||||
where: a.organization_id == ^organization_id,
|
||||
from(a in AlertQuery.for_organization(organization_id),
|
||||
where: a.alert_type == "device_down",
|
||||
where: is_nil(a.resolved_at),
|
||||
order_by: [desc: a.triggered_at],
|
||||
|
|
@ -154,8 +153,7 @@ defmodule Towerops.Alerts do
|
|||
@spec count_active_alerts(String.t()) :: integer()
|
||||
def count_active_alerts(organization_id) do
|
||||
Repo.aggregate(
|
||||
from(a in Alert,
|
||||
where: a.organization_id == ^organization_id,
|
||||
from(a in AlertQuery.for_organization(organization_id),
|
||||
where: a.alert_type == "device_down",
|
||||
where: is_nil(a.resolved_at)
|
||||
),
|
||||
|
|
@ -207,8 +205,7 @@ defmodule Towerops.Alerts do
|
|||
def list_organization_alerts(organization_id, filters) when is_map(filters) do
|
||||
limit = filters["limit"] || 100
|
||||
|
||||
from(a in Alert,
|
||||
where: a.organization_id == ^organization_id,
|
||||
from(a in AlertQuery.for_organization(organization_id),
|
||||
order_by: [desc: a.triggered_at],
|
||||
limit: ^limit,
|
||||
preload: [device: [:site], acknowledged_by: []]
|
||||
|
|
|
|||
|
|
@ -43,9 +43,8 @@ defmodule Towerops.Devices do
|
|||
"""
|
||||
@spec list_organization_devices(String.t(), map()) :: [DeviceSchema.t()]
|
||||
def list_organization_devices(organization_id, filters \\ %{}) do
|
||||
from(e in DeviceSchema,
|
||||
from(e in DeviceQuery.for_organization(organization_id),
|
||||
left_join: s in assoc(e, :site),
|
||||
where: e.organization_id == ^organization_id,
|
||||
order_by: [asc: e.display_order, asc: e.name],
|
||||
preload: [site: s]
|
||||
)
|
||||
|
|
@ -154,8 +153,7 @@ defmodule Towerops.Devices do
|
|||
"""
|
||||
@spec get_device_status_counts(String.t()) :: %{atom() => non_neg_integer()}
|
||||
def get_device_status_counts(organization_id) do
|
||||
from(d in DeviceSchema,
|
||||
where: d.organization_id == ^organization_id,
|
||||
from(d in DeviceQuery.for_organization(organization_id),
|
||||
group_by: d.status,
|
||||
select: {d.status, count(d.id)}
|
||||
)
|
||||
|
|
@ -457,8 +455,7 @@ defmodule Towerops.Devices do
|
|||
"""
|
||||
def propagate_organization_mikrotik_change(organization_id, attrs) do
|
||||
device_query =
|
||||
from(d in DeviceSchema,
|
||||
where: d.organization_id == ^organization_id,
|
||||
from(d in DeviceQuery.for_organization(organization_id),
|
||||
where: d.mikrotik_credential_source == "organization"
|
||||
)
|
||||
|
||||
|
|
@ -525,8 +522,7 @@ defmodule Towerops.Devices do
|
|||
"""
|
||||
def propagate_organization_snmpv3_change(organization_id, attrs) do
|
||||
device_query =
|
||||
from(d in DeviceSchema,
|
||||
where: d.organization_id == ^organization_id,
|
||||
from(d in DeviceQuery.for_organization(organization_id),
|
||||
where: d.snmpv3_credential_source == "organization"
|
||||
)
|
||||
|
||||
|
|
@ -839,8 +835,7 @@ defmodule Towerops.Devices do
|
|||
"""
|
||||
def propagate_organization_community_change(organization_id, new_community) do
|
||||
device_query =
|
||||
from(d in DeviceSchema,
|
||||
where: d.organization_id == ^organization_id,
|
||||
from(d in DeviceQuery.for_organization(organization_id),
|
||||
where: d.snmp_community_source == "organization"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ defmodule Towerops.Gaiia do
|
|||
alias Towerops.Gaiia.Account
|
||||
alias Towerops.Gaiia.BillingSubscription
|
||||
alias Towerops.Gaiia.DeviceSubscriberLink
|
||||
alias Towerops.Gaiia.DeviceSubscriberLinkQuery
|
||||
alias Towerops.Gaiia.InventoryItem
|
||||
alias Towerops.Gaiia.NetworkSite
|
||||
alias Towerops.Repo
|
||||
|
|
@ -520,12 +521,11 @@ defmodule Towerops.Gaiia do
|
|||
# Get all expected MACs from device_subscriber_links with requested confidence
|
||||
expected_macs =
|
||||
Repo.all(
|
||||
from(l in DeviceSubscriberLink,
|
||||
from(l in DeviceSubscriberLinkQuery.for_organization(organization_id),
|
||||
join: d in Device,
|
||||
on: l.device_id == d.id,
|
||||
join: a in Account,
|
||||
on: l.gaiia_account_id == a.id,
|
||||
where: l.organization_id == ^organization_id,
|
||||
where: not is_nil(l.subscriber_mac),
|
||||
where: l.confidence in ^confidence_levels,
|
||||
select: %{device_id: l.device_id, device: d, account: a, mac: l.subscriber_mac, confidence: l.confidence}
|
||||
|
|
|
|||
25
lib/towerops/gaiia/device_subscriber_link_query.ex
Normal file
25
lib/towerops/gaiia/device_subscriber_link_query.ex
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
defmodule Towerops.Gaiia.DeviceSubscriberLinkQuery do
|
||||
@moduledoc """
|
||||
Composable query fragments for `Towerops.Gaiia.DeviceSubscriberLink`.
|
||||
|
||||
Lets the Gaiia context build queries by piping filters together instead of
|
||||
repeating `where: l.organization_id == ^...` clauses inline.
|
||||
|
||||
## Example
|
||||
|
||||
DeviceSubscriberLinkQuery.base()
|
||||
|> DeviceSubscriberLinkQuery.for_organization(org_id)
|
||||
|> Repo.all()
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Gaiia.DeviceSubscriberLink
|
||||
|
||||
@doc "Starting point for a DeviceSubscriberLink query."
|
||||
def base, do: DeviceSubscriberLink
|
||||
|
||||
@doc "Filter to a single organization."
|
||||
def for_organization(query \\ base(), organization_id) do
|
||||
where(query, [l], l.organization_id == ^organization_id)
|
||||
end
|
||||
end
|
||||
|
|
@ -14,7 +14,7 @@ defmodule Towerops.Gaiia.SiteAggregation do
|
|||
alias Towerops.Devices.Device
|
||||
alias Towerops.Gaiia.Account
|
||||
alias Towerops.Gaiia.BillingSubscription
|
||||
alias Towerops.Gaiia.DeviceSubscriberLink
|
||||
alias Towerops.Gaiia.DeviceSubscriberLinkQuery
|
||||
alias Towerops.Gaiia.InventoryItem
|
||||
alias Towerops.Gaiia.NetworkSite
|
||||
alias Towerops.Repo
|
||||
|
|
@ -86,11 +86,11 @@ defmodule Towerops.Gaiia.SiteAggregation do
|
|||
end
|
||||
|
||||
defp linked_account_gaiia_ids(organization_id, towerops_site_id) do
|
||||
DeviceSubscriberLink
|
||||
organization_id
|
||||
|> DeviceSubscriberLinkQuery.for_organization()
|
||||
|> join(:inner, [l], d in Device, on: l.device_id == d.id)
|
||||
|> join(:inner, [l, _d], a in Account, on: l.gaiia_account_id == a.id)
|
||||
|> where([l, d, _a], d.site_id == ^towerops_site_id)
|
||||
|> where([l, _d, _a], l.organization_id == ^organization_id)
|
||||
|> select([_l, _d, a], a.gaiia_id)
|
||||
|> distinct(true)
|
||||
|> Repo.all()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ defmodule Towerops.Gaiia.SubscriberMatching do
|
|||
alias Towerops.Devices.Device
|
||||
alias Towerops.Gaiia.Account
|
||||
alias Towerops.Gaiia.DeviceSubscriberLink
|
||||
alias Towerops.Gaiia.DeviceSubscriberLinkQuery
|
||||
alias Towerops.Gaiia.InventoryItem
|
||||
alias Towerops.Gaiia.NetworkSite
|
||||
alias Towerops.Integrations
|
||||
|
|
@ -112,7 +113,7 @@ defmodule Towerops.Gaiia.SubscriberMatching do
|
|||
|
||||
# Transaction: delete old + insert new
|
||||
Repo.transaction(fn ->
|
||||
Repo.delete_all(from(d in DeviceSubscriberLink, where: d.organization_id == ^organization_id))
|
||||
Repo.delete_all(DeviceSubscriberLinkQuery.for_organization(organization_id))
|
||||
|
||||
if unique_links != [] do
|
||||
Repo.insert_all(DeviceSubscriberLink, unique_links)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defmodule Towerops.Maintenance do
|
|||
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Maintenance.MaintenanceWindow
|
||||
alias Towerops.Maintenance.MaintenanceWindowQuery
|
||||
alias Towerops.Repo
|
||||
|
||||
@preloads [:organization, :site, :device, :created_by]
|
||||
|
|
@ -39,8 +40,8 @@ defmodule Towerops.Maintenance do
|
|||
def list_windows(organization_id, opts \\ []) do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
MaintenanceWindow
|
||||
|> where([w], w.organization_id == ^organization_id)
|
||||
organization_id
|
||||
|> MaintenanceWindowQuery.for_organization()
|
||||
|> apply_filter(opts[:filter], now)
|
||||
|> order_by([w], desc: w.starts_at)
|
||||
|> Repo.all()
|
||||
|
|
@ -129,9 +130,9 @@ defmodule Towerops.Maintenance do
|
|||
def active_windows_for_org(organization_id) do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
MaintenanceWindow
|
||||
|> where([w], w.organization_id == ^organization_id)
|
||||
|> where([w], w.starts_at <= ^now and w.ends_at >= ^now)
|
||||
organization_id
|
||||
|> MaintenanceWindowQuery.for_organization()
|
||||
|> MaintenanceWindowQuery.active(now)
|
||||
|> Repo.all()
|
||||
|> Repo.preload(@preloads)
|
||||
end
|
||||
|
|
|
|||
31
lib/towerops/maintenance/maintenance_window_query.ex
Normal file
31
lib/towerops/maintenance/maintenance_window_query.ex
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
defmodule Towerops.Maintenance.MaintenanceWindowQuery do
|
||||
@moduledoc """
|
||||
Composable query fragments for `Towerops.Maintenance.MaintenanceWindow`.
|
||||
|
||||
Lets the Maintenance context build queries by piping filters together instead
|
||||
of repeating `where: w.organization_id == ^...` clauses inline.
|
||||
|
||||
## Example
|
||||
|
||||
MaintenanceWindowQuery.base()
|
||||
|> MaintenanceWindowQuery.for_organization(org_id)
|
||||
|> MaintenanceWindowQuery.active(now)
|
||||
|> Repo.all()
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Maintenance.MaintenanceWindow
|
||||
|
||||
@doc "Starting point for a MaintenanceWindow query."
|
||||
def base, do: MaintenanceWindow
|
||||
|
||||
@doc "Filter to a single organization."
|
||||
def for_organization(query \\ base(), organization_id) do
|
||||
where(query, [w], w.organization_id == ^organization_id)
|
||||
end
|
||||
|
||||
@doc "Filter to windows that are active at the given datetime."
|
||||
def active(query \\ base(), now) do
|
||||
where(query, [w], w.starts_at <= ^now and w.ends_at >= ^now)
|
||||
end
|
||||
end
|
||||
|
|
@ -5,8 +5,9 @@ defmodule Towerops.OnCall do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Devices.DeviceQuery
|
||||
alias Towerops.OnCall.EscalationPolicy
|
||||
alias Towerops.OnCall.EscalationPolicyQuery
|
||||
alias Towerops.OnCall.EscalationRule
|
||||
alias Towerops.OnCall.EscalationTarget
|
||||
alias Towerops.OnCall.Layer
|
||||
|
|
@ -14,15 +15,16 @@ defmodule Towerops.OnCall do
|
|||
alias Towerops.OnCall.Override
|
||||
alias Towerops.OnCall.Resolver
|
||||
alias Towerops.OnCall.Schedule
|
||||
alias Towerops.OnCall.ScheduleQuery
|
||||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Repo
|
||||
|
||||
# --- Schedules ---
|
||||
|
||||
def list_schedules(organization_id) do
|
||||
Schedule
|
||||
|> where([s], s.organization_id == ^organization_id)
|
||||
|> order_by([s], asc: s.name)
|
||||
organization_id
|
||||
|> ScheduleQuery.for_organization()
|
||||
|> ScheduleQuery.order_by_name()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
|
|
@ -33,8 +35,9 @@ defmodule Towerops.OnCall do
|
|||
end
|
||||
|
||||
def get_schedule!(id, organization_id) do
|
||||
Schedule
|
||||
|> where([s], s.id == ^id and s.organization_id == ^organization_id)
|
||||
organization_id
|
||||
|> ScheduleQuery.for_organization()
|
||||
|> ScheduleQuery.with_id(id)
|
||||
|> Repo.one!()
|
||||
|> Repo.preload(overrides: :user, layers: [members: :user])
|
||||
end
|
||||
|
|
@ -229,9 +232,9 @@ defmodule Towerops.OnCall do
|
|||
# --- Escalation Policies ---
|
||||
|
||||
def list_escalation_policies(organization_id) do
|
||||
EscalationPolicy
|
||||
|> where([p], p.organization_id == ^organization_id)
|
||||
|> order_by([p], asc: p.name)
|
||||
organization_id
|
||||
|> EscalationPolicyQuery.for_organization()
|
||||
|> EscalationPolicyQuery.order_by_name()
|
||||
|> Repo.all()
|
||||
|> Repo.preload(:rules)
|
||||
end
|
||||
|
|
@ -243,8 +246,9 @@ defmodule Towerops.OnCall do
|
|||
end
|
||||
|
||||
def get_escalation_policy!(id, organization_id) do
|
||||
EscalationPolicy
|
||||
|> where([p], p.id == ^id and p.organization_id == ^organization_id)
|
||||
organization_id
|
||||
|> EscalationPolicyQuery.for_organization()
|
||||
|> EscalationPolicyQuery.with_id(id)
|
||||
|> Repo.one!()
|
||||
|> Repo.preload(rules: :targets)
|
||||
end
|
||||
|
|
@ -366,8 +370,8 @@ defmodule Towerops.OnCall do
|
|||
where: o.id == ^organization_id and o.default_escalation_policy_id == ^policy_id,
|
||||
select: o.id
|
||||
|
||||
Device
|
||||
|> where([d], d.organization_id == ^organization_id)
|
||||
organization_id
|
||||
|> DeviceQuery.for_organization()
|
||||
|> where(
|
||||
[d],
|
||||
d.escalation_policy_id == ^policy_id or
|
||||
|
|
|
|||
35
lib/towerops/on_call/escalation_policy_query.ex
Normal file
35
lib/towerops/on_call/escalation_policy_query.ex
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
defmodule Towerops.OnCall.EscalationPolicyQuery do
|
||||
@moduledoc """
|
||||
Composable query fragments for `Towerops.OnCall.EscalationPolicy`.
|
||||
|
||||
Lets the OnCall context build queries by piping filters together instead of
|
||||
repeating `where: p.organization_id == ^...` clauses inline.
|
||||
|
||||
## Example
|
||||
|
||||
EscalationPolicyQuery.base()
|
||||
|> EscalationPolicyQuery.for_organization(org_id)
|
||||
|> Repo.all()
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.OnCall.EscalationPolicy
|
||||
|
||||
@doc "Starting point for an EscalationPolicy query."
|
||||
def base, do: EscalationPolicy
|
||||
|
||||
@doc "Filter to a single organization."
|
||||
def for_organization(query \\ base(), organization_id) do
|
||||
where(query, [p], p.organization_id == ^organization_id)
|
||||
end
|
||||
|
||||
@doc "Filter by id."
|
||||
def with_id(query \\ base(), id) do
|
||||
where(query, [p], p.id == ^id)
|
||||
end
|
||||
|
||||
@doc "Default ordering: alphabetical by name."
|
||||
def order_by_name(query \\ base()) do
|
||||
order_by(query, [p], asc: p.name)
|
||||
end
|
||||
end
|
||||
35
lib/towerops/on_call/schedule_query.ex
Normal file
35
lib/towerops/on_call/schedule_query.ex
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
defmodule Towerops.OnCall.ScheduleQuery do
|
||||
@moduledoc """
|
||||
Composable query fragments for `Towerops.OnCall.Schedule`.
|
||||
|
||||
Lets the OnCall context build queries by piping filters together instead of
|
||||
repeating `where: s.organization_id == ^...` clauses inline.
|
||||
|
||||
## Example
|
||||
|
||||
ScheduleQuery.base()
|
||||
|> ScheduleQuery.for_organization(org_id)
|
||||
|> Repo.all()
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.OnCall.Schedule
|
||||
|
||||
@doc "Starting point for a Schedule query."
|
||||
def base, do: Schedule
|
||||
|
||||
@doc "Filter to a single organization."
|
||||
def for_organization(query \\ base(), organization_id) do
|
||||
where(query, [s], s.organization_id == ^organization_id)
|
||||
end
|
||||
|
||||
@doc "Filter by id within an organization."
|
||||
def with_id(query \\ base(), id) do
|
||||
where(query, [s], s.id == ^id)
|
||||
end
|
||||
|
||||
@doc "Default ordering: alphabetical by name."
|
||||
def order_by_name(query \\ base()) do
|
||||
order_by(query, [s], asc: s.name)
|
||||
end
|
||||
end
|
||||
|
|
@ -9,8 +9,11 @@ defmodule Towerops.Organizations do
|
|||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Contexts.ConfigChangeTracker
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Devices.DeviceQuery
|
||||
alias Towerops.Organizations.Invitation
|
||||
alias Towerops.Organizations.InvitationQuery
|
||||
alias Towerops.Organizations.Membership
|
||||
alias Towerops.Organizations.MembershipQuery
|
||||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Organizations.Policy
|
||||
alias Towerops.Organizations.SubscriptionLimits
|
||||
|
|
@ -59,10 +62,10 @@ defmodule Towerops.Organizations do
|
|||
"""
|
||||
@spec user_has_access?(String.t(), String.t()) :: boolean()
|
||||
def user_has_access?(user_id, organization_id) do
|
||||
Repo.exists?(
|
||||
from m in Membership,
|
||||
where: m.user_id == ^user_id and m.organization_id == ^organization_id
|
||||
)
|
||||
organization_id
|
||||
|> MembershipQuery.for_organization()
|
||||
|> MembershipQuery.for_user(user_id)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -224,8 +227,7 @@ defmodule Towerops.Organizations do
|
|||
"""
|
||||
def list_organization_memberships(organization_id) do
|
||||
Repo.all(
|
||||
from(m in Membership,
|
||||
where: m.organization_id == ^organization_id,
|
||||
from(m in MembershipQuery.for_organization(organization_id),
|
||||
preload: [:user],
|
||||
order_by: [asc: m.inserted_at]
|
||||
)
|
||||
|
|
@ -238,8 +240,7 @@ defmodule Towerops.Organizations do
|
|||
"""
|
||||
def list_organization_members(organization_id) do
|
||||
Repo.all(
|
||||
from(m in Membership,
|
||||
where: m.organization_id == ^organization_id,
|
||||
from(m in MembershipQuery.for_organization(organization_id),
|
||||
join: u in assoc(m, :user),
|
||||
preload: [user: u],
|
||||
order_by: [
|
||||
|
|
@ -281,10 +282,8 @@ defmodule Towerops.Organizations do
|
|||
"""
|
||||
def list_organization_notification_recipients(organization_id) do
|
||||
Repo.all(
|
||||
from(u in User,
|
||||
join: m in Membership,
|
||||
on: m.user_id == u.id,
|
||||
where: m.organization_id == ^organization_id,
|
||||
from(m in MembershipQuery.for_organization(organization_id),
|
||||
join: u in assoc(m, :user),
|
||||
where: m.role in [:owner, :admin],
|
||||
select: u
|
||||
)
|
||||
|
|
@ -351,8 +350,9 @@ defmodule Towerops.Organizations do
|
|||
end
|
||||
|
||||
defp set_default_query(user_id, organization_id) do
|
||||
from m in Membership,
|
||||
where: m.user_id == ^user_id and m.organization_id == ^organization_id
|
||||
organization_id
|
||||
|> MembershipQuery.for_organization()
|
||||
|> MembershipQuery.for_user(user_id)
|
||||
end
|
||||
|
||||
## Invitations
|
||||
|
|
@ -364,8 +364,7 @@ defmodule Towerops.Organizations do
|
|||
now = DateTime.utc_now()
|
||||
|
||||
Repo.all(
|
||||
from(i in Invitation,
|
||||
where: i.organization_id == ^organization_id,
|
||||
from(i in InvitationQuery.for_organization(organization_id),
|
||||
where: is_nil(i.accepted_at),
|
||||
where: i.expires_at > ^now,
|
||||
preload: [:invited_by],
|
||||
|
|
@ -395,9 +394,8 @@ defmodule Towerops.Organizations do
|
|||
"""
|
||||
def get_organization_invitation(invitation_id, organization_id) do
|
||||
Repo.one(
|
||||
from(i in Invitation,
|
||||
where: i.id == ^invitation_id,
|
||||
where: i.organization_id == ^organization_id
|
||||
from(i in InvitationQuery.for_organization(organization_id),
|
||||
where: i.id == ^invitation_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
|
@ -490,7 +488,7 @@ defmodule Towerops.Organizations do
|
|||
if organization.default_agent_token_id do
|
||||
# device IDs for this organization (includes devices with and without sites)
|
||||
device_ids =
|
||||
Repo.all(from(e in Device, where: e.organization_id == ^organization_id, select: e.id))
|
||||
Repo.all(from(e in DeviceQuery.for_organization(organization_id), select: e.id))
|
||||
|
||||
# Delete existing assignments
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
|
||||
|
|
@ -513,7 +511,7 @@ defmodule Towerops.Organizations do
|
|||
else
|
||||
# No default agent set, delete all assignments
|
||||
device_ids =
|
||||
Repo.all(from(e in Device, where: e.organization_id == ^organization_id, select: e.id))
|
||||
Repo.all(from(e in DeviceQuery.for_organization(organization_id), select: e.id))
|
||||
|
||||
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
|
||||
end
|
||||
|
|
@ -527,7 +525,7 @@ defmodule Towerops.Organizations do
|
|||
def clear_all_site_assignments(organization_id) do
|
||||
# Clear site assignments from all devices
|
||||
Repo.update_all(
|
||||
from(d in Device, where: d.organization_id == ^organization_id),
|
||||
DeviceQuery.for_organization(organization_id),
|
||||
set: [
|
||||
site_id: nil,
|
||||
updated_at: Towerops.Time.now()
|
||||
|
|
@ -589,13 +587,13 @@ defmodule Towerops.Organizations do
|
|||
@spec get_primary_owner(Organization.t()) :: User.t()
|
||||
def get_primary_owner(%Organization{} = organization) do
|
||||
Repo.one!(
|
||||
from m in Membership,
|
||||
from(m in MembershipQuery.for_organization(organization.id),
|
||||
join: u in assoc(m, :user),
|
||||
where: m.organization_id == ^organization.id,
|
||||
where: m.role == :owner,
|
||||
order_by: [desc: m.is_default, asc: m.inserted_at, asc: m.id],
|
||||
limit: 1,
|
||||
select: u
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
33
lib/towerops/organizations/invitation_query.ex
Normal file
33
lib/towerops/organizations/invitation_query.ex
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
defmodule Towerops.Organizations.InvitationQuery do
|
||||
@moduledoc """
|
||||
Composable query fragments for `Towerops.Organizations.Invitation`.
|
||||
|
||||
Lets the Organizations context build queries by piping filters together
|
||||
instead of repeating `where: i.organization_id == ^...` clauses inline.
|
||||
|
||||
## Example
|
||||
|
||||
InvitationQuery.base()
|
||||
|> InvitationQuery.for_organization(org_id)
|
||||
|> InvitationQuery.pending(now)
|
||||
|> Repo.all()
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Organizations.Invitation
|
||||
|
||||
@doc "Starting point for an Invitation query."
|
||||
def base, do: Invitation
|
||||
|
||||
@doc "Filter to a single organization."
|
||||
def for_organization(query \\ base(), organization_id) do
|
||||
where(query, [i], i.organization_id == ^organization_id)
|
||||
end
|
||||
|
||||
@doc "Filter to invitations not yet accepted and not expired at the given datetime."
|
||||
def pending(query \\ base(), now) do
|
||||
query
|
||||
|> where([i], is_nil(i.accepted_at))
|
||||
|> where([i], i.expires_at > ^now)
|
||||
end
|
||||
end
|
||||
41
lib/towerops/organizations/membership_query.ex
Normal file
41
lib/towerops/organizations/membership_query.ex
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
defmodule Towerops.Organizations.MembershipQuery do
|
||||
@moduledoc """
|
||||
Composable query fragments for `Towerops.Organizations.Membership`.
|
||||
|
||||
Lets the Organizations context build queries by piping filters together
|
||||
instead of repeating `where: m.organization_id == ^...` clauses inline.
|
||||
|
||||
## Example
|
||||
|
||||
MembershipQuery.base()
|
||||
|> MembershipQuery.for_organization(org_id)
|
||||
|> MembershipQuery.with_role(:owner)
|
||||
|> Repo.all()
|
||||
"""
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Organizations.Membership
|
||||
|
||||
@doc "Starting point for a Membership query."
|
||||
def base, do: Membership
|
||||
|
||||
@doc "Filter to a single organization."
|
||||
def for_organization(query \\ base(), organization_id) do
|
||||
where(query, [m], m.organization_id == ^organization_id)
|
||||
end
|
||||
|
||||
@doc "Filter to a single user."
|
||||
def for_user(query \\ base(), user_id) do
|
||||
where(query, [m], m.user_id == ^user_id)
|
||||
end
|
||||
|
||||
@doc "Filter by exact role (e.g. :owner, :admin)."
|
||||
def with_role(query \\ base(), role) do
|
||||
where(query, [m], m.role == ^role)
|
||||
end
|
||||
|
||||
@doc "Filter to memberships whose role is in the given list."
|
||||
def with_role_in(query \\ base(), roles) when is_list(roles) do
|
||||
where(query, [m], m.role in ^roles)
|
||||
end
|
||||
end
|
||||
|
|
@ -9,7 +9,7 @@ defmodule Towerops.Organizations.SubscriptionLimits do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Devices.DeviceQuery
|
||||
alias Towerops.Organizations.Membership
|
||||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Repo
|
||||
|
|
@ -148,9 +148,7 @@ defmodule Towerops.Organizations.SubscriptionLimits do
|
|||
"""
|
||||
def count_organization_devices(organization_id) do
|
||||
Repo.aggregate(
|
||||
from(d in Device,
|
||||
where: d.organization_id == ^organization_id
|
||||
),
|
||||
DeviceQuery.for_organization(organization_id),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ defmodule Towerops.Search do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Alerts.Alert
|
||||
alias Towerops.Alerts.AlertQuery
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Devices.DeviceQuery
|
||||
alias Towerops.Gaiia.Account
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Sites.Site
|
||||
|
|
@ -33,8 +34,8 @@ defmodule Towerops.Search do
|
|||
end
|
||||
|
||||
defp search_devices(organization_id, pattern) do
|
||||
Device
|
||||
|> where([d], d.organization_id == ^organization_id)
|
||||
organization_id
|
||||
|> DeviceQuery.for_organization()
|
||||
|> where([d], ilike(d.name, ^pattern) or ilike(d.ip_address, ^pattern))
|
||||
|> order_by([d], d.name)
|
||||
|> limit(@limit)
|
||||
|
|
@ -72,9 +73,9 @@ defmodule Towerops.Search do
|
|||
end
|
||||
|
||||
defp search_alerts(organization_id, pattern) do
|
||||
Alert
|
||||
organization_id
|
||||
|> AlertQuery.for_organization()
|
||||
|> join(:inner, [a], d in Device, on: a.device_id == d.id)
|
||||
|> where([a, d], d.organization_id == ^organization_id)
|
||||
|> where([a, _d], ilike(a.message, ^pattern))
|
||||
|> where([a, _d], is_nil(a.resolved_at))
|
||||
|> order_by([a, _d], desc: a.triggered_at)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue