fix(devices): make upsert_devices/1 work with Repo.insert_all
Three related bugs prevented upsert_devices/1 from ever succeeding in
production:
- The JSON map keys ("identifier", "vendor", etc.) were passed through
to Repo.insert_all, which only accepts atom field names.
- updated_at was a %DateTime{} but the Devices schema uses :naive_datetime.
- inserted_at was never set, violating the NOT NULL constraint.
Fix by whitelisting known string keys and converting them to atoms via
String.to_existing_atom/1 (safe against atom exhaustion), using
NaiveDateTime.utc_now/1, and setting both timestamps explicitly.
This commit is contained in:
parent
5ef97adaf7
commit
1ad1bef59f
1 changed files with 18 additions and 5 deletions
|
|
@ -135,6 +135,10 @@ defmodule Aprsme.DeviceIdentification do
|
|||
end
|
||||
end
|
||||
|
||||
# The Devices schema uses :naive_datetime for its timestamps, so insert_all
|
||||
# needs a NaiveDateTime, not a DateTime.
|
||||
defp now_for_insert, do: NaiveDateTime.utc_now(:second)
|
||||
|
||||
def fetch_and_upsert_devices do
|
||||
case CircuitBreaker.call(:aprs_foundation_api, &fetch_devices_from_url/0, 15_000) do
|
||||
{:ok, result} -> result
|
||||
|
|
@ -159,7 +163,7 @@ defmodule Aprsme.DeviceIdentification do
|
|||
tocalls = Map.get(json, "tocalls", %{})
|
||||
mice = Map.get(json, "mice", %{})
|
||||
micelegacy = Map.get(json, "micelegacy", %{})
|
||||
now = DateTime.utc_now()
|
||||
now = now_for_insert()
|
||||
|
||||
all_devices =
|
||||
Enum.flat_map([tocalls, mice, micelegacy], fn group ->
|
||||
|
|
@ -177,15 +181,24 @@ defmodule Aprsme.DeviceIdentification do
|
|||
:ok
|
||||
end
|
||||
|
||||
# Whitelist of JSON keys that map directly to Devices schema fields.
|
||||
# Using an explicit list avoids String.to_atom on untrusted input.
|
||||
@device_fields ~w(identifier class model vendor os contact features)
|
||||
|
||||
defp process_device_attrs(attrs, identifier, now) do
|
||||
attrs
|
||||
|> Map.put("identifier", identifier)
|
||||
|> Map.update("features", nil, fn f ->
|
||||
if is_list(f), do: f, else: [f]
|
||||
end)
|
||||
|> Map.put("updated_at", now)
|
||||
|> Map.update("features", nil, &normalize_features/1)
|
||||
|> Map.take(@device_fields)
|
||||
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||
|> Map.put(:inserted_at, now)
|
||||
|> Map.put(:updated_at, now)
|
||||
end
|
||||
|
||||
defp normalize_features(features) when is_list(features), do: features
|
||||
defp normalize_features(nil), do: nil
|
||||
defp normalize_features(other), do: [other]
|
||||
|
||||
@doc """
|
||||
Looks up a device by identifier, using ? as a single-character wildcard.
|
||||
Returns the device struct if found, or nil.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue