chore: add gaiia library dep, remove stale honeybadger/tidewave/cbor

- Add vendored Gaiia library (path: vendor/gaiia) with auto-generated GraphQL
  queries and mutations from the Gaiia API schema
- Add startAddInventoryItemsJob + updateInventoryItem mutations
- Remove `:cbor` (was unused — no references anywhere)
- Remove stale Honeybadger filter module, tests, router plug, and config
- Remove stale Tidewave plug from endpoint (dev-only, dep already gone)
- Unlock leftover deps from lock file
This commit is contained in:
Graham McIntire 2026-05-11 14:46:27 -05:00
parent 485748d253
commit 1f9fe2ee29
23 changed files with 12130 additions and 438 deletions

View file

@ -25,13 +25,6 @@ config :esbuild,
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
]
config :honeybadger,
api_key: "hbp_xe5xMnpLZ2XJsXQJujoEkgCmiqCfwa0uYA3Y",
environment_name: config_env(),
insights_enabled: true,
use_logger: true,
filter: Towerops.HoneybadgerFilter
# Configure Elixir's Logger
config :logger, :default_formatter,
format: "$time $metadata[$level] $message\n",

View file

@ -1,12 +1,5 @@
import Config
# Disable Honeybadger in development
config :honeybadger,
environment_name: :dev,
exclude_envs: [:dev],
# Don't use custom filter in dev since Honeybadger is excluded
filter: Honeybadger.Filter.Default
# Console backend configuration
config :logger, :console,
format: "[$level] $message\n",

View file

@ -3,8 +3,7 @@ defmodule Towerops.ErrorTrackerIgnorer do
Drops benign errors from ErrorTracker that occur during K8s pod shutdown
or transient DB connection cycling.
Matches the same patterns as `Towerops.HoneybadgerFilter` and
`Towerops.LoggerFilters.drop_shutdown_errors/2`.
Matches the same patterns as `Towerops.LoggerFilters.drop_shutdown_errors/2`.
"""
@behaviour ErrorTracker.Ignorer

View file

@ -1,169 +0,0 @@
defmodule Towerops.HoneybadgerFilter do
@moduledoc """
Custom Honeybadger filter to exclude noisy errors during deployments
and filter sensitive SNMP credentials from error reports.
Filters out errors that occur during normal application shutdown/restart:
- OTP supervisor/gen_server termination errors
- Redix connection errors (tcp_closed, econnrefused)
- DBConnection errors during shutdown
- Normal process exits (:normal, :shutdown, {:shutdown, _})
Also filters sensitive data from error reports (passwords, tokens, SNMP communities).
"""
use Honeybadger.Filter.Mixin
# Named supervisors/processes that generate errors during normal shutdown
@ignored_registered_names [
"memsup",
"cpu_sup",
"Elixir.PollerSupervisor"
]
# Sensitive keys that should be filtered from error reports
@sensitive_keys [
"password",
"snmp_community",
"community",
"secret",
"token",
"api_key",
"auth_password",
"priv_password",
"auth_pass",
"priv_pass"
]
@impl Honeybadger.Filter
def filter_context(context) do
cond do
# Named process in our ignore list
get_in(context, [:registered_name]) in @ignored_registered_names ->
nil
# Anonymous gen_server shutdown errors (OTP domain, gen_server error_info)
otp_gen_server_shutdown?(context) ->
nil
# Redix connection errors during shutdown/restart
redix_connection_error?(context) ->
nil
# DBConnection errors during shutdown/restart
db_connection_error?(context) ->
nil
# Normal process shutdown (exit :normal, :shutdown, {:shutdown, _})
normal_shutdown?(context) ->
nil
true ->
context
|> super()
|> filter_sensitive_data()
end
end
@impl Honeybadger.Filter
def filter_params(params) when is_map(params) do
Map.new(params, fn {key, value} ->
if should_filter?(key) do
{key, "[FILTERED]"}
else
{key, filter_params(value)}
end
end)
end
def filter_params(params), do: params
# Detect anonymous gen_server termination during shutdown
defp otp_gen_server_shutdown?(context) do
domain = get_in(context, [:domain])
mfa = get_in(context, [:mfa])
domain == ["otp"] and match?(["gen_server", "error_info", _], mfa)
end
# Detect Redix connection errors during shutdown/restart
defp redix_connection_error?(context) do
reason = get_in(context, [:reason])
message = get_in(context, [:message])
cond do
# Redix connection closed errors
is_tuple(reason) and elem(reason, 0) == :tcp_closed ->
true
# Redix connection refused (during restart)
is_tuple(reason) and elem(reason, 0) == :econnrefused ->
true
# String message check for Redix errors
is_binary(message) and String.contains?(message, "Redix") ->
true
true ->
false
end
end
# Detect DBConnection errors during shutdown/restart
defp db_connection_error?(context) do
reason = get_in(context, [:reason])
message = get_in(context, [:message])
cond do
# DBConnection.ConnectionError
is_exception(reason) and reason.__struct__ == DBConnection.ConnectionError ->
true
# String message check
is_binary(message) and String.contains?(message, "DBConnection") ->
true
true ->
false
end
end
# Detect normal process shutdown
defp normal_shutdown?(context) do
reason = get_in(context, [:reason])
case reason do
:normal -> true
:shutdown -> true
{:shutdown, _} -> true
{:port_died, :normal} -> true
{:port_died, :shutdown} -> true
_ -> false
end
end
# Filter sensitive data from context
defp filter_sensitive_data(nil), do: nil
defp filter_sensitive_data(context) when is_map(context) do
context
|> Map.update(:cgi_data, %{}, &filter_params/1)
|> Map.update(:params, %{}, &filter_params/1)
|> Map.update(:session, %{}, &filter_params/1)
|> Map.update(:context, %{}, &filter_params/1)
end
defp filter_sensitive_data(context), do: context
# Check if a key should be filtered
defp should_filter?(key) when is_binary(key) do
key_lower = String.downcase(key)
Enum.any?(@sensitive_keys, &String.contains?(key_lower, &1))
end
defp should_filter?(key) when is_atom(key) do
should_filter?(Atom.to_string(key))
end
defp should_filter?(_), do: false
end

View file

@ -49,10 +49,6 @@ defmodule ToweropsWeb.Endpoint do
from: Application.compile_env(:towerops, :coverage_storage_dir, {:towerops, "priv/static/coverage"}),
gzip: false
if Mix.env() == :dev do
plug Tidewave
end
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do

View file

@ -2,7 +2,6 @@ defmodule ToweropsWeb.Router do
@moduledoc false
use ToweropsWeb, :router
use Honeybadger.Plug
use ErrorTracker.Web, :router
import Oban.Web.Router

View file

@ -96,8 +96,8 @@ defmodule Towerops.MixProject do
github: "tailwindlabs/heroicons", tag: "v2.2.0", sparse: "optimized", app: false, compile: false, depth: 1},
{:swoosh, "~> 1.24"},
{:gen_smtp, "~> 1.0"},
{:cbor, "~> 1.0"},
{:req, "~> 0.5"},
{:gaiia, path: "vendor/gaiia"},
{:castore, "~> 1.0"},
{:sweet_xml, "~> 0.7"},
{:yaml_elixir, "~> 2.9"},
@ -120,7 +120,6 @@ defmodule Towerops.MixProject do
{:absinthe, "~> 1.7"},
{:absinthe_plug, "~> 1.5"},
{:absinthe_phoenix, "~> 2.0"},
{:honeybadger, "~> 0.24"},
{:error_tracker, "~> 0.7"},
{:error_tracker_notifier, "~> 0.2"},
{:stream_data, "~> 1.1", only: :test},
@ -134,8 +133,7 @@ defmodule Towerops.MixProject do
{:cloak_ecto, "~> 1.3"},
{:geo_postgis, "~> 3.7"},
{:logger_file_backend, "~> 0.0.13", only: :dev},
{:logger_backends, "~> 1.0", only: :dev},
{:tidewave, "~> 0.5", only: :dev}
{:logger_backends, "~> 1.0", only: :dev}
]
end

View file

@ -6,10 +6,8 @@
"bandit": {:hex, :bandit, "1.11.0", "dbdd9c9963f146ee9da9860d1ee5b0ffd65cea51fe2aab3f3273df84329d133a", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c949d93a325a28da2333dde5a9ab61986ad2c2b7226347db6a28303b9139865e"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
"cbor": {:hex, :cbor, "1.0.2", "9b0af85af291a556e10a0ffd48ba9a21a75e711828fafd3af193d56d95f0907f", [:mix], [], "hexpm", "edbc9b4a16eb93a582437b9b249c340a75af03958e338fb43d8c1be9fc65b864"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"},
"cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"},
"cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
@ -40,7 +38,6 @@
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
"honeybadger": {:hex, :honeybadger, "0.27.0", "970efc796b5fb6db5c6a6e8e68660865b249bd184c306cfa323797a4b3570be9", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: true]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:hackney, "~> 1.8 or ~> 4.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.0.0 and < 2.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:process_tree, "~> 0.2.1", [hex: :process_tree, repo: "hexpm", optional: false]}, {:req, "~> 0.5.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "236612eeac103619c6d4297090f6a3bf517c0f5ad5f210fec7ba8a90d47a8209"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"httpoison": {:hex, :httpoison, "2.3.0", "10eef046405bc44ba77dc5b48957944df8952cc4966364b3cf6aa71dce6de587", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d388ee70be56d31a901e333dbcdab3682d356f651f93cf492ba9f06056436a2c"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
@ -80,7 +77,6 @@
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.22.1", "b3665ad17e15441557da8f45eeebfcd56e4a2b0b98538b855679a13d05e5cc5d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "df59f828b167b49a5853f645b65f57eb1bc5f3b230497ceaca7af5d8ac05afef"},
"process_tree": {:hex, :process_tree, "0.2.1", "4ebcaa96c64a7833467909f49fee28a8e62eed04975613f4c81b4b99424f7e8a", [:mix], [], "hexpm", "68eee6bf0514351aeeda7037f1a6003c0e25de48fe6b7d15a1b0aebb4b35e713"},
"prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"redix": {:hex, :redix, "1.5.3", "4eaae29c75e3285c0ff9957046b7c209aa7f72a023a17f0a9ea51c2a50ab5b0f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7b06fb5246373af41f5826b03334dfa3f636347d4d5d98b4d455b699d425ae7e"},
@ -97,7 +93,6 @@
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"tidewave": {:hex, :tidewave, "0.5.6", "91f35540b5599640443f1d3a1c6166bf506e202840261a6344e384e8813c1f64", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "dc82d52b8b6ffc04680544b17cd340c7d4166bb0d63999eb960850526866b533"},
"tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},

View file

@ -1,239 +0,0 @@
defmodule Towerops.HoneybadgerFilterTest do
use ExUnit.Case, async: true
alias Towerops.HoneybadgerFilter
describe "filter_params/1" do
test "filters SNMP community strings" do
params = %{
"name" => "Device 1",
"snmp_community" => "public",
"ip_address" => "192.168.1.1"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["name"] == "Device 1"
assert filtered["snmp_community"] == "[FILTERED]"
assert filtered["ip_address"] == "192.168.1.1"
end
test "filters community strings with just 'community' key" do
params = %{"community" => "secret123", "device_id" => "abc"}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["community"] == "[FILTERED]"
assert filtered["device_id"] == "abc"
end
test "filters passwords" do
params = %{"email" => "user@example.com", "password" => "secret"}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["email"] == "user@example.com"
assert filtered["password"] == "[FILTERED]"
end
test "filters SNMPv3 credentials" do
params = %{
"snmp_version" => "3",
"auth_password" => "authpass",
"priv_password" => "privpass",
"username" => "admin"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["snmp_version"] == "3"
assert filtered["auth_password"] == "[FILTERED]"
assert filtered["priv_password"] == "[FILTERED]"
assert filtered["username"] == "admin"
end
test "filters API keys and tokens" do
params = %{
"api_key" => "key123",
"token" => "token456",
"secret" => "secret789"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["api_key"] == "[FILTERED]"
assert filtered["token"] == "[FILTERED]"
assert filtered["secret"] == "[FILTERED]"
end
test "filters nested params" do
params = %{
"device" => %{
"name" => "Router",
"snmp_community" => "private",
"ip_address" => "10.0.0.1"
}
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["device"]["name"] == "Router"
assert filtered["device"]["snmp_community"] == "[FILTERED]"
assert filtered["device"]["ip_address"] == "10.0.0.1"
end
test "filters atom keys" do
params = %{
name: "Device",
snmp_community: "public",
ip_address: "192.168.1.1"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered[:name] == "Device"
assert filtered[:snmp_community] == "[FILTERED]"
assert filtered[:ip_address] == "192.168.1.1"
end
test "handles case-insensitive filtering" do
params = %{
"SNMP_COMMUNITY" => "public",
"SnmpCommunity" => "private",
"API_KEY" => "secret"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["SNMP_COMMUNITY"] == "[FILTERED]"
assert filtered["SnmpCommunity"] == "[FILTERED]"
assert filtered["API_KEY"] == "[FILTERED]"
end
test "does not filter non-sensitive data" do
params = %{
"name" => "Device",
"ip_address" => "192.168.1.1",
"description" => "Main router"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered == params
end
end
describe "filter_context/1" do
test "filters sensitive data from context params" do
context = %{
params: %{
"device" => %{"snmp_community" => "public", "name" => "Router"}
}
}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered.params["device"]["name"] == "Router"
assert filtered.params["device"]["snmp_community"] == "[FILTERED]"
end
test "filters sensitive data from cgi_data" do
context = %{
cgi_data: %{"auth_token" => "Bearer token123", "user_agent" => "Mozilla/5.0"}
}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered.cgi_data["auth_token"] == "[FILTERED]"
assert filtered.cgi_data["user_agent"] == "Mozilla/5.0"
end
test "filters ignored registered names" do
context = %{registered_name: "memsup"}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters Redix connection errors (tcp_closed)" do
context = %{reason: {:tcp_closed, :some_data}}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters Redix connection errors (econnrefused)" do
context = %{reason: {:econnrefused, :some_data}}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters Redix errors by message" do
context = %{message: "Redix connection failed"}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters DBConnection errors by exception" do
context = %{reason: %DBConnection.ConnectionError{message: "connection lost"}}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters DBConnection errors by message" do
context = %{message: "DBConnection timeout"}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters normal shutdown" do
context = %{reason: :normal}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters shutdown" do
context = %{reason: :shutdown}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters shutdown with reason" do
context = %{reason: {:shutdown, :application_stopped}}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters port_died with :normal reason" do
context = %{reason: {:port_died, :normal}}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
test "filters port_died with :shutdown reason" do
context = %{reason: {:port_died, :shutdown}}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
end
end

34
vendor/gaiia/lib/gaiia.ex vendored Normal file
View file

@ -0,0 +1,34 @@
defmodule Gaiia do
@moduledoc """
Elixir client for the [Gaiia](https://gaiia.com) GraphQL API.
The top-level `Gaiia` module is a thin convenience wrapper that
builds an implicit client from application config:
config :gaiia,
endpoint: "https://api.gaiia.com/graphql",
token: System.get_env("GAIIA_TOKEN")
Gaiia.query("query { account(id: $id) { id name } }", %{"id" => "..."})
For per-call control over headers, request options, or to use multiple
endpoints simultaneously, build a `Gaiia.Client` explicitly:
client = Gaiia.Client.new(token: "...", headers: [{"x-trace-id", "abc"}])
Gaiia.Client.query(client, "{ __typename }")
"""
alias Gaiia.Client
@doc "Run a GraphQL query using the default client built from app config."
@spec query(String.t(), map(), keyword()) :: {:ok, map()} | {:error, Gaiia.Error.t()}
def query(query, variables \\ %{}, opts \\ []), do: Client.query(default_client(), query, variables, opts)
@doc "Run a GraphQL mutation using the default client built from app config."
@spec mutate(String.t(), map(), keyword()) :: {:ok, map()} | {:error, Gaiia.Error.t()}
def mutate(mutation, variables \\ %{}, opts \\ []), do: Client.mutate(default_client(), mutation, variables, opts)
@doc "Build a default client from application config."
@spec default_client() :: Client.t()
def default_client, do: Client.new()
end

142
vendor/gaiia/lib/gaiia/client.ex vendored Normal file
View file

@ -0,0 +1,142 @@
defmodule Gaiia.Client do
@moduledoc """
HTTP client for the Gaiia GraphQL API.
A `Gaiia.Client` is a lightweight, immutable struct holding the endpoint,
optional bearer token, custom headers, and pass-through options forwarded
to `Req`.
## Configuration
Defaults can be supplied through application config:
config :gaiia,
endpoint: "https://api.gaiia.com/graphql",
token: System.get_env("GAIIA_TOKEN")
## Example
client = Gaiia.Client.new()
{:ok, %{"account" => account}} =
Gaiia.Client.query(client, ~S\"\"\"
query($id: GlobalID!) {
account(id: $id) { id name }
}
\"\"\", %{"id" => "account_8rnXNuR5sKP5uNwoPL41Zp"})
"""
alias Gaiia.Error
@type t :: %__MODULE__{
endpoint: String.t(),
token: String.t() | nil,
headers: [{String.t(), String.t()}],
req_options: keyword()
}
@enforce_keys [:endpoint]
defstruct endpoint: nil, token: nil, headers: [], req_options: []
@doc """
Build a new client.
## Options
* `:endpoint` Base GraphQL endpoint URL. Required unless set in app env.
* `:token` Bearer token sent in the `Authorization` header.
* `:headers` Extra headers as `[{name, value}]`.
* `:req_options` Keyword list of options forwarded to `Req.request/1`.
Useful for testing (`:adapter`, `:plug`) and tuning
(`:retry`, `:receive_timeout`, etc.).
Raises `ArgumentError` when no endpoint can be resolved.
"""
@spec new(keyword()) :: t()
def new(opts \\ []) do
endpoint = fetch_required(opts, :endpoint)
%__MODULE__{
endpoint: endpoint,
token: Keyword.get(opts, :token, Application.get_env(:gaiia, :token)),
headers: Keyword.get(opts, :headers, []),
req_options: Keyword.get(opts, :req_options, Application.get_env(:gaiia, :req_options, []))
}
end
@doc """
Run a GraphQL query.
Returns `{:ok, data}` on success, where `data` is the value of the
`data` field in the GraphQL response. Returns `{:error, %Gaiia.Error{}}`
on any failure mode.
## Options
* `:operation_name` GraphQL operation name to send (`operationName`).
"""
@spec query(t(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
def query(%__MODULE__{} = client, query, variables \\ %{}, opts \\ []) do
body = build_body(query, variables, opts)
client
|> build_request(body)
|> Req.request()
|> handle_response()
end
@doc """
Run a GraphQL mutation. Mechanically identical to `query/4` provided
for readability at call sites.
"""
@spec mutate(t(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
def mutate(client, mutation, variables \\ %{}, opts \\ []), do: query(client, mutation, variables, opts)
## ---- request construction ----
@spec build_body(String.t(), map(), keyword()) :: map()
defp build_body(query, variables, opts) do
put_operation_name(%{"query" => query, "variables" => variables}, Keyword.get(opts, :operation_name))
end
defp put_operation_name(body, nil), do: body
defp put_operation_name(body, name), do: Map.put(body, "operationName", name)
@spec build_request(t(), map()) :: Req.Request.t()
defp build_request(%__MODULE__{} = client, body) do
[
method: :post,
url: client.endpoint,
json: body,
headers: headers_for(client)
]
|> Keyword.merge(client.req_options)
|> Req.new()
end
defp headers_for(%__MODULE__{token: nil, headers: extra}), do: extra
defp headers_for(%__MODULE__{token: token, headers: extra}), do: [{"authorization", "Bearer " <> token} | extra]
## ---- response handling ----
@spec handle_response({:ok, Req.Response.t()} | {:error, Exception.t()}) ::
{:ok, map()} | {:error, Error.t()}
defp handle_response({:ok, %Req.Response{status: status, body: body}}) when status in 200..299, do: decode_graphql(body)
defp handle_response({:ok, %Req.Response{status: status, body: body}}), do: {:error, Error.http(status, body)}
defp handle_response({:error, exception}), do: {:error, Error.network(exception)}
defp decode_graphql(%{"errors" => [_ | _] = errors}), do: {:error, Error.graphql(errors)}
defp decode_graphql(%{"data" => data}), do: {:ok, data}
defp decode_graphql(other), do: {:error, Error.decode(other)}
## ---- option resolution ----
defp fetch_required(opts, key) do
case Keyword.get(opts, key, Application.get_env(:gaiia, key)) do
nil -> raise ArgumentError, "missing required Gaiia.Client option: #{inspect(key)}"
value -> value
end
end
end

81
vendor/gaiia/lib/gaiia/error.ex vendored Normal file
View file

@ -0,0 +1,81 @@
defmodule Gaiia.Error do
@moduledoc """
Structured error returned by `Gaiia.Client` calls.
The `:kind` discriminates the failure mode so callers can pattern match
rather than parse messages:
* `:graphql` the server returned a 200 with one or more entries in `errors`
* `:http` the server returned a non-2xx HTTP response
* `:network` the request never reached the server (connection refused, DNS, etc.)
* `:decode` the response was 2xx but the body was not a valid GraphQL envelope
"""
@type kind :: :graphql | :http | :network | :decode
@type t :: %__MODULE__{
kind: kind(),
message: String.t(),
status: pos_integer() | nil,
errors: [map()] | nil,
details: term()
}
@enforce_keys [:kind, :message]
defexception [:kind, :message, :status, :errors, :details]
@doc "Build a `:graphql` error from a list of GraphQL error maps."
@spec graphql([map()]) :: t()
def graphql(errors) when is_list(errors) do
%__MODULE__{
kind: :graphql,
message: "GraphQL error: " <> format_graphql_messages(errors),
errors: errors
}
end
@doc "Build an `:http` error from a status code and response body."
@spec http(pos_integer(), term()) :: t()
def http(status, body) when is_integer(status) do
%__MODULE__{
kind: :http,
message: "HTTP #{status} from Gaiia API",
status: status,
details: body
}
end
@doc "Build a `:network` error from a transport-layer exception."
@spec network(Exception.t() | term()) :: t()
def network(%{__exception__: true} = exception) do
%__MODULE__{
kind: :network,
message: "Network error: " <> Exception.message(exception),
details: exception
}
end
def network(other) do
%__MODULE__{
kind: :network,
message: "Network error: " <> inspect(other),
details: other
}
end
@doc "Build a `:decode` error from an undecodable response body."
@spec decode(term()) :: t()
def decode(body) do
%__MODULE__{
kind: :decode,
message: "Could not decode Gaiia response body",
details: body
}
end
defp format_graphql_messages([]), do: "<no message>"
defp format_graphql_messages(errors) do
Enum.map_join(errors, "; ", &Map.get(&1, "message", "<no message>"))
end
end

160
vendor/gaiia/lib/gaiia/global_id.ex vendored Normal file
View file

@ -0,0 +1,160 @@
defmodule Gaiia.GlobalID do
@moduledoc """
Encode and decode Gaiia Global IDs.
Global IDs combine a snake_cased type name with a Base58-encoded UUID,
separated by an underscore: `account_8rnXNuR5sKP5uNwoPL41Zp`.
"""
@alphabet "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
@base 58
@target_len 22
@doc """
Encodes a type name and UUID into a Global ID.
## Examples
iex> Gaiia.GlobalID.encode("Account", "3c3b1978-6a68-4a13-bdc2-2d51c8ef7519")
"account_8rnXNuR5sKP5uNwoPL41Zp"
"""
@spec encode(String.t(), String.t()) :: String.t()
def encode(type_name, uuid_str) do
snake = snake_case(type_name)
short = short_uuid(uuid_str)
"#{snake}_#{short}"
end
@doc """
Decodes a Global ID into a tuple of `{type, uuid}`.
## Examples
iex> Gaiia.GlobalID.decode("account_8rnXNuR5sKP5uNwoPL41Zp")
{"account", "3c3b1978-6a68-4a13-bdc2-2d51c8ef7519"}
"""
@spec decode(String.t()) :: {String.t(), String.t()}
def decode(global_id) do
short_id = String.slice(global_id, -@target_len, @target_len)
type = String.slice(global_id, 0, byte_size(global_id) - @target_len - 1)
{type, short_to_uuid(short_id)}
end
@doc """
Extracts the type name from a Global ID.
## Examples
iex> Gaiia.GlobalID.type("account_8rnXNuR5sKP5uNwoPL41Zp")
"account"
"""
@spec type(String.t()) :: String.t()
def type(global_id) do
type_len = byte_size(global_id) - @target_len - 1
String.slice(global_id, 0, type_len)
end
@doc """
Extracts the UUID from a Global ID.
## Examples
iex> Gaiia.GlobalID.to_uuid("account_8rnXNuR5sKP5uNwoPL41Zp")
"3c3b1978-6a68-4a13-bdc2-2d51c8ef7519"
"""
@spec to_uuid(String.t()) :: String.t()
def to_uuid(global_id) do
{_type, uuid} = decode(global_id)
uuid
end
@doc """
Converts a type name and UUID to a Global ID. Alias for `encode/2`.
## Examples
iex> Gaiia.GlobalID.from_uuid("Account", "3c3b1978-6a68-4a13-bdc2-2d51c8ef7519")
"account_8rnXNuR5sKP5uNwoPL41Zp"
"""
@spec from_uuid(String.t(), String.t()) :: String.t()
def from_uuid(type_name, uuid_str), do: encode(type_name, uuid_str)
@doc """
Encodes a UUID to a 22-character Base58 short UUID.
## Examples
iex> Gaiia.GlobalID.short_uuid("3c3b1978-6a68-4a13-bdc2-2d51c8ef7519")
"8rnXNuR5sKP5uNwoPL41Zp"
"""
@spec short_uuid(String.t()) :: String.t()
def short_uuid(uuid_str) do
stripped = String.downcase(String.replace(uuid_str, "-", ""))
num = String.to_integer(stripped, 16)
encode_base58(num)
end
@doc """
Decodes a short UUID back to a standard UUID string.
"""
@spec short_to_uuid(String.t()) :: String.t()
def short_to_uuid(short_id) do
num = decode_base58(short_id)
hex = num |> Integer.to_string(16) |> String.pad_leading(32, "0") |> String.downcase()
format_uuid(hex)
end
# -- Private: encode --
defp encode_base58(0), do: String.duplicate(<<:binary.at(@alphabet, 0)>>, @target_len)
defp encode_base58(num) do
chars = do_encode(num, [])
first = <<:binary.at(@alphabet, 0)>>
Enum.join(pad_list(chars, @target_len, first))
end
defp do_encode(0, acc), do: acc
defp do_encode(n, acc) do
idx = rem(n, @base)
char = <<:binary.at(@alphabet, idx)>>
do_encode(div(n, @base), [char | acc])
end
# -- Private: decode --
defp decode_base58(short_id) do
short_id
|> String.to_charlist()
|> Enum.reduce(0, fn char, acc -> acc * @base + alpha_index(char) end)
end
@alphabet_index_map for {ch, i} <- Enum.with_index(String.to_charlist(@alphabet)), into: %{}, do: {ch, i}
defp alpha_index(char), do: Map.fetch!(@alphabet_index_map, char)
# -- Private: helpers --
defp pad_list(chars, target_len, pad_char) do
if length(chars) < target_len do
List.duplicate(pad_char, target_len - length(chars)) ++ chars
else
chars
end
end
defp format_uuid(hex) do
"#{String.slice(hex, 0, 8)}-#{String.slice(hex, 8, 4)}-#{String.slice(hex, 12, 4)}-#{String.slice(hex, 16, 4)}-#{String.slice(hex, 20, 12)}"
end
defp snake_case(type_name) do
type_name |> String.replace(~r/([a-z])([A-Z])/, "\\1_\\2") |> String.downcase()
end
end

35
vendor/gaiia/lib/gaiia/mutations.ex vendored Normal file
View file

@ -0,0 +1,35 @@
defmodule Gaiia.Mutations do
@moduledoc """
Auto-generated functions for every mutation operation in the Gaiia
GraphQL API.
Signature, semantics, and return-value handling are identical to
`Gaiia.Queries` see that module for details.
"""
alias Gaiia.Client
alias Gaiia.Operation
alias Gaiia.Schema
for op <- Schema.mutations() do
@name op["name"]
@fun_name Schema.function_name(@name)
@args op["args"]
@description op["description"] || "GraphQL mutation `#{@name}`."
@doc """
#{@description}
GraphQL: `mutation { #{@name} }`
"""
@spec unquote(@fun_name)(Client.t(), map(), String.t(), keyword()) ::
{:ok, term()} | {:error, Gaiia.Error.t()}
def unquote(@fun_name)(client, variables \\ %{}, selection \\ "", opts \\ []) do
operation = Operation.build(:mutation, unquote(@name), unquote(Macro.escape(@args)), variables, selection)
with {:ok, data} <- Client.mutate(client, operation, variables, opts) do
{:ok, Map.get(data, unquote(@name))}
end
end
end
end

55
vendor/gaiia/lib/gaiia/operation.ex vendored Normal file
View file

@ -0,0 +1,55 @@
defmodule Gaiia.Operation do
@moduledoc """
Build GraphQL operation strings from introspection-style argument specs
at runtime.
Operations have shape:
<op_type> <op_name>($a: T!, $b: U) { <op_name>(a: $a, b: $b) { <selection> } }
Only variables that the caller actually supplies are declared and passed,
so GraphQL doesn't reject the request for unused variables.
"""
alias Gaiia.TypeRef
@type op_type :: :query | :mutation
@type arg_spec :: %{required(String.t()) => term()}
@doc """
Build a GraphQL operation string.
* `op_type` `:query` or `:mutation`
* `name` the operation/field name (e.g. `"account"`)
* `args` list of argument specs `[%{"name" => "id", "type" => type_ref}, ...]`
* `variables` variables map; only keys present here are declared/passed
* `selection` selection set body (without braces). Empty string means no selection.
"""
@spec build(op_type(), String.t(), [arg_spec()], map(), String.t()) :: String.t()
def build(op_type, name, args, variables, selection) do
used = filter_used_args(args, variables)
"#{keyword(op_type)} #{name}#{declarations(used)} { #{name}#{call_args(used)}#{selection_block(selection)} }"
end
defp keyword(:query), do: "query"
defp keyword(:mutation), do: "mutation"
defp filter_used_args(args, variables) do
Enum.filter(args, fn %{"name" => name} -> Map.has_key?(variables, name) end)
end
defp declarations([]), do: ""
defp declarations(args) do
"(" <> Enum.map_join(args, ", ", fn %{"name" => n, "type" => t} -> "$#{n}: #{TypeRef.render(t)}" end) <> ")"
end
defp call_args([]), do: ""
defp call_args(args) do
"(" <> Enum.map_join(args, ", ", fn %{"name" => n} -> "#{n}: $#{n}" end) <> ")"
end
defp selection_block(""), do: ""
defp selection_block(selection), do: " { " <> selection <> " }"
end

79
vendor/gaiia/lib/gaiia/pagination.ex vendored Normal file
View file

@ -0,0 +1,79 @@
defmodule Gaiia.Pagination do
@moduledoc """
Helpers for traversing Relay-style cursor pagination over the Gaiia API.
The GraphQL schema exposes paginated lists with a `nodes` field and a
`pageInfo { hasNextPage endCursor }` envelope. `stream/4` follows
these cursors transparently and yields individual node maps.
## Example
query = ~S\"\"\"
query Accounts($first: Int, $after: String) {
accounts(first: $first, after: $after) {
nodes { id name }
pageInfo { hasNextPage endCursor }
}
}
\"\"\"
client
|> Gaiia.Pagination.stream(query, %{"first" => 50}, path: ["accounts"])
|> Stream.take(200)
|> Enum.to_list()
"""
alias Gaiia.Client
@type opts :: [
path: [String.t()],
cursor_variable: String.t()
]
@doc """
Stream all nodes from a paginated GraphQL field.
## Options
* `:path` A list of keys describing where the connection lives inside
the `data` payload, for example `["accounts"]`. Required.
* `:cursor_variable` Variable name for the after-cursor (default `"after"`).
"""
@spec stream(Client.t(), String.t(), map(), opts()) :: Enumerable.t()
def stream(%Client{} = client, query, variables, opts) do
path = Keyword.fetch!(opts, :path)
cursor_var = Keyword.get(opts, :cursor_variable, "after")
Stream.resource(
fn -> {:cont, nil} end,
&next_page(&1, client, query, variables, path, cursor_var),
fn _ -> :ok end
)
end
defp next_page(:halt, _client, _query, _variables, _path, _cursor_var), do: {:halt, :halt}
defp next_page({:cont, cursor}, client, query, variables, path, cursor_var) do
vars = put_cursor(variables, cursor_var, cursor)
client
|> Client.query(query, vars)
|> handle_page(path)
end
defp put_cursor(variables, _cursor_var, nil), do: variables
defp put_cursor(variables, cursor_var, cursor), do: Map.put(variables, cursor_var, cursor)
defp handle_page({:ok, data}, path) do
connection = get_in(data, path) || %{}
nodes = Map.get(connection, "nodes", [])
page_info = Map.get(connection, "pageInfo", %{})
{nodes, advance_from(page_info)}
end
defp handle_page({:error, error}, _path), do: raise(error)
defp advance_from(%{"hasNextPage" => true, "endCursor" => cursor}) when is_binary(cursor), do: {:cont, cursor}
defp advance_from(_page_info), do: :halt
end

50
vendor/gaiia/lib/gaiia/queries.ex vendored Normal file
View file

@ -0,0 +1,50 @@
defmodule Gaiia.Queries do
@moduledoc """
Auto-generated functions for every query operation in the Gaiia
GraphQL API.
Each function has the signature:
<name>(client, variables \\\\ %{}, selection \\\\ "", opts \\\\ [])
:: {:ok, map() | term()} | {:error, Gaiia.Error.t()}
Arguments:
* `client` a `Gaiia.Client` struct (build with `Gaiia.Client.new/1`)
* `variables` map of variables matching the GraphQL argument names
(camelCase, string keys). Only keys present in this map are sent.
* `selection` selection-set body (no surrounding braces). Required
for object return types; pass `""` for scalar return types.
* `opts` forwarded to `Gaiia.Client.query/4` (e.g. `:operation_name`)
Return value is the contents of the named field, unwrapped from the
`data` envelope i.e. `Gaiia.Queries.account(client, %{"id" => id}, "id")`
returns `{:ok, %{"id" => "..."}}`, not `{:ok, %{"account" => %{"id" => "..."}}}`.
"""
alias Gaiia.Client
alias Gaiia.Operation
alias Gaiia.Schema
for op <- Schema.queries() do
@name op["name"]
@fun_name Schema.function_name(@name)
@args op["args"]
@description op["description"] || "GraphQL query `#{@name}`."
@doc """
#{@description}
GraphQL: `query { #{@name} }`
"""
@spec unquote(@fun_name)(Client.t(), map(), String.t(), keyword()) ::
{:ok, term()} | {:error, Gaiia.Error.t()}
def unquote(@fun_name)(client, variables \\ %{}, selection \\ "", opts \\ []) do
operation = Operation.build(:query, unquote(@name), unquote(Macro.escape(@args)), variables, selection)
with {:ok, data} <- Client.query(client, operation, variables, opts) do
{:ok, Map.get(data, unquote(@name))}
end
end
end
end

33
vendor/gaiia/lib/gaiia/schema.ex vendored Normal file
View file

@ -0,0 +1,33 @@
defmodule Gaiia.Schema do
@moduledoc """
Compile-time access to the Gaiia GraphQL schema.
At compile time this module loads the operation specs from
`priv/queries.json` and `priv/mutations.json` (extracted from the
full introspection schema) so generator modules can iterate over
every operation.
"""
@queries_path :gaiia |> :code.priv_dir() |> to_string() |> Path.join("queries.json")
@mutations_path :gaiia |> :code.priv_dir() |> to_string() |> Path.join("mutations.json")
@external_resource @queries_path
@external_resource @mutations_path
@queries @queries_path |> File.read!() |> Jason.decode!()
@mutations @mutations_path |> File.read!() |> Jason.decode!()
# The compile-time-loaded literals would force dialyzer to infer the
# full nested map shape; we keep these specless so callers can treat
# them as the opaque "introspection envelope" they are.
@doc "Return the list of all query operation specs."
def queries, do: @queries
@doc "Return the list of all mutation operation specs."
def mutations, do: @mutations
@doc "Convert a camelCase GraphQL name to an Elixir-friendly snake_case atom."
@spec function_name(String.t()) :: atom()
def function_name(camel_name), do: camel_name |> Macro.underscore() |> String.to_atom()
end

26
vendor/gaiia/lib/gaiia/type_ref.ex vendored Normal file
View file

@ -0,0 +1,26 @@
defmodule Gaiia.TypeRef do
@moduledoc """
Helpers for working with GraphQL type-reference maps as returned by
introspection (`__Type`).
A type reference is a recursive map of the shape:
%{"kind" => "NON_NULL" | "LIST" | "SCALAR" | ..., "name" => name, "ofType" => inner_or_nil}
This module renders such structures to GraphQL syntax (`Int!`,
`[String!]!`, etc.) and unwraps modifiers to find the underlying named type.
"""
@type t :: %{required(String.t()) => term()}
@doc "Render a type-reference map as a GraphQL type string."
@spec render(t()) :: String.t()
def render(%{"kind" => "NON_NULL", "ofType" => inner}), do: render(inner) <> "!"
def render(%{"kind" => "LIST", "ofType" => inner}), do: "[" <> render(inner) <> "]"
def render(%{"name" => name}) when is_binary(name), do: name
@doc "Unwrap NON_NULL/LIST modifiers to return the innermost named type ref."
@spec named_type(t()) :: t()
def named_type(%{"kind" => kind, "ofType" => inner}) when kind in ["NON_NULL", "LIST"], do: named_type(inner)
def named_type(%{"name" => name} = type) when is_binary(name), do: type
end

35
vendor/gaiia/mix.exs vendored Normal file
View file

@ -0,0 +1,35 @@
defmodule Gaiia.MixProject do
use Mix.Project
def project do
[
app: :gaiia,
version: "0.1.0",
elixir: "~> 1.19",
start_permanent: Mix.env() == :prod,
elixirc_paths: elixirc_paths(Mix.env()),
deps: deps(),
dialyzer: [
plt_add_apps: [:ex_unit],
flags: [:error_handling, :underspecs, :unmatched_returns]
]
]
end
def application do
[extra_applications: [:logger]]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp deps do
[
{:req, "~> 0.5"},
{:jason, "~> 1.4"},
{:styler, "~> 1.5", only: [:dev, :test], runtime: false},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
]
end
end

17
vendor/gaiia/mix.lock vendored Normal file
View file

@ -0,0 +1,17 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
}

4574
vendor/gaiia/priv/mutations.json vendored Normal file

File diff suppressed because it is too large Load diff

6806
vendor/gaiia/priv/queries.json vendored Normal file

File diff suppressed because it is too large Load diff