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:
Graham McIntire 2026-04-30 14:11:42 -05:00
parent a680d5ea94
commit 2528e78ff9
19 changed files with 315 additions and 81 deletions

View file

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

View 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

View file

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

View file

@ -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: []]

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View 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

View file

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

View 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

View 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

View file

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

View 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

View 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

View file

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

View file

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