From 80c32474c02413a54f91e90e1684a2f1d5a9412a Mon Sep 17 00:00:00 2001 From: Mendy Berger <12537668+MendyBerger@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:05:32 -0400 Subject: [PATCH] feat(be): Added liked calls for playlist and resource --- backend/api/sqlx-data.json | 104 +++++++++++++++++++++ backend/api/src/algolia.rs | 4 +- backend/api/src/db/jig.rs | 15 +++ backend/api/src/db/playlist.rs | 48 +++++++++- backend/api/src/db/resource.rs | 48 +++++++++- backend/api/src/http/endpoints/jig.rs | 7 +- backend/api/src/http/endpoints/playlist.rs | 31 +++++- backend/api/src/http/endpoints/resource.rs | 31 +++++- shared/rust/src/api/endpoints/playlist.rs | 15 ++- shared/rust/src/api/endpoints/resource.rs | 25 +++-- shared/rust/src/domain/jig.rs | 3 + shared/rust/src/domain/playlist.rs | 27 ++++++ shared/rust/src/domain/resource.rs | 27 ++++++ 13 files changed, 367 insertions(+), 18 deletions(-) diff --git a/backend/api/sqlx-data.json b/backend/api/sqlx-data.json index bd32e50282..fe52533c0a 100644 --- a/backend/api/sqlx-data.json +++ b/backend/api/sqlx-data.json @@ -3266,6 +3266,26 @@ }, "query": "\n update resource_curation_data\n set affiliations = $2\n where resource_id = $1 and $2 is distinct from affiliations\n " }, + "354551f41670236438723df78615a1dc227c51567c58da594d5c5b1d9d479d35": { + "describe": { + "columns": [ + { + "name": "count!: i64", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n select count(playlist_id) as \"count!: i64\"\n from playlist_like\n where user_id = $1\n " + }, "354efe9ea705cda595830f09d4640485ba06d16430389192a75f3b153f39d7f5": { "describe": { "columns": [], @@ -3637,6 +3657,26 @@ }, "query": "\nupdate course_data_module\nset\n index = case when index = $2 then $3 else index + 1 end,\n updated_at = now()\nwhere course_data_id = $1 and index between $3 and $2\n" }, + "3a007a80e7e68375ba5214665427f07766a63456a087416a1ea3d31df9ee3f4e": { + "describe": { + "columns": [ + { + "name": "count!: i64", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n select count(resource_id) as \"count!: i64\"\n from resource_like\n where user_id = $1\n " + }, "3a4bccd79e5f29fff7f31c82be3d3cb1f6f7a068503bd0b3104182f8f47ef0a9": { "describe": { "columns": [ @@ -6707,6 +6747,28 @@ }, "query": "\n insert into jig_report(jig_id, report_type)\n values ($1, $2)\n returning id as \"id!: ReportId\"\n " }, + "686590f948fdbdb9c46f9bfb96684d7473fff17d578d42227c4ec372b51a017e": { + "describe": { + "columns": [ + { + "name": "resource_id", + "ordinal": 0, + "type_info": "Uuid" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Int8" + ] + } + }, + "query": "\n select resource_id\n from resource_like\n where user_id = $1\n order by created_at desc\n offset $2\n limit $3\n \n " + }, "688ca554263ad9bee35f1fe5d4c99682f7d47c847249d304422858b265bce880": { "describe": { "columns": [ @@ -8166,6 +8228,28 @@ }, "query": "delete from user_account where account_id = $1" }, + "783a8e17a4d48a5920ebc70ecd59004eeba69f873654561edb6c9721d1a70dac": { + "describe": { + "columns": [ + { + "name": "playlist_id", + "ordinal": 0, + "type_info": "Uuid" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Int8" + ] + } + }, + "query": "\n select playlist_id\n from playlist_like\n where user_id = $1\n order by created_at desc\n offset $2\n limit $3\n \n " + }, "78b48e52d416cf94b8daaf7ce4b6dcbb7bae4ba6c5f85bc4121c626f37c4df4e": { "describe": { "columns": [], @@ -14448,6 +14532,26 @@ }, "query": "\nselect published_at as \"published_at?\"\nfrom course\nwhere id = $1\n " }, + "d07feb752f409708df595d2507f3fbcd52d8e96649fa0dc0174f57300d2fac08": { + "describe": { + "columns": [ + { + "name": "count!: i64", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n select count(jig_id) as \"count!: i64\"\n from jig_like\n where user_id = $1\n " + }, "d1095b2f79187d6c7b251d3f39a7470e5e97b69c4424b638aec44fcb0f1d3deb": { "describe": { "columns": [ diff --git a/backend/api/src/algolia.rs b/backend/api/src/algolia.rs index 4e152b6697..8c5d6d0224 100644 --- a/backend/api/src/algolia.rs +++ b/backend/api/src/algolia.rs @@ -2191,7 +2191,7 @@ impl Client { page_limit: u32, blocked: Option, is_rated: Option, - ) -> anyhow::Result, u32, u64)>> { + ) -> anyhow::Result, u32, u64)>> { let mut and_filters = algolia::filter::AndFilter { filters: vec![] }; if let Some(author_id) = author_id { @@ -2327,7 +2327,7 @@ impl Client { translated_keywords: Option, privacy_level: &[PrivacyLevel], page_limit: u32, - ) -> anyhow::Result, u32, u64)>> { + ) -> anyhow::Result, u32, u64)>> { let mut and_filters = algolia::filter::AndFilter { filters: vec![] }; if let Some(author_id) = author_id { diff --git a/backend/api/src/db/jig.rs b/backend/api/src/db/jig.rs index ea277912e1..e5b6394bc4 100644 --- a/backend/api/src/db/jig.rs +++ b/backend/api/src/db/jig.rs @@ -1724,6 +1724,21 @@ pub async fn list_liked( Ok(rows.into_iter().map(|row| JigId(row.jig_id)).collect()) } +pub async fn liked_count(db: &PgPool, user_id: UserId) -> sqlx::Result { + let res = sqlx::query!( + r#" + select count(jig_id) as "count!: i64" + from jig_like + where user_id = $1 + "#, + user_id.0, + ) + .fetch_one(db) + .await?; + + Ok(res.count as u64) +} + pub async fn list_played( db: &PgPool, user_id: UserId, diff --git a/backend/api/src/db/playlist.rs b/backend/api/src/db/playlist.rs index b7c86257c4..b547f8c8a5 100644 --- a/backend/api/src/db/playlist.rs +++ b/backend/api/src/db/playlist.rs @@ -276,7 +276,7 @@ from playlist_data pub async fn get_by_ids( db: &PgPool, - ids: &[Uuid], + ids: &[PlaylistId], draft_or_live: DraftOrLive, user_id: Option, ) -> sqlx::Result> { @@ -308,7 +308,7 @@ inner join unnest($1::uuid[]) inner join playlist_admin_data "admin" on admin.playlist_id = playlist.id order by ord asc "#, - ids, + &ids.iter().map(|i| i.0).collect::>(), user_id.map(|x| x.0) ) .fetch_all(&mut txn) @@ -1170,6 +1170,50 @@ select exists ( Ok(exists) } +pub async fn list_liked( + db: &PgPool, + user_id: UserId, + page: u32, + page_limit: u32, +) -> sqlx::Result> { + let rows = sqlx::query!( + r#" + select playlist_id + from playlist_like + where user_id = $1 + order by created_at desc + offset $2 + limit $3 + + "#, + user_id.0, + (page * page_limit) as i32, + page_limit as i32, + ) + .fetch_all(db) + .await?; + + Ok(rows + .into_iter() + .map(|row| PlaylistId(row.playlist_id)) + .collect()) +} + +pub async fn liked_count(db: &PgPool, user_id: UserId) -> sqlx::Result { + let res = sqlx::query!( + r#" + select count(playlist_id) as "count!: i64" + from playlist_like + where user_id = $1 + "#, + user_id.0, + ) + .fetch_one(db) + .await?; + + Ok(res.count as u64) +} + pub async fn playlist_like(db: &PgPool, user_id: UserId, id: PlaylistId) -> anyhow::Result<()> { let mut txn = db.begin().await?; diff --git a/backend/api/src/db/resource.rs b/backend/api/src/db/resource.rs index 172eea5234..e7438643cc 100644 --- a/backend/api/src/db/resource.rs +++ b/backend/api/src/db/resource.rs @@ -278,7 +278,7 @@ from resource_data #[instrument(skip(db))] pub async fn get_by_ids( db: &PgPool, - ids: &[Uuid], + ids: &[ResourceId], draft_or_live: DraftOrLive, user_id: Option, ) -> sqlx::Result> { @@ -310,7 +310,7 @@ inner join unnest($1::uuid[]) inner join resource_admin_data "admin" on admin.resource_id = resource.id order by ord asc "#, - ids, + &ids.iter().map(|i| i.0).collect::>(), user_id.map(|x| x.0) ) .fetch_all(&mut txn) @@ -1240,6 +1240,50 @@ select exists ( Ok(exists) } +pub async fn list_liked( + db: &PgPool, + user_id: UserId, + page: u32, + page_limit: u32, +) -> sqlx::Result> { + let rows = sqlx::query!( + r#" + select resource_id + from resource_like + where user_id = $1 + order by created_at desc + offset $2 + limit $3 + + "#, + user_id.0, + (page * page_limit) as i32, + page_limit as i32, + ) + .fetch_all(db) + .await?; + + Ok(rows + .into_iter() + .map(|row| ResourceId(row.resource_id)) + .collect()) +} + +pub async fn liked_count(db: &PgPool, user_id: UserId) -> sqlx::Result { + let res = sqlx::query!( + r#" + select count(resource_id) as "count!: i64" + from resource_like + where user_id = $1 + "#, + user_id.0, + ) + .fetch_one(db) + .await?; + + Ok(res.count as u64) +} + pub async fn authz( db: &PgPool, user_id: UserId, diff --git a/backend/api/src/http/endpoints/jig.rs b/backend/api/src/http/endpoints/jig.rs index 70525e9871..17d93e42c7 100644 --- a/backend/api/src/http/endpoints/jig.rs +++ b/backend/api/src/http/endpoints/jig.rs @@ -481,7 +481,12 @@ async fn list_liked( .await .into_anyhow()?; - Ok(Json(ListLikedResponse { jigs })) + let total_jig_count = db::jig::liked_count(&*db, user_id).await?; + + Ok(Json(ListLikedResponse { + jigs, + total_jig_count, + })) } /// Get users played jigs diff --git a/backend/api/src/http/endpoints/playlist.rs b/backend/api/src/http/endpoints/playlist.rs index e76a53d01e..7f21685a15 100644 --- a/backend/api/src/http/endpoints/playlist.rs +++ b/backend/api/src/http/endpoints/playlist.rs @@ -4,7 +4,7 @@ use actix_web::{ }; use futures::try_join; use ji_core::settings::RuntimeSettings; -use shared::domain::user::UserScope; +use shared::domain::{playlist::ListLikedResponse, user::UserScope}; use shared::{ api::{endpoints::playlist, ApiEndpoint, PathParts}, domain::{ @@ -441,6 +441,31 @@ async fn unlike( Ok(HttpResponse::NoContent().finish()) } +/// Get users liked playlist +async fn list_liked( + db: Data, + claims: TokenUser, + query: Option::Req>>, +) -> Result::Res>, error::Server> { + let user_id = claims.user_id(); + let query = query.map_or_else(Default::default, Query::into_inner); + + let page_limit = page_limit(query.page_limit).await?; + + let ids = db::playlist::list_liked(&*db, user_id, query.page.unwrap_or(0), page_limit).await?; + + let playlists = db::playlist::get_by_ids(&db, &ids, DraftOrLive::Live, Some(user_id)) + .await + .into_anyhow()?; + + let total_playlist_count = db::playlist::liked_count(&*db, user_id).await?; + + Ok(Json(ListLikedResponse { + playlists, + total_playlist_count, + })) +} + /// Add a play to a playlist async fn view( db: Data, @@ -522,6 +547,10 @@ pub fn configure(cfg: &mut ServiceConfig) { ::Path::PATH, playlist::Unlike::METHOD.route().to(unlike), ) + .route( + ::Path::PATH, + playlist::ListLiked::METHOD.route().to(list_liked), + ) .route( ::Path::PATH, playlist::PlaylistAdminDataUpdate::METHOD diff --git a/backend/api/src/http/endpoints/resource.rs b/backend/api/src/http/endpoints/resource.rs index a81da08970..21aa78e5bd 100644 --- a/backend/api/src/http/endpoints/resource.rs +++ b/backend/api/src/http/endpoints/resource.rs @@ -4,7 +4,7 @@ use actix_web::{ }; use futures::try_join; use ji_core::settings::RuntimeSettings; -use shared::domain::user::UserScope; +use shared::domain::{resource::ListLikedResponse, user::UserScope}; use shared::error::{ServiceError, ServiceKindError}; use shared::{ api::{endpoints::resource, ApiEndpoint, PathParts}, @@ -430,6 +430,31 @@ async fn unlike( Ok(HttpResponse::NoContent().finish()) } +/// Get users liked resources +async fn list_liked( + db: Data, + claims: TokenUser, + query: Option::Req>>, +) -> Result::Res>, error::Server> { + let user_id = claims.user_id(); + let query = query.map_or_else(Default::default, Query::into_inner); + + let page_limit = page_limit(query.page_limit).await?; + + let ids = db::resource::list_liked(&*db, user_id, query.page.unwrap_or(0), page_limit).await?; + + let resources = db::resource::get_by_ids(&db, &ids, DraftOrLive::Live, Some(user_id)) + .await + .into_anyhow()?; + + let total_resource_count = db::resource::liked_count(&*db, user_id).await?; + + Ok(Json(ListLikedResponse { + resources, + total_resource_count, + })) +} + /// Add a play to a resource async fn view( db: Data, @@ -586,5 +611,9 @@ pub fn configure(cfg: &mut ServiceConfig) { .route( ::Path::PATH, resource::Unlike::METHOD.route().to(unlike), + ) + .route( + ::Path::PATH, + resource::ListLiked::METHOD.route().to(list_liked), ); } diff --git a/shared/rust/src/api/endpoints/playlist.rs b/shared/rust/src/api/endpoints/playlist.rs index f77558b1cf..986b8458d7 100644 --- a/shared/rust/src/api/endpoints/playlist.rs +++ b/shared/rust/src/api/endpoints/playlist.rs @@ -1,14 +1,15 @@ -use crate::domain::playlist::{PlaylistAdminDataUpdatePath, PlaylistUpdateAdminDataRequest}; use crate::{ api::Method, domain::{ playlist::{ + ListLikedPath, ListLikedRequest, ListLikedResponse, PlaylistAdminDataUpdatePath, PlaylistBrowsePath, PlaylistBrowseQuery, PlaylistBrowseResponse, PlaylistClonePath, PlaylistCreatePath, PlaylistCreateRequest, PlaylistDeletePath, PlaylistGetDraftPath, PlaylistGetLivePath, PlaylistId, PlaylistLikePath, PlaylistLikedPath, PlaylistLikedResponse, PlaylistPublishPath, PlaylistResponse, PlaylistSearchPath, PlaylistSearchQuery, PlaylistSearchResponse, PlaylistUnlikePath, - PlaylistUpdateDraftDataPath, PlaylistUpdateDraftDataRequest, PlaylistViewPath, + PlaylistUpdateAdminDataRequest, PlaylistUpdateDraftDataPath, + PlaylistUpdateDraftDataRequest, PlaylistViewPath, }, CreateResponse, }, @@ -195,6 +196,16 @@ impl ApiEndpoint for Unlike { const METHOD: Method = Method::Delete; } +/// List user's liked Playlists. +pub struct ListLiked; +impl ApiEndpoint for ListLiked { + type Req = ListLikedRequest; + type Res = ListLikedResponse; + type Path = ListLikedPath; + type Err = EmptyError; + const METHOD: Method = Method::Get; +} + /// Is a Playlist liked by a user /// /// # Authorization diff --git a/shared/rust/src/api/endpoints/resource.rs b/shared/rust/src/api/endpoints/resource.rs index 806fe2f0bb..4a32de7d75 100644 --- a/shared/rust/src/api/endpoints/resource.rs +++ b/shared/rust/src/api/endpoints/resource.rs @@ -2,13 +2,14 @@ use crate::{ api::Method, domain::{ resource::{ - ResourceAdminDataUpdatePath, ResourceBrowsePath, ResourceBrowseQuery, - ResourceBrowseResponse, ResourceClonePath, ResourceCountPath, ResourceCountResponse, - ResourceCoverPath, ResourceCreatePath, ResourceCreateRequest, ResourceDeleteAllPath, - ResourceDeletePath, ResourceGetDraftPath, ResourceGetLivePath, ResourceId, - ResourceLikePath, ResourceLikedPath, ResourceLikedResponse, ResourcePublishPath, - ResourceResponse, ResourceSearchPath, ResourceSearchQuery, ResourceSearchResponse, - ResourceUnlikePath, ResourceUpdateAdminDataRequest, ResourceUpdateDraftDataPath, + ListLikedPath, ListLikedRequest, ListLikedResponse, ResourceAdminDataUpdatePath, + ResourceBrowsePath, ResourceBrowseQuery, ResourceBrowseResponse, ResourceClonePath, + ResourceCountPath, ResourceCountResponse, ResourceCoverPath, ResourceCreatePath, + ResourceCreateRequest, ResourceDeleteAllPath, ResourceDeletePath, ResourceGetDraftPath, + ResourceGetLivePath, ResourceId, ResourceLikePath, ResourceLikedPath, + ResourceLikedResponse, ResourcePublishPath, ResourceResponse, ResourceSearchPath, + ResourceSearchQuery, ResourceSearchResponse, ResourceUnlikePath, + ResourceUpdateAdminDataRequest, ResourceUpdateDraftDataPath, ResourceUpdateDraftDataRequest, ResourceViewPath, }, CreateResponse, @@ -249,6 +250,16 @@ impl ApiEndpoint for Liked { const METHOD: Method = Method::Get; } +/// List user's liked resources. +pub struct ListLiked; +impl ApiEndpoint for ListLiked { + type Req = ListLikedRequest; + type Res = ListLikedResponse; + type Path = ListLikedPath; + type Err = EmptyError; + const METHOD: Method = Method::Get; +} + /// View a Resource /// /// # Authorization diff --git a/shared/rust/src/domain/jig.rs b/shared/rust/src/domain/jig.rs index edf11db475..ad5f73c201 100644 --- a/shared/rust/src/domain/jig.rs +++ b/shared/rust/src/domain/jig.rs @@ -827,6 +827,9 @@ pub struct JigFeaturedUpdateRequest { pub struct ListLikedResponse { /// the jigs returned. pub jigs: Vec, + + /// The total number of jigs liked + pub total_jig_count: u64, } /// Response for successfully finding the draft of a jig. diff --git a/shared/rust/src/domain/playlist.rs b/shared/rust/src/domain/playlist.rs index 77bb0df37c..bf22ac254e 100644 --- a/shared/rust/src/domain/playlist.rs +++ b/shared/rust/src/domain/playlist.rs @@ -469,6 +469,33 @@ make_path_parts!(PlaylistUnlikePath => "/v1/playlist/{}/unlike" => PlaylistId); make_path_parts!(PlaylistLikedPath => "/v1/playlist/{}/like" => PlaylistId); +make_path_parts!(ListLikedPath => "/v1/playlist/likes"); + +/// Response for request for list of liked playlists. +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct ListLikedRequest { + /// The page number of the playlists to get. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + + /// The hits per page to be returned + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub page_limit: Option, +} +/// Response for request for list of liked playlists. +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListLikedResponse { + /// the playlists returned. + pub playlists: Vec, + + /// The total number of playlists liked + pub total_playlist_count: u64, +} + make_path_parts!(PlaylistViewPath => "/v1/playlist/{}/view" => PlaylistId); make_path_parts!(PlaylistAdminDataUpdatePath => "/v1/playlist/{}/admin" => PlaylistId); diff --git a/shared/rust/src/domain/resource.rs b/shared/rust/src/domain/resource.rs index 479f02c5c5..67aef763c3 100644 --- a/shared/rust/src/domain/resource.rs +++ b/shared/rust/src/domain/resource.rs @@ -517,6 +517,33 @@ make_path_parts!(ResourceUnlikePath => "/v1/resource/{}/unlike" => ResourceId); make_path_parts!(ResourceLikedPath => "/v1/resource/{}/like" => ResourceId); +make_path_parts!(ListLikedPath => "/v1/resource/likes"); + +/// Response for request for list of liked resources. +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct ListLikedRequest { + /// The page number of the resources to get. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + + /// The hits per page to be returned + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub page_limit: Option, +} +/// Response for request for list of liked resources. +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListLikedResponse { + /// the resources returned. + pub resources: Vec, + + /// The total number of resources liked + pub total_resource_count: u64, +} + make_path_parts!(ResourceViewPath => "/v1/resource/{}/view" => ResourceId); make_path_parts!(ResourceAdminDataUpdatePath => "/v1/resource/{}/admin" => ResourceId);