Extend the GraphQL API and socket to accept mobile session tokens alongside existing API tokens. Add OrganizationScope middleware that extracts organization_id from query args for mobile users. Add my_organizations query for mobile app org listing. Reviewed-on: graham/towerops-web#48
522 lines
16 KiB
Elixir
522 lines
16 KiB
Elixir
defmodule ToweropsWeb.GraphQL.Schema do
|
|
@moduledoc """
|
|
GraphQL schema for the TowerOps API.
|
|
|
|
Provides a complete GraphQL interface for managing devices, sites, alerts,
|
|
agents, organization settings, members, integrations, and activity feeds.
|
|
|
|
Queries that require an organization scope accept an optional `organization_id`
|
|
argument for mobile authentication. API token users can omit it (org comes from token).
|
|
"""
|
|
use Absinthe.Schema
|
|
|
|
alias ToweropsWeb.GraphQL.Middleware.OrganizationScope
|
|
|
|
import_types(ToweropsWeb.GraphQL.Types.Common)
|
|
import_types(ToweropsWeb.GraphQL.Types.Device)
|
|
import_types(ToweropsWeb.GraphQL.Types.Site)
|
|
import_types(ToweropsWeb.GraphQL.Types.Alert)
|
|
import_types(ToweropsWeb.GraphQL.Types.Agent)
|
|
import_types(ToweropsWeb.GraphQL.Types.Organization)
|
|
import_types(ToweropsWeb.GraphQL.Types.Member)
|
|
import_types(ToweropsWeb.GraphQL.Types.Integration)
|
|
import_types(ToweropsWeb.GraphQL.Types.Activity)
|
|
import_types(ToweropsWeb.GraphQL.Types.Schedule)
|
|
import_types(ToweropsWeb.GraphQL.Types.EscalationPolicy)
|
|
import_types(ToweropsWeb.GraphQL.Types.MaintenanceWindow)
|
|
import_types(ToweropsWeb.GraphQL.Types.TimeSeries)
|
|
|
|
query do
|
|
# User's organizations (no org scope needed — for mobile org picker)
|
|
field :my_organizations, list_of(:organization) do
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Organization.list_mine/3)
|
|
end
|
|
|
|
# Devices
|
|
field :devices, list_of(:device) do
|
|
arg(:organization_id, :id)
|
|
arg(:site_id, :id)
|
|
arg(:status, :string)
|
|
arg(:limit, :integer, default_value: 100)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Device.list/3)
|
|
end
|
|
|
|
field :device, :device do
|
|
arg(:organization_id, :id)
|
|
arg(:id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Device.get/3)
|
|
end
|
|
|
|
# Sites
|
|
field :sites, list_of(:site) do
|
|
arg(:organization_id, :id)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Site.list/3)
|
|
end
|
|
|
|
field :site, :site do
|
|
arg(:organization_id, :id)
|
|
arg(:id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Site.get/3)
|
|
end
|
|
|
|
# Alerts
|
|
field :alerts, list_of(:alert) do
|
|
arg(:organization_id, :id)
|
|
arg(:status, :string)
|
|
arg(:device_id, :id)
|
|
arg(:limit, :integer, default_value: 100)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Alert.list/3)
|
|
end
|
|
|
|
field :alert, :alert do
|
|
arg(:organization_id, :id)
|
|
arg(:id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Alert.get/3)
|
|
end
|
|
|
|
# Agents
|
|
field :agents, list_of(:agent) do
|
|
arg(:organization_id, :id)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Agent.list/3)
|
|
end
|
|
|
|
field :agent, :agent do
|
|
arg(:organization_id, :id)
|
|
arg(:id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Agent.get/3)
|
|
end
|
|
|
|
# Organization
|
|
field :organization, :organization do
|
|
arg(:organization_id, :id)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Organization.get/3)
|
|
end
|
|
|
|
# Members
|
|
field :members, list_of(:member) do
|
|
arg(:organization_id, :id)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Member.list/3)
|
|
end
|
|
|
|
# Integrations
|
|
field :integrations, list_of(:integration) do
|
|
arg(:organization_id, :id)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Integration.list/3)
|
|
end
|
|
|
|
# Activity feed
|
|
field :activity, list_of(:activity_item) do
|
|
arg(:organization_id, :id)
|
|
arg(:limit, :integer, default_value: 50)
|
|
arg(:type, :string)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Activity.list/3)
|
|
end
|
|
|
|
# Device metrics
|
|
field :device_metrics, list_of(:metric_point) do
|
|
arg(:organization_id, :id)
|
|
arg(:device_id, non_null(:id))
|
|
arg(:sensor_type, :string)
|
|
arg(:time_range, :string, default_value: "24h")
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Device.metrics/3)
|
|
end
|
|
|
|
# Device interfaces
|
|
field :device_interfaces, list_of(:interface) do
|
|
arg(:organization_id, :id)
|
|
arg(:device_id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Device.interfaces/3)
|
|
end
|
|
|
|
# Time-series queries
|
|
field :device_sensors, list_of(:sensor) do
|
|
arg(:organization_id, :id)
|
|
arg(:device_id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.TimeSeries.device_sensors/3)
|
|
end
|
|
|
|
field :sensor_readings, list_of(:sensor_reading) do
|
|
arg(:organization_id, :id)
|
|
arg(:sensor_id, non_null(:id))
|
|
arg(:time_range, :string, default_value: "24h")
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.TimeSeries.sensor_readings/3)
|
|
end
|
|
|
|
field :interface_traffic, list_of(:interface_traffic_point) do
|
|
arg(:organization_id, :id)
|
|
arg(:interface_id, non_null(:id))
|
|
arg(:time_range, :string, default_value: "24h")
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.TimeSeries.interface_traffic/3)
|
|
end
|
|
|
|
field :check_results, list_of(:check_result_point) do
|
|
arg(:organization_id, :id)
|
|
arg(:check_id, non_null(:id))
|
|
arg(:time_range, :string, default_value: "1h")
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.TimeSeries.check_results/3)
|
|
end
|
|
|
|
# Schedules
|
|
field :schedules, list_of(:schedule) do
|
|
arg(:organization_id, :id)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.list/3)
|
|
end
|
|
|
|
field :schedule, :schedule do
|
|
arg(:organization_id, :id)
|
|
arg(:id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.get/3)
|
|
end
|
|
|
|
field :on_call, :on_call_result do
|
|
arg(:organization_id, :id)
|
|
arg(:schedule_id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.on_call/3)
|
|
end
|
|
|
|
# Escalation Policies
|
|
field :escalation_policies, list_of(:escalation_policy) do
|
|
arg(:organization_id, :id)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.list/3)
|
|
end
|
|
|
|
field :escalation_policy, :escalation_policy do
|
|
arg(:organization_id, :id)
|
|
arg(:id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.get/3)
|
|
end
|
|
|
|
# Maintenance Windows
|
|
field :maintenance_windows, list_of(:maintenance_window) do
|
|
arg(:organization_id, :id)
|
|
arg(:filter, :string)
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.list/3)
|
|
end
|
|
|
|
field :maintenance_window, :maintenance_window do
|
|
arg(:organization_id, :id)
|
|
arg(:id, non_null(:id))
|
|
middleware(OrganizationScope)
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.get/3)
|
|
end
|
|
end
|
|
|
|
mutation do
|
|
# Device CRUD
|
|
field :create_device, :device do
|
|
arg(:input, non_null(:device_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Device.create/3)
|
|
end
|
|
|
|
field :update_device, :device do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:device_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Device.update/3)
|
|
end
|
|
|
|
field :delete_device, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Device.delete/3)
|
|
end
|
|
|
|
# Site CRUD
|
|
field :create_site, :site do
|
|
arg(:input, non_null(:site_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Site.create/3)
|
|
end
|
|
|
|
field :update_site, :site do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:site_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Site.update/3)
|
|
end
|
|
|
|
field :delete_site, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Site.delete/3)
|
|
end
|
|
|
|
# Alert actions
|
|
field :acknowledge_alert, :alert do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Alert.acknowledge/3)
|
|
end
|
|
|
|
field :resolve_alert, :alert do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Alert.resolve_alert/3)
|
|
end
|
|
|
|
# Agent management
|
|
field :create_agent, :agent_with_token do
|
|
arg(:name, non_null(:string))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Agent.create/3)
|
|
end
|
|
|
|
field :delete_agent, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Agent.delete/3)
|
|
end
|
|
|
|
# Organization settings
|
|
field :update_organization, :organization do
|
|
arg(:input, non_null(:organization_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Organization.update/3)
|
|
end
|
|
|
|
# Member management
|
|
field :send_invitation, :invitation do
|
|
arg(:email, non_null(:string))
|
|
arg(:role, :string, default_value: "technician")
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Member.invite/3)
|
|
end
|
|
|
|
field :cancel_invitation, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Member.cancel_invitation/3)
|
|
end
|
|
|
|
field :remove_member, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Member.remove/3)
|
|
end
|
|
|
|
field :update_member_role, :member do
|
|
arg(:id, non_null(:id))
|
|
arg(:role, non_null(:string))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Member.update_role/3)
|
|
end
|
|
|
|
# Integrations
|
|
field :create_integration, :integration do
|
|
arg(:input, non_null(:integration_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Integration.create/3)
|
|
end
|
|
|
|
field :update_integration, :integration do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:integration_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Integration.update/3)
|
|
end
|
|
|
|
field :delete_integration, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Integration.delete/3)
|
|
end
|
|
|
|
field :test_integration, :test_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Integration.test_connection/3)
|
|
end
|
|
|
|
# Schedule CRUD
|
|
field :create_schedule, :schedule do
|
|
arg(:input, non_null(:schedule_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.create/3)
|
|
end
|
|
|
|
field :update_schedule, :schedule do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:schedule_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.update/3)
|
|
end
|
|
|
|
field :delete_schedule, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.delete/3)
|
|
end
|
|
|
|
# Layer CRUD
|
|
field :create_layer, :layer do
|
|
arg(:schedule_id, non_null(:id))
|
|
arg(:input, non_null(:layer_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.create_layer/3)
|
|
end
|
|
|
|
field :update_layer, :layer do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:layer_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.update_layer/3)
|
|
end
|
|
|
|
field :delete_layer, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.delete_layer/3)
|
|
end
|
|
|
|
# Layer member management
|
|
field :add_layer_member, :layer_member do
|
|
arg(:layer_id, non_null(:id))
|
|
arg(:user_id, non_null(:id))
|
|
arg(:position, non_null(:integer))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.add_member/3)
|
|
end
|
|
|
|
field :remove_layer_member, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.remove_member/3)
|
|
end
|
|
|
|
# Override management
|
|
field :create_override, :override do
|
|
arg(:schedule_id, non_null(:id))
|
|
arg(:input, non_null(:override_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.create_override/3)
|
|
end
|
|
|
|
field :delete_override, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.Schedule.delete_override/3)
|
|
end
|
|
|
|
# Escalation Policy CRUD
|
|
field :create_escalation_policy, :escalation_policy do
|
|
arg(:input, non_null(:escalation_policy_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.create/3)
|
|
end
|
|
|
|
field :update_escalation_policy, :escalation_policy do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:escalation_policy_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.update/3)
|
|
end
|
|
|
|
field :delete_escalation_policy, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.delete/3)
|
|
end
|
|
|
|
# Escalation Rule CRUD
|
|
field :create_escalation_rule, :escalation_rule do
|
|
arg(:escalation_policy_id, non_null(:id))
|
|
arg(:input, non_null(:escalation_rule_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.create_rule/3)
|
|
end
|
|
|
|
field :update_escalation_rule, :escalation_rule do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:escalation_rule_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.update_rule/3)
|
|
end
|
|
|
|
field :delete_escalation_rule, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.delete_rule/3)
|
|
end
|
|
|
|
# Escalation Target management
|
|
field :create_escalation_target, :escalation_target do
|
|
arg(:escalation_rule_id, non_null(:id))
|
|
arg(:input, non_null(:escalation_target_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.create_target/3)
|
|
end
|
|
|
|
field :delete_escalation_target, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.delete_target/3)
|
|
end
|
|
|
|
# Maintenance Window CRUD
|
|
field :create_maintenance_window, :maintenance_window do
|
|
arg(:input, non_null(:maintenance_window_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.create/3)
|
|
end
|
|
|
|
field :update_maintenance_window, :maintenance_window do
|
|
arg(:id, non_null(:id))
|
|
arg(:input, non_null(:maintenance_window_input))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.update/3)
|
|
end
|
|
|
|
field :delete_maintenance_window, :delete_result do
|
|
arg(:id, non_null(:id))
|
|
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.delete/3)
|
|
end
|
|
end
|
|
|
|
subscription do
|
|
field :device_status_changed, :device_status_event do
|
|
arg(:organization_id, :id)
|
|
arg(:device_id, :id)
|
|
|
|
config(fn args, resolution ->
|
|
org_id = resolve_subscription_org_id(args, resolution)
|
|
|
|
if org_id do
|
|
topic =
|
|
if args[:device_id],
|
|
do: "gql:device_status:#{org_id}:#{args.device_id}",
|
|
else: "gql:device_status:#{org_id}"
|
|
|
|
{:ok, topic: topic}
|
|
else
|
|
{:error, "organization_id is required"}
|
|
end
|
|
end)
|
|
end
|
|
|
|
field :alert_event, :alert_event do
|
|
arg(:organization_id, :id)
|
|
|
|
config(fn args, resolution ->
|
|
org_id = resolve_subscription_org_id(args, resolution)
|
|
|
|
if org_id do
|
|
{:ok, topic: "gql:alerts:#{org_id}"}
|
|
else
|
|
{:error, "organization_id is required"}
|
|
end
|
|
end)
|
|
end
|
|
|
|
field :sensor_readings_updated, :sensor_readings_event do
|
|
arg(:organization_id, :id)
|
|
arg(:device_id, non_null(:id))
|
|
|
|
config(fn args, resolution ->
|
|
org_id = resolve_subscription_org_id(args, resolution)
|
|
|
|
if org_id do
|
|
{:ok, topic: "gql:sensor_readings:#{org_id}:#{args.device_id}"}
|
|
else
|
|
{:error, "organization_id is required"}
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
# Resolves org_id from context (API token) or args (mobile token)
|
|
defp resolve_subscription_org_id(args, %{context: %{organization_id: org_id}}) do
|
|
args[:organization_id] || org_id
|
|
end
|
|
|
|
defp resolve_subscription_org_id(%{organization_id: org_id}, %{context: %{user: user}}) when is_binary(org_id) do
|
|
if Towerops.Organizations.user_has_access?(user.id, org_id), do: org_id
|
|
end
|
|
|
|
defp resolve_subscription_org_id(_args, _resolution), do: nil
|
|
end
|