diff --git a/lib/towerops/agents.ex b/lib/towerops/agents.ex index 6b88cc42..701885a6 100644 --- a/lib/towerops/agents.ex +++ b/lib/towerops/agents.ex @@ -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 diff --git a/lib/towerops/agents/agent_token_query.ex b/lib/towerops/agents/agent_token_query.ex new file mode 100644 index 00000000..113280b6 --- /dev/null +++ b/lib/towerops/agents/agent_token_query.ex @@ -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 diff --git a/lib/towerops/agents/stats.ex b/lib/towerops/agents/stats.ex index 00edd4d0..48df1ff0 100644 --- a/lib/towerops/agents/stats.ex +++ b/lib/towerops/agents/stats.ex @@ -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} ) diff --git a/lib/towerops/alerts.ex b/lib/towerops/alerts.ex index 793a280a..136011d6 100644 --- a/lib/towerops/alerts.ex +++ b/lib/towerops/alerts.ex @@ -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: []] diff --git a/lib/towerops/devices.ex b/lib/towerops/devices.ex index 72bea743..cc0fe416 100644 --- a/lib/towerops/devices.ex +++ b/lib/towerops/devices.ex @@ -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" ) diff --git a/lib/towerops/gaiia.ex b/lib/towerops/gaiia.ex index 45fe7277..64ab98a6 100644 --- a/lib/towerops/gaiia.ex +++ b/lib/towerops/gaiia.ex @@ -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} diff --git a/lib/towerops/gaiia/device_subscriber_link_query.ex b/lib/towerops/gaiia/device_subscriber_link_query.ex new file mode 100644 index 00000000..c13b519b --- /dev/null +++ b/lib/towerops/gaiia/device_subscriber_link_query.ex @@ -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 diff --git a/lib/towerops/gaiia/site_aggregation.ex b/lib/towerops/gaiia/site_aggregation.ex index be0f8555..dc927744 100644 --- a/lib/towerops/gaiia/site_aggregation.ex +++ b/lib/towerops/gaiia/site_aggregation.ex @@ -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() diff --git a/lib/towerops/gaiia/subscriber_matching.ex b/lib/towerops/gaiia/subscriber_matching.ex index b8eb13cc..d86f09a2 100644 --- a/lib/towerops/gaiia/subscriber_matching.ex +++ b/lib/towerops/gaiia/subscriber_matching.ex @@ -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) diff --git a/lib/towerops/maintenance.ex b/lib/towerops/maintenance.ex index d1a922e3..6bc58b3a 100644 --- a/lib/towerops/maintenance.ex +++ b/lib/towerops/maintenance.ex @@ -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 diff --git a/lib/towerops/maintenance/maintenance_window_query.ex b/lib/towerops/maintenance/maintenance_window_query.ex new file mode 100644 index 00000000..c79cc950 --- /dev/null +++ b/lib/towerops/maintenance/maintenance_window_query.ex @@ -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 diff --git a/lib/towerops/on_call.ex b/lib/towerops/on_call.ex index b5ad5b36..733d4860 100644 --- a/lib/towerops/on_call.ex +++ b/lib/towerops/on_call.ex @@ -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 diff --git a/lib/towerops/on_call/escalation_policy_query.ex b/lib/towerops/on_call/escalation_policy_query.ex new file mode 100644 index 00000000..ae7e01f8 --- /dev/null +++ b/lib/towerops/on_call/escalation_policy_query.ex @@ -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 diff --git a/lib/towerops/on_call/schedule_query.ex b/lib/towerops/on_call/schedule_query.ex new file mode 100644 index 00000000..33cf83ae --- /dev/null +++ b/lib/towerops/on_call/schedule_query.ex @@ -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 diff --git a/lib/towerops/organizations.ex b/lib/towerops/organizations.ex index 1886e126..29da6f5f 100644 --- a/lib/towerops/organizations.ex +++ b/lib/towerops/organizations.ex @@ -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 diff --git a/lib/towerops/organizations/invitation_query.ex b/lib/towerops/organizations/invitation_query.ex new file mode 100644 index 00000000..92f83fea --- /dev/null +++ b/lib/towerops/organizations/invitation_query.ex @@ -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 diff --git a/lib/towerops/organizations/membership_query.ex b/lib/towerops/organizations/membership_query.ex new file mode 100644 index 00000000..11ac85a5 --- /dev/null +++ b/lib/towerops/organizations/membership_query.ex @@ -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 diff --git a/lib/towerops/organizations/subscription_limits.ex b/lib/towerops/organizations/subscription_limits.ex index 84dfff7a..731a848f 100644 --- a/lib/towerops/organizations/subscription_limits.ex +++ b/lib/towerops/organizations/subscription_limits.ex @@ -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 diff --git a/lib/towerops/search.ex b/lib/towerops/search.ex index ae0529df..f3f22608 100644 --- a/lib/towerops/search.ex +++ b/lib/towerops/search.ex @@ -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)