more dialyzer specs

This commit is contained in:
Graham McIntire 2026-02-05 13:11:18 -06:00
parent 6e730c6558
commit a803ddb893
No known key found for this signature in database
10 changed files with 47 additions and 1 deletions

View file

@ -33,6 +33,7 @@ defmodule Towerops.Accounts do
nil
"""
@spec get_user_by_email(String.t()) :: User.t() | nil
def get_user_by_email(email) when is_binary(email) do
Repo.get_by(User, email: email)
end
@ -49,6 +50,7 @@ defmodule Towerops.Accounts do
nil
"""
@spec get_user_by_email_and_password(String.t(), String.t()) :: User.t() | nil
def get_user_by_email_and_password(email, password) when is_binary(email) and is_binary(password) do
user = Repo.get_by(User, email: email)
if User.valid_password?(user, password), do: user
@ -68,6 +70,7 @@ defmodule Towerops.Accounts do
nil
"""
@spec get_user(String.t()) :: User.t() | nil
def get_user(id) when is_binary(id) do
Repo.get(User, id)
end
@ -86,6 +89,7 @@ defmodule Towerops.Accounts do
** (Ecto.NoResultsError)
"""
@spec get_user!(String.t()) :: User.t()
def get_user!(id), do: Repo.get!(User, id)
## User registration
@ -99,6 +103,7 @@ defmodule Towerops.Accounts do
%Ecto.Changeset{data: %User{}}
"""
@spec change_user_registration(User.t(), map()) :: Ecto.Changeset.t()
def change_user_registration(%User{} = user, attrs \\ %{}) do
User.registration_changeset(user, attrs, hash_password: false, validate_unique: false)
end
@ -115,6 +120,7 @@ defmodule Towerops.Accounts do
{:error, %Ecto.Changeset{}}
"""
@spec register_user(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def register_user(attrs) do
user_changeset =
%User{}
@ -147,6 +153,7 @@ defmodule Towerops.Accounts do
"""
@dialyzer {:nowarn_function, register_user_with_organization: 1}
@spec register_user_with_organization(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def register_user_with_organization(attrs) do
user_changeset =
%User{}

View file

@ -29,6 +29,7 @@ defmodule Towerops.Admin do
iex> list_all_users(limit: 50, offset: 100)
[%User{}, ...]
"""
@spec list_all_users(Keyword.t()) :: [User.t()]
def list_all_users(opts \\ []) do
limit = Keyword.get(opts, :limit, 100)
offset = Keyword.get(opts, :offset, 0)
@ -49,6 +50,7 @@ defmodule Towerops.Admin do
iex> count_users()
42
"""
@spec count_users() :: integer()
def count_users do
Repo.aggregate(User, :count)
end
@ -64,6 +66,7 @@ defmodule Towerops.Admin do
iex> delete_user(invalid_id, superuser_id, "127.0.0.1")
** (Ecto.NoResultsError)
"""
@spec delete_user(String.t(), String.t(), String.t()) :: {:ok, User.t()} | {:error, any()}
def delete_user(user_id, superuser_id, ip_address) do
user = Repo.get!(User, user_id)

View file

@ -30,6 +30,7 @@ defmodule Towerops.Agents do
{:error, %Ecto.Changeset{}}
"""
@spec create_agent_token(String.t(), String.t()) :: {:ok, AgentToken.t(), String.t()} | {:error, Ecto.Changeset.t()}
def create_agent_token(organization_id, name) do
{token, changeset} = AgentToken.build_token(organization_id, name)
@ -54,6 +55,7 @@ defmodule Towerops.Agents do
{:ok, %AgentToken{is_cloud_poller: true}, "abc123..."}
"""
@spec create_cloud_poller(String.t()) :: {:ok, AgentToken.t(), String.t()} | {:error, Ecto.Changeset.t()}
def create_cloud_poller(name) do
{token, changeset} = AgentToken.build_token(nil, name, is_cloud_poller: true)
@ -75,6 +77,7 @@ defmodule Towerops.Agents do
[%AgentToken{}, ...]
"""
@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)
@ -93,6 +96,7 @@ defmodule Towerops.Agents do
[%AgentToken{is_cloud_poller: true}, ...]
"""
@spec list_cloud_pollers() :: [AgentToken.t()]
def list_cloud_pollers do
AgentToken
|> where([t], t.is_cloud_poller == true)

View file

@ -11,6 +11,7 @@ defmodule Towerops.Alerts do
@doc """
Creates an alert for device status change.
"""
@spec create_alert(map()) :: {:ok, Alert.t()} | {:error, Ecto.Changeset.t()}
def create_alert(attrs) do
%Alert{}
|> Alert.changeset(attrs)
@ -20,6 +21,7 @@ defmodule Towerops.Alerts do
@doc """
Returns the list of alerts for an device.
"""
@spec list_devices_alerts(String.t(), integer()) :: [Alert.t()]
def list_devices_alerts(device_id, limit \\ 100) do
Repo.all(
from(a in Alert,
@ -35,6 +37,7 @@ defmodule Towerops.Alerts do
Returns the list of active (unresolved) alerts for an organization.
Only returns equipment_down alerts, as equipment_up alerts are informational.
"""
@spec list_organization_active_alerts(String.t()) :: [Alert.t()]
def list_organization_active_alerts(organization_id) do
Repo.all(
from(a in Alert,
@ -52,6 +55,7 @@ defmodule Towerops.Alerts do
@doc """
Returns the count of active (unresolved) alerts for an organization.
"""
@spec count_active_alerts(String.t()) :: integer()
def count_active_alerts(organization_id) do
Repo.aggregate(
from(a in Alert,
@ -74,6 +78,7 @@ defmodule Towerops.Alerts do
- status: Filter by status ("active", "acknowledged", "resolved")
- limit: Max number of alerts to return
"""
@spec list_organization_alerts(String.t(), integer() | map()) :: [Alert.t()]
def list_organization_alerts(organization_id, limit) when is_integer(limit) do
list_organization_alerts(organization_id, %{"limit" => limit})
end

View file

@ -21,6 +21,7 @@ defmodule Towerops.Devices do
Returns the list of devices for a site.
Ordered by custom display_order (if set), then alphabetically by name.
"""
@spec list_site_devices(String.t()) :: [DeviceSchema.t()]
def list_site_devices(site_id) do
Repo.all(
from(e in DeviceSchema,
@ -38,6 +39,7 @@ defmodule Towerops.Devices do
- site_id: Filter by specific site
- status: Filter by status ("up", "down", "unknown")
"""
@spec list_organization_devices(String.t(), map()) :: [DeviceSchema.t()]
def list_organization_devices(organization_id, filters \\ %{}) do
query =
from(e in DeviceSchema,
@ -67,6 +69,7 @@ defmodule Towerops.Devices do
@doc """
Returns all devices for multiple organizations (for GDPR data access).
"""
@spec list_devices_for_organizations([String.t()]) :: [DeviceSchema.t()]
def list_devices_for_organizations(organization_ids) when is_list(organization_ids) do
Repo.all(
from(e in DeviceSchema,
@ -82,6 +85,7 @@ defmodule Towerops.Devices do
@doc """
Returns the count of devices for an organization.
"""
@spec count_organization_devices(String.t()) :: integer()
def count_organization_devices(organization_id) do
Repo.aggregate(
from(e in DeviceSchema,
@ -94,6 +98,7 @@ defmodule Towerops.Devices do
@doc """
Returns the count of devices for a site.
"""
@spec count_site_devices(String.t()) :: integer()
def count_site_devices(site_id) do
Repo.aggregate(
from(e in DeviceSchema, where: e.site_id == ^site_id),

View file

@ -12,6 +12,7 @@ defmodule Towerops.Monitoring do
@doc """
Creates a monitoring check.
"""
@spec create_check(map()) :: {:ok, Check.t()} | {:error, Ecto.Changeset.t()}
def create_check(attrs) do
%Check{}
|> Check.changeset(attrs)
@ -21,6 +22,7 @@ defmodule Towerops.Monitoring do
@doc """
Returns the list of checks for an device.
"""
@spec list_devices_checks(String.t(), integer()) :: [Check.t()]
def list_devices_checks(device_id, limit \\ 100) do
Repo.all(from(c in Check, where: c.device_id == ^device_id, order_by: [desc: c.checked_at], limit: ^limit))
end
@ -28,6 +30,7 @@ defmodule Towerops.Monitoring do
@doc """
Returns the latest check for an device.
"""
@spec get_latest_check(String.t()) :: Check.t() | nil
def get_latest_check(device_id) do
Repo.one(from(c in Check, where: c.device_id == ^device_id, order_by: [desc: c.checked_at], limit: 1))
end
@ -35,6 +38,7 @@ defmodule Towerops.Monitoring do
@doc """
Deletes old monitoring checks older than the given date.
"""
@spec delete_old_checks(DateTime.t()) :: {integer(), nil | [term()]}
def delete_old_checks(older_than_date) do
Repo.delete_all(from(c in Check, where: c.checked_at < ^older_than_date))
end
@ -44,6 +48,7 @@ defmodule Towerops.Monitoring do
Uses TimescaleDB continuous aggregates for performance when available,
falls back to raw query calculation otherwise.
"""
@spec get_hourly_stats(String.t(), DateTime.t(), DateTime.t()) :: {:ok, Postgrex.Result.t()} | {:error, Exception.t()}
def get_hourly_stats(device_id, start_time, end_time) do
# Try continuous aggregate first (much faster for large datasets)
query = """

View file

@ -21,6 +21,7 @@ defmodule Towerops.Organizations do
@doc """
Returns the list of organizations for a user.
"""
@spec list_user_organizations(String.t()) :: [Organization.t()]
def list_user_organizations(user_id) do
Repo.all(
from(o in Organization,
@ -36,6 +37,7 @@ defmodule Towerops.Organizations do
@doc """
Checks if a user has access to an organization.
"""
@spec user_has_access?(String.t(), String.t()) :: boolean()
def user_has_access?(user_id, organization_id) do
Repo.exists?(
from m in Membership,
@ -46,11 +48,13 @@ defmodule Towerops.Organizations do
@doc """
Gets a single organization by ID.
"""
@spec get_organization!(String.t()) :: Organization.t()
def get_organization!(id), do: Repo.get!(Organization, id)
@doc """
Gets a single organization by slug.
"""
@spec get_organization_by_slug!(String.t()) :: Organization.t()
def get_organization_by_slug!(slug) do
Repo.get_by!(Organization, slug: slug)
end
@ -63,6 +67,7 @@ defmodule Towerops.Organizations do
"""
@dialyzer {:nowarn_function, create_organization: 2}
@dialyzer {:nowarn_function, create_organization: 3}
@spec create_organization(map(), String.t(), Keyword.t()) :: {:ok, Organization.t()} | {:error, Ecto.Changeset.t()}
def create_organization(attrs, user_id, opts \\ []) do
bypass_limits = Keyword.get(opts, :bypass_limits, false)
subscription_plan = Map.get(attrs, :subscription_plan) || Map.get(attrs, "subscription_plan") || "free"

View file

@ -15,6 +15,7 @@ defmodule Towerops.Sites do
Returns the list of sites for an organization.
Ordered by custom display_order (if set), then alphabetically by name.
"""
@spec list_organization_sites(String.t()) :: [Site.t()]
def list_organization_sites(organization_id) do
Repo.all(
from(s in Site,
@ -28,6 +29,7 @@ defmodule Towerops.Sites do
@doc """
Returns the count of sites for an organization.
"""
@spec count_organization_sites(String.t()) :: integer()
def count_organization_sites(organization_id) do
Repo.aggregate(
from(s in Site, where: s.organization_id == ^organization_id),
@ -39,6 +41,7 @@ defmodule Towerops.Sites do
Returns the list of root sites (sites without a parent) for an organization.
Ordered by custom display_order (if set), then alphabetically by name.
"""
@spec list_root_sites(String.t()) :: [Site.t()]
def list_root_sites(organization_id) do
Repo.all(
from(s in Site,
@ -53,6 +56,7 @@ defmodule Towerops.Sites do
Returns the list of child sites for a parent site.
Ordered by custom display_order (if set), then alphabetically by name.
"""
@spec list_child_sites(String.t()) :: [Site.t()]
def list_child_sites(parent_site_id) do
Repo.all(
from(s in Site,
@ -65,6 +69,7 @@ defmodule Towerops.Sites do
@doc """
Gets a single site.
"""
@spec get_site!(String.t()) :: Site.t()
def get_site!(id) do
Site
|> Repo.get!(id)
@ -74,6 +79,7 @@ defmodule Towerops.Sites do
@doc """
Gets a single site. Returns nil if not found.
"""
@spec get_site(String.t()) :: Site.t() | nil
def get_site(id) do
case Repo.get(Site, id) do
nil -> nil
@ -84,6 +90,7 @@ defmodule Towerops.Sites do
@doc """
Gets a single site belonging to an organization.
"""
@spec get_organization_site!(String.t(), String.t()) :: Site.t()
def get_organization_site!(organization_id, site_id) do
Repo.one!(
from(s in Site,
@ -97,6 +104,7 @@ defmodule Towerops.Sites do
@doc """
Creates a site.
"""
@spec create_site(map()) :: {:ok, Site.t()} | {:error, Ecto.Changeset.t()}
def create_site(attrs) do
%Site{}
|> Site.changeset(attrs)

View file

@ -38,6 +38,7 @@ defmodule Towerops.Snmp do
iex> test_connection("192.168.1.99", "wrong", "2c")
{:error, :timeout}
"""
@spec test_connection(String.t(), String.t(), String.t(), integer()) :: {:ok, String.t()} | {:error, term()}
def test_connection(ip, community, version, port \\ 161) do
Client.test_connection(
ip: ip,
@ -62,6 +63,7 @@ defmodule Towerops.Snmp do
iex> discover_device(device)
{:ok, %Device{}}
"""
@spec discover_device(DeviceSchema.t()) :: {:ok, Device.t()} | {:error, term()}
def discover_device(%DeviceSchema{} = device) do
Discovery.discover_device(device)
end
@ -76,6 +78,7 @@ defmodule Towerops.Snmp do
iex> discover_all_for_org(org_id)
{:ok, %{success: 10, failed: 2, errors: [:timeout, :no_response]}}
"""
@spec discover_all_for_org(String.t()) :: {:ok, map()}
def discover_all_for_org(org_id) do
Discovery.discover_all(org_id)
end
@ -93,6 +96,7 @@ defmodule Towerops.Snmp do
iex> get_device(nonexistent_id)
nil
"""
@spec get_device(String.t()) :: Device.t() | nil
def get_device(device_id) do
Repo.get_by(Device, device_id: device_id)
end

View file

@ -98,7 +98,7 @@ defmodule Towerops.MixProject do
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
# Faster incremental analysis
plt_add_deps: :apps_direct,
plt_add_apps: [:mix, :ex_unit],
plt_add_apps: [:mix, :ex_unit, :ecto],
flags: [:unmatched_returns, :error_handling, :underspecs, :unknown],
ignore_warnings: ".dialyzer_ignore.exs"
]