towerops/lib/towerops_web/graphql/resolvers/schedule.ex
Graham McIntire f6e9063577
feat: add REST API and GraphQL for on-call schedules and escalation policies
REST API v1 endpoints for schedules (14 endpoints) and escalation
policies (10 endpoints) with full CRUD for nested resources (layers,
members, overrides, rules, targets). GraphQL types, resolvers, and
schema mutations for both resource trees.
2026-03-11 14:01:15 -05:00

223 lines
7.2 KiB
Elixir

defmodule ToweropsWeb.GraphQL.Resolvers.Schedule do
@moduledoc "GraphQL resolvers for on-call schedule queries and mutations."
alias Towerops.OnCall
alias Towerops.OnCall.Layer
alias Towerops.OnCall.LayerMember
alias Towerops.OnCall.Override
alias Towerops.OnCall.Schedule
alias Towerops.Repo
alias ToweropsWeb.GraphQL.Resolvers.Helpers
alias ToweropsWeb.ScopedResource
# --- Queries ---
def list(_parent, _args, %{context: %{organization_id: org_id}}) do
{:ok, OnCall.list_schedules(org_id)}
end
def list(_parent, _args, _resolution), do: Helpers.authentication_error()
def get(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
case ScopedResource.fetch_preload(Schedule, id, org_id,
overrides: :user,
layers: [members: :user]
) do
{:ok, schedule} -> {:ok, schedule}
{:error, _} -> {:error, "Schedule not found"}
end
end
def get(_parent, _args, _resolution), do: Helpers.authentication_error()
def on_call(_parent, %{schedule_id: id}, %{context: %{organization_id: org_id}}) do
case ScopedResource.fetch(Schedule, id, org_id) do
{:ok, _} ->
case OnCall.who_is_on_call(id) do
nil -> {:ok, nil}
user -> {:ok, %{user_id: user.id, email: user.email}}
end
{:error, _} ->
{:error, "Schedule not found"}
end
end
def on_call(_parent, _args, _resolution), do: Helpers.authentication_error()
# --- Mutations ---
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 OnCall.create_schedule(attrs) do
{:ok, schedule} -> {:ok, schedule}
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
end
end
def create(_parent, _args, _resolution), do: Helpers.authentication_error()
def update(_parent, %{id: id, input: input}, %{context: %{organization_id: org_id}}) do
with {:ok, schedule} <- fetch_org_schedule(id, org_id) do
attrs = Map.new(input, fn {k, v} -> {to_string(k), v} end)
case OnCall.update_schedule(schedule, attrs) do
{:ok, updated} -> {:ok, updated}
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
end
end
end
def update(_parent, _args, _resolution), do: Helpers.authentication_error()
def delete(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
with {:ok, schedule} <- fetch_org_schedule(id, org_id) do
case OnCall.delete_schedule(schedule) do
{:ok, _} -> {:ok, %{success: true, message: "Schedule deleted"}}
{:error, _} -> {:ok, %{success: false, message: "Could not delete schedule"}}
end
end
end
def delete(_parent, _args, _resolution), do: Helpers.authentication_error()
# --- Layer mutations ---
def create_layer(_parent, %{schedule_id: schedule_id, input: input}, %{context: %{organization_id: org_id}}) do
with {:ok, _schedule} <- fetch_org_schedule(schedule_id, org_id) do
attrs =
input
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> Map.put("schedule_id", schedule_id)
case OnCall.create_layer(attrs) do
{:ok, layer} -> {:ok, layer}
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
end
end
end
def create_layer(_parent, _args, _resolution), do: Helpers.authentication_error()
def update_layer(_parent, %{id: id, input: input}, %{context: %{organization_id: org_id}}) do
with {:ok, layer} <- fetch_layer_by_org(id, org_id) do
attrs = Map.new(input, fn {k, v} -> {to_string(k), v} end)
case OnCall.update_layer(layer, attrs) do
{:ok, updated} -> {:ok, updated}
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
end
end
end
def update_layer(_parent, _args, _resolution), do: Helpers.authentication_error()
def delete_layer(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
with {:ok, layer} <- fetch_layer_by_org(id, org_id) do
case OnCall.delete_layer(layer) do
{:ok, _} -> {:ok, %{success: true, message: "Layer deleted"}}
{:error, _} -> {:ok, %{success: false, message: "Could not delete layer"}}
end
end
end
def delete_layer(_parent, _args, _resolution), do: Helpers.authentication_error()
# --- Member mutations ---
def add_member(_parent, %{layer_id: layer_id, user_id: user_id, position: position}, %{
context: %{organization_id: org_id}
}) do
with {:ok, _layer} <- fetch_layer_by_org(layer_id, org_id) do
attrs = %{
"layer_id" => layer_id,
"user_id" => user_id,
"position" => position
}
case OnCall.add_layer_member(attrs) do
{:ok, member} -> {:ok, member}
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
end
end
end
def add_member(_parent, _args, _resolution), do: Helpers.authentication_error()
def remove_member(_parent, %{id: id}, %{context: %{organization_id: _org_id}}) do
case Repo.get(LayerMember, id) do
nil ->
{:error, "Member not found"}
member ->
case OnCall.remove_layer_member(member) do
{:ok, _} -> {:ok, %{success: true, message: "Member removed"}}
{:error, _} -> {:ok, %{success: false, message: "Could not remove member"}}
end
end
end
def remove_member(_parent, _args, _resolution), do: Helpers.authentication_error()
# --- Override mutations ---
def create_override(_parent, %{schedule_id: schedule_id, input: input}, %{context: %{organization_id: org_id}}) do
with {:ok, _schedule} <- fetch_org_schedule(schedule_id, org_id) do
attrs =
input
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> Map.put("schedule_id", schedule_id)
case OnCall.create_override(attrs) do
{:ok, override} -> {:ok, override}
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
end
end
end
def create_override(_parent, _args, _resolution), do: Helpers.authentication_error()
def delete_override(_parent, %{id: id}, %{context: %{organization_id: _org_id}}) do
case Repo.get(Override, id) do
nil ->
{:error, "Override not found"}
override ->
case OnCall.delete_override(override) do
{:ok, _} -> {:ok, %{success: true, message: "Override deleted"}}
{:error, _} -> {:ok, %{success: false, message: "Could not delete override"}}
end
end
end
def delete_override(_parent, _args, _resolution), do: Helpers.authentication_error()
# --- Private helpers ---
defp fetch_org_schedule(id, org_id) do
case ScopedResource.fetch(Schedule, id, org_id) do
{:ok, schedule} -> {:ok, schedule}
{:error, _} -> {:error, "Schedule not found"}
end
end
defp fetch_layer_by_org(layer_id, org_id) do
case Repo.get(Layer, layer_id) do
nil ->
{:error, "Layer not found"}
layer ->
layer = Repo.preload(layer, :schedule)
if layer.schedule.organization_id == org_id do
{:ok, layer}
else
{:error, "Layer not found"}
end
end
end
end