mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
feat (api): add additional system/connections methods (#351)
This commit is contained in:
490
lib/wanderer_app/map/map_operations.ex
Normal file
490
lib/wanderer_app/map/map_operations.ex
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
defmodule WandererApp.Map.Operations do
|
||||||
|
@moduledoc """
|
||||||
|
Orchestrates map systems and connections.
|
||||||
|
Centralizes cross-repo logic for controllers, templates, and batch operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Cache TTL in milliseconds (24 hours)
|
||||||
|
@owner_info_cache_ttl 86_400_000
|
||||||
|
|
||||||
|
alias WandererApp.{
|
||||||
|
MapRepo,
|
||||||
|
MapSystemRepo,
|
||||||
|
MapConnectionRepo,
|
||||||
|
MapCharacterSettingsRepo,
|
||||||
|
MapUserSettingsRepo
|
||||||
|
}
|
||||||
|
alias WandererApp.Map.Server
|
||||||
|
alias WandererApp.Character
|
||||||
|
alias WandererApp.Character.TrackingUtils
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fetch main character ID for the map owner.
|
||||||
|
|
||||||
|
Returns {:ok, %{id: character_id, user_id: user_id}} on success
|
||||||
|
Returns {:error, reason} on failure
|
||||||
|
"""
|
||||||
|
@spec get_owner_character_id(String.t()) :: {:ok, %{id: term(), user_id: term()}} | {:error, String.t()}
|
||||||
|
def get_owner_character_id(map_id) do
|
||||||
|
case WandererApp.Cache.lookup!("map_#{map_id}:owner_info") do
|
||||||
|
nil ->
|
||||||
|
with {:ok, owner} <- fetch_map_owner(map_id),
|
||||||
|
{:ok, char_ids} <- fetch_character_ids(map_id),
|
||||||
|
{:ok, characters} <- load_characters(char_ids),
|
||||||
|
{:ok, user_settings} <- MapUserSettingsRepo.get(map_id, owner.id),
|
||||||
|
{:ok, main} <- TrackingUtils.get_main_character(user_settings, characters, characters) do
|
||||||
|
result = %{id: main.id, user_id: main.user_id}
|
||||||
|
WandererApp.Cache.insert("map_#{map_id}:owner_info", result, ttl: @owner_info_cache_ttl)
|
||||||
|
{:ok, result}
|
||||||
|
else
|
||||||
|
{:error, msg} ->
|
||||||
|
{:error, msg}
|
||||||
|
_ ->
|
||||||
|
{:error, "Failed to resolve main character"}
|
||||||
|
end
|
||||||
|
cached ->
|
||||||
|
{:ok, cached}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_map_owner(map_id) do
|
||||||
|
case MapRepo.get(map_id, [:owner]) do
|
||||||
|
{:ok, %{owner: %_{} = owner}} -> {:ok, owner}
|
||||||
|
{:ok, %{owner: nil}} -> {:error, "Map has no owner"}
|
||||||
|
{:error, _} -> {:error, "Map not found"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_character_ids(map_id) do
|
||||||
|
case MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||||
|
{:ok, settings} when is_list(settings) and settings != [] -> {:ok, Enum.map(settings, & &1.character_id)}
|
||||||
|
{:ok, []} -> {:error, "No character settings found"}
|
||||||
|
{:error, _} -> {:error, "Failed to fetch character settings"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_characters(ids) when is_list(ids) do
|
||||||
|
ids
|
||||||
|
|> Enum.map(&Character.get_character/1)
|
||||||
|
|> Enum.flat_map(fn
|
||||||
|
{:ok, ch} -> [ch]
|
||||||
|
_ -> []
|
||||||
|
end)
|
||||||
|
|> case do
|
||||||
|
[] -> {:error, "No valid characters found"}
|
||||||
|
chars -> {:ok, chars}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "List visible systems"
|
||||||
|
@spec list_systems(String.t()) :: [any()]
|
||||||
|
def list_systems(map_id) do
|
||||||
|
case MapSystemRepo.get_visible_by_map(map_id) do
|
||||||
|
{:ok, systems} -> systems
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Get a specific system"
|
||||||
|
@spec get_system(String.t(), integer()) :: {:ok, any()} | {:error, :not_found}
|
||||||
|
def get_system(map_id, sid) do
|
||||||
|
MapSystemRepo.get_by_map_and_solar_system_id(map_id, sid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Create or update a system in a map"
|
||||||
|
@spec create_system(String.t(), map()) :: {:ok, any()} | {:error, String.t()}
|
||||||
|
def create_system(map_id, params) do
|
||||||
|
with {:ok, %{id: char_id, user_id: user_id}} <- get_owner_character_id(map_id),
|
||||||
|
{:ok, system_id} <- fetch_system_id(params),
|
||||||
|
coords <- normalize_coordinates(params),
|
||||||
|
:ok <- Server.add_system(map_id, %{solar_system_id: system_id, coordinates: coords}, user_id, char_id),
|
||||||
|
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
|
||||||
|
{:ok, system}
|
||||||
|
else
|
||||||
|
{:error, reason} when is_binary(reason) ->
|
||||||
|
{:error, reason}
|
||||||
|
_ ->
|
||||||
|
{:error, "Unable to create system"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Update attributes of an existing system"
|
||||||
|
@spec update_system(String.t(), integer(), map()) :: {:ok, any()} | {:error, String.t()}
|
||||||
|
def update_system(map_id, system_id, attrs) do
|
||||||
|
# Fetch current system to get its position if not provided
|
||||||
|
with {:ok, current_system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
|
||||||
|
x_raw <- Map.get(attrs, "position_x", Map.get(attrs, :position_x, current_system.position_x)),
|
||||||
|
y_raw <- Map.get(attrs, "position_y", Map.get(attrs, :position_y, current_system.position_y)),
|
||||||
|
{:ok, x} <- parse_int(x_raw, "position_x"),
|
||||||
|
{:ok, y} <- parse_int(y_raw, "position_y"),
|
||||||
|
coords = %{x: x, y: y},
|
||||||
|
:ok <- apply_system_updates(map_id, system_id, attrs, coords),
|
||||||
|
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
|
||||||
|
{:ok, system}
|
||||||
|
else
|
||||||
|
{:error, reason} when is_binary(reason) ->
|
||||||
|
{:error, reason}
|
||||||
|
_ ->
|
||||||
|
{:error, "Error updating system"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec delete_system(String.t(), integer()) :: {:ok, integer()} | {:error, term()}
|
||||||
|
def delete_system(map_id, system_id) do
|
||||||
|
with {:ok, %{id: char_id, user_id: user_id}} <- get_owner_character_id(map_id),
|
||||||
|
{:ok, _system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
|
||||||
|
:ok <- Server.delete_systems(map_id, [system_id], user_id, char_id) do
|
||||||
|
{:ok, 1}
|
||||||
|
else
|
||||||
|
{:error, :not_found} -> {:error, :not_found}
|
||||||
|
_ ->
|
||||||
|
{:error, "Failed to delete system"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create a new connection if missing
|
||||||
|
|
||||||
|
Returns :ok on success
|
||||||
|
Returns {:skip, :exists} if connection already exists
|
||||||
|
Returns {:error, reason} on failure
|
||||||
|
"""
|
||||||
|
@spec create_connection(map(), String.t()) :: {:ok, any()} | {:skip, :exists} | {:error, String.t()}
|
||||||
|
def create_connection(attrs, map_id) when is_map(attrs) do
|
||||||
|
with {:ok, %{id: char_id}} <- get_owner_character_id(map_id) do
|
||||||
|
do_create_connection(attrs, map_id, char_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create a new connection if missing with explicit character ID
|
||||||
|
|
||||||
|
Returns :ok on success
|
||||||
|
Returns {:skip, :exists} if connection already exists
|
||||||
|
Returns {:error, reason} on failure
|
||||||
|
"""
|
||||||
|
@spec create_connection(map(), String.t(), integer()) :: {:ok, any()} | {:skip, :exists} | {:error, String.t()}
|
||||||
|
def create_connection(attrs, map_id, char_id) when is_map(attrs), do: do_create_connection(attrs, map_id, char_id)
|
||||||
|
|
||||||
|
defp do_create_connection(attrs, map_id, char_id) do
|
||||||
|
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
|
||||||
|
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target"),
|
||||||
|
info = build_connection_info(source, target, char_id, attrs["type"]),
|
||||||
|
:ok <- Server.add_connection(map_id, info),
|
||||||
|
{:ok, [conn | _]} <- MapConnectionRepo.get_by_locations(map_id, source, target) do
|
||||||
|
{:ok, conn}
|
||||||
|
else
|
||||||
|
{:ok, []} ->
|
||||||
|
{:ok, :created}
|
||||||
|
{:error, %Ash.Error.Invalid{errors: errors}} = err ->
|
||||||
|
if Enum.any?(errors, &is_unique_constraint_error?/1) do
|
||||||
|
{:skip, :exists}
|
||||||
|
else
|
||||||
|
err
|
||||||
|
end
|
||||||
|
{:error, _reason} = err ->
|
||||||
|
err
|
||||||
|
_ ->
|
||||||
|
{:error, "Failed to create connection"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_connection_info(source, target, char_id, type) do
|
||||||
|
%{
|
||||||
|
solar_system_source_id: source,
|
||||||
|
solar_system_target_id: target,
|
||||||
|
character_id: char_id,
|
||||||
|
type: parse_type(type)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Delete an existing connection"
|
||||||
|
@spec delete_connection(String.t(), integer(), integer()) :: :ok | {:error, term()}
|
||||||
|
def delete_connection(map_id, src, tgt) do
|
||||||
|
case Server.delete_connection(map_id, %{solar_system_source_id: src, solar_system_target_id: tgt}) do
|
||||||
|
:ok -> :ok
|
||||||
|
{:error, :not_found} -> {:error, :not_found}
|
||||||
|
{:error, _reason} = err -> err
|
||||||
|
_ -> {:error, :unknown}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to detect Ash 'not found' errors
|
||||||
|
defp is_not_found_error({:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}}), do: true
|
||||||
|
defp is_not_found_error({:error, %Ash.Error.Invalid{errors: errors}}) when is_list(errors), do: Enum.any?(errors, &match?(%Ash.Error.Query.NotFound{}, &1))
|
||||||
|
defp is_not_found_error(_), do: false
|
||||||
|
|
||||||
|
@spec upsert_systems_and_connections(String.t(), [map()], [map()]) :: {:ok, map()} | {:error, String.t()}
|
||||||
|
def upsert_systems_and_connections(map_id, systems, connections) do
|
||||||
|
with {:ok, %{id: char_id}} <- get_owner_character_id(map_id) do
|
||||||
|
system_results = upsert_each(systems, fn sys -> create_system(map_id, sys) end, 0, 0)
|
||||||
|
connection_results = Enum.reduce(connections, %{created: 0, updated: 0, skipped: 0}, fn conn, acc ->
|
||||||
|
upsert_connection_branch(map_id, conn, char_id, acc)
|
||||||
|
end)
|
||||||
|
{:ok, format_upsert_results(system_results, {connection_results.created, connection_results.updated})}
|
||||||
|
else
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Private: Handles a single connection upsert branch for batch upsert
|
||||||
|
defp upsert_connection_branch(map_id, conn, char_id, acc) do
|
||||||
|
with {:ok, source} <- parse_int(conn["solar_system_source"], "solar_system_source"),
|
||||||
|
{:ok, target} <- parse_int(conn["solar_system_target"], "solar_system_target") do
|
||||||
|
case get_connection_by_systems(map_id, source, target) do
|
||||||
|
{:ok, existing_conn} when is_map(existing_conn) and not is_nil(existing_conn) ->
|
||||||
|
case update_connection(map_id, existing_conn.id, conn) do
|
||||||
|
{:ok, _} -> %{acc | updated: acc.updated + 1}
|
||||||
|
error ->
|
||||||
|
if is_not_found_error(error) do
|
||||||
|
case create_connection(conn, map_id, char_id) do
|
||||||
|
{:ok, _} -> %{acc | created: acc.created + 1}
|
||||||
|
{:skip, :exists} -> %{acc | updated: acc.updated + 1}
|
||||||
|
{:error, _} -> %{acc | skipped: acc.skipped + 1}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
%{acc | skipped: acc.skipped + 1}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{:ok, _} ->
|
||||||
|
case create_connection(conn, map_id, char_id) do
|
||||||
|
{:ok, _} -> %{acc | created: acc.created + 1}
|
||||||
|
{:skip, :exists} -> %{acc | updated: acc.updated + 1}
|
||||||
|
{:error, _} -> %{acc | skipped: acc.skipped + 1}
|
||||||
|
end
|
||||||
|
{:error, :not_found} ->
|
||||||
|
case create_connection(conn, map_id, char_id) do
|
||||||
|
{:ok, _} -> %{acc | created: acc.created + 1}
|
||||||
|
{:skip, :exists} -> %{acc | updated: acc.updated + 1}
|
||||||
|
{:error, _} -> %{acc | skipped: acc.skipped + 1}
|
||||||
|
end
|
||||||
|
_ ->
|
||||||
|
%{acc | skipped: acc.skipped + 1}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:error, _} ->
|
||||||
|
%{acc | skipped: acc.skipped + 1}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to get a connection by source/target system IDs
|
||||||
|
def get_connection_by_systems(map_id, source, target) do
|
||||||
|
case WandererApp.Map.find_connection(map_id, source, target) do
|
||||||
|
{:ok, nil} ->
|
||||||
|
WandererApp.Map.find_connection(map_id, target, source)
|
||||||
|
{:ok, conn} ->
|
||||||
|
{:ok, conn}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_upsert_results({created_s, updated_s, _}, {created_c, updated_c}) do
|
||||||
|
%{
|
||||||
|
systems: %{created: created_s, updated: updated_s},
|
||||||
|
connections: %{created: created_c, updated: updated_c}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Get connection by ID"
|
||||||
|
@spec get_connection(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
|
||||||
|
def get_connection(map_id, id) do
|
||||||
|
case MapConnectionRepo.get_by_id(map_id, id) do
|
||||||
|
{:ok, %{} = conn} -> {:ok, conn}
|
||||||
|
{:error, _} -> {:error, "Connection not found"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Helpers ---------------------------------------------------------------
|
||||||
|
|
||||||
|
defp fetch_system_id(%{"solar_system_id" => id}), do: parse_int(id, "solar_system_id")
|
||||||
|
defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id), do: parse_int(id, "solar_system_id")
|
||||||
|
defp fetch_system_id(%{"name" => name}) when is_binary(name) and name != "", do: find_by_name(name)
|
||||||
|
defp fetch_system_id(%{name: name}) when is_binary(name) and name != "", do: find_by_name(name)
|
||||||
|
defp fetch_system_id(_), do: {:error, "Missing system identifier (id or name)"}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Find system ID by name
|
||||||
|
Uses EveDataService for lookup
|
||||||
|
"""
|
||||||
|
defp find_by_name(name) do
|
||||||
|
case WandererApp.EveDataService.find_system_id_by_name(name) do
|
||||||
|
{:ok, id} when is_integer(id) -> {:ok, id}
|
||||||
|
{:ok, _} ->
|
||||||
|
{:error, "Invalid system name: #{name}"}
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, "Failed to find system by name '#{name}': #{reason}"}
|
||||||
|
_ ->
|
||||||
|
{:error, "Unknown system name: #{name}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||||
|
defp parse_int(val, field) when is_binary(val) do
|
||||||
|
case Integer.parse(val) do
|
||||||
|
{i, _} -> {:ok, i}
|
||||||
|
_ -> {:error, "Invalid #{field}: #{val}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
defp parse_int(nil, field), do: {:error, "Missing #{field}"}
|
||||||
|
defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
||||||
|
|
||||||
|
defp parse_type(val) when is_binary(val) do
|
||||||
|
case Integer.parse(val) do
|
||||||
|
{i, _} -> i
|
||||||
|
_ -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
defp parse_type(val) when is_integer(val), do: val
|
||||||
|
defp parse_type(_), do: 0
|
||||||
|
|
||||||
|
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}}) when is_number(x) and is_number(y), do: %{x: x, y: y}
|
||||||
|
defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y), do: %{x: x, y: y}
|
||||||
|
defp normalize_coordinates(params) do
|
||||||
|
%{
|
||||||
|
x: params |> Map.get("position_x", Map.get(params, :position_x, 0)),
|
||||||
|
y: params |> Map.get("position_y", Map.get(params, :position_y, 0))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do
|
||||||
|
with :ok <- Server.update_system_position(map_id, %{solar_system_id: system_id, position_x: round(x), position_y: round(y)}) do
|
||||||
|
attrs
|
||||||
|
|> Map.drop([:coordinates, :position_x, :position_y, :solar_system_id,
|
||||||
|
"coordinates", "position_x", "position_y", "solar_system_id"])
|
||||||
|
|> Enum.reduce_while(:ok, fn {key, val}, _acc ->
|
||||||
|
case update_system_field(map_id, system_id, to_string(key), val) do
|
||||||
|
:ok -> {:cont, :ok}
|
||||||
|
{:error, _} = err -> {:halt, err}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_system_field(map_id, system_id, field, val) do
|
||||||
|
case field do
|
||||||
|
"status" -> Server.update_system_status(map_id, %{solar_system_id: system_id, status: convert_status(val)})
|
||||||
|
"description" -> Server.update_system_description(map_id, %{solar_system_id: system_id, description: val})
|
||||||
|
"tag" -> Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val})
|
||||||
|
"locked" ->
|
||||||
|
bool = val in [true, "true", 1, "1"]
|
||||||
|
Server.update_system_locked(map_id, %{solar_system_id: system_id, locked: bool})
|
||||||
|
f when f in ["label", "labels"] ->
|
||||||
|
labels =
|
||||||
|
cond do
|
||||||
|
is_list(val) -> val
|
||||||
|
is_binary(val) -> String.split(val, ",", trim: true)
|
||||||
|
true -> []
|
||||||
|
end
|
||||||
|
Server.update_system_labels(map_id, %{solar_system_id: system_id, labels: Enum.join(labels, ",")})
|
||||||
|
"temporary_name" -> Server.update_system_temporary_name(map_id, %{solar_system_id: system_id, temporary_name: val})
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp convert_status("CLEAR"), do: 0
|
||||||
|
defp convert_status("DANGEROUS"), do: 1
|
||||||
|
defp convert_status("OCCUPIED"), do: 2
|
||||||
|
defp convert_status("MASS_CRITICAL"), do: 3
|
||||||
|
defp convert_status("TIME_CRITICAL"), do: 4
|
||||||
|
defp convert_status("REINFORCED"), do: 5
|
||||||
|
defp convert_status(i) when is_integer(i), do: i
|
||||||
|
defp convert_status(s) when is_binary(s) do
|
||||||
|
case Integer.parse(s) do
|
||||||
|
{i, _} -> i
|
||||||
|
_ -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
defp convert_status(_), do: 0
|
||||||
|
|
||||||
|
defp upsert_each(list, fun, c, u), do: upsert_each(list, fun, c, u, 0)
|
||||||
|
defp upsert_each([], _fun, c, u, d), do: {c, u, d}
|
||||||
|
defp upsert_each([item | rest], fun, c, u, d) do
|
||||||
|
case fun.(item) do
|
||||||
|
{:ok, _} -> upsert_each(rest, fun, c + 1, u, d)
|
||||||
|
:ok -> upsert_each(rest, fun, c + 1, u, d)
|
||||||
|
{:skip, _} -> upsert_each(rest, fun, c, u + 1, d)
|
||||||
|
_ -> upsert_each(rest, fun, c, u, d + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Update an existing connection"
|
||||||
|
@spec update_connection(String.t(), String.t(), map()) :: {:ok, any()} | {:error, String.t()}
|
||||||
|
def update_connection(map_id, connection_id, attrs) do
|
||||||
|
with {:ok, conn} <- MapConnectionRepo.get_by_id(map_id, connection_id),
|
||||||
|
{:ok, %{id: char_id}} <- get_owner_character_id(map_id),
|
||||||
|
:ok <- validate_connection_update(conn, attrs),
|
||||||
|
:ok <- apply_connection_updates(map_id, conn, attrs, char_id),
|
||||||
|
{:ok, updated_conn} <- MapConnectionRepo.get_by_id(map_id, connection_id) do
|
||||||
|
{:ok, updated_conn}
|
||||||
|
else
|
||||||
|
{:error, reason} when is_binary(reason) ->
|
||||||
|
{:error, reason}
|
||||||
|
{:error, %Ash.Error.Invalid{} = ash_error} ->
|
||||||
|
{:error, ash_error}
|
||||||
|
_error ->
|
||||||
|
{:error, "Failed to update connection"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_connection_update(_conn, _attrs), do: :ok
|
||||||
|
|
||||||
|
defp apply_connection_updates(map_id, conn, attrs, _char_id) do
|
||||||
|
with :ok <- maybe_update_mass_status(map_id, conn, Map.get(attrs, "mass_status", conn.mass_status)),
|
||||||
|
:ok <- maybe_update_ship_size_type(map_id, conn, Map.get(attrs, "ship_size_type", conn.ship_size_type)),
|
||||||
|
:ok <- maybe_update_type(map_id, conn, Map.get(attrs, "type", conn.type)) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_update_mass_status(map_id, conn, value) when not is_nil(value) do
|
||||||
|
Server.update_connection_mass_status(map_id, %{
|
||||||
|
solar_system_source_id: conn.solar_system_source,
|
||||||
|
solar_system_target_id: conn.solar_system_target,
|
||||||
|
mass_status: value
|
||||||
|
})
|
||||||
|
end
|
||||||
|
defp maybe_update_mass_status(_map_id, _conn, nil), do: :ok
|
||||||
|
|
||||||
|
defp maybe_update_ship_size_type(map_id, conn, value) when not is_nil(value) do
|
||||||
|
Server.update_connection_ship_size_type(map_id, %{
|
||||||
|
solar_system_source_id: conn.solar_system_source,
|
||||||
|
solar_system_target_id: conn.solar_system_target,
|
||||||
|
ship_size_type: value
|
||||||
|
})
|
||||||
|
end
|
||||||
|
defp maybe_update_ship_size_type(_map_id, _conn, nil), do: :ok
|
||||||
|
|
||||||
|
defp maybe_update_type(map_id, conn, value) when not is_nil(value) do
|
||||||
|
Server.update_connection_type(map_id, %{
|
||||||
|
solar_system_source_id: conn.solar_system_source,
|
||||||
|
solar_system_target_id: conn.solar_system_target,
|
||||||
|
type: value
|
||||||
|
})
|
||||||
|
end
|
||||||
|
defp maybe_update_type(_map_id, _conn, nil), do: :ok
|
||||||
|
|
||||||
|
@doc "List all connections for a map"
|
||||||
|
@spec list_connections(String.t()) :: [map()]
|
||||||
|
def list_connections(map_id) do
|
||||||
|
case MapConnectionRepo.get_by_map(map_id) do
|
||||||
|
{:ok, connections} -> connections
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "List connections for a map involving a specific system (source or target)"
|
||||||
|
@spec list_connections(String.t(), integer()) :: [map()]
|
||||||
|
def list_connections(map_id, system_id) do
|
||||||
|
list_connections(map_id)
|
||||||
|
|> Enum.filter(fn conn ->
|
||||||
|
conn.solar_system_source == system_id or conn.solar_system_target == system_id
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to detect unique constraint errors in Ash error lists
|
||||||
|
defp is_unique_constraint_error?(%{constraint: :unique}), do: true
|
||||||
|
defp is_unique_constraint_error?(%{constraint: :unique_constraint}), do: true
|
||||||
|
defp is_unique_constraint_error?(_), do: false
|
||||||
|
end
|
||||||
@@ -95,4 +95,12 @@ defmodule WandererApp.MapConnectionRepo do
|
|||||||
do:
|
do:
|
||||||
connection
|
connection
|
||||||
|> WandererApp.Api.MapConnection.update_custom_info(update)
|
|> WandererApp.Api.MapConnection.update_custom_info(update)
|
||||||
|
|
||||||
|
def get_by_id(map_id, id) do
|
||||||
|
case WandererApp.Api.MapConnection.by_id(id) do
|
||||||
|
{:ok, conn} when conn.map_id == map_id -> {:ok, conn}
|
||||||
|
{:ok, _} -> {:error, :not_found}
|
||||||
|
{:error, _} -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
|||||||
use OpenApiSpex.ControllerSpecs
|
use OpenApiSpex.ControllerSpecs
|
||||||
|
|
||||||
alias WandererApp.Api.{AccessList, Character}
|
alias WandererApp.Api.{AccessList, Character}
|
||||||
alias WandererAppWeb.UtilAPIController, as: Util
|
alias WandererAppWeb.Helpers.APIUtils
|
||||||
import Ash.Query
|
import Ash.Query
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
|||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
def index(conn, params) do
|
def index(conn, params) do
|
||||||
case Util.fetch_map_id(params) do
|
case APIUtils.fetch_map_id(params) do
|
||||||
{:ok, map_identifier} ->
|
{:ok, map_identifier} ->
|
||||||
with {:ok, map} <- get_map(map_identifier),
|
with {:ok, map} <- get_map(map_identifier),
|
||||||
{:ok, loaded_map} <- Ash.load(map, acls: [:owner]) do
|
{:ok, loaded_map} <- Ash.load(map, acls: [:owner]) do
|
||||||
@@ -320,7 +320,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
|||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
def create(conn, params) do
|
def create(conn, params) do
|
||||||
with {:ok, map_identifier} <- Util.fetch_map_id(params),
|
with {:ok, map_identifier} <- APIUtils.fetch_map_id(params),
|
||||||
{:ok, map} <- get_map(map_identifier),
|
{:ok, map} <- get_map(map_identifier),
|
||||||
%{"acl" => acl_params} <- params,
|
%{"acl" => acl_params} <- params,
|
||||||
owner_eve_id when not is_nil(owner_eve_id) <- Map.get(acl_params, "owner_eve_id"),
|
owner_eve_id when not is_nil(owner_eve_id) <- Map.get(acl_params, "owner_eve_id"),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ defmodule WandererAppWeb.CommonAPIController do
|
|||||||
use OpenApiSpex.ControllerSpecs
|
use OpenApiSpex.ControllerSpecs
|
||||||
|
|
||||||
alias WandererApp.CachedInfo
|
alias WandererApp.CachedInfo
|
||||||
alias WandererAppWeb.UtilAPIController, as: Util
|
alias WandererAppWeb.Helpers.APIUtils
|
||||||
alias WandererApp.EveDataService
|
alias WandererApp.EveDataService
|
||||||
|
|
||||||
@system_static_response_schema %OpenApiSpex.Schema{
|
@system_static_response_schema %OpenApiSpex.Schema{
|
||||||
@@ -87,8 +87,8 @@ defmodule WandererAppWeb.CommonAPIController do
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
def show_system_static(conn, params) do
|
def show_system_static(conn, params) do
|
||||||
with {:ok, solar_system_str} <- Util.require_param(params, "id"),
|
with {:ok, solar_system_str} <- APIUtils.require_param(params, "id"),
|
||||||
{:ok, solar_system_id} <- Util.parse_int(solar_system_str) do
|
{:ok, solar_system_id} <- APIUtils.parse_int(solar_system_str) do
|
||||||
case CachedInfo.get_system_static_info(solar_system_id) do
|
case CachedInfo.get_system_static_info(solar_system_id) do
|
||||||
{:ok, system} ->
|
{:ok, system} ->
|
||||||
# Get basic system data
|
# Get basic system data
|
||||||
|
|||||||
50
lib/wanderer_app_web/controllers/fallback_controller.ex
Normal file
50
lib/wanderer_app_web/controllers/fallback_controller.ex
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
defmodule WandererAppWeb.FallbackController do
|
||||||
|
use WandererAppWeb, :controller
|
||||||
|
|
||||||
|
alias WandererAppWeb.Helpers.APIUtils
|
||||||
|
|
||||||
|
# Handles not_found errors from with/else
|
||||||
|
def call(conn, {:error, :not_found}) do
|
||||||
|
APIUtils.error_response(conn, :not_found, "Not found", "The requested resource could not be found")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles invalid_id errors
|
||||||
|
def call(conn, {:error, :invalid_id}) do
|
||||||
|
APIUtils.error_response(conn, :bad_request, "Invalid system ID")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles invalid_coordinates_format errors
|
||||||
|
def call(conn, {:error, :invalid_coordinates_format}) do
|
||||||
|
APIUtils.error_response(conn, :bad_request, "Invalid coordinates format. Use %{\"coordinates\" => %{\"x\" => number, \"y\" => number}}")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles not_associated errors
|
||||||
|
def call(conn, {:error, :not_associated}) do
|
||||||
|
APIUtils.error_response(conn, :not_found, "Connection not associated with specified system")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles not_involved errors
|
||||||
|
def call(conn, {:error, :not_involved}) do
|
||||||
|
APIUtils.error_response(conn, :bad_request, "Connection must involve specified system")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles creation_failed errors
|
||||||
|
def call(conn, {:error, :creation_failed}) do
|
||||||
|
APIUtils.error_response(conn, :internal_server_error, "Failed to create resource")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles deletion_failed errors
|
||||||
|
def call(conn, {:error, :deletion_failed}) do
|
||||||
|
APIUtils.error_response(conn, :internal_server_error, "Failed to delete resource")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles any other {:error, message} returns
|
||||||
|
def call(conn, {:error, msg}) when is_binary(msg) do
|
||||||
|
APIUtils.error_response(conn, :bad_request, msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles any other unmatched errors
|
||||||
|
def call(conn, _error) do
|
||||||
|
APIUtils.error_response(conn, :internal_server_error, "An unexpected error occurred")
|
||||||
|
end
|
||||||
|
end
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
|||||||
|
|
||||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||||
|
|
||||||
alias WandererAppWeb.UtilAPIController, as: Util
|
alias WandererAppWeb.Helpers.APIUtils
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# Inline Schemas
|
# Inline Schemas
|
||||||
@@ -117,8 +117,8 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
|||||||
)
|
)
|
||||||
|
|
||||||
def index(conn, params) do
|
def index(conn, params) do
|
||||||
with {:ok, map_id} <- Util.fetch_map_id(params),
|
with {:ok, map_id} <- APIUtils.fetch_map_id(params),
|
||||||
{:ok, period} <- Util.require_param(params, "period"),
|
{:ok, period} <- APIUtils.require_param(params, "period"),
|
||||||
query <- WandererApp.Map.Audit.get_activity_query(map_id, period, "all"),
|
query <- WandererApp.Map.Audit.get_activity_query(map_id, period, "all"),
|
||||||
{:ok, data} <-
|
{:ok, data} <-
|
||||||
Api.read(query) do
|
Api.read(query) do
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
# lib/wanderer_app_web/controllers/map_connection_api_controller.ex
|
||||||
|
defmodule WandererAppWeb.MapConnectionAPIController do
|
||||||
|
@moduledoc """
|
||||||
|
API controller for managing map connections.
|
||||||
|
Provides operations to list, show, create, delete, and batch-delete connections, with legacy routing support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use WandererAppWeb, :controller
|
||||||
|
use OpenApiSpex.ControllerSpecs
|
||||||
|
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias WandererApp.Map, as: MapData
|
||||||
|
alias WandererApp.Map.Operations
|
||||||
|
alias WandererAppWeb.Helpers.APIUtils
|
||||||
|
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||||
|
|
||||||
|
action_fallback WandererAppWeb.FallbackController
|
||||||
|
|
||||||
|
# -- JSON Schemas --
|
||||||
|
@map_connection_schema %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string, description: "Unique connection ID"},
|
||||||
|
map_id: %Schema{type: :string, description: "Map UUID"},
|
||||||
|
solar_system_source: %Schema{type: :integer, description: "Source system ID"},
|
||||||
|
solar_system_target: %Schema{type: :integer, description: "Target system ID"},
|
||||||
|
type: %Schema{type: :integer, description: "Connection type"},
|
||||||
|
mass_status: %Schema{type: :integer, description: "Mass status (0-3)"},
|
||||||
|
time_status: %Schema{type: :integer, description: "Time status (0-3)"},
|
||||||
|
ship_size_type: %Schema{type: :integer, description: "Ship size limit (0-3)"},
|
||||||
|
locked: %Schema{type: :boolean, description: "Locked flag"},
|
||||||
|
custom_info: %Schema{type: :string, nullable: true, description: "Optional metadata"},
|
||||||
|
wormhole_type: %Schema{type: :string, nullable: true, description: "Wormhole code"}
|
||||||
|
},
|
||||||
|
required: ~w(id map_id solar_system_source solar_system_target)a
|
||||||
|
}
|
||||||
|
|
||||||
|
@connection_request_schema %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
solar_system_source: %Schema{type: :integer, description: "Source system ID"},
|
||||||
|
solar_system_target: %Schema{type: :integer, description: "Target system ID"},
|
||||||
|
type: %Schema{type: :integer, description: "Connection type (default 0)"}
|
||||||
|
},
|
||||||
|
required: ~w(solar_system_source solar_system_target)a,
|
||||||
|
example: %{solar_system_source: 30_000_142, solar_system_target: 30_000_144, type: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
@batch_delete_schema %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
connection_ids: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string, description: "Connection UUID"},
|
||||||
|
description: "IDs to delete"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["connection_ids"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@list_response_schema ApiSchemas.data_wrapper(%Schema{type: :array, items: @map_connection_schema})
|
||||||
|
@detail_response_schema ApiSchemas.data_wrapper(@map_connection_schema)
|
||||||
|
@batch_delete_response_schema ApiSchemas.data_wrapper(
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{deleted_count: %Schema{type: :integer, description: "Deleted count"}},
|
||||||
|
required: ["deleted_count"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Actions --
|
||||||
|
|
||||||
|
operation :index,
|
||||||
|
summary: "List Map Connections",
|
||||||
|
parameters: [
|
||||||
|
map_slug: [in: :path, type: :string],
|
||||||
|
map_id: [in: :path, type: :string],
|
||||||
|
solar_system_source: [in: :query, type: :integer, required: false],
|
||||||
|
solar_system_target: [in: :query, type: :integer, required: false]
|
||||||
|
],
|
||||||
|
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||||
|
def index(%{assigns: %{map_id: map_id}} = conn, params) do
|
||||||
|
with {:ok, src_filter} <- parse_optional(params, "solar_system_source"),
|
||||||
|
{:ok, tgt_filter} <- parse_optional(params, "solar_system_target") do
|
||||||
|
conns = MapData.list_connections!(map_id)
|
||||||
|
conns =
|
||||||
|
conns
|
||||||
|
|> filter_by_source(src_filter)
|
||||||
|
|> filter_by_target(tgt_filter)
|
||||||
|
data = Enum.map(conns, &APIUtils.connection_to_json/1)
|
||||||
|
APIUtils.respond_data(conn, data)
|
||||||
|
else
|
||||||
|
{:error, msg} when is_binary(msg) ->
|
||||||
|
conn
|
||||||
|
|> Plug.Conn.put_status(:bad_request)
|
||||||
|
|> APIUtils.error_response(:bad_request, msg)
|
||||||
|
{:error, _} ->
|
||||||
|
conn
|
||||||
|
|> Plug.Conn.put_status(:bad_request)
|
||||||
|
|> APIUtils.error_response(:bad_request, "Invalid filter parameter")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_optional(params, key) do
|
||||||
|
case Map.get(params, key) do
|
||||||
|
nil -> {:ok, nil}
|
||||||
|
val -> APIUtils.parse_int(val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter_by_source(conns, nil), do: conns
|
||||||
|
defp filter_by_source(conns, s), do: Enum.filter(conns, &(&1.solar_system_source == s))
|
||||||
|
|
||||||
|
defp filter_by_target(conns, nil), do: conns
|
||||||
|
defp filter_by_target(conns, t), do: Enum.filter(conns, &(&1.solar_system_target == t))
|
||||||
|
|
||||||
|
operation :show,
|
||||||
|
summary: "Show Connection (by id or by source/target)",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path], id: [in: :path, required: false], solar_system_source: [in: :query, type: :integer, required: false], solar_system_target: [in: :query, type: :integer, required: false]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||||
|
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||||
|
case Operations.get_connection(map_id, id) do
|
||||||
|
{:ok, conn_struct} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(conn_struct))
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def show(%{assigns: %{map_id: map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||||
|
with {:ok, source} <- APIUtils.parse_int(src),
|
||||||
|
{:ok, target} <- APIUtils.parse_int(tgt),
|
||||||
|
{:ok, conn_struct} <- Operations.get_connection_by_systems(map_id, source, target) do
|
||||||
|
APIUtils.respond_data(conn, APIUtils.connection_to_json(conn_struct))
|
||||||
|
else
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
operation :create,
|
||||||
|
summary: "Create Connection",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path], system_id: [in: :path]],
|
||||||
|
request_body: {"Connection create", "application/json", @connection_request_schema},
|
||||||
|
responses: ResponseSchemas.create_responses(@detail_response_schema)
|
||||||
|
def create(conn, params) do
|
||||||
|
map_id = conn.assigns[:map_id]
|
||||||
|
case Operations.create_connection(params, map_id) do
|
||||||
|
{:ok, conn_struct} when is_map(conn_struct) ->
|
||||||
|
conn
|
||||||
|
|> APIUtils.respond_data(APIUtils.connection_to_json(conn_struct), :created)
|
||||||
|
{:ok, :created} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:created)
|
||||||
|
|> json(%{data: %{result: "created"}})
|
||||||
|
{:skip, :exists} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:ok)
|
||||||
|
|> json(%{data: %{result: "exists"}})
|
||||||
|
{:error, reason} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: reason})
|
||||||
|
other ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: "Unexpected error"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(_, _), do: {:error, :bad_request}
|
||||||
|
|
||||||
|
operation :delete,
|
||||||
|
summary: "Delete Connection (by id or by source/target)",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path], id: [in: :path, required: false], solar_system_source: [in: :query, type: :integer, required: false], solar_system_target: [in: :query, type: :integer, required: false]],
|
||||||
|
responses: ResponseSchemas.delete_responses(nil)
|
||||||
|
def delete(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||||
|
case Operations.get_connection(map_id, id) do
|
||||||
|
{:ok, conn_struct} ->
|
||||||
|
case MapData.remove_connection(map_id, conn_struct) do
|
||||||
|
:ok -> send_resp(conn, :no_content, "")
|
||||||
|
error -> {:error, error}
|
||||||
|
end
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def delete(%{assigns: %{map_id: map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||||
|
with {:ok, source} <- APIUtils.parse_int(src),
|
||||||
|
{:ok, target} <- APIUtils.parse_int(tgt),
|
||||||
|
{:ok, conn_struct} <- Operations.get_connection_by_systems(map_id, source, target) do
|
||||||
|
case MapData.remove_connection(map_id, conn_struct) do
|
||||||
|
:ok -> send_resp(conn, :no_content, "")
|
||||||
|
error -> {:error, error}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
operation :batch_delete,
|
||||||
|
summary: "Batch Delete Connections",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path]],
|
||||||
|
request_body: {"Batch delete", "application/json", @batch_delete_schema},
|
||||||
|
responses: ResponseSchemas.standard_responses(@batch_delete_response_schema),
|
||||||
|
deprecated: true,
|
||||||
|
description: "Deprecated. Use individual DELETE requests instead."
|
||||||
|
def batch_delete(%{assigns: %{map_id: map_id}} = conn, %{"connection_ids" => ids})
|
||||||
|
when is_list(ids) do
|
||||||
|
deleted_count =
|
||||||
|
ids
|
||||||
|
|> Enum.map(&fetch_and_delete(map_id, &1))
|
||||||
|
|> Enum.count(&(&1 == :ok))
|
||||||
|
|
||||||
|
APIUtils.respond_data(conn, %{deleted_count: deleted_count})
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch_delete(_, _), do: {:error, :bad_request}
|
||||||
|
|
||||||
|
# -- Legacy route --
|
||||||
|
@deprecated "Use GET /api/maps/:map_identifier/systems/:system_id/connections instead"
|
||||||
|
operation :list_all_connections,
|
||||||
|
summary: "List All Connections (Legacy)",
|
||||||
|
deprecated: true,
|
||||||
|
parameters: [map_id: [in: :query]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||||
|
defdelegate list_all_connections(conn, params), to: __MODULE__, as: :index
|
||||||
|
|
||||||
|
operation :update,
|
||||||
|
summary: "Update Connection (by id or by source/target)",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path], id: [in: :path, required: false], solar_system_source: [in: :query, type: :integer, required: false], solar_system_target: [in: :query, type: :integer, required: false]],
|
||||||
|
request_body: {"Connection update", "application/json", @connection_request_schema},
|
||||||
|
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||||
|
def update(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||||
|
allowed_fields = ["mass_status", "ship_size_type", "locked", "custom_info", "type"]
|
||||||
|
attrs =
|
||||||
|
conn.body_params
|
||||||
|
|> Map.take(allowed_fields)
|
||||||
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
case Operations.update_connection(map_id, id, attrs) do
|
||||||
|
{:ok, updated_conn} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(updated_conn))
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def update(%{assigns: %{map_id: map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||||
|
allowed_fields = ["mass_status", "ship_size_type", "locked", "custom_info", "type"]
|
||||||
|
attrs =
|
||||||
|
conn.body_params
|
||||||
|
|> Map.take(allowed_fields)
|
||||||
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
with {:ok, source} <- APIUtils.parse_int(src),
|
||||||
|
{:ok, target} <- APIUtils.parse_int(tgt),
|
||||||
|
{:ok, conn_struct} <- Operations.get_connection_by_systems(map_id, source, target),
|
||||||
|
{:ok, updated_conn} <- Operations.update_connection(map_id, conn_struct.id, attrs) do
|
||||||
|
APIUtils.respond_data(conn, APIUtils.connection_to_json(updated_conn))
|
||||||
|
else
|
||||||
|
{:error, :not_found} ->
|
||||||
|
{:error, :not_found}
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
error ->
|
||||||
|
{:error, :internal_server_error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Helpers --
|
||||||
|
|
||||||
|
defp involves_system?(%{"solar_system_source" => s, "solar_system_target" => t}, id),
|
||||||
|
do: s == id or t == id
|
||||||
|
|
||||||
|
defp involves_system?(%{solar_system_source: s, solar_system_target: t}, id),
|
||||||
|
do: s == id or t == id
|
||||||
|
|
||||||
|
defp fetch_and_delete(map_id, id) do
|
||||||
|
case Operations.get_connection(map_id, id) do
|
||||||
|
{:ok, conn_struct} -> MapData.remove_connection(map_id, conn_struct)
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_connection!(map_id, id) do
|
||||||
|
MapData.list_connections!(map_id)
|
||||||
|
|> Enum.find(&(&1.id == id))
|
||||||
|
|> case do
|
||||||
|
nil -> raise "Connection #{id} not found"
|
||||||
|
conn -> conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
298
lib/wanderer_app_web/controllers/map_system_api_controller.ex
Normal file
298
lib/wanderer_app_web/controllers/map_system_api_controller.ex
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# lib/wanderer_app_web/controllers/map_system_api_controller.ex
|
||||||
|
defmodule WandererAppWeb.MapSystemAPIController do
|
||||||
|
@moduledoc """
|
||||||
|
API controller for managing map systems and their associated connections.
|
||||||
|
Provides CRUD operations and batch upsert for systems and connections.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use WandererAppWeb, :controller
|
||||||
|
use OpenApiSpex.ControllerSpecs
|
||||||
|
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias WandererApp.Map.Operations
|
||||||
|
alias WandererAppWeb.Helpers.APIUtils
|
||||||
|
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||||
|
|
||||||
|
action_fallback WandererAppWeb.FallbackController
|
||||||
|
|
||||||
|
# -- JSON Schemas --
|
||||||
|
@map_system_schema %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string, description: "Map system UUID"},
|
||||||
|
map_id: %Schema{type: :string, description: "Map UUID"},
|
||||||
|
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
|
||||||
|
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
|
||||||
|
region_name: %Schema{type: :string, description: "EVE region name"},
|
||||||
|
position_x: %Schema{type: :number, format: :float, description: "X coordinate"},
|
||||||
|
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
|
||||||
|
status: %Schema{type: :string, description: "System status"},
|
||||||
|
visible: %Schema{type: :boolean, description: "Visibility flag"},
|
||||||
|
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||||
|
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||||
|
locked: %Schema{type: :boolean, description: "Lock flag"},
|
||||||
|
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||||
|
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||||
|
},
|
||||||
|
required: ~w(id map_id solar_system_id)a
|
||||||
|
}
|
||||||
|
|
||||||
|
@system_request_schema %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
|
||||||
|
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
|
||||||
|
position_x: %Schema{type: :number, format: :float, description: "X coordinate"},
|
||||||
|
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
|
||||||
|
status: %Schema{type: :string, description: "System status"},
|
||||||
|
visible: %Schema{type: :boolean, description: "Visibility flag"},
|
||||||
|
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||||
|
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||||
|
locked: %Schema{type: :boolean, description: "Lock flag"},
|
||||||
|
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||||
|
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||||
|
},
|
||||||
|
required: ~w(solar_system_id)a,
|
||||||
|
example: %{
|
||||||
|
solar_system_id: 30_000_142,
|
||||||
|
solar_system_name: "Jita",
|
||||||
|
position_x: 100.5,
|
||||||
|
position_y: 200.3,
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@system_update_schema %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
solar_system_name: %Schema{type: :string, description: "EVE solar system name", nullable: true},
|
||||||
|
position_x: %Schema{type: :number, format: :float, description: "X coordinate", nullable: true},
|
||||||
|
position_y: %Schema{type: :number, format: :float, description: "Y coordinate", nullable: true},
|
||||||
|
status: %Schema{type: :string, description: "System status", nullable: true},
|
||||||
|
visible: %Schema{type: :boolean, description: "Visibility flag", nullable: true},
|
||||||
|
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||||
|
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||||
|
locked: %Schema{type: :boolean, description: "Lock flag", nullable: true},
|
||||||
|
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||||
|
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
position_x: 100.5,
|
||||||
|
position_y: 200.3,
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@list_response_schema ApiSchemas.data_wrapper(%Schema{type: :array, items: @map_system_schema})
|
||||||
|
@detail_response_schema ApiSchemas.data_wrapper(@map_system_schema)
|
||||||
|
@delete_response_schema ApiSchemas.data_wrapper(%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{deleted: %Schema{type: :boolean, description: "Deleted flag"}},
|
||||||
|
required: ["deleted"]
|
||||||
|
})
|
||||||
|
|
||||||
|
@batch_response_schema ApiSchemas.data_wrapper(%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
systems: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{created: %Schema{type: :integer}, updated: %Schema{type: :integer}},
|
||||||
|
required: ~w(created updated)a
|
||||||
|
},
|
||||||
|
connections: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
created: %Schema{type: :integer},
|
||||||
|
updated: %Schema{type: :integer},
|
||||||
|
deleted: %Schema{type: :integer}
|
||||||
|
},
|
||||||
|
required: ~w(created updated deleted)a
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ~w(systems connections)a
|
||||||
|
})
|
||||||
|
|
||||||
|
@batch_delete_schema %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
system_ids: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :integer},
|
||||||
|
description: "IDs to delete"
|
||||||
|
},
|
||||||
|
connection_ids: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description: "Connection UUIDs to delete",
|
||||||
|
nullable: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["system_ids"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@batch_delete_response_schema ApiSchemas.data_wrapper(%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{deleted_count: %Schema{type: :integer, description: "Deleted count"}},
|
||||||
|
required: ["deleted_count"]
|
||||||
|
})
|
||||||
|
|
||||||
|
@batch_request_schema ApiSchemas.data_wrapper(%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
systems: %Schema{type: :array, items: @system_request_schema},
|
||||||
|
connections: %Schema{type: :array, items: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
solar_system_source: %Schema{type: :integer, description: "Source system ID"},
|
||||||
|
solar_system_target: %Schema{type: :integer, description: "Target system ID"},
|
||||||
|
type: %Schema{type: :integer, description: "Connection type (default 0)"},
|
||||||
|
mass_status: %Schema{type: :integer, description: "Mass status (0-3)", nullable: true},
|
||||||
|
time_status: %Schema{type: :integer, description: "Time decay status (0-3)", nullable: true},
|
||||||
|
ship_size_type: %Schema{type: :integer, description: "Ship size limit (0-3)", nullable: true},
|
||||||
|
locked: %Schema{type: :boolean, description: "Lock flag", nullable: true},
|
||||||
|
custom_info: %Schema{type: :string, description: "Optional metadata", nullable: true}
|
||||||
|
},
|
||||||
|
required: ~w(solar_system_source solar_system_target)a
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# -- Actions --
|
||||||
|
|
||||||
|
operation :index,
|
||||||
|
summary: "List Map Systems and Connections",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||||
|
def index(%{assigns: %{map_id: map_id}} = conn, _params) do
|
||||||
|
systems = Operations.list_systems(map_id) |> Enum.map(&APIUtils.map_system_to_json/1)
|
||||||
|
connections = Operations.list_connections(map_id) |> Enum.map(&APIUtils.connection_to_json/1)
|
||||||
|
APIUtils.respond_data(conn, %{systems: systems, connections: connections})
|
||||||
|
end
|
||||||
|
|
||||||
|
operation :show,
|
||||||
|
summary: "Show Map System",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path], id: [in: :path]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||||
|
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||||
|
with {:ok, system_id} <- APIUtils.parse_int(id),
|
||||||
|
{:ok, system} <- Operations.get_system(map_id, system_id) do
|
||||||
|
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
operation :create,
|
||||||
|
summary: "Upsert Systems and Connections (batch or single)",
|
||||||
|
request_body: {"Systems+Connections upsert", "application/json", @batch_request_schema},
|
||||||
|
responses: ResponseSchemas.standard_responses(@batch_response_schema)
|
||||||
|
def create(%{assigns: %{map_id: map_id}} = conn, params) do
|
||||||
|
systems = Map.get(params, "systems", [])
|
||||||
|
connections = Map.get(params, "connections", [])
|
||||||
|
with {:ok, result} <- Operations.upsert_systems_and_connections(map_id, systems, connections) do
|
||||||
|
APIUtils.respond_data(conn, result)
|
||||||
|
else
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
operation :update,
|
||||||
|
summary: "Update System",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path], id: [in: :path]],
|
||||||
|
request_body: {"System update request", "application/json", @system_update_schema},
|
||||||
|
responses: ResponseSchemas.update_responses(@detail_response_schema)
|
||||||
|
def update(%{assigns: %{map_id: map_id}} = conn, %{"id" => id} = params) do
|
||||||
|
with {:ok, sid} <- APIUtils.parse_int(id),
|
||||||
|
{:ok, attrs} <- APIUtils.extract_update_params(params),
|
||||||
|
update_attrs = Map.put(attrs, "solar_system_id", sid),
|
||||||
|
{:ok, system} <- Operations.update_system(map_id, sid, update_attrs) do
|
||||||
|
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
operation :delete,
|
||||||
|
summary: "Batch Delete Systems and Connections",
|
||||||
|
request_body: {"Batch delete", "application/json", @batch_delete_schema},
|
||||||
|
responses: ResponseSchemas.standard_responses(@batch_delete_response_schema)
|
||||||
|
def delete(%{assigns: %{map_id: map_id}} = conn, params) do
|
||||||
|
system_ids = Map.get(params, "system_ids", [])
|
||||||
|
connection_ids = Map.get(params, "connection_ids", [])
|
||||||
|
|
||||||
|
deleted_systems = Enum.map(system_ids, fn id ->
|
||||||
|
case APIUtils.parse_int(id) do
|
||||||
|
{:ok, sid} -> Operations.delete_system(map_id, sid)
|
||||||
|
_ -> {:error, :invalid_id}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
deleted_connections = Enum.map(connection_ids, fn id ->
|
||||||
|
case Operations.get_connection(map_id, id) do
|
||||||
|
{:ok, conn_struct} ->
|
||||||
|
case WandererApp.Map.Server.delete_connection(map_id, conn_struct) do
|
||||||
|
:ok -> {:ok, conn_struct}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
_ -> {:error, :invalid_id}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
systems_deleted = Enum.count(deleted_systems, &match?({:ok, _}, &1))
|
||||||
|
connections_deleted = Enum.count(deleted_connections, &match?({:ok, _}, &1))
|
||||||
|
deleted_count = systems_deleted + connections_deleted
|
||||||
|
|
||||||
|
APIUtils.respond_data(conn, %{deleted_count: deleted_count})
|
||||||
|
end
|
||||||
|
|
||||||
|
operation :delete_single,
|
||||||
|
summary: "Delete a single Map System",
|
||||||
|
parameters: [map_slug: [in: :path], map_id: [in: :path], id: [in: :path]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@delete_response_schema)
|
||||||
|
def delete_single(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||||
|
with {:ok, sid} <- APIUtils.parse_int(id),
|
||||||
|
{:ok, _} <- Operations.delete_system(map_id, sid) do
|
||||||
|
APIUtils.respond_data(conn, %{deleted: true})
|
||||||
|
else
|
||||||
|
{:error, :not_found} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> APIUtils.respond_data(%{deleted: false, error: "System not found"})
|
||||||
|
{:error, reason} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> APIUtils.respond_data(%{deleted: false, error: "Failed to delete system", reason: reason})
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> APIUtils.respond_data(%{deleted: false, error: "Invalid system ID format"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Legacy endpoints --
|
||||||
|
|
||||||
|
operation :list_systems,
|
||||||
|
summary: "List Map Systems (Legacy)",
|
||||||
|
deprecated: true,
|
||||||
|
description: "Deprecated, use GET /api/maps/:map_identifier/systems instead",
|
||||||
|
parameters: [map_id: [in: :query]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||||
|
defdelegate list_systems(conn, params), to: __MODULE__, as: :index
|
||||||
|
|
||||||
|
operation :show_system,
|
||||||
|
summary: "Show Map System (Legacy)",
|
||||||
|
deprecated: true,
|
||||||
|
description: "Deprecated, use GET /api/maps/:map_identifier/systems/:id instead",
|
||||||
|
parameters: [map_id: [in: :query], id: [in: :query]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||||
|
defdelegate show_system(conn, params), to: __MODULE__, as: :show
|
||||||
|
|
||||||
|
@deprecated "Use GET /api/maps/:map_identifier/systems instead"
|
||||||
|
operation :list_all_connections,
|
||||||
|
summary: "List All Connections (Legacy)",
|
||||||
|
deprecated: true,
|
||||||
|
parameters: [map_id: [in: :query]],
|
||||||
|
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||||
|
def list_all_connections(%{assigns: %{map_id: map_id}} = conn, _params) do
|
||||||
|
connections = Operations.list_connections(map_id)
|
||||||
|
data = Enum.map(connections, &APIUtils.connection_to_json/1)
|
||||||
|
APIUtils.respond_data(conn, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,52 +1,116 @@
|
|||||||
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||||
@moduledoc """
|
@behaviour Plug
|
||||||
A plug that checks the "Authorization: Bearer <token>" header
|
|
||||||
against the map's stored public_api_key. Halts with 401 if invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
alias WandererAppWeb.UtilAPIController, as: Util
|
alias Plug.Crypto
|
||||||
|
alias WandererApp.Api.Map, as: ApiMap
|
||||||
|
alias WandererAppWeb.Schemas.ResponseSchemas, as: R
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@impl true
|
||||||
def init(opts), do: opts
|
def init(opts), do: opts
|
||||||
|
|
||||||
|
@impl true
|
||||||
def call(conn, _opts) do
|
def call(conn, _opts) do
|
||||||
header = get_req_header(conn, "authorization") |> List.first()
|
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
|
||||||
|
{:ok, map_id} <- fetch_map_id(conn),
|
||||||
|
{:ok, map} <- ApiMap.by_id(map_id),
|
||||||
|
true <- is_binary(map.public_api_key) &&
|
||||||
|
Crypto.secure_compare(map.public_api_key, token)
|
||||||
|
do
|
||||||
|
conn
|
||||||
|
|> assign(:map, map)
|
||||||
|
|> assign(:map_id, map.id)
|
||||||
|
else
|
||||||
|
[] ->
|
||||||
|
Logger.warning("Missing or invalid 'Bearer' token")
|
||||||
|
conn |> respond(401, "Missing or invalid 'Bearer' token") |> halt()
|
||||||
|
|
||||||
case header do
|
{:error, :bad_request, msg} ->
|
||||||
"Bearer " <> incoming_token ->
|
Logger.warning("Bad request: #{msg}")
|
||||||
case fetch_map(conn.query_params) do
|
conn |> respond(400, msg) |> halt()
|
||||||
{:ok, map} ->
|
|
||||||
if map.public_api_key == incoming_token do
|
|
||||||
conn
|
|
||||||
else
|
|
||||||
conn
|
|
||||||
|> put_resp_content_type("application/json")
|
|
||||||
|> send_resp(401, Jason.encode!(%{error: "Unauthorized (invalid token for map)"}))
|
|
||||||
|> halt()
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, :not_found, msg} ->
|
||||||
conn
|
Logger.warning("Not found: #{msg}")
|
||||||
|> put_resp_content_type("application/json")
|
conn |> respond(404, msg) |> halt()
|
||||||
|> send_resp(404, Jason.encode!(%{error: "Map not found"}))
|
|
||||||
|> halt()
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
{:error, _} ->
|
||||||
|
Logger.warning("Map identifier required")
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/json")
|
|> respond(400, "Map identifier required. Provide `map_identifier` in the path or `map_id`/`slug` in query.")
|
||||||
|> send_resp(401, Jason.encode!(%{error: "Missing or invalid 'Bearer' token"}))
|
|
||||||
|> halt()
|
|> halt()
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_map(query_params) do
|
false ->
|
||||||
case Util.fetch_map_id(query_params) do
|
Logger.warning("Unauthorized: invalid token for map #{inspect(conn.params["map_identifier"])}")
|
||||||
{:ok, map_id} ->
|
conn |> respond(401, "Unauthorized (invalid token for map)") |> halt()
|
||||||
WandererApp.Api.Map.by_id(map_id)
|
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
error
|
Logger.error("Unexpected error: #{inspect(error)}")
|
||||||
|
conn |> respond(500, "Unexpected error") |> halt()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Try unified path param first, then fall back to legacy query params
|
||||||
|
defp fetch_map_id(%Plug.Conn{params: %{"map_identifier" => id}}) when is_binary(id) and id != "" do
|
||||||
|
resolve_identifier(id)
|
||||||
|
end
|
||||||
|
defp fetch_map_id(conn), do: legacy_fetch(conn)
|
||||||
|
|
||||||
|
# Try ID lookup first, then slug lookup
|
||||||
|
defp resolve_identifier(id) do
|
||||||
|
case ApiMap.by_id(id) do
|
||||||
|
{:ok, %{id: map_id}} ->
|
||||||
|
{:ok, map_id}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
case ApiMap.get_map_by_slug(id) do
|
||||||
|
{:ok, %{id: map_id}} ->
|
||||||
|
{:ok, map_id}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :not_found, "Map not found for identifier: #{id}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Legacy: check assigns, then params["map_id"], then params["slug"]
|
||||||
|
defp legacy_fetch(conn) do
|
||||||
|
map_id_from_assign = conn.assigns[:map_id]
|
||||||
|
map_id_param = conn.params["map_id"]
|
||||||
|
slug_param = conn.params["slug"]
|
||||||
|
|
||||||
|
cond do
|
||||||
|
is_binary(map_id_from_assign) and map_id_from_assign != "" ->
|
||||||
|
{:ok, map_id_from_assign}
|
||||||
|
|
||||||
|
is_binary(map_id_param) and map_id_param != "" ->
|
||||||
|
{:ok, map_id_param}
|
||||||
|
|
||||||
|
is_binary(slug_param) and slug_param != "" ->
|
||||||
|
case ApiMap.get_map_by_slug(slug_param) do
|
||||||
|
{:ok, %{id: map_id}} -> {:ok, map_id}
|
||||||
|
_ -> {:error, :not_found, "Map not found for slug: #{slug_param}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:error, :bad_request,
|
||||||
|
"Map identifier required. Provide `map_identifier` in the path or `map_id`/`slug` in query."}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pick the right shared schema and send JSON
|
||||||
|
defp respond(conn, status, msg) do
|
||||||
|
{_desc, content_type, _schema} =
|
||||||
|
case status do
|
||||||
|
400 -> R.bad_request(msg)
|
||||||
|
401 -> R.unauthorized(msg)
|
||||||
|
404 -> R.not_found(msg)
|
||||||
|
500 -> R.internal_server_error(msg)
|
||||||
|
_ -> R.internal_server_error("Unexpected error")
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type(content_type)
|
||||||
|
|> send_resp(status, Jason.encode!(%{error: msg}))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ defmodule WandererAppWeb.Plugs.CheckMapSubscription do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
require Logger
|
||||||
|
|
||||||
def init(opts), do: opts
|
def init(opts), do: opts
|
||||||
|
|
||||||
def call(conn, _opts) do
|
def call(conn, _opts) do
|
||||||
case fetch_map_id(conn.query_params) do
|
# First check if map_id is already in conn.assigns (from CheckMapApiKey)
|
||||||
|
case get_map_id_from_assigns_or_params(conn) do
|
||||||
{:ok, map_id} ->
|
{:ok, map_id} ->
|
||||||
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
||||||
|
|
||||||
@@ -28,6 +30,17 @@ defmodule WandererAppWeb.Plugs.CheckMapSubscription do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# First try to get map_id from conn.assigns
|
||||||
|
defp get_map_id_from_assigns_or_params(conn) do
|
||||||
|
if Map.has_key?(conn.assigns, :map_id) do
|
||||||
|
Logger.debug("Found map_id in conn.assigns: #{conn.assigns.map_id}")
|
||||||
|
{:ok, conn.assigns.map_id}
|
||||||
|
else
|
||||||
|
# Fall back to query params if not in assigns
|
||||||
|
fetch_map_id(conn.query_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||||
{:ok, mid}
|
{:ok, mid}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
defmodule WandererAppWeb.UtilAPIController do
|
|
||||||
@moduledoc """
|
|
||||||
Utility functions for parameter handling, fetch helpers, etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias WandererApp.Api
|
|
||||||
|
|
||||||
def fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
|
||||||
{:ok, mid}
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
|
|
||||||
case Api.Map.get_map_by_slug(slug) do
|
|
||||||
{:ok, map} ->
|
|
||||||
{:ok, map.id}
|
|
||||||
|
|
||||||
{:error, _reason} ->
|
|
||||||
{:error, "No map found for slug=#{slug}"}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_map_id(_),
|
|
||||||
do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
|
||||||
|
|
||||||
# Require a given param to be present and non-empty
|
|
||||||
def require_param(params, key) do
|
|
||||||
case params[key] do
|
|
||||||
nil -> {:error, "Missing required param: #{key}"}
|
|
||||||
"" -> {:error, "Param #{key} cannot be empty"}
|
|
||||||
val -> {:ok, val}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse a string into an integer
|
|
||||||
def parse_int(str) do
|
|
||||||
case Integer.parse(str) do
|
|
||||||
{num, ""} -> {:ok, num}
|
|
||||||
_ -> {:error, "Invalid integer for param id=#{str}"}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
294
lib/wanderer_app_web/helpers/api_utils.ex
Normal file
294
lib/wanderer_app_web/helpers/api_utils.ex
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
defmodule WandererAppWeb.Helpers.APIUtils do
|
||||||
|
@moduledoc """
|
||||||
|
Unified helper module for API operations:
|
||||||
|
- Parameter parsing and validation
|
||||||
|
- Map ID resolution
|
||||||
|
- Standardized responses
|
||||||
|
- JSON serialization
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Explicit imports to avoid unnecessary dependencies
|
||||||
|
import Plug.Conn, only: [put_status: 2]
|
||||||
|
import Phoenix.Controller, only: [json: 2]
|
||||||
|
|
||||||
|
alias WandererApp.Api.Map, as: MapApi
|
||||||
|
alias WandererApp.Api.MapSolarSystem
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Map ID Resolution
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@spec fetch_map_id(map()) :: {:ok, String.t()} | {:error, String.t()}
|
||||||
|
def fetch_map_id(%{"map_id" => id}) when is_binary(id) do
|
||||||
|
case Ecto.UUID.cast(id) do
|
||||||
|
{:ok, _} -> {:ok, id}
|
||||||
|
:error -> {:error, "Invalid UUID format for map_id: #{id}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_map_id(%{"slug" => slug}) when is_binary(slug) do
|
||||||
|
case MapApi.get_map_by_slug(slug) do
|
||||||
|
{:ok, %{id: id}} -> {:ok, id}
|
||||||
|
_ -> {:error, "No map found for slug=#{slug}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Parameter Validators and Parsers
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@spec require_param(map(), String.t()) :: {:ok, any()} | {:error, String.t()}
|
||||||
|
def require_param(params, key) do
|
||||||
|
case Map.fetch(params, key) do
|
||||||
|
{:ok, val} when is_binary(val) ->
|
||||||
|
trimmed = String.trim(val)
|
||||||
|
if trimmed == "" do
|
||||||
|
{:error, "Param #{key} cannot be empty"}
|
||||||
|
else
|
||||||
|
{:ok, trimmed}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, val} ->
|
||||||
|
{:ok, val}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:error, "Missing required param: #{key}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec parse_int(binary() | integer()) :: {:ok, integer()} | {:error, String.t()}
|
||||||
|
def parse_int(str) when is_binary(str) do
|
||||||
|
Logger.debug("Parsing integer from: #{inspect(str)}")
|
||||||
|
|
||||||
|
case Integer.parse(str) do
|
||||||
|
{num, ""} -> {:ok, num}
|
||||||
|
_ -> {:error, "Invalid integer format: #{str}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_int(num) when is_integer(num), do: {:ok, num}
|
||||||
|
def parse_int(other), do: {:error, "Expected integer or string, got: #{inspect(other)}"}
|
||||||
|
|
||||||
|
@spec parse_int!(binary() | integer()) :: integer()
|
||||||
|
def parse_int!(str) do
|
||||||
|
case parse_int(str) do
|
||||||
|
{:ok, num} -> num
|
||||||
|
{:error, msg} -> raise ArgumentError, msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec validate_uuid(any()) :: {:ok, String.t()} | {:error, String.t()}
|
||||||
|
def validate_uuid(id) when is_binary(id) do
|
||||||
|
case Ecto.UUID.cast(id) do
|
||||||
|
{:ok, uuid} -> {:ok, uuid}
|
||||||
|
:error -> {:error, "Invalid UUID format: #{id}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_uuid(_), do: {:error, "ID must be a UUID string"}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Parameter Extraction
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Extract and validate parameters for upserting a system.
|
||||||
|
Returns {:ok, attrs} or {:error, error_message}.
|
||||||
|
"""
|
||||||
|
@spec extract_upsert_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||||
|
def extract_upsert_params(params) when is_map(params) do
|
||||||
|
required = ["solar_system_id"]
|
||||||
|
optional = [
|
||||||
|
"solar_system_name", "position_x", "position_y", "coordinates",
|
||||||
|
"status", "visible", "description", "tag",
|
||||||
|
"locked", "temporary_name", "labels"
|
||||||
|
]
|
||||||
|
|
||||||
|
case Map.fetch(params, "solar_system_id") do
|
||||||
|
:error -> {:error, "Missing solar_system_id in request body"}
|
||||||
|
{:ok, _} ->
|
||||||
|
params
|
||||||
|
|> Map.take(required ++ optional)
|
||||||
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
|> then(&{:ok, &1})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Extract and validate parameters for updating a system.
|
||||||
|
Returns {:ok, attrs} or {:error, error_message}.
|
||||||
|
"""
|
||||||
|
@spec extract_update_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||||
|
def extract_update_params(params) when is_map(params) do
|
||||||
|
allowed = [
|
||||||
|
"solar_system_name", "position_x", "position_y", "coordinates",
|
||||||
|
"status", "visible", "description", "tag",
|
||||||
|
"locked", "temporary_name", "labels"
|
||||||
|
]
|
||||||
|
|
||||||
|
attrs =
|
||||||
|
params
|
||||||
|
|> Map.take(allowed)
|
||||||
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
|
||||||
|
{:ok, attrs}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec normalize_connection_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||||
|
def normalize_connection_params(params) do
|
||||||
|
# Convert all keys to strings for consistent access
|
||||||
|
string_params = for {k, v} <- params, into: %{} do
|
||||||
|
{to_string(k), v}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Define parameter mappings for normalization
|
||||||
|
aliases = %{
|
||||||
|
"source" => "solar_system_source",
|
||||||
|
"source_id" => "solar_system_source",
|
||||||
|
"target" => "solar_system_target",
|
||||||
|
"target_id" => "solar_system_target"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Normalize parameters using aliases
|
||||||
|
normalized_params = Enum.reduce(aliases, string_params, fn {alias_key, std_key}, acc ->
|
||||||
|
if Map.has_key?(acc, alias_key) && !Map.has_key?(acc, std_key) do
|
||||||
|
Map.put(acc, std_key, acc[alias_key])
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Handle required parameters
|
||||||
|
with {:ok, src} <- parse_to_int(normalized_params["solar_system_source"], "solar_system_source"),
|
||||||
|
{:ok, tgt} <- parse_to_int(normalized_params["solar_system_target"], "solar_system_target") do
|
||||||
|
|
||||||
|
# Handle optional parameters with sane defaults
|
||||||
|
type = normalized_params["type"] || 0
|
||||||
|
mass_status = normalized_params["mass_status"] || 0
|
||||||
|
time_status = normalized_params["time_status"] || 0
|
||||||
|
ship_size_type = normalized_params["ship_size_type"] || 0
|
||||||
|
locked = normalized_params["locked"] || false
|
||||||
|
custom_info = normalized_params["custom_info"]
|
||||||
|
wormhole_type = normalized_params["wormhole_type"]
|
||||||
|
|
||||||
|
# Build standardized attrs map
|
||||||
|
attrs = %{
|
||||||
|
"solar_system_source" => src,
|
||||||
|
"solar_system_target" => tgt,
|
||||||
|
"type" => parse_optional_int(type, 0),
|
||||||
|
"mass_status" => parse_optional_int(mass_status, 0),
|
||||||
|
"time_status" => parse_optional_int(time_status, 0),
|
||||||
|
"ship_size_type" => parse_optional_int(ship_size_type, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add non-nil optional attributes
|
||||||
|
attrs = if is_nil(locked), do: attrs, else: Map.put(attrs, "locked", locked)
|
||||||
|
attrs = if is_nil(custom_info), do: attrs, else: Map.put(attrs, "custom_info", custom_info)
|
||||||
|
attrs = if is_nil(wormhole_type), do: attrs, else: Map.put(attrs, "wormhole_type", wormhole_type)
|
||||||
|
|
||||||
|
{:ok, attrs}
|
||||||
|
else
|
||||||
|
{:error, msg} -> {:error, msg}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to handle various input formats
|
||||||
|
defp parse_to_int(nil, field), do: {:error, "Missing #{field}"}
|
||||||
|
defp parse_to_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||||
|
defp parse_to_int(val, field) when is_binary(val) do
|
||||||
|
case Integer.parse(val) do
|
||||||
|
{i, ""} -> {:ok, i}
|
||||||
|
:error -> {:error, "Invalid #{field}: #{val}"}
|
||||||
|
_ -> {:error, "Invalid #{field}: #{val}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
defp parse_to_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
||||||
|
|
||||||
|
defp parse_optional_int(nil, default), do: default
|
||||||
|
defp parse_optional_int(i, _default) when is_integer(i), do: i
|
||||||
|
defp parse_optional_int(s, default) when is_binary(s) do
|
||||||
|
case Integer.parse(s) do
|
||||||
|
{i, _} -> i
|
||||||
|
:error -> default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Standardized JSON Responses
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@spec respond_data(Plug.Conn.t(), any(), atom() | integer()) :: Plug.Conn.t()
|
||||||
|
def respond_data(conn, data, status \\ :ok) do
|
||||||
|
conn
|
||||||
|
|> put_status(status)
|
||||||
|
|> json(%{data: data})
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec error_response(Plug.Conn.t(), atom() | integer(), String.t(), map() | nil) :: Plug.Conn.t()
|
||||||
|
def error_response(conn, status, message, details \\ nil) do
|
||||||
|
body = if details, do: %{error: message, details: details}, else: %{error: message}
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_status(status)
|
||||||
|
|> json(body)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec error_not_found(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
|
||||||
|
def error_not_found(conn, message), do: error_response(conn, :not_found, message)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Formats error messages for consistent display.
|
||||||
|
"""
|
||||||
|
@spec format_error(any()) :: String.t()
|
||||||
|
def format_error(error) when is_binary(error), do: error
|
||||||
|
def format_error(error) when is_atom(error), do: Atom.to_string(error)
|
||||||
|
def format_error(error), do: inspect(error)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# JSON Serialization
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@spec map_system_to_json(struct()) :: map()
|
||||||
|
def map_system_to_json(system) do
|
||||||
|
base =
|
||||||
|
Map.take(system, ~w(
|
||||||
|
id map_id solar_system_id custom_name temporary_name description tag labels
|
||||||
|
locked visible status position_x position_y inserted_at updated_at
|
||||||
|
)a)
|
||||||
|
|
||||||
|
original = get_original_name(system.solar_system_id)
|
||||||
|
name = pick_name(system)
|
||||||
|
|
||||||
|
base
|
||||||
|
|> Map.put(:original_name, original)
|
||||||
|
|> Map.put(:name, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_original_name(id) do
|
||||||
|
case MapSolarSystem.by_solar_system_id(id) do
|
||||||
|
{:ok, sys} -> sys.solar_system_name
|
||||||
|
_ -> "System #{id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pick_name(%{temporary_name: t, custom_name: c, solar_system_id: id}) do
|
||||||
|
cond do
|
||||||
|
t not in [nil, ""] -> t
|
||||||
|
c not in [nil, ""] -> c
|
||||||
|
true -> get_original_name(id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec connection_to_json(struct()) :: map()
|
||||||
|
def connection_to_json(conn) do
|
||||||
|
Map.take(conn, ~w(
|
||||||
|
id map_id solar_system_source solar_system_target mass_status
|
||||||
|
time_status ship_size_type type wormhole_type inserted_at updated_at
|
||||||
|
)a)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -206,17 +206,37 @@ defmodule WandererAppWeb.Router do
|
|||||||
scope "/api/map", WandererAppWeb do
|
scope "/api/map", WandererAppWeb do
|
||||||
pipe_through [:api, :api_map]
|
pipe_through [:api, :api_map]
|
||||||
get "/audit", MapAuditAPIController, :index
|
get "/audit", MapAuditAPIController, :index
|
||||||
get "/systems", MapAPIController, :list_systems
|
# Deprecated routes - use /api/maps/:map_identifier/systems instead
|
||||||
get "/system", MapAPIController, :show_system
|
get "/systems", MapSystemAPIController, :list_systems
|
||||||
get "/connections", MapAPIController, :list_connections
|
get "/system", MapSystemAPIController, :show_system
|
||||||
get "/characters", MapAPIController, :tracked_characters_with_info
|
get "/connections", MapSystemAPIController, :list_all_connections
|
||||||
|
get "/characters", MapAPIController, :list_tracked_characters
|
||||||
get "/structure-timers", MapAPIController, :show_structure_timers
|
get "/structure-timers", MapAPIController, :show_structure_timers
|
||||||
get "/character-activity", MapAPIController, :character_activity
|
get "/character-activity", MapAPIController, :character_activity
|
||||||
get "/user_characters", MapAPIController, :user_characters
|
get "/user_characters", MapAPIController, :user_characters
|
||||||
|
|
||||||
get "/acls", MapAccessListAPIController, :index
|
get "/acls", MapAccessListAPIController, :index
|
||||||
post "/acls", MapAccessListAPIController, :create
|
post "/acls", MapAccessListAPIController, :create
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unified RESTful routes for systems & connections by slug or ID
|
||||||
|
#
|
||||||
|
scope "/api/maps/:map_identifier", WandererAppWeb do
|
||||||
|
pipe_through [:api, :api_map]
|
||||||
|
|
||||||
|
patch "/connections", MapConnectionAPIController, :update
|
||||||
|
delete "/connections", MapConnectionAPIController, :delete
|
||||||
|
delete "/systems", MapSystemAPIController, :delete
|
||||||
|
resources "/systems", MapSystemAPIController, only: [:index, :show, :create, :update, :delete]
|
||||||
|
resources "/connections", MapConnectionAPIController, only: [:index, :show, :create, :update, :delete], param: "id"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Other API routes
|
||||||
|
#
|
||||||
scope "/api/characters", WandererAppWeb do
|
scope "/api/characters", WandererAppWeb do
|
||||||
pipe_through [:api, :api_character]
|
pipe_through [:api, :api_character]
|
||||||
get "/", CharactersAPIController, :index
|
get "/", CharactersAPIController, :index
|
||||||
|
|||||||
156
lib/wanderer_app_web/schemas/api_schemas.ex
Normal file
156
lib/wanderer_app_web/schemas/api_schemas.ex
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
defmodule WandererAppWeb.Schemas.ApiSchemas do
|
||||||
|
@moduledoc """
|
||||||
|
Shared OpenAPI schema definitions for the Wanderer API.
|
||||||
|
|
||||||
|
This module defines common schema components that can be reused
|
||||||
|
across different controller specifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
|
||||||
|
# Standard response wrappers
|
||||||
|
def data_wrapper(schema) do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
data: schema
|
||||||
|
},
|
||||||
|
required: ["data"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Standard error responses
|
||||||
|
def error_response(description \\ "Error") do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
error: %Schema{type: :string, description: "Brief error message"},
|
||||||
|
details: %Schema{type: :string, description: "Detailed explanation", nullable: true},
|
||||||
|
code: %Schema{type: :string, description: "Optional error code", nullable: true}
|
||||||
|
},
|
||||||
|
required: ["error"],
|
||||||
|
example: %{"error" => description, "details" => "Additional information about the error"}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Common entity schemas
|
||||||
|
def character_schema do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
eve_id: %Schema{type: :string},
|
||||||
|
name: %Schema{type: :string},
|
||||||
|
corporation_id: %Schema{type: :string},
|
||||||
|
corporation_ticker: %Schema{type: :string},
|
||||||
|
alliance_id: %Schema{type: :string},
|
||||||
|
alliance_ticker: %Schema{type: :string}
|
||||||
|
},
|
||||||
|
required: ["eve_id", "name"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Common system schema based on what we've seen in controllers
|
||||||
|
def solar_system_basic_schema do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
solar_system_id: %Schema{type: :integer},
|
||||||
|
solar_system_name: %Schema{type: :string},
|
||||||
|
region_id: %Schema{type: :integer},
|
||||||
|
region_name: %Schema{type: :string},
|
||||||
|
constellation_id: %Schema{type: :integer},
|
||||||
|
constellation_name: %Schema{type: :string},
|
||||||
|
security: %Schema{type: :string}
|
||||||
|
},
|
||||||
|
required: ["solar_system_id", "solar_system_name"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Map schema with common fields
|
||||||
|
def map_basic_schema do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
name: %Schema{type: :string},
|
||||||
|
slug: %Schema{type: :string},
|
||||||
|
description: %Schema{type: :string},
|
||||||
|
owner_id: %Schema{type: :string},
|
||||||
|
inserted_at: %Schema{type: :string, format: :date_time},
|
||||||
|
updated_at: %Schema{type: :string, format: :date_time}
|
||||||
|
},
|
||||||
|
required: ["id", "name", "slug"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# License schema
|
||||||
|
def license_schema do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
license_key: %Schema{type: :string},
|
||||||
|
is_valid: %Schema{type: :boolean},
|
||||||
|
expire_at: %Schema{type: :string, format: :date_time},
|
||||||
|
map_id: %Schema{type: :string}
|
||||||
|
},
|
||||||
|
required: ["id", "license_key", "is_valid", "map_id"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Access list schema
|
||||||
|
def access_list_schema do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
name: %Schema{type: :string},
|
||||||
|
description: %Schema{type: :string},
|
||||||
|
owner_id: %Schema{type: :string},
|
||||||
|
api_key: %Schema{type: :string},
|
||||||
|
inserted_at: %Schema{type: :string, format: :date_time},
|
||||||
|
updated_at: %Schema{type: :string, format: :date_time}
|
||||||
|
},
|
||||||
|
required: ["id", "name"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Access list member schema
|
||||||
|
def access_list_member_schema do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
name: %Schema{type: :string},
|
||||||
|
role: %Schema{type: :string},
|
||||||
|
eve_character_id: %Schema{type: :string},
|
||||||
|
eve_corporation_id: %Schema{type: :string},
|
||||||
|
eve_alliance_id: %Schema{type: :string},
|
||||||
|
inserted_at: %Schema{type: :string, format: :date_time},
|
||||||
|
updated_at: %Schema{type: :string, format: :date_time}
|
||||||
|
},
|
||||||
|
required: ["id", "name", "role"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Common paginated response wrapper
|
||||||
|
def paginated_response(items_schema) do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
data: items_schema,
|
||||||
|
pagination: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
page: %Schema{type: :integer},
|
||||||
|
page_size: %Schema{type: :integer},
|
||||||
|
total_pages: %Schema{type: :integer},
|
||||||
|
total_count: %Schema{type: :integer}
|
||||||
|
},
|
||||||
|
required: ["page", "page_size", "total_count"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["data", "pagination"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
115
lib/wanderer_app_web/schemas/response_schemas.ex
Normal file
115
lib/wanderer_app_web/schemas/response_schemas.ex
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
defmodule WandererAppWeb.Schemas.ResponseSchemas do
|
||||||
|
@moduledoc """
|
||||||
|
Standard response schema definitions for API responses.
|
||||||
|
|
||||||
|
This module provides helper functions to create standardized
|
||||||
|
HTTP response schemas for OpenAPI documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias WandererAppWeb.Schemas.ApiSchemas
|
||||||
|
|
||||||
|
# Standard response status codes
|
||||||
|
def ok(schema, description \\ "Successful operation") do
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
"application/json",
|
||||||
|
schema
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def created(schema, description \\ "Resource created") do
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
"application/json",
|
||||||
|
schema
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def bad_request(description \\ "Bad request") do
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
"application/json",
|
||||||
|
ApiSchemas.error_response(description)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_found(description \\ "Resource not found") do
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
"application/json",
|
||||||
|
ApiSchemas.error_response(description)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def internal_server_error(description \\ "Internal server error") do
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
"application/json",
|
||||||
|
ApiSchemas.error_response(description)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def unauthorized(description \\ "Unauthorized") do
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
"application/json",
|
||||||
|
ApiSchemas.error_response(description)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def forbidden(description \\ "Forbidden") do
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
"application/json",
|
||||||
|
ApiSchemas.error_response(description)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper for common response patterns
|
||||||
|
def standard_responses(success_schema, success_description \\ "Successful operation") do
|
||||||
|
[
|
||||||
|
ok: ok(success_schema, success_description),
|
||||||
|
bad_request: bad_request(),
|
||||||
|
not_found: not_found(),
|
||||||
|
internal_server_error: internal_server_error()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper for create operation responses
|
||||||
|
def create_responses(created_schema, created_description \\ "Resource created") do
|
||||||
|
[
|
||||||
|
created: created(created_schema, created_description),
|
||||||
|
bad_request: bad_request(),
|
||||||
|
internal_server_error: internal_server_error()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper for update operation responses
|
||||||
|
def update_responses(updated_schema, updated_description \\ "Resource updated") do
|
||||||
|
[
|
||||||
|
ok: ok(updated_schema, updated_description),
|
||||||
|
bad_request: bad_request(),
|
||||||
|
not_found: not_found(),
|
||||||
|
internal_server_error: internal_server_error()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper for delete operation responses
|
||||||
|
def delete_responses(deleted_schema \\ nil, deleted_description \\ "Resource deleted") do
|
||||||
|
if deleted_schema do
|
||||||
|
[
|
||||||
|
ok: ok(deleted_schema, deleted_description),
|
||||||
|
not_found: not_found(),
|
||||||
|
internal_server_error: internal_server_error()
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[
|
||||||
|
no_content:
|
||||||
|
{deleted_description <> " (no content)", nil, nil},
|
||||||
|
not_found: not_found(),
|
||||||
|
internal_server_error: internal_server_error()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
501
priv/posts/2025/04-30-systems-connections-api.md
Normal file
501
priv/posts/2025/04-30-systems-connections-api.md
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
%{
|
||||||
|
title: "Guide: Systems and Connections API",
|
||||||
|
author: "Wanderer Team",
|
||||||
|
cover_image_uri: "/images/news/03-06-systems/api-endpoints.png",
|
||||||
|
tags: ~w(api map systems connections documentation),
|
||||||
|
description: "Detailed guide for Wanderer's systems and connections API endpoints, including batch operations, updates, and deletions."
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Guide to Wanderer's Systems and Connections API
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This guide covers Wanderer's dedicated API endpoints for managing systems and connections on your maps. These endpoints provide fine-grained control over individual systems and connections, as well as batch operations for efficient updates.
|
||||||
|
|
||||||
|
With these APIs, you can:
|
||||||
|
|
||||||
|
- Create, update, and delete individual systems
|
||||||
|
- Create, update, and delete individual connections
|
||||||
|
- Perform batch operations on systems and connections
|
||||||
|
- Query system and connection details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All endpoints require a Map API Token, which you can generate in your map settings. Pass the token in the Authorization header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Authorization: Bearer <YOUR_MAP_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Systems Endpoints
|
||||||
|
|
||||||
|
### 1. List Systems
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/maps/:map_identifier/systems
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Retrieves all systems and their connections for the specified map.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"id": "<SYSTEM_UUID>",
|
||||||
|
"solar_system_id": 30000142,
|
||||||
|
"solar_system_name": "Jita",
|
||||||
|
"position_x": 100.5,
|
||||||
|
"position_y": 200.3,
|
||||||
|
"status": "clear",
|
||||||
|
"visible": true,
|
||||||
|
"description": "Trade hub",
|
||||||
|
"tag": "TRADE",
|
||||||
|
"locked": false,
|
||||||
|
"labels": ["market", "highsec"],
|
||||||
|
"map_id": "<MAP_UUID>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "<CONNECTION_UUID>",
|
||||||
|
"solar_system_source": 30000142,
|
||||||
|
"solar_system_target": 30000144,
|
||||||
|
"type": 0,
|
||||||
|
"mass_status": 0,
|
||||||
|
"time_status": 0,
|
||||||
|
"ship_size_type": 1,
|
||||||
|
"wormhole_type": "K162",
|
||||||
|
"count_of_passage": 0,
|
||||||
|
"locked": false,
|
||||||
|
"custom_info": "Fresh hole"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Show Single System
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/maps/:map_identifier/systems/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Retrieves details for a specific system.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
- `id` (required) — the system's solar_system_id.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "<SYSTEM_UUID>",
|
||||||
|
"solar_system_id": 30000142,
|
||||||
|
"solar_system_name": "Jita",
|
||||||
|
"position_x": 100.5,
|
||||||
|
"position_y": 200.3,
|
||||||
|
"status": "clear",
|
||||||
|
"visible": true,
|
||||||
|
"description": "Trade hub",
|
||||||
|
"tag": "TRADE",
|
||||||
|
"locked": false,
|
||||||
|
"labels": ["market", "highsec"],
|
||||||
|
"map_id": "<MAP_UUID>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create/Update System
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/maps/:map_identifier/systems
|
||||||
|
PUT /api/maps/:map_identifier/systems/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Creates a new system or updates an existing one.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
- `id` (required for PUT) — the system's solar_system_id.
|
||||||
|
|
||||||
|
#### Example Create Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"solar_system_id": 30000142,
|
||||||
|
"solar_system_name": "Jita",
|
||||||
|
"position_x": 100.5,
|
||||||
|
"position_y": 200.3,
|
||||||
|
"status": "clear",
|
||||||
|
"visible": true,
|
||||||
|
"description": "Trade hub",
|
||||||
|
"tag": "TRADE",
|
||||||
|
"locked": false,
|
||||||
|
"labels": ["market", "highsec"]
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example Update Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"status": "hostile",
|
||||||
|
"description": "Hostiles reported",
|
||||||
|
"tag": "DANGER"
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Delete System
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DELETE /api/maps/:map_identifier/systems/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Deletes a specific system and its associated connections.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
- `id` (required) — the system's solar_system_id.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Batch Delete Systems
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DELETE /api/maps/:map_identifier/systems
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Deletes multiple systems and their connections in a single operation.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"system_ids": [30000142, 30000144, 30000145]
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connections Endpoints
|
||||||
|
|
||||||
|
### 1. List Connections
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/maps/:map_identifier/connections
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Retrieves all connections for the specified map.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/connections"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "<CONNECTION_UUID>",
|
||||||
|
"solar_system_source": 30000142,
|
||||||
|
"solar_system_target": 30000144,
|
||||||
|
"type": 0,
|
||||||
|
"mass_status": 0,
|
||||||
|
"time_status": 0,
|
||||||
|
"ship_size_type": 1,
|
||||||
|
"wormhole_type": "K162",
|
||||||
|
"count_of_passage": 0,
|
||||||
|
"locked": false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/maps/:map_identifier/connections
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Creates a new connection between two systems.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"solar_system_source": 30000142,
|
||||||
|
"solar_system_target": 30000144,
|
||||||
|
"type": 0,
|
||||||
|
"mass_status": 0,
|
||||||
|
"time_status": 0,
|
||||||
|
"ship_size_type": 1,
|
||||||
|
"locked": false,
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/connections"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Update Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PATCH /api/maps/:map_identifier/connections
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Updates an existing connection's properties.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
- Query parameters:
|
||||||
|
- `solar_system_source` (required) — source system ID
|
||||||
|
- `solar_system_target` (required) — target system ID
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PATCH \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"mass_status": 1,
|
||||||
|
"time_status": 1,
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/connections?solar_system_source=30000142&solar_system_target=30000144"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Delete Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DELETE /api/maps/:map_identifier/connections
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Deletes a connection between two systems.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
- Query parameters:
|
||||||
|
- `solar_system_source` (required) — source system ID
|
||||||
|
- `solar_system_target` (required) — target system ID
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/connections?solar_system_source=30000142&solar_system_target=30000144"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Batch Operations
|
||||||
|
|
||||||
|
### 1. Batch Upsert Systems and Connections
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/maps/:map_identifier/systems
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Creates or updates multiple systems and connections in a single operation.
|
||||||
|
- **Authentication:** Requires Map API Token.
|
||||||
|
- **Parameters:**
|
||||||
|
- `map_identifier` (required) — the map's slug or UUID.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"solar_system_id": 30000142,
|
||||||
|
"solar_system_name": "Jita",
|
||||||
|
"position_x": 100.5,
|
||||||
|
"position_y": 200.3,
|
||||||
|
"status": "clear"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"solar_system_id": 30000144,
|
||||||
|
"solar_system_name": "Perimeter",
|
||||||
|
"position_x": 150.5,
|
||||||
|
"position_y": 250.3,
|
||||||
|
"status": "clear"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"solar_system_source": 30000142,
|
||||||
|
"solar_system_target": 30000144,
|
||||||
|
"type": 0,
|
||||||
|
"mass_status": 0,
|
||||||
|
"ship_size_type": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"systems": {
|
||||||
|
"created": 2,
|
||||||
|
"updated": 0
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"created": 1,
|
||||||
|
"updated": 0,
|
||||||
|
"deleted": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The response includes counts for:
|
||||||
|
- Systems created and updated
|
||||||
|
- Connections created, updated, and deleted (if any)
|
||||||
|
|
||||||
|
Note: The `deleted` count in connections will be 0 for batch operations as deletion is handled through separate endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Practical Examples
|
||||||
|
|
||||||
|
### Backup and Restore Map State
|
||||||
|
|
||||||
|
We provide a utility script that demonstrates how to use these endpoints to backup and restore your map state:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# backup_restore_test.sh
|
||||||
|
|
||||||
|
# 1. Backup current state
|
||||||
|
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems" \
|
||||||
|
> map_backup.json
|
||||||
|
|
||||||
|
# 2. Delete everything (after confirmation)
|
||||||
|
read -p "Delete all systems? (y/N) " confirm
|
||||||
|
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
# Get system IDs
|
||||||
|
systems=$(cat map_backup.json | jq -r '.data.systems[].solar_system_id')
|
||||||
|
|
||||||
|
# Create deletion payload
|
||||||
|
payload=$(jq -n --argjson ids "$(echo "$systems" | jq -R . | jq -s .)" \
|
||||||
|
'{system_ids: $ids}')
|
||||||
|
|
||||||
|
# Delete all systems
|
||||||
|
curl -X DELETE \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Restore from backup (after confirmation)
|
||||||
|
read -p "Restore from backup? (y/N) " confirm
|
||||||
|
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
# Extract systems and connections
|
||||||
|
backup_data=$(cat map_backup.json)
|
||||||
|
systems=$(echo "$backup_data" | jq '.data.systems')
|
||||||
|
connections=$(echo "$backup_data" | jq '.data.connections')
|
||||||
|
|
||||||
|
# Create restore payload
|
||||||
|
payload="{\"systems\": $systems, \"connections\": $connections}"
|
||||||
|
|
||||||
|
# Restore everything
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
This script demonstrates a practical application of the batch operations endpoints for backing up and restoring map data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
These endpoints provide powerful tools for managing your map's systems and connections programmatically. Key features include:
|
||||||
|
|
||||||
|
1. Individual system and connection management
|
||||||
|
2. Efficient batch operations
|
||||||
|
3. Flexible update options
|
||||||
|
4. Robust error handling
|
||||||
|
5. Consistent response formats
|
||||||
|
|
||||||
|
For the most up-to-date and interactive documentation, remember to check the Swagger UI at `/swaggerui`.
|
||||||
|
|
||||||
|
If you have questions about these endpoints or need assistance, please reach out to the Wanderer Team.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Fly safe,
|
||||||
|
**The Wanderer Team**
|
||||||
|
|
||||||
|
----
|
||||||
163
test/manual/api/map_api_backup_restore_test.sh
Executable file
163
test/manual/api/map_api_backup_restore_test.sh
Executable file
@@ -0,0 +1,163 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# test/manual/api/backup_restore_test.sh
|
||||||
|
# ─── Backup and Restore Test for Map Systems and Connections ────────────────────────
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./backup_restore_test.sh # Run with default settings
|
||||||
|
# ./backup_restore_test.sh -v # Run in verbose mode
|
||||||
|
# ./backup_restore_test.sh -h # Show help
|
||||||
|
#
|
||||||
|
source "$(dirname "$0")/utils.sh"
|
||||||
|
|
||||||
|
# Set to "true" to see detailed output, "false" for minimal output
|
||||||
|
VERBOSE=${VERBOSE:-false}
|
||||||
|
|
||||||
|
# Parse command line options
|
||||||
|
while getopts "vh" opt; do
|
||||||
|
case $opt in
|
||||||
|
v)
|
||||||
|
VERBOSE=true
|
||||||
|
;;
|
||||||
|
h)
|
||||||
|
echo "Usage: $0 [-v] [-h]"
|
||||||
|
echo " -v Verbose mode (show detailed output)"
|
||||||
|
echo " -h Show this help message"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Invalid option: -$OPTARG" >&2
|
||||||
|
echo "Use -h for help"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
|
||||||
|
# File to store backup data
|
||||||
|
BACKUP_FILE="/tmp/wanderer_map_backup.json"
|
||||||
|
|
||||||
|
# ─── UTILITY FUNCTIONS ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Function to backup current map state
|
||||||
|
backup_map_state() {
|
||||||
|
echo "==== Backing Up Map State ===="
|
||||||
|
|
||||||
|
echo "Fetching current map state..."
|
||||||
|
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
local response=$(parse_response "$raw")
|
||||||
|
echo "$response" > "$BACKUP_FILE"
|
||||||
|
|
||||||
|
local system_count=$(echo "$response" | jq '.data.systems | length')
|
||||||
|
local conn_count=$(echo "$response" | jq '.data.connections | length')
|
||||||
|
|
||||||
|
echo "✅ Backed up $system_count systems and $conn_count connections to $BACKUP_FILE"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Backup data:" && cat "$BACKUP_FILE" | jq '.'
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "❌ Failed to backup map state. Status: $status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to delete all systems (which will cascade to connections)
|
||||||
|
delete_all() {
|
||||||
|
echo "==== Deleting All Systems ===="
|
||||||
|
|
||||||
|
# Get current systems
|
||||||
|
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
local response=$(parse_response "$raw")
|
||||||
|
local system_ids=$(echo "$response" | jq -r '.data.systems[].solar_system_id')
|
||||||
|
|
||||||
|
if [ -z "$system_ids" ]; then
|
||||||
|
echo "No systems to delete."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert system IDs to JSON array and create payload
|
||||||
|
local system_ids_json=$(echo "$system_ids" | jq -R . | jq -s .)
|
||||||
|
local payload=$(jq -n --argjson system_ids "$system_ids_json" '{system_ids: $system_ids}')
|
||||||
|
|
||||||
|
# Send batch delete request
|
||||||
|
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "✅ Successfully deleted all systems and their connections"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "❌ Failed to delete systems. Status: $status"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ Failed to fetch systems for deletion. Status: $status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to restore map state from backup
|
||||||
|
restore_map_state() {
|
||||||
|
echo "==== Restoring Map State ===="
|
||||||
|
|
||||||
|
if [ ! -f "$BACKUP_FILE" ]; then
|
||||||
|
echo "❌ No backup file found at $BACKUP_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local backup_data=$(cat "$BACKUP_FILE")
|
||||||
|
local systems=$(echo "$backup_data" | jq '.data.systems')
|
||||||
|
local connections=$(echo "$backup_data" | jq '.data.connections')
|
||||||
|
|
||||||
|
# Create payload for batch upsert
|
||||||
|
local payload="{\"systems\": $systems, \"connections\": $connections}"
|
||||||
|
|
||||||
|
# Send batch upsert request
|
||||||
|
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
local response=$(parse_response "$raw")
|
||||||
|
local systems_created=$(echo "$response" | jq '.data.systems.created')
|
||||||
|
local systems_updated=$(echo "$response" | jq '.data.systems.updated')
|
||||||
|
local conns_created=$(echo "$response" | jq '.data.connections.created')
|
||||||
|
local conns_updated=$(echo "$response" | jq '.data.connections.updated')
|
||||||
|
|
||||||
|
echo "✅ Restore successful:"
|
||||||
|
echo " Systems: $systems_created created, $systems_updated updated"
|
||||||
|
echo " Connections: $conns_created created, $conns_updated updated"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "❌ Failed to restore map state. Status: $status"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── MAIN EXECUTION FLOW ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo "Starting backup/restore test sequence..."
|
||||||
|
|
||||||
|
# Step 1: Backup current state
|
||||||
|
backup_map_state || { echo "Backup failed, aborting."; exit 1; }
|
||||||
|
|
||||||
|
echo -e "\nBackup complete. Press Enter to proceed with deletion..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
# Step 2: Delete everything
|
||||||
|
delete_all || { echo "Deletion failed, aborting."; exit 1; }
|
||||||
|
|
||||||
|
echo -e "\nDeletion complete. Press Enter to proceed with restore..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
# Step 3: Restore from backup
|
||||||
|
restore_map_state || { echo "Restore failed."; exit 1; }
|
||||||
|
|
||||||
|
echo -e "\nTest sequence completed."
|
||||||
|
exit 0
|
||||||
18
test/manual/api/run_tests.sh
Executable file
18
test/manual/api/run_tests.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test/manual/api/run_tests.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Main entry point for all manual API tests
|
||||||
|
# Usage: ./run_tests.sh [all|create|update|delete|-v]
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Running all improved API tests..."
|
||||||
|
"$SCRIPT_DIR/improved_api_tests.sh"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass through any arguments to improved_api_tests.sh
|
||||||
|
"$SCRIPT_DIR/improved_api_tests.sh" "$@"
|
||||||
|
exit $?
|
||||||
531
test/manual/api/system_api_legacy_tests.sh
Executable file
531
test/manual/api/system_api_legacy_tests.sh
Executable file
@@ -0,0 +1,531 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ─── Legacy Map endpoint tests ───────────────────────────────────────────────────────
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/utils.sh"
|
||||||
|
|
||||||
|
# Track created IDs for cleanup - use space-delimited strings to match utils.sh
|
||||||
|
CREATED_SYSTEM_IDS=""
|
||||||
|
CREATED_CONNECTION_IDS=""
|
||||||
|
|
||||||
|
# Optional environment variables to control verbosity:
|
||||||
|
# VERBOSE_LOGGING=1 - Show full API responses
|
||||||
|
QUIET_MODE=1 # Show minimal output (just test names and results)
|
||||||
|
|
||||||
|
# DUMP RESPONSE - Call this to see the complete raw API response
|
||||||
|
dump_complete_response() {
|
||||||
|
local url="$1"
|
||||||
|
|
||||||
|
# Only show full response dumps if VERBOSE_LOGGING is set
|
||||||
|
if [ "${VERBOSE_LOGGING:-0}" -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "🔍 DUMPING COMPLETE RESPONSE FOR: $url"
|
||||||
|
echo "────────────────────────────────────────────────────────────────────────────────"
|
||||||
|
curl -s -H "Authorization: Bearer $API_TOKEN" "$url"
|
||||||
|
echo ""
|
||||||
|
echo "────────────────────────────────────────────────────────────────────────────────"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
# In non-verbose mode, just do the curl but don't show output
|
||||||
|
curl -s -H "Authorization: Bearer $API_TOKEN" "$url" > /dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initial test to show raw API response structure for system endpoint
|
||||||
|
test_dump_system_response() {
|
||||||
|
# If verbose logging is not enabled, skip this test
|
||||||
|
if [ "${VERBOSE_LOGGING:-0}" -ne 1 ]; then
|
||||||
|
#echo "Skipping raw response dump (enable with VERBOSE_LOGGING=1)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local id="30000142" # Jita
|
||||||
|
echo "Getting complete raw API response for system ID $id..."
|
||||||
|
dump_complete_response "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$id"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to add element to space-delimited string list
|
||||||
|
add_to_list() {
|
||||||
|
local list="$1"
|
||||||
|
local item="$2"
|
||||||
|
if [ -z "$list" ]; then
|
||||||
|
echo "$item"
|
||||||
|
else
|
||||||
|
echo "$list $item"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to count items in a space-delimited list
|
||||||
|
count_items() {
|
||||||
|
local list="$1"
|
||||||
|
if [ -z "$list" ]; then
|
||||||
|
echo "0"
|
||||||
|
else
|
||||||
|
echo "$list" | wc -w
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse JSON response with error handling
|
||||||
|
parse_response() {
|
||||||
|
local raw="$1"
|
||||||
|
|
||||||
|
# Skip HTTP headers and get the JSON body
|
||||||
|
local json_body=$(echo "$raw" | sed '1,/^\s*$/d')
|
||||||
|
|
||||||
|
# If JSON is valid, return it. Otherwise, return empty object
|
||||||
|
if echo "$json_body" | jq . >/dev/null 2>&1; then
|
||||||
|
echo "$json_body"
|
||||||
|
else
|
||||||
|
echo "{}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get and display detailed system information including visibility
|
||||||
|
fetch_system_details() {
|
||||||
|
local system_id=$1
|
||||||
|
local verbose=${2:-0} # Default to non-verbose mode
|
||||||
|
|
||||||
|
# Skip detailed output in quiet mode
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Fetching system details for ID $system_id..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the complete raw response
|
||||||
|
local raw
|
||||||
|
raw=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id")
|
||||||
|
|
||||||
|
# Only show raw response in verbose mode
|
||||||
|
if [ "$verbose" -eq 1 ] && [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Raw response from curl:"
|
||||||
|
echo "$raw" | jq '.' 2>/dev/null || echo "$raw"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract key information
|
||||||
|
local name=""
|
||||||
|
local visible=""
|
||||||
|
|
||||||
|
# First attempt to extract from data wrapper
|
||||||
|
if echo "$raw" | jq -e '.data' >/dev/null 2>&1; then
|
||||||
|
name=$(echo "$raw" | jq -r '.data.name // .data.solar_system_name // ""')
|
||||||
|
visible=$(echo "$raw" | jq -r '.data.visible // ""')
|
||||||
|
else
|
||||||
|
# Use grep as a last resort
|
||||||
|
if echo "$raw" | grep -q '"visible":true'; then
|
||||||
|
visible="true"
|
||||||
|
elif echo "$raw" | grep -q '"visible":false'; then
|
||||||
|
visible="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$raw" | grep -q '"name":"[^"]*"'; then
|
||||||
|
name=$(echo "$raw" | grep -o '"name":"[^"]*"' | head -1 | cut -d':' -f2 | tr -d '"')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show results only if not in quiet mode
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "SYSTEM NAME: $name"
|
||||||
|
echo "VISIBILITY: $visible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Return success if we found both name and visibility
|
||||||
|
if [ ! -z "$name" ] && [ ! -z "$visible" ]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_direct_api_access() {
|
||||||
|
local raw status
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
[[ "$status" =~ ^2[0-9]{2}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
test_missing_params() {
|
||||||
|
local raw status
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/systems")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
[[ "$status" =~ ^4[0-9]{2}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
test_invalid_auth() {
|
||||||
|
local old="$API_TOKEN" raw status
|
||||||
|
API_TOKEN="invalid-token"
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
API_TOKEN="$old"
|
||||||
|
[[ "$status" == "401" || "$status" == "403" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
test_invalid_slug() {
|
||||||
|
local raw status
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=nonexistent")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
[[ "$status" =~ ^4[0-9]{2}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create and then show systems for legacy API
|
||||||
|
test_show_systems() {
|
||||||
|
# Use two well-known systems (use actual EVE IDs for clarity)
|
||||||
|
local jita_id=30000142 # Jita
|
||||||
|
local amarr_id=30002187 # Amarr
|
||||||
|
local success_count=0
|
||||||
|
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Creating and verifying systems: Jita and Amarr"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create first system - Jita with coordinates
|
||||||
|
local payload raw status response
|
||||||
|
payload=$(jq -n \
|
||||||
|
--argjson sid "$jita_id" \
|
||||||
|
--argjson visible true \
|
||||||
|
'{solar_system_id:$sid,solar_system_name:"Jita",coordinates:{"x":100,"y":200},visible:$visible}')
|
||||||
|
|
||||||
|
# Create the system using the RESTful API
|
||||||
|
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" == "201" || "$status" == "200" ]]; then
|
||||||
|
success_count=$((success_count + 1))
|
||||||
|
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$jita_id")
|
||||||
|
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ Created Jita system (ID: $jita_id)"
|
||||||
|
echo "Verifying system $jita_id is visible after creation..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Allow a moment for system to be registered
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Verify the system is visible
|
||||||
|
fetch_system_details "$jita_id"
|
||||||
|
else
|
||||||
|
echo "Warning: Couldn't create Jita system, status: $status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create second system - Amarr with coordinates
|
||||||
|
payload=$(jq -n \
|
||||||
|
--argjson sid "$amarr_id" \
|
||||||
|
--argjson visible true \
|
||||||
|
'{solar_system_id:$sid,solar_system_name:"Amarr",coordinates:{"x":300,"y":400},visible:$visible}')
|
||||||
|
|
||||||
|
# Create the system using the RESTful API
|
||||||
|
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" == "201" || "$status" == "200" ]]; then
|
||||||
|
success_count=$((success_count + 1))
|
||||||
|
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$amarr_id")
|
||||||
|
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ Created Amarr system (ID: $amarr_id)"
|
||||||
|
echo "Verifying system $amarr_id is visible after creation..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Allow a moment for system to be registered
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Verify the system is visible
|
||||||
|
fetch_system_details "$amarr_id"
|
||||||
|
else
|
||||||
|
echo "Warning: Couldn't create Amarr system, status: $status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we couldn't create any systems, test fails
|
||||||
|
if [ $success_count -eq 0 ]; then
|
||||||
|
echo "Couldn't create any test systems for legacy API"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify systems are in the list API
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Checking if systems appear in the list API after creation..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
response_body=$(echo "$raw" | sed '1,/^\s*$/d')
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
# Parse the response appropriately depending on structure
|
||||||
|
local data_array=""
|
||||||
|
|
||||||
|
# Check if the response has data array structure
|
||||||
|
if echo "$response_body" | jq -e '.data' >/dev/null 2>&1; then
|
||||||
|
data_array=$(echo "$response_body" | jq '.data')
|
||||||
|
else
|
||||||
|
data_array="$response_body"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check each created system
|
||||||
|
local all_systems_in_list=true
|
||||||
|
for sid in $CREATED_SYSTEM_IDS; do
|
||||||
|
if echo "$data_array" | jq -e ".[] | select(.solar_system_id == $sid)" >/dev/null 2>&1; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ System $sid appears in list API after creation"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
all_systems_in_list=false
|
||||||
|
echo "⚠ WARNING: System $sid does not appear in list API after creation"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "ERROR: Failed to get systems list: status $status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Now test the legacy API endpoint for each created system
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Verifying systems are accessible via legacy API..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local legacy_success=true
|
||||||
|
|
||||||
|
for sid in $CREATED_SYSTEM_IDS; do
|
||||||
|
local raw status
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/system?id=$sid&slug=$MAP_SLUG")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ ! "$status" =~ ^2[0-9]{2}$ ]]; then
|
||||||
|
echo "Failed to retrieve system $sid via legacy API: status $status"
|
||||||
|
legacy_success=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$legacy_success" = "true" ] && [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ All systems accessible via legacy API"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
test_verify_connections() {
|
||||||
|
# Even if we don't have systems, we can still test the legacy connections API endpoint
|
||||||
|
# by checking that it returns a valid response
|
||||||
|
local raw status response
|
||||||
|
|
||||||
|
# Try to check all connections via legacy API
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/connections?slug=$MAP_SLUG")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
# If the endpoint exists and returns a success status, the test passes
|
||||||
|
if [[ "$status" =~ ^2[0-9]{2}$ ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
test_delete_systems() {
|
||||||
|
# If we don't have system IDs, skip the test
|
||||||
|
if [ $(count_items "$CREATED_SYSTEM_IDS") -eq 0 ]; then
|
||||||
|
echo "No systems to delete, skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local success_count=0
|
||||||
|
local total_systems=$(count_items "$CREATED_SYSTEM_IDS")
|
||||||
|
local deleted_ids=""
|
||||||
|
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "TEST: Delete Systems API"
|
||||||
|
echo "------------------------"
|
||||||
|
echo "Testing system deletion for existing systems in map $MAP_SLUG"
|
||||||
|
echo "Systems to delete: $CREATED_SYSTEM_IDS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try batch delete first
|
||||||
|
if [ $(count_items "$CREATED_SYSTEM_IDS") -gt 1 ]; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Attempting batch delete of systems: $CREATED_SYSTEM_IDS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local payload=$(echo "$CREATED_SYSTEM_IDS" | tr ' ' '\n' | jq -R . | jq -s '{system_ids: .}')
|
||||||
|
local raw status
|
||||||
|
|
||||||
|
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems/batch_delete" "$payload")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ Batch delete successful"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify systems are gone from the list
|
||||||
|
sleep 1
|
||||||
|
local list_response
|
||||||
|
list_response=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
|
||||||
|
# Check if all systems are gone
|
||||||
|
local all_deleted=1
|
||||||
|
for system_id in $CREATED_SYSTEM_IDS; do
|
||||||
|
if echo "$list_response" | jq -e --arg id "$system_id" '.data[] | select(.solar_system_id == ($id|tonumber) and .visible == true)' >/dev/null 2>&1; then
|
||||||
|
all_deleted=0
|
||||||
|
else
|
||||||
|
success_count=$((success_count + 1))
|
||||||
|
deleted_ids=$(add_to_list "$deleted_ids" "$system_id")
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ System $system_id no longer visible in list API after batch deletion"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $all_deleted -eq 1 ]; then
|
||||||
|
# Update the list of created systems to remove successfully deleted ones
|
||||||
|
for id in $deleted_ids; do
|
||||||
|
CREATED_SYSTEM_IDS=$(echo "$CREATED_SYSTEM_IDS" | sed "s/\b$id\b//g" | tr -s ' ' | sed 's/^ //g' | sed 's/ $//g')
|
||||||
|
done
|
||||||
|
|
||||||
|
# If batch delete worked for all systems, we're done
|
||||||
|
if [ $success_count -eq $total_systems ]; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✅ All systems successfully deleted via batch delete"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Batch delete failed with status $status, trying individual deletes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If batch delete didn't work, try individual deletes
|
||||||
|
for system_id in $CREATED_SYSTEM_IDS; do
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Attempting to delete system with ID: $system_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local raw status
|
||||||
|
|
||||||
|
# Use the RESTful DELETE endpoint
|
||||||
|
raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ Delete API call successful for system $system_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Allow time for change to propagate
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Get the complete system list after deletion
|
||||||
|
local list_response
|
||||||
|
list_response=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
|
||||||
|
# Check if the system appears in the list (deleted systems shouldn't appear or should be invisible)
|
||||||
|
local system_still_visible=0
|
||||||
|
|
||||||
|
if echo "$list_response" | jq -e --arg id "$system_id" '.data[] | select(.solar_system_id == ($id|tonumber) and .visible == true)' >/dev/null 2>&1; then
|
||||||
|
system_still_visible=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $system_still_visible -eq 0 ]; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ System $system_id no longer visible in list API after deletion"
|
||||||
|
fi
|
||||||
|
success_count=$((success_count + 1))
|
||||||
|
deleted_ids=$(add_to_list "$deleted_ids" "$system_id")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ Failed to delete system $system_id: status $status"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Update the list of created systems to remove successfully deleted ones
|
||||||
|
for id in $deleted_ids; do
|
||||||
|
CREATED_SYSTEM_IDS=$(echo "$CREATED_SYSTEM_IDS" | sed "s/\b$id\b//g" | tr -s ' ' | sed 's/^ //g' | sed 's/ $//g')
|
||||||
|
done
|
||||||
|
|
||||||
|
# Report results
|
||||||
|
if [ $success_count -eq $total_systems ]; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✅ All systems successfully deleted (no longer visible in list API): $success_count / $total_systems"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "⚠ Some systems still appear visible in list API after deletion: $success_count / $total_systems deleted"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test the system list API endpoint
|
||||||
|
test_system_list() {
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Testing system list API endpoint..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local raw status
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" != "200" ]]; then
|
||||||
|
echo "ERROR: Failed to get system list: status $status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test legacy system list endpoint too
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "Testing legacy system list API endpoint..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||||
|
status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" != "200" ]]; then
|
||||||
|
echo "ERROR: Failed to get legacy system list: status $status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that both APIs return the same number of systems
|
||||||
|
local restful_count=$(echo "$raw" | sed '1,/^\s*$/d' | jq '.data | length // length')
|
||||||
|
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||||
|
local legacy_count=$(echo "$raw" | sed '1,/^\s*$/d' | jq '.data | length // length')
|
||||||
|
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "RESTful API returned $restful_count systems, Legacy API returned $legacy_count systems"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$restful_count" == "$legacy_count" ]]; then
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo "✓ Both APIs return the same number of systems"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARNING: APIs return different numbers of systems"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Execute Tests ────────────────────────────────────────────────────────────
|
||||||
|
# Function to run a test and report success/failure
|
||||||
|
run_test() {
|
||||||
|
local name="$1"
|
||||||
|
local func="$2"
|
||||||
|
|
||||||
|
# Only print test name if not in quiet mode
|
||||||
|
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||||
|
echo -n "Testing: $name... "
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the test function
|
||||||
|
if $func; then
|
||||||
|
echo "✅ $name"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "❌ $name"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test "Dump Raw API Response" test_dump_system_response
|
||||||
|
run_test "Direct API access" test_direct_api_access
|
||||||
|
run_test "Missing params (4xx)" test_missing_params
|
||||||
|
run_test "Invalid auth (401/403)" test_invalid_auth
|
||||||
|
run_test "Invalid slug on GET" test_invalid_slug
|
||||||
|
run_test "Show systems" test_show_systems
|
||||||
|
run_test "System list" test_system_list
|
||||||
|
run_test "Verify connections" test_verify_connections
|
||||||
|
run_test "Delete systems" test_delete_systems
|
||||||
676
test/manual/api/system_api_tests.sh
Executable file
676
test/manual/api/system_api_tests.sh
Executable file
@@ -0,0 +1,676 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# test/manual/api/improved_api_tests.sh
|
||||||
|
# ─── Improved API Tests for Map System and Connection APIs ────────────────────────
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./improved_api_tests.sh # Run all tests with menu selection
|
||||||
|
# ./improved_api_tests.sh create # Run only creation tests
|
||||||
|
# ./improved_api_tests.sh update # Run only update tests
|
||||||
|
# ./improved_api_tests.sh delete # Run only deletion tests
|
||||||
|
# ./improved_api_tests.sh -v # Run in verbose mode
|
||||||
|
#
|
||||||
|
source "$(dirname "$0")/utils.sh"
|
||||||
|
|
||||||
|
# Set to "true" to see detailed output, "false" for minimal output
|
||||||
|
VERBOSE=${VERBOSE:-false}
|
||||||
|
|
||||||
|
# Parse command line options
|
||||||
|
while getopts "vh" opt; do
|
||||||
|
case $opt in
|
||||||
|
v)
|
||||||
|
VERBOSE=true
|
||||||
|
;;
|
||||||
|
h)
|
||||||
|
echo "Usage: $0 [-v] [-h] [all|create|update|delete]"
|
||||||
|
echo " -v Verbose mode (show detailed test output)"
|
||||||
|
echo " -h Show this help message"
|
||||||
|
echo " all Run all tests (default with menu)"
|
||||||
|
echo " create Run only creation tests"
|
||||||
|
echo " update Run only update tests"
|
||||||
|
echo " delete Run only deletion tests"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Invalid option: -$OPTARG" >&2
|
||||||
|
echo "Use -h for help"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
COMMAND=${1:-"all"}
|
||||||
|
|
||||||
|
# File to store system and connection IDs for persistence between command runs
|
||||||
|
SYSTEMS_FILE="/tmp/wanderer_test_systems.txt"
|
||||||
|
CONNECTIONS_FILE="/tmp/wanderer_test_connections.txt"
|
||||||
|
|
||||||
|
# Track created IDs for cleanup
|
||||||
|
CREATED_SYSTEM_IDS=""
|
||||||
|
CREATED_CONNECTION_IDS=""
|
||||||
|
|
||||||
|
# Array of valid EVE system IDs and names (first 5 for individual creation)
|
||||||
|
declare -a EVE_SYSTEMS=(
|
||||||
|
"30005304:Alentene"
|
||||||
|
"30003380:Alf"
|
||||||
|
"30003811:Algasienan"
|
||||||
|
"30004972:Algogille"
|
||||||
|
"30002698:Aliette"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Next 5 for batch upsert
|
||||||
|
declare -a BATCH_EVE_SYSTEMS=(
|
||||||
|
"30002754:Alikara"
|
||||||
|
"30002712:Alillere"
|
||||||
|
"30003521:Alkabsi"
|
||||||
|
"30000034:Alkez"
|
||||||
|
"30004995:Allamotte"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─── UTILITY FUNCTIONS ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Function to save created system IDs to file
|
||||||
|
save_systems() {
|
||||||
|
echo "$CREATED_SYSTEM_IDS" > "$SYSTEMS_FILE"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Saved $(wc -w < "$SYSTEMS_FILE") systems to $SYSTEMS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to load system IDs from file
|
||||||
|
load_systems() {
|
||||||
|
if [ -f "$SYSTEMS_FILE" ]; then
|
||||||
|
CREATED_SYSTEM_IDS=$(cat "$SYSTEMS_FILE")
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Loaded $(wc -w < "$SYSTEMS_FILE") systems from $SYSTEMS_FILE"
|
||||||
|
else
|
||||||
|
echo "No systems file found at $SYSTEMS_FILE. Run creation tests first."
|
||||||
|
CREATED_SYSTEM_IDS=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to save created connection IDs to file
|
||||||
|
save_connections() {
|
||||||
|
echo "$CREATED_CONNECTION_IDS" > "$CONNECTIONS_FILE"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Saved $(wc -w < "$CONNECTIONS_FILE") connections to $CONNECTIONS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to load connection IDs from file
|
||||||
|
load_connections() {
|
||||||
|
if [ -f "$CONNECTIONS_FILE" ]; then
|
||||||
|
CREATED_CONNECTION_IDS=$(cat "$CONNECTIONS_FILE")
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Loaded $(wc -w < "$CONNECTIONS_FILE") connections from $CONNECTIONS_FILE"
|
||||||
|
else
|
||||||
|
echo "No connections file found at $CONNECTIONS_FILE. Run creation tests first."
|
||||||
|
CREATED_CONNECTION_IDS=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to add item to space-delimited list
|
||||||
|
add_to_list() {
|
||||||
|
local list="$1"
|
||||||
|
local item="$2"
|
||||||
|
if [ -z "$list" ]; then
|
||||||
|
echo "$item"
|
||||||
|
else
|
||||||
|
echo "$list $item"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── TEST FUNCTIONS ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# FUNCTION: Create systems
|
||||||
|
create_systems() {
|
||||||
|
echo "==== Creating Systems ===="
|
||||||
|
local system_count=0
|
||||||
|
local center_x=500
|
||||||
|
local center_y=500
|
||||||
|
local radius=250
|
||||||
|
|
||||||
|
# Only clear the systems file if we're starting fresh
|
||||||
|
> "$SYSTEMS_FILE"
|
||||||
|
CREATED_SYSTEM_IDS=""
|
||||||
|
|
||||||
|
# Build all system payloads as a JSON array
|
||||||
|
local systems_payload="["
|
||||||
|
local num_systems=${#EVE_SYSTEMS[@]}
|
||||||
|
for i in $(seq 0 $((num_systems-1))); do
|
||||||
|
IFS=':' read -r system_id system_name <<< "${EVE_SYSTEMS[$i]}"
|
||||||
|
local angle=$(echo "scale=6; $i * 6.28318 / $num_systems" | bc -l)
|
||||||
|
local x=$(echo "scale=2; $center_x + $radius * c($angle)" | bc -l)
|
||||||
|
local y=$(echo "scale=2; $center_y + $radius * s($angle)" | bc -l)
|
||||||
|
local system_json=$(jq -n \
|
||||||
|
--argjson sid "$system_id" \
|
||||||
|
--arg name "$system_name" \
|
||||||
|
--argjson x "$x" \
|
||||||
|
--argjson y "$y" \
|
||||||
|
'{
|
||||||
|
solar_system_id: $sid,
|
||||||
|
solar_system_name: $name,
|
||||||
|
position_x: $x,
|
||||||
|
position_y: $y,
|
||||||
|
status: "clear",
|
||||||
|
visible: true,
|
||||||
|
description: "Test system",
|
||||||
|
tag: "TEST",
|
||||||
|
locked: false
|
||||||
|
}')
|
||||||
|
systems_payload+="$system_json"
|
||||||
|
if [ $i -lt $((num_systems-1)) ]; then
|
||||||
|
systems_payload+=","
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
systems_payload+="]"
|
||||||
|
|
||||||
|
# Wrap in the 'systems' key
|
||||||
|
local payload="{\"systems\": $systems_payload}"
|
||||||
|
|
||||||
|
# Send the batch create request
|
||||||
|
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "✅ Created all systems in batch"
|
||||||
|
# Track the system IDs for later cleanup
|
||||||
|
for i in $(seq 0 $((num_systems-1))); do
|
||||||
|
IFS=':' read -r system_id _ <<< "${EVE_SYSTEMS[$i]}"
|
||||||
|
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$system_id")
|
||||||
|
system_count=$((system_count+1))
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "❌ Failed to create systems in batch. Status: $status"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Total systems created: $system_count/$num_systems"
|
||||||
|
save_systems
|
||||||
|
|
||||||
|
# Validate actual state after creation
|
||||||
|
echo "Validating systems after dedicated creation:"
|
||||||
|
list_systems_and_connections
|
||||||
|
}
|
||||||
|
|
||||||
|
# FUNCTION: Create connections
|
||||||
|
create_connections() {
|
||||||
|
echo "==== Creating Connections ===="
|
||||||
|
load_systems
|
||||||
|
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||||
|
echo "No systems available. Run system creation first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
> "$CONNECTIONS_FILE"
|
||||||
|
CREATED_CONNECTION_IDS=""
|
||||||
|
local connection_count=0
|
||||||
|
local total_connections=0
|
||||||
|
local system_array=($CREATED_SYSTEM_IDS)
|
||||||
|
|
||||||
|
echo "Testing dedicated connection endpoints..."
|
||||||
|
# Create connections one by one using the dedicated endpoint
|
||||||
|
for i in $(seq 0 $((${#system_array[@]}-1))); do
|
||||||
|
local source=${system_array[$i]}
|
||||||
|
local target=${system_array[$(( (i+1) % ${#system_array[@]} ))]}
|
||||||
|
total_connections=$((total_connections+1))
|
||||||
|
|
||||||
|
# Create single connection payload
|
||||||
|
local payload=$(jq -n \
|
||||||
|
--argjson source "$source" \
|
||||||
|
--argjson target "$target" \
|
||||||
|
'{
|
||||||
|
solar_system_source: $source,
|
||||||
|
solar_system_target: $target,
|
||||||
|
type: 0,
|
||||||
|
mass_status: 0,
|
||||||
|
time_status: 0,
|
||||||
|
ship_size_type: 1,
|
||||||
|
wormhole_type: "K162",
|
||||||
|
count_of_passage: 0,
|
||||||
|
locked: false,
|
||||||
|
custom_info: "Test connection"
|
||||||
|
}')
|
||||||
|
|
||||||
|
# Send create request to dedicated endpoint
|
||||||
|
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/connections" "$payload")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "✅ Created connection from $source to $target"
|
||||||
|
local response=$(parse_response "$raw")
|
||||||
|
# Store source and target for later use
|
||||||
|
CREATED_CONNECTION_IDS=$(add_to_list "$CREATED_CONNECTION_IDS" "${source}:${target}")
|
||||||
|
connection_count=$((connection_count+1))
|
||||||
|
else
|
||||||
|
echo "❌ Failed to create connection from $source to $target. Status: $status"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Total connections created via dedicated endpoint: $connection_count/$total_connections"
|
||||||
|
save_connections
|
||||||
|
|
||||||
|
# Validate actual state after dedicated connection creation
|
||||||
|
echo "Validating connections after dedicated creation:"
|
||||||
|
list_systems_and_connections
|
||||||
|
|
||||||
|
echo -e "\nTesting batch upsert functionality..."
|
||||||
|
# Build batch upsert payload using BATCH_EVE_SYSTEMS
|
||||||
|
local batch_systems_json="["
|
||||||
|
local batch_connections_json="["
|
||||||
|
local num_batch_systems=${#BATCH_EVE_SYSTEMS[@]}
|
||||||
|
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||||
|
IFS=':' read -r system_id system_name <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||||
|
local angle=$(echo "scale=6; $i * 6.28318 / $num_batch_systems" | bc -l)
|
||||||
|
local x=$(echo "scale=2; 500 + 250 * c($angle)" | bc -l)
|
||||||
|
local y=$(echo "scale=2; 500 + 250 * s($angle)" | bc -l)
|
||||||
|
local system_json=$(jq -n \
|
||||||
|
--argjson sid "$system_id" \
|
||||||
|
--arg name "$system_name" \
|
||||||
|
--argjson x "$x" \
|
||||||
|
--argjson y "$y" \
|
||||||
|
'{
|
||||||
|
solar_system_id: $sid,
|
||||||
|
solar_system_name: $name,
|
||||||
|
position_x: $x,
|
||||||
|
position_y: $y,
|
||||||
|
status: "clear",
|
||||||
|
visible: true,
|
||||||
|
description: "Test system (batch)",
|
||||||
|
tag: "BATCH",
|
||||||
|
locked: false
|
||||||
|
}')
|
||||||
|
batch_systems_json+="$system_json"
|
||||||
|
if [ $i -lt $((num_batch_systems-1)) ]; then
|
||||||
|
batch_systems_json+=","
|
||||||
|
fi
|
||||||
|
# Build connections in a ring
|
||||||
|
local source=$system_id
|
||||||
|
local next_index=$(( (i+1) % num_batch_systems ))
|
||||||
|
IFS=':' read -r target_id _ <<< "${BATCH_EVE_SYSTEMS[$next_index]}"
|
||||||
|
batch_connections_json+="{\"solar_system_source\":$source,\"solar_system_target\":$target_id,\"mass_status\":0,\"ship_size_type\":1,\"type\":0}"
|
||||||
|
if [ $i -lt $((num_batch_systems-1)) ]; then
|
||||||
|
batch_connections_json+=","
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
batch_systems_json+="]"
|
||||||
|
batch_connections_json+="]"
|
||||||
|
|
||||||
|
echo "[SCRIPT] Batch upsert systems: $batch_systems_json"
|
||||||
|
echo "[SCRIPT] Batch upsert connections: $batch_connections_json"
|
||||||
|
|
||||||
|
# Check for API_TOKEN
|
||||||
|
if [ -z "$API_TOKEN" ]; then
|
||||||
|
echo "❌ API_TOKEN is not set. Please export API_TOKEN before running the script."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Send batch upsert request
|
||||||
|
local response=$(curl -s -X POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $API_TOKEN" \
|
||||||
|
-d "{\"systems\":$batch_systems_json,\"connections\":$batch_connections_json}")
|
||||||
|
|
||||||
|
echo "[SCRIPT] Batch upsert response: $response"
|
||||||
|
|
||||||
|
# Add batch system IDs to CREATED_SYSTEM_IDS
|
||||||
|
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||||
|
IFS=':' read -r system_id _ <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||||
|
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$system_id")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Add batch connection pairs to CREATED_CONNECTION_IDS
|
||||||
|
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||||
|
IFS=':' read -r source _ <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||||
|
next_index=$(( (i+1) % num_batch_systems ))
|
||||||
|
IFS=':' read -r target _ <<< "${BATCH_EVE_SYSTEMS[$next_index]}"
|
||||||
|
CREATED_CONNECTION_IDS=$(add_to_list "$CREATED_CONNECTION_IDS" "${source}:${target}")
|
||||||
|
done
|
||||||
|
save_systems
|
||||||
|
save_connections
|
||||||
|
|
||||||
|
list_systems_and_connections
|
||||||
|
|
||||||
|
echo "Total connections updated: $connection_count/${#system_array[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# FUNCTION: Update systems
|
||||||
|
update_systems() {
|
||||||
|
echo "==== Updating Systems ===="
|
||||||
|
load_systems
|
||||||
|
|
||||||
|
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||||
|
echo "No systems available. Run system creation first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local update_count=0
|
||||||
|
local system_array=($CREATED_SYSTEM_IDS)
|
||||||
|
local num_systems=${#system_array[@]}
|
||||||
|
|
||||||
|
for i in $(seq 0 $((num_systems-1))); do
|
||||||
|
local system_id=${system_array[$i]}
|
||||||
|
|
||||||
|
# Get system name from EVE_SYSTEMS array if available
|
||||||
|
local system_name="System $system_id"
|
||||||
|
for j in $(seq 0 $((${#EVE_SYSTEMS[@]}-1))); do
|
||||||
|
IFS=':' read -r curr_id curr_name <<< "${EVE_SYSTEMS[$j]}"
|
||||||
|
if [ "$curr_id" = "$system_id" ]; then
|
||||||
|
system_name=$curr_name
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Updating system $((i+1))/$num_systems: $system_name (ID: $system_id)"
|
||||||
|
|
||||||
|
# Create update payload with new values
|
||||||
|
local status_values=("clear" "friendly" "hostile" "occupied")
|
||||||
|
local status=${status_values[$((RANDOM % 4))]}
|
||||||
|
local desc="Updated description for $system_name"
|
||||||
|
local tag="UPDATED"
|
||||||
|
|
||||||
|
local payload=$(jq -n \
|
||||||
|
--arg status "$status" \
|
||||||
|
--arg desc "$desc" \
|
||||||
|
--arg tag "$tag" \
|
||||||
|
'{
|
||||||
|
status: $status,
|
||||||
|
description: $desc,
|
||||||
|
tag: $tag,
|
||||||
|
locked: false
|
||||||
|
}')
|
||||||
|
|
||||||
|
# Send the update request
|
||||||
|
local raw=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id" "$payload")
|
||||||
|
local status_code=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "✅ Updated system $system_name with status: $status"
|
||||||
|
update_count=$((update_count+1))
|
||||||
|
else
|
||||||
|
echo "❌ Failed to update system $system_name. Status: $status_code"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Total systems updated: $update_count/$num_systems"
|
||||||
|
}
|
||||||
|
|
||||||
|
# FUNCTION: Update connections
|
||||||
|
update_connections() {
|
||||||
|
echo "==== Updating Connections ===="
|
||||||
|
load_systems
|
||||||
|
load_connections
|
||||||
|
|
||||||
|
if [ -z "$CREATED_SYSTEM_IDS" ] || [ -z "$CREATED_CONNECTION_IDS" ]; then
|
||||||
|
echo "No systems or connections available. Run creation tests first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing connection updates..."
|
||||||
|
local update_count=0
|
||||||
|
local conn_array=($CREATED_CONNECTION_IDS)
|
||||||
|
|
||||||
|
for triple in "${conn_array[@]}"; do
|
||||||
|
local source=$(echo $triple | cut -d: -f1)
|
||||||
|
local target=$(echo $triple | cut -d: -f2)
|
||||||
|
|
||||||
|
# Create update payload
|
||||||
|
local mass_values=(0 1 2)
|
||||||
|
local ship_values=(0 1 2 3)
|
||||||
|
local mass=${mass_values[$((RANDOM % 3))]}
|
||||||
|
local ship=${ship_values[$((RANDOM % 4))]}
|
||||||
|
local payload=$(jq -n \
|
||||||
|
--argjson mass "$mass" \
|
||||||
|
--argjson ship "$ship" \
|
||||||
|
'{
|
||||||
|
mass_status: $mass,
|
||||||
|
ship_size_type: $ship,
|
||||||
|
locked: false,
|
||||||
|
custom_info: "Updated via PATCH"
|
||||||
|
}')
|
||||||
|
|
||||||
|
# Try source/target update
|
||||||
|
local raw=$(make_request PATCH "$API_BASE_URL/api/maps/$MAP_SLUG/connections?solar_system_source=$source&solar_system_target=$target" "$payload")
|
||||||
|
local status_code=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "✅ Updated connection $source->$target"
|
||||||
|
update_count=$((update_count+1))
|
||||||
|
else
|
||||||
|
echo "❌ Failed to update connection $source->$target. Status: $status_code"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Total connections updated: $update_count/${#conn_array[@]}"
|
||||||
|
|
||||||
|
echo -e "\nTesting batch connection updates..."
|
||||||
|
# Create batch update payload for all connections
|
||||||
|
local batch_connections="["
|
||||||
|
local first=true
|
||||||
|
for triple in "${conn_array[@]}"; do
|
||||||
|
local source=$(echo $triple | cut -d: -f1)
|
||||||
|
local target=$(echo $triple | cut -d: -f2)
|
||||||
|
|
||||||
|
local mass=${mass_values[$((RANDOM % 3))]}
|
||||||
|
local ship=${ship_values[$((RANDOM % 4))]}
|
||||||
|
|
||||||
|
if [ "$first" = true ]; then
|
||||||
|
first=false
|
||||||
|
else
|
||||||
|
batch_connections+=","
|
||||||
|
fi
|
||||||
|
|
||||||
|
batch_connections+=$(jq -n \
|
||||||
|
--argjson source "$source" \
|
||||||
|
--argjson target "$target" \
|
||||||
|
--argjson mass "$mass" \
|
||||||
|
--argjson ship "$ship" \
|
||||||
|
'{
|
||||||
|
solar_system_source: $source,
|
||||||
|
solar_system_target: $target,
|
||||||
|
mass_status: $mass,
|
||||||
|
ship_size_type: $ship,
|
||||||
|
custom_info: "Batch updated"
|
||||||
|
}')
|
||||||
|
done
|
||||||
|
batch_connections+="]"
|
||||||
|
|
||||||
|
local batch_payload="{\"connections\": $batch_connections}"
|
||||||
|
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$batch_payload")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
local response=$(parse_response "$raw")
|
||||||
|
local updated_count=$(echo "$response" | jq '.data.connections.updated')
|
||||||
|
if [ "$updated_count" != "null" ]; then
|
||||||
|
echo "✅ Batch update successful - Updated connections: $updated_count"
|
||||||
|
else
|
||||||
|
echo "❌ Batch update returned null for updated count"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ Batch update failed. Status: $status"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# FUNCTION: List systems and connections
|
||||||
|
list_systems_and_connections() {
|
||||||
|
echo "==== Listing Systems and Connections ===="
|
||||||
|
load_systems
|
||||||
|
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||||
|
echo "No systems available. Run system creation first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "Testing list all systems and connections endpoint"
|
||||||
|
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
local response=$(parse_response "$raw")
|
||||||
|
local system_count=$(echo "$response" | jq '.data.systems | length')
|
||||||
|
local conn_count=$(echo "$response" | jq '.data.connections | length')
|
||||||
|
echo "✅ Listed $system_count systems and $conn_count connections"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "$response" | jq '.'
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "❌ Failed to list systems and connections. Status: $status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# FUNCTION: Delete connections and systems
|
||||||
|
delete_everything() {
|
||||||
|
echo "==== Deleting Connections and Systems ===="
|
||||||
|
load_connections
|
||||||
|
load_systems
|
||||||
|
|
||||||
|
echo "Cleaning up connections..."
|
||||||
|
# Delete connections using source/target pairs
|
||||||
|
local conn_array=($CREATED_CONNECTION_IDS)
|
||||||
|
for triple in "${conn_array[@]}"; do
|
||||||
|
local source=$(echo $triple | cut -d: -f1)
|
||||||
|
local target=$(echo $triple | cut -d: -f2)
|
||||||
|
|
||||||
|
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/connections?solar_system_source=$source&solar_system_target=$target")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "✅ Deleted connection $source->$target"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to delete connection $source->$target. Status: $status"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Cleaning up systems..."
|
||||||
|
# Use batch delete for systems
|
||||||
|
local system_array=($CREATED_SYSTEM_IDS)
|
||||||
|
echo "Attempting batch delete of systems..."
|
||||||
|
echo "System ${system_array[@]}"
|
||||||
|
|
||||||
|
local system_ids_json=$(printf '%s\n' "${system_array[@]}" | jq -R . | jq -s .)
|
||||||
|
local payload=$(jq -n --argjson system_ids "$system_ids_json" '{system_ids: $system_ids}')
|
||||||
|
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||||
|
local status=$(parse_status "$raw")
|
||||||
|
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "✅ Batch delete successful for all systems"
|
||||||
|
> "$SYSTEMS_FILE"
|
||||||
|
> "$CONNECTIONS_FILE"
|
||||||
|
CREATED_SYSTEM_IDS=""
|
||||||
|
CREATED_CONNECTION_IDS=""
|
||||||
|
else
|
||||||
|
echo "❌ Batch delete failed. Status: $status"
|
||||||
|
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── MENU AND INTERACTION LOGIC ─────────────────────────────────────────
|
||||||
|
|
||||||
|
show_menu() {
|
||||||
|
echo "===== Map System and Connection API Tests ====="
|
||||||
|
echo "1. Run all tests in sequence (with pauses)"
|
||||||
|
echo "2. Create systems"
|
||||||
|
echo "3. Create connections"
|
||||||
|
echo "4. Update systems"
|
||||||
|
echo "5. Update connections"
|
||||||
|
echo "6. List systems and connections"
|
||||||
|
echo "7. Delete everything"
|
||||||
|
echo "8. Exit"
|
||||||
|
echo "================================================"
|
||||||
|
echo "Enter your choice [1-8]: "
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── MAIN EXECUTION FLOW ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Main execution based on command
|
||||||
|
case "$COMMAND" in
|
||||||
|
"all")
|
||||||
|
# If no specific command was provided, show the menu
|
||||||
|
if [ -t 0 ]; then # Only show menu if running interactively
|
||||||
|
# Interactive mode with menu
|
||||||
|
while true; do
|
||||||
|
show_menu
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
# Run all tests in sequence with pauses
|
||||||
|
create_systems || echo "System creation failed/skipped"
|
||||||
|
echo "Press Enter to continue with connection creation..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
create_connections || echo "Connection creation failed/skipped"
|
||||||
|
echo "Press Enter to continue with system updates..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
update_systems || echo "System update failed/skipped"
|
||||||
|
echo "Press Enter to continue with connection updates..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
update_connections || echo "Connection update failed/skipped"
|
||||||
|
echo "Press Enter to continue with listing tests..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
list_systems_and_connections || echo "Listing failed/skipped"
|
||||||
|
echo "Press Enter to continue with deletion..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
delete_everything || echo "Cleanup failed/skipped"
|
||||||
|
echo "All tests completed."
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
create_systems
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
create_connections
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
update_systems
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
update_connections
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
list_systems_and_connections
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
delete_everything
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
# Offer to clean up before exiting
|
||||||
|
read -p "Clean up any remaining test data before exiting? (y/n): " confirm
|
||||||
|
if [[ "$confirm" =~ ^[Yy] ]]; then
|
||||||
|
delete_everything
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid option. Please try again."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Non-interactive mode, run all tests in sequence
|
||||||
|
create_systems || echo "System creation failed/skipped"
|
||||||
|
create_connections || echo "Connection creation failed/skipped"
|
||||||
|
update_systems || echo "System update failed/skipped"
|
||||||
|
update_connections || echo "Connection update failed/skipped"
|
||||||
|
list_systems_and_connections || echo "Listing failed/skipped"
|
||||||
|
delete_everything || echo "Cleanup failed/skipped"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"create")
|
||||||
|
create_systems
|
||||||
|
create_connections
|
||||||
|
;;
|
||||||
|
"update")
|
||||||
|
update_systems
|
||||||
|
update_connections
|
||||||
|
list_systems_and_connections
|
||||||
|
;;
|
||||||
|
"delete")
|
||||||
|
delete_everything
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid command: $COMMAND"
|
||||||
|
echo "Use -h for help"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
167
test/manual/api/utils.sh
Executable file
167
test/manual/api/utils.sh
Executable file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# ─── Dependencies ─────────────────────────────────────────────────────────────
|
||||||
|
for cmd in curl jq; do
|
||||||
|
if ! command -v "$cmd" > /dev/null 2>&1; then
|
||||||
|
echo "Error: '$cmd' is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Load .env if present ─────────────────────────────────────────────────────
|
||||||
|
load_env_file() {
|
||||||
|
echo "📄 Loading env file: $1"
|
||||||
|
set -o allexport
|
||||||
|
source "$1"
|
||||||
|
set +o allexport
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||||
|
load_env_file "$SCRIPT_DIR/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if API_TOKEN is set
|
||||||
|
: "${API_TOKEN:?Error: API_TOKEN environment variable not set}"
|
||||||
|
|
||||||
|
# ─── HTTP Request Helper ──────────────────────────────────────────────────────
|
||||||
|
make_request() {
|
||||||
|
local method=$1 url=$2 data=${3:-}
|
||||||
|
local curl_cmd=(curl -s -w $'\n%{http_code}' -H "Authorization: Bearer $API_TOKEN")
|
||||||
|
|
||||||
|
if [ "$method" != "GET" ]; then
|
||||||
|
curl_cmd+=(-X "$method" -H "Content-Type: application/json")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$data" ]; then
|
||||||
|
curl_cmd+=(-d "$data")
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${curl_cmd[@]}" "$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Response Parsers ─────────────────────────────────────────────────────────
|
||||||
|
parse_response() { # strips the final newline+status line
|
||||||
|
local raw="$1"
|
||||||
|
echo "${raw%$'\n'*}"
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_status() { # returns only the status code (last line)
|
||||||
|
local raw="$1"
|
||||||
|
echo "${raw##*$'\n'}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Assertion Helper ─────────────────────────────────────────────────────────
|
||||||
|
verify_http_code() {
|
||||||
|
local got=$1 want=$2 label=$3
|
||||||
|
if [ "$got" -eq "$want" ]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "🚫 $label: expected HTTP $want, got $got" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Test Runner & Summary ────────────────────────────────────────────────────
|
||||||
|
# Only initialize counters once to accumulate across multiple suite sources
|
||||||
|
if [ -z "${TOTAL_TESTS+x}" ]; then
|
||||||
|
TOTAL_TESTS=0
|
||||||
|
PASSED_TESTS=0
|
||||||
|
FAILED_TESTS=0
|
||||||
|
FAILED_LIST=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local label=$1 fn=$2
|
||||||
|
TOTAL_TESTS=$((TOTAL_TESTS+1))
|
||||||
|
if "$fn"; then
|
||||||
|
echo "✅ $label"
|
||||||
|
PASSED_TESTS=$((PASSED_TESTS+1))
|
||||||
|
else
|
||||||
|
echo "❌ $label"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS+1))
|
||||||
|
FAILED_LIST="$FAILED_LIST $label"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Cleanup on Exit ──────────────────────────────────────────────────────────
|
||||||
|
CREATED_SYSTEM_IDS=""
|
||||||
|
CREATED_CONNECTION_IDS=""
|
||||||
|
|
||||||
|
cleanup_map_systems() {
|
||||||
|
# First delete connections
|
||||||
|
if [ -n "$CREATED_CONNECTION_IDS" ]; then
|
||||||
|
echo "Cleaning up connections..."
|
||||||
|
for conn_id in $CREATED_CONNECTION_IDS; do
|
||||||
|
# Try with a direct DELETE request to the connection endpoint
|
||||||
|
make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/connections/$conn_id" > /dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Then delete systems
|
||||||
|
if [ -n "$CREATED_SYSTEM_IDS" ]; then
|
||||||
|
echo "Cleaning up systems..."
|
||||||
|
|
||||||
|
# First try batch delete if we have multiple systems
|
||||||
|
if [ $(echo "$CREATED_SYSTEM_IDS" | wc -w) -gt 1 ]; then
|
||||||
|
echo "Attempting batch delete of systems..."
|
||||||
|
|
||||||
|
# Use the official batch_delete endpoint
|
||||||
|
local payload=$(echo "$CREATED_SYSTEM_IDS" | tr ' ' '\n' | jq -R . | jq -s '{system_ids: .}')
|
||||||
|
local raw
|
||||||
|
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems/batch_delete" "$payload" 2>/dev/null) || true
|
||||||
|
|
||||||
|
# Check if batch delete was successful by looking for systems
|
||||||
|
sleep 1
|
||||||
|
local success=1
|
||||||
|
|
||||||
|
for sys_id in $CREATED_SYSTEM_IDS; do
|
||||||
|
# Check if system still exists and is visible
|
||||||
|
local check=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
if echo "$check" | grep -q "\"solar_system_id\":$sys_id"; then
|
||||||
|
if echo "$check" | grep -q "\"solar_system_id\":$sys_id.*\"visible\":true"; then
|
||||||
|
success=0
|
||||||
|
else
|
||||||
|
echo "System $sys_id exists but is not visible (batch delete worked)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "System $sys_id no longer found (batch delete worked)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If batch delete was successful for all systems, we're done
|
||||||
|
if [ $success -eq 1 ]; then
|
||||||
|
echo "✅ Batch delete successful for all systems"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If batch delete failed or we have only one system, try individual deletes
|
||||||
|
echo "Performing individual system deletions..."
|
||||||
|
|
||||||
|
for sys_id in $CREATED_SYSTEM_IDS; do
|
||||||
|
echo "Deleting system $sys_id..."
|
||||||
|
|
||||||
|
# Try standard DELETE request
|
||||||
|
make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$sys_id" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Verify the system was deleted or at least made invisible
|
||||||
|
sleep 1
|
||||||
|
local check=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||||
|
|
||||||
|
if echo "$check" | grep -q "\"solar_system_id\":$sys_id"; then
|
||||||
|
if echo "$check" | grep -q "\"solar_system_id\":$sys_id.*\"visible\":true"; then
|
||||||
|
echo "⚠️ System $sys_id is still visible after all deletion attempts"
|
||||||
|
else
|
||||||
|
echo "System $sys_id exists but is not visible (deletion worked)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "System $sys_id no longer found (deletion worked)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup_map_systems EXIT
|
||||||
Reference in New Issue
Block a user