api specs
This commit is contained in:
353
phoenix/lib/shooting_event_phx/domain/event_data.ex
Normal file
353
phoenix/lib/shooting_event_phx/domain/event_data.ex
Normal file
@@ -0,0 +1,353 @@
|
||||
defmodule ShootingEventPhx.Domain.EventData do
|
||||
@moduledoc false
|
||||
import Ecto.Query
|
||||
alias ShootingEventPhx.Repo
|
||||
alias ShootingEventPhx.Domain.{Player, Score, ScoreAttachment, Stages}
|
||||
|
||||
def list_players do
|
||||
Repo.all(from p in Player, order_by: [asc: p.id])
|
||||
end
|
||||
|
||||
def ensure_score_rows(players) do
|
||||
Repo.transaction(fn ->
|
||||
for player <- players, stage <- Stages.score_stages() do
|
||||
Repo.query!(
|
||||
"""
|
||||
INSERT OR IGNORE INTO scores(stage, player_id, score)
|
||||
VALUES(?1, ?2, 0)
|
||||
""",
|
||||
[stage, player.id]
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def read_scores(players) do
|
||||
base =
|
||||
for stage <- Stages.score_stages(), into: %{} do
|
||||
{stage, Map.new(players, &{&1.id, 0})}
|
||||
end
|
||||
|
||||
rows =
|
||||
Repo.all(
|
||||
from s in Score,
|
||||
select: {s.stage, s.player_id, s.score}
|
||||
)
|
||||
|
||||
Enum.reduce(rows, base, fn {stage, player_id, score}, acc ->
|
||||
if Map.has_key?(acc, stage) do
|
||||
Map.update!(acc, stage, &Map.put(&1, player_id, score))
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def read_score_proofs(players) do
|
||||
base =
|
||||
for stage <- Stages.score_stages(), into: %{} do
|
||||
{stage, Map.new(players, &{&1.id, ""})}
|
||||
end
|
||||
|
||||
rows =
|
||||
Repo.all(
|
||||
from sa in ScoreAttachment,
|
||||
select: {sa.stage, sa.player_id, sa.image_data}
|
||||
)
|
||||
|
||||
Enum.reduce(rows, base, fn {stage, player_id, image_data}, acc ->
|
||||
if Map.has_key?(acc, stage) do
|
||||
Map.update!(acc, stage, &Map.put(&1, player_id, image_data || ""))
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def score_map_to_json(scores) do
|
||||
for {stage, stage_map} <- scores, into: %{} do
|
||||
{stage,
|
||||
for({player_id, value} <- stage_map, into: %{}, do: {Integer.to_string(player_id), value})}
|
||||
end
|
||||
end
|
||||
|
||||
def proof_map_to_json(proofs) do
|
||||
for {stage, stage_map} <- proofs, into: %{} do
|
||||
compact =
|
||||
for {player_id, image_data} <- stage_map,
|
||||
String.trim(to_string(image_data)) != "",
|
||||
into: %{} do
|
||||
{Integer.to_string(player_id), image_data}
|
||||
end
|
||||
|
||||
{stage, compact}
|
||||
end
|
||||
end
|
||||
|
||||
def create_player(attrs) do
|
||||
name_ar = attrs["nameAr"] |> to_string() |> String.trim()
|
||||
name_en = attrs["nameEn"] |> to_string() |> String.trim()
|
||||
group_code = attrs["groupCode"] |> normalize_group()
|
||||
|
||||
cond do
|
||||
name_ar == "" or name_en == "" ->
|
||||
{:error, :bad_request, "both Arabic and English names are required"}
|
||||
|
||||
true ->
|
||||
Repo.transaction(fn ->
|
||||
{:ok, player} =
|
||||
%Player{name_ar: name_ar, name_en: name_en, group_code: group_code, image_data: ""}
|
||||
|> Repo.insert()
|
||||
|
||||
for stage <- Stages.score_stages() do
|
||||
Repo.query!(
|
||||
"""
|
||||
INSERT OR IGNORE INTO scores(stage, player_id, score) VALUES(?1, ?2, 0)
|
||||
""",
|
||||
[stage, player.id]
|
||||
)
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_player(id, attrs) do
|
||||
with %Player{} = player <- Repo.get(Player, id) do
|
||||
updates = player_updates(attrs)
|
||||
|
||||
cond do
|
||||
Map.has_key?(updates, :_error) ->
|
||||
{:error, :bad_request, Map.get(updates, :_error)}
|
||||
|
||||
map_size(updates) == 0 ->
|
||||
{:error, :bad_request, "no fields to update"}
|
||||
|
||||
true ->
|
||||
case Repo.update(Ecto.Changeset.change(player, updates)) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, changeset} -> {:error, :bad_request, format_changeset_errors(changeset)}
|
||||
end
|
||||
end
|
||||
else
|
||||
nil -> {:error, :not_found, "player not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_player(id) do
|
||||
case Repo.get(Player, id) do
|
||||
nil ->
|
||||
{:error, :not_found, "player not found"}
|
||||
|
||||
player ->
|
||||
case Repo.delete(player) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auto_group_players(groups) do
|
||||
normalized_groups =
|
||||
groups
|
||||
|> List.wrap()
|
||||
|> Enum.map(&normalize_group/1)
|
||||
|> Enum.reject(&(&1 == ""))
|
||||
|> Enum.uniq()
|
||||
|
||||
if normalized_groups == [] do
|
||||
{:error, :bad_request, "at least one group is required"}
|
||||
else
|
||||
player_ids = Repo.all(from p in Player, select: p.id, order_by: [asc: p.id])
|
||||
shuffled = Enum.shuffle(player_ids)
|
||||
|
||||
case Repo.transaction(fn ->
|
||||
Enum.with_index(shuffled)
|
||||
|> Enum.each(fn {player_id, idx} ->
|
||||
group_code = Enum.at(normalized_groups, rem(idx, length(normalized_groups)))
|
||||
|
||||
Repo.query!(
|
||||
"UPDATE players SET group_code = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
|
||||
[group_code, player_id]
|
||||
)
|
||||
end)
|
||||
end) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_score(stage, player_id, score) when is_integer(score) do
|
||||
if score < 0 or score > 9999 do
|
||||
{:error, :bad_request, "score must be between 0 and 9999"}
|
||||
else
|
||||
Repo.query!(
|
||||
"""
|
||||
INSERT INTO scores(stage, player_id, score)
|
||||
VALUES(?1, ?2, ?3)
|
||||
ON CONFLICT(stage, player_id) DO UPDATE SET score = excluded.score, updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
[stage, player_id, score]
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def update_score_proof(stage, player_id, image_data) do
|
||||
payload = to_string(image_data || "") |> String.trim()
|
||||
|
||||
cond do
|
||||
payload == "" ->
|
||||
{:error, :bad_request, "imageData is required"}
|
||||
|
||||
byte_size(payload) > 5_000_000 ->
|
||||
{:error, :bad_request, "image payload too large"}
|
||||
|
||||
true ->
|
||||
Repo.query!(
|
||||
"""
|
||||
INSERT INTO score_attachments(stage, player_id, image_data)
|
||||
VALUES(?1, ?2, ?3)
|
||||
ON CONFLICT(stage, player_id) DO UPDATE SET image_data = excluded.image_data, updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
[stage, player_id, payload]
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def delete_score_proof(stage, player_id) do
|
||||
Repo.query!("DELETE FROM score_attachments WHERE stage = ?1 AND player_id = ?2", [
|
||||
stage,
|
||||
player_id
|
||||
])
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def reset_stage_scores(stages, reset_proofs?) do
|
||||
case Repo.transaction(fn ->
|
||||
Enum.each(stages, fn stage ->
|
||||
Repo.query!(
|
||||
"UPDATE scores SET score = 0, updated_at = CURRENT_TIMESTAMP WHERE stage = ?1",
|
||||
[stage]
|
||||
)
|
||||
|
||||
if reset_proofs? do
|
||||
Repo.query!("DELETE FROM score_attachments WHERE stage = ?1", [stage])
|
||||
end
|
||||
end)
|
||||
end) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||||
end
|
||||
end
|
||||
|
||||
def load_score_proof_image(stage, player_id) do
|
||||
case Repo.one(
|
||||
from sa in ScoreAttachment,
|
||||
where: sa.stage == ^stage and sa.player_id == ^player_id,
|
||||
select: sa.image_data
|
||||
) do
|
||||
nil -> {:error, :not_found}
|
||||
image -> {:ok, image}
|
||||
end
|
||||
end
|
||||
|
||||
def load_current_score(stage, player_id) do
|
||||
score =
|
||||
Repo.one(
|
||||
from s in Score, where: s.stage == ^stage and s.player_id == ^player_id, select: s.score
|
||||
)
|
||||
|
||||
{:ok, score || 0}
|
||||
end
|
||||
|
||||
defp player_updates(attrs) do
|
||||
updates = %{}
|
||||
|
||||
updates =
|
||||
case Map.get(attrs, "nameAr") do
|
||||
nil ->
|
||||
updates
|
||||
|
||||
name ->
|
||||
trimmed = to_string(name) |> String.trim()
|
||||
|
||||
if trimmed == "",
|
||||
do: Map.put(updates, :_error, "arabic name cannot be empty"),
|
||||
else: Map.put(updates, :name_ar, trimmed)
|
||||
end
|
||||
|
||||
updates =
|
||||
case Map.get(attrs, "nameEn") do
|
||||
nil ->
|
||||
updates
|
||||
|
||||
name ->
|
||||
trimmed = to_string(name) |> String.trim()
|
||||
|
||||
if trimmed == "",
|
||||
do: Map.put(updates, :_error, "english name cannot be empty"),
|
||||
else: Map.put(updates, :name_en, trimmed)
|
||||
end
|
||||
|
||||
updates =
|
||||
if Map.has_key?(attrs, "groupCode"),
|
||||
do: Map.put(updates, :group_code, normalize_group(Map.get(attrs, "groupCode"))),
|
||||
else: updates
|
||||
|
||||
updates =
|
||||
if Map.has_key?(attrs, "imageData") do
|
||||
image = attrs |> Map.get("imageData") |> to_string() |> String.trim()
|
||||
Map.put(updates, :image_data, image)
|
||||
else
|
||||
updates
|
||||
end
|
||||
|
||||
updates =
|
||||
if truthy?(Map.get(attrs, "clearImage")) do
|
||||
Map.put(updates, :image_data, "")
|
||||
else
|
||||
updates
|
||||
end
|
||||
|
||||
case Map.pop(updates, :_error) do
|
||||
{nil, sanitized} ->
|
||||
if byte_size(Map.get(sanitized, :image_data, "")) > 2_500_000 do
|
||||
%{_error: "image payload too large"}
|
||||
else
|
||||
sanitized
|
||||
end
|
||||
|
||||
{message, _} ->
|
||||
%{_error: message}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_group(group), do: group |> to_string() |> String.trim()
|
||||
|
||||
defp truthy?(value) do
|
||||
case value do
|
||||
true -> true
|
||||
1 -> true
|
||||
"1" -> true
|
||||
"true" -> true
|
||||
"yes" -> true
|
||||
"on" -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp format_changeset_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
||||
|> Enum.map(fn {field, msgs} -> "#{field} #{Enum.join(msgs, ",")}" end)
|
||||
|> Enum.join("; ")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user