towerops/lib/towerops_web/graphql/resolvers/device.ex
Graham McIntire 146f5745cf
fix: resolve compilation errors, test failures, and credo issues
- Escape HEEx template braces in GraphQL/API docs with raw(~S[...])
- Fix test assertions for updated marketing copy and UI text
- Extract helper functions in GraphQL resolvers to reduce nesting depth
- Create shared ErrorHelpers module for API controllers
- Fix ETS race condition in brute force whitelist cache for async tests
- Fix property test generators to use ASCII instead of printable unicode
- Add alert_severity helper to site_live/show
- Update accounts fixtures for explicit user confirmation
2026-02-14 12:23:10 -06:00

136 lines
4.4 KiB
Elixir

defmodule ToweropsWeb.GraphQL.Resolvers.Device do
@moduledoc "GraphQL resolvers for device queries and mutations."
alias Towerops.Devices
alias Towerops.Devices.Device
alias Towerops.Monitoring
alias Towerops.Repo
alias Towerops.Snmp
def list(_parent, args, %{context: %{organization_id: org_id}}) do
params = Map.new(args, fn {k, v} -> {to_string(k), v} end)
devices = Devices.list_organization_devices(org_id, params)
{:ok, devices}
end
def list(_parent, _args, _resolution), do: {:error, "Authentication required"}
def get(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
with {:ok, device} <- fetch_org_device(id, org_id) do
{:ok, Repo.preload(device, :site)}
end
end
def get(_parent, _args, _resolution), do: {:error, "Authentication required"}
def create(_parent, %{input: input}, %{context: %{organization_id: org_id}}) do
attrs =
input
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> Map.put("organization_id", org_id)
case Devices.create_device(attrs) do
{:ok, device} -> {:ok, Repo.preload(device, :site)}
{:error, changeset} -> {:error, format_errors(changeset)}
end
end
def create(_parent, _args, _resolution), do: {:error, "Authentication required"}
def update(_parent, %{id: id, input: input}, %{context: %{organization_id: org_id}}) do
with {:ok, device} <- fetch_org_device(id, org_id) do
attrs = Map.new(input, fn {k, v} -> {to_string(k), v} end)
case Devices.update_device(device, attrs) do
{:ok, updated} -> {:ok, Repo.preload(updated, :site, force: true)}
{:error, changeset} -> {:error, format_errors(changeset)}
end
end
end
def update(_parent, _args, _resolution), do: {:error, "Authentication required"}
def delete(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
with {:ok, device} <- fetch_org_device(id, org_id) do
case Devices.delete_device(device) do
{:ok, _} -> {:ok, %{success: true, message: "Device deleted"}}
{:error, _} -> {:ok, %{success: false, message: "Could not delete device"}}
end
end
end
def delete(_parent, _args, _resolution), do: {:error, "Authentication required"}
def metrics(_parent, %{device_id: device_id} = args, %{context: %{organization_id: org_id}}) do
with {:ok, device} <- fetch_org_device(device_id, org_id) do
{:ok, build_device_metrics(device, org_id, args)}
end
end
def metrics(_parent, _args, _resolution), do: {:error, "Authentication required"}
def interfaces(_parent, %{device_id: device_id}, %{context: %{organization_id: org_id}}) do
with {:ok, device} <- fetch_org_device(device_id, org_id) do
device = Repo.preload(device, :snmp_device)
interfaces =
case device.snmp_device do
nil -> []
snmp_device -> Snmp.list_interfaces(snmp_device.id)
end
{:ok, interfaces}
end
end
def interfaces(_parent, _args, _resolution), do: {:error, "Authentication required"}
defp fetch_org_device(id, org_id) do
case Repo.get(Device, id) do
nil -> {:error, "Device not found"}
%Device{organization_id: ^org_id} = device -> {:ok, device}
%Device{} -> {:error, "Device not found"}
end
end
defp build_device_metrics(device, org_id, args) do
hours = parse_time_range(Map.get(args, :time_range, "24h"))
from_time = DateTime.add(DateTime.utc_now(), -hours * 3600, :second)
check_type = args[:sensor_type]
checks = Monitoring.list_checks(org_id, device_id: device.id, check_type: check_type)
Enum.flat_map(checks, fn check ->
check.id
|> Monitoring.get_check_results(from: from_time)
|> Enum.map(&format_check_result(&1, check))
end)
end
defp format_check_result(result, check) do
%{
timestamp: to_string(result.checked_at),
value: result.value,
status: result.status,
check_name: check.name,
check_type: check.check_type
}
end
defp parse_time_range(range) do
case Integer.parse(String.replace(range, "h", "")) do
{hours, _} -> hours
:error -> 24
end
end
defp format_errors(changeset) do
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
|> Enum.map_join("; ", fn {field, messages} -> "#{field}: #{Enum.join(messages, ", ")}" end)
end
end