Skip to content

Commit 2bb6a2f

Browse files
authored
feat: allow upserting machine detections on idempotency_key (#977)
1 parent 9b344ff commit 2bb6a2f

File tree

4 files changed

+122
-72
lines changed

4 files changed

+122
-72
lines changed

server/lib/orcasite/radio/detection.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,11 @@ defmodule Orcasite.Radio.Detection do
303303
end
304304

305305
create :submit_machine_detection do
306+
upsert? true
307+
upsert_identity :idempotency_key
308+
306309
accept [:timestamp, :feed_id, :description, :idempotency_key]
310+
upsert_fields [:timestamp, :description, :playlist_timestamp, :player_offset]
307311

308312
change set_attribute(:user_id, actor(:id))
309313
change set_attribute(:source_ip, context(:actor_ip))

server/lib/orcasite/radio/detection/changes/update_candidate.ex

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule Orcasite.Radio.Detection.Changes.UpdateCandidate do
1515
feed_id: detection.feed_id,
1616
category: detection.category
1717
})
18-
|> Ash.read!()
18+
|> Ash.read!(load: :detections, authorize?: false)
1919
|> case do
2020
[] ->
2121
Candidate
@@ -29,11 +29,26 @@ defmodule Orcasite.Radio.Detection.Changes.UpdateCandidate do
2929
|> Ash.create!()
3030

3131
[candidate] ->
32+
detections =
33+
candidate.detections
34+
|> Kernel.++([detection])
35+
|> Enum.uniq_by(& &1.id)
36+
37+
detection_count = Enum.count(detections)
38+
39+
min_time =
40+
tl(detections)
41+
|> Enum.reduce(hd(detections).timestamp, &datetime_min(&1.timestamp, &2))
42+
43+
max_time =
44+
tl(detections)
45+
|> Enum.reduce(hd(detections).timestamp, &datetime_max(&1.timestamp, &2))
46+
3247
candidate
3348
|> Ash.Changeset.for_update(:update, %{
34-
detection_count: candidate.detection_count + 1,
35-
min_time: datetime_min(candidate.min_time, detection.timestamp),
36-
max_time: datetime_max(candidate.max_time, detection.timestamp)
49+
detection_count: detection_count,
50+
min_time: min_time,
51+
max_time: max_time
3752
})
3853
|> Ash.update!(authorize?: false)
3954
end

server/test/orcasite_web/json_api/detections_test.exs

Lines changed: 97 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -18,88 +18,117 @@ defmodule OrcasiteWeb.JsonApi.DetectionsTest do
1818
data: %{
1919
attributes: %{
2020
timestamp: DateTime.to_iso8601(DateTime.utc_now()),
21+
description: "Description",
2122
feed_id: feed.id,
2223
idempotency_key: "idempotency"
2324
}
2425
}
2526
}
2627

27-
assert %{
28-
"data" => %{
29-
"attributes" => %{
30-
"category" => "whale",
31-
"description" => nil,
32-
"inserted_at" => _,
33-
"listener_count" => 0,
34-
"player_offset" => _,
35-
"playlist_timestamp" => _,
36-
"timestamp" => _,
37-
"source" => "machine"
38-
},
39-
"id" => _,
40-
"links" => %{},
41-
"meta" => %{},
42-
"relationships" => %{
43-
"candidate" => %{"links" => %{}, "meta" => %{}},
44-
"feed" => %{"links" => %{}, "meta" => %{}}
28+
capture_log(fn ->
29+
assert %{
30+
"data" => %{
31+
"attributes" => %{
32+
"category" => "whale",
33+
"description" => "Description",
34+
"inserted_at" => _,
35+
"listener_count" => 0,
36+
"player_offset" => _,
37+
"playlist_timestamp" => _,
38+
"timestamp" => _,
39+
"source" => "machine"
40+
},
41+
"id" => _,
42+
"links" => %{},
43+
"meta" => %{},
44+
"relationships" => %{
45+
"candidate" => %{"links" => %{}, "meta" => %{}},
46+
"feed" => %{"links" => %{}, "meta" => %{}}
47+
},
48+
"type" => "detection"
4549
},
46-
"type" => "detection"
47-
},
48-
"jsonapi" => %{"version" => "1.0"},
49-
"links" => %{"self" => "http://www.example.com/api/json/detections"},
50-
"meta" => %{}
51-
} =
52-
conn
53-
|> put_req_header("content-type", "application/vnd.api+json")
54-
|> put_req_header("authorization", "Bearer #{api_key}")
55-
|> post("/api/json/detections", params)
56-
|> json_response(201)
50+
"jsonapi" => %{"version" => "1.0"},
51+
"links" => %{"self" => "http://www.example.com/api/json/detections"},
52+
"meta" => %{}
53+
} =
54+
conn
55+
|> put_req_header("content-type", "application/vnd.api+json")
56+
|> put_req_header("authorization", "Bearer #{api_key}")
57+
|> post("/api/json/detections", params)
58+
|> json_response(201)
59+
end)
60+
61+
updated_params = %{
62+
data: %{
63+
attributes: %{
64+
timestamp: DateTime.to_iso8601(DateTime.utc_now() |> DateTime.add(-1, :minute)),
65+
feed_id: feed.id,
66+
description: "New description",
67+
idempotency_key: "idempotency"
68+
}
69+
}
70+
}
5771

58-
assert %{
59-
"errors" => [
60-
%{
61-
"code" => "invalid_attribute",
72+
capture_log(fn ->
73+
assert %{
74+
"data" => %{
75+
"attributes" => %{
76+
"category" => "whale",
77+
"description" => "New description",
78+
"inserted_at" => _,
79+
"listener_count" => 0,
80+
"player_offset" => _,
81+
"playlist_timestamp" => _,
82+
"timestamp" => _,
83+
"source" => "machine"
84+
},
6285
"id" => _,
86+
"links" => %{},
6387
"meta" => %{},
64-
"status" => "400",
65-
"title" => "InvalidAttribute",
66-
"source" => %{"pointer" => "/data/attributes/idempotency_key"},
67-
"detail" => "has already been taken"
68-
}
69-
],
70-
"jsonapi" => %{"version" => "1.0"}
71-
} =
72-
conn
73-
|> put_req_header("content-type", "application/vnd.api+json")
74-
|> put_req_header("authorization", "Bearer #{api_key}")
75-
|> post("/api/json/detections", params)
76-
|> json_response(400)
88+
"relationships" => %{
89+
"candidate" => %{"links" => %{}, "meta" => %{}},
90+
"feed" => %{"links" => %{}, "meta" => %{}}
91+
},
92+
"type" => "detection"
93+
},
94+
"jsonapi" => %{"version" => "1.0"},
95+
"links" => %{"self" => "http://www.example.com/api/json/detections"},
96+
"meta" => %{}
97+
} =
98+
conn
99+
|> put_req_header("content-type", "application/vnd.api+json")
100+
|> put_req_header("authorization", "Bearer #{api_key}")
101+
|> post("/api/json/detections", updated_params)
102+
|> json_response(201)
103+
end)
77104
end
78105

79106
test "fails without user", %{conn: conn, feed: feed} do
80-
assert %{
81-
"errors" => [
82-
%{
83-
"code" => "forbidden",
84-
"detail" => "forbidden",
85-
"id" => _,
86-
"status" => "403",
87-
"title" => "Forbidden"
88-
}
89-
],
90-
"jsonapi" => %{"version" => "1.0"}
91-
} =
92-
conn
93-
|> put_req_header("content-type", "application/vnd.api+json")
94-
|> post("/api/json/detections", %{
95-
data: %{
96-
attributes: %{
97-
timestamp: DateTime.to_iso8601(DateTime.utc_now()),
98-
feed_id: feed.id
107+
capture_log(fn ->
108+
assert %{
109+
"errors" => [
110+
%{
111+
"code" => "forbidden",
112+
"detail" => "forbidden",
113+
"id" => _,
114+
"status" => "403",
115+
"title" => "Forbidden"
116+
}
117+
],
118+
"jsonapi" => %{"version" => "1.0"}
119+
} =
120+
conn
121+
|> put_req_header("content-type", "application/vnd.api+json")
122+
|> post("/api/json/detections", %{
123+
data: %{
124+
attributes: %{
125+
timestamp: DateTime.to_iso8601(DateTime.utc_now()),
126+
feed_id: feed.id
127+
}
99128
}
100-
}
101-
})
102-
|> json_response(403)
129+
})
130+
|> json_response(403)
131+
end)
103132
end
104133
end
105134
end

server/test/support/conn_case.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ defmodule OrcasiteWeb.ConnCase do
2828
import Plug.Conn
2929
import Phoenix.ConnTest
3030
import OrcasiteWeb.ConnCase
31+
32+
import ExUnit.CaptureLog
3133
end
3234
end
3335

0 commit comments

Comments
 (0)