From 7d2c15cbbd1da4b25fee761ccf6999994cc87818 Mon Sep 17 00:00:00 2001 From: Tom Konidas Date: Mon, 16 Sep 2024 19:32:05 -0400 Subject: [PATCH] Add `last_updated` filter to GET /v1/apps (#367) * Add last_updated filter to /v1/apps * Add updated_at to app JSON object * Update App timestamp every new rating --- lib/plexus/apps.ex | 14 ++++++++++++- lib/plexus/ratings.ex | 14 ++++++++----- lib/plexus/schemas/app.ex | 2 +- .../controllers/api/v1/app_controller.ex | 21 ++++++++++++++++++- lib/plexus_web/controllers/api/v1/app_json.ex | 3 ++- .../controllers/api/v1/schemas/app.ex | 1 + .../api/v1/schemas/app_response.ex | 1 + .../api/v1/schemas/apps_response.ex | 1 + test/plexus/ratings_test.exs | 4 ++-- 9 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/plexus/apps.ex b/lib/plexus/apps.ex index c55f6c52..638d3be7 100644 --- a/lib/plexus/apps.ex +++ b/lib/plexus/apps.ex @@ -50,6 +50,14 @@ defmodule Plexus.Apps do |> Repo.one!() end + @spec fetch_app(String.t()) :: {:ok, App.t()} | {:error, :not_found} + def fetch_app(package) do + case Repo.get(App, package) do + %App{} = app -> {:ok, app} + nil -> {:error, :not_found} + end + end + @spec create_app(%{ optional(:icon_url) => String.t(), package: String.t(), @@ -63,8 +71,9 @@ defmodule Plexus.Apps do end @spec update_app(App.t(), %{ + optional(:updated_at) => DateTime.t(), optional(:icon_url) => String.t(), - name: String.t() + optional(:name) => String.t() }) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} def update_app(%App{} = app, params) do app @@ -126,6 +135,9 @@ defmodule Plexus.Apps do {_, ""}, query -> query + {:updated_at_greater_than_or_equal_to, dt}, query -> + from q in query, where: q.updated_at >= ^dt + {:search_term, search_term}, query -> pattern = "%#{search_term}%" diff --git a/lib/plexus/ratings.ex b/lib/plexus/ratings.ex index 5ea73c6b..97f66a2d 100644 --- a/lib/plexus/ratings.ex +++ b/lib/plexus/ratings.ex @@ -4,6 +4,7 @@ defmodule Plexus.Ratings do """ import Ecto.Query + alias Plexus.Apps alias Plexus.PaginationHelpers alias Plexus.QueryHelpers alias Plexus.Repo @@ -55,11 +56,14 @@ defmodule Plexus.Ratings do installation_source: String.t(), rating_type: atom(), score: pos_integer() - }) :: {:ok, Rating.t()} | {:error, Ecto.Changeset.t()} - def create_rating(params) do - %Rating{} - |> Rating.changeset(params) - |> Repo.insert() + }) :: {:ok, Rating.t()} | {:error, :not_found} | {:error, Ecto.Changeset.t()} + def create_rating(%{app_package: app_package} = params) do + Repo.transact(fn -> + with {:ok, app} <- Apps.fetch_app(app_package), + {:ok, _app} <- Apps.update_app(app, %{updated_at: DateTime.utc_now()}) do + Repo.insert(Rating.changeset(%Rating{}, params)) + end + end) |> broadcast(:app_rating_updated) end diff --git a/lib/plexus/schemas/app.ex b/lib/plexus/schemas/app.ex index ee096569..bf7c4300 100644 --- a/lib/plexus/schemas/app.ex +++ b/lib/plexus/schemas/app.ex @@ -20,7 +20,7 @@ defmodule Plexus.Schemas.App do @spec changeset(App.t(), map()) :: Ecto.Changeset.t() def changeset(%App{} = app, params) do app - |> cast(params, [:package, :name, :icon_url]) + |> cast(params, [:package, :name, :icon_url, :updated_at]) |> validate_required([:package, :name]) |> unique_constraint(:package, name: :apps_pkey) end diff --git a/lib/plexus_web/controllers/api/v1/app_controller.ex b/lib/plexus_web/controllers/api/v1/app_controller.ex index 70f3b140..89a5e435 100644 --- a/lib/plexus_web/controllers/api/v1/app_controller.ex +++ b/lib/plexus_web/controllers/api/v1/app_controller.ex @@ -17,7 +17,17 @@ defmodule PlexusWeb.API.V1.AppController do page: [in: :query, description: "Page number", type: :integer, example: 1], limit: [in: :query, description: "Max results per page", type: :integer, example: 25], scores: [in: :query, description: "Include scores", type: :boolean, example: true], - q: [in: :query, description: "Search query", type: :string, example: "Signal"] + q: [in: :query, description: "Search query", type: :string, example: "Signal"], + last_updated: [ + in: :query, + description: "Apps that have updates after or on your datetime. Using RFC 3339", + type: %OpenApiSpex.Schema{type: :string, format: "date-time"}, + example: + DateTime.utc_now() + |> DateTime.add(-7, :day) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601(:extended) + ] ], responses: [ ok: {"Applications", "application/json", AppsResponse} @@ -77,6 +87,15 @@ defmodule PlexusWeb.API.V1.AppController do {"scores", "true"}, acc -> Keyword.put(acc, :scores, true) + {"last_updated", last_updated}, acc -> + case DateTime.from_iso8601(last_updated) do + {:ok, last_updated_dt, _utc_offset} -> + Keyword.put(acc, :updated_at_greater_than_or_equal_to, last_updated_dt) + + _ -> + acc + end + {"page", page}, acc -> case Integer.parse(page) do {value, _remainder} -> diff --git a/lib/plexus_web/controllers/api/v1/app_json.ex b/lib/plexus_web/controllers/api/v1/app_json.ex index 290c079c..b964387e 100644 --- a/lib/plexus_web/controllers/api/v1/app_json.ex +++ b/lib/plexus_web/controllers/api/v1/app_json.ex @@ -19,7 +19,8 @@ defmodule PlexusWeb.API.V1.AppJSON do %{ package: app.package, name: app.name, - icon_url: app.icon_url + icon_url: app.icon_url, + updated_at: DateTime.truncate(app.updated_at, :second) } |> merge_scores(app.scores) end diff --git a/lib/plexus_web/controllers/api/v1/schemas/app.ex b/lib/plexus_web/controllers/api/v1/schemas/app.ex index 8cbfb146..5e900681 100644 --- a/lib/plexus_web/controllers/api/v1/schemas/app.ex +++ b/lib/plexus_web/controllers/api/v1/schemas/app.ex @@ -16,6 +16,7 @@ defmodule PlexusWeb.API.V1.Schemas.App do example: %{ "name" => "Signal", "package" => "org.thoughtcrime.securesms", + "updated_at" => "2024-04-30T22:41:19Z", "scores" => %{ "native" => %{ "rating_type" => "native", diff --git a/lib/plexus_web/controllers/api/v1/schemas/app_response.ex b/lib/plexus_web/controllers/api/v1/schemas/app_response.ex index 07164c61..c0aefbdc 100644 --- a/lib/plexus_web/controllers/api/v1/schemas/app_response.ex +++ b/lib/plexus_web/controllers/api/v1/schemas/app_response.ex @@ -14,6 +14,7 @@ defmodule PlexusWeb.API.V1.Schemas.AppResponse do %{ "name" => "Signal", "package" => "org.thoughtcrime.securesms", + "updated_at" => "2024-04-30T22:41:19Z", "scores" => %{ "native" => %{ "rating_type" => "native", diff --git a/lib/plexus_web/controllers/api/v1/schemas/apps_response.ex b/lib/plexus_web/controllers/api/v1/schemas/apps_response.ex index 6e6777f2..b943b09b 100644 --- a/lib/plexus_web/controllers/api/v1/schemas/apps_response.ex +++ b/lib/plexus_web/controllers/api/v1/schemas/apps_response.ex @@ -17,6 +17,7 @@ defmodule PlexusWeb.API.V1.Schemas.AppsResponse do %{ "name" => "Signal", "package" => "org.thoughtcrime.securesms", + "updated_at" => "2024-04-30T22:41:19Z", "scores" => %{ "native" => %{ "rating_type" => "native", diff --git a/test/plexus/ratings_test.exs b/test/plexus/ratings_test.exs index 54555332..74046653 100644 --- a/test/plexus/ratings_test.exs +++ b/test/plexus/ratings_test.exs @@ -8,7 +8,7 @@ defmodule Plexus.RatingsTest do alias Plexus.Schemas.Rating @invalid_attrs %{ - app_package: nil, + app_package: "", app_build_number: nil, app_version: nil, rating_type: nil, @@ -65,7 +65,7 @@ defmodule Plexus.RatingsTest do end test "invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Ratings.create_rating(@invalid_attrs) + assert {:error, _reason} = Ratings.create_rating(@invalid_attrs) end end end