From db0301c824b4e899c2138eff2c1e952ce6c70e50 Mon Sep 17 00:00:00 2001 From: Thomas Churchman Date: Tue, 19 Mar 2024 18:10:14 +0100 Subject: [PATCH] feat(api): add lastSeen kit field and lastSeenSince filter to the /kits route --- astroplant-api/src/controllers/kit/mod.rs | 98 +++++++++++++++++---- astroplant-api/src/controllers/user/mod.rs | 20 +++-- astroplant-api/src/models/kit.rs | 42 ++++++++- astroplant-api/src/models/kit_membership.rs | 28 ++++-- astroplant-api/src/models/mod.rs | 2 +- astroplant-api/src/views.rs | 6 +- openapi.yaml | 9 ++ 7 files changed, 173 insertions(+), 32 deletions(-) diff --git a/astroplant-api/src/controllers/kit/mod.rs b/astroplant-api/src/controllers/kit/mod.rs index 522cca0..81e0d7f 100644 --- a/astroplant-api/src/controllers/kit/mod.rs +++ b/astroplant-api/src/controllers/kit/mod.rs @@ -7,9 +7,10 @@ use serde::{Deserialize, Serialize}; use crate::authorization::KitAction; use crate::database::PgPool; use crate::helpers::kit_permission_or_forbidden; -use crate::models::{KitMembership, User}; +use crate::models::{Kit, KitMembership, User}; use crate::problem::{self, Problem}; use crate::response::{Response, ResponseBuilder}; +use crate::schema::kit_last_seen; use crate::utils::deserialize_some; use crate::{helpers, models, schema, views}; @@ -17,20 +18,49 @@ mod archive; pub use archive::{archive, archive_authorize}; #[derive(Deserialize)] -pub struct CursorPage { +#[serde(rename_all = "camelCase")] +pub struct KitsQuery { + last_seen_since: Option>, after: Option, } -/// Handles the `GET /kits/?after=afterId` route. +/// Handles the `GET /kits/?lastSeenSince=dateTime&after=afterId` route. pub async fn kits( Extension(pg): Extension, - cursor: crate::extract::Query, + cursor: crate::extract::Query, ) -> Result { let conn = pg.get().await?; + let kits = conn .interact_flatten_err(move |conn| { - models::Kit::cursor_page(conn, cursor.after, 100) - .map(|kits| kits.into_iter().map(views::Kit::from).collect::>()) + use diesel::prelude::*; + + use crate::schema::kits; + + const LIMIT: i64 = 100; + + let mut q = Kit::all() + .filter(Kit::public()) + .order(kits::columns::id.asc()) + .limit(LIMIT) + .left_join(kit_last_seen::table) + .select(( + models::Kit::as_select(), + kit_last_seen::datetime_last_seen.nullable(), + )) + .into_boxed(); + + if let Some(last_seen_since) = cursor.last_seen_since { + q = q.filter(kit_last_seen::columns::datetime_last_seen.ge(last_seen_since)); + } + + if let Some(after) = cursor.after { + q = q.filter(kits::columns::id.gt(after)) + } + + let kits: QueryResult>)>> = q.load(conn); + + kits.map(|kits| kits.into_iter().map(views::Kit::from).collect::>()) }) .await?; @@ -49,13 +79,29 @@ pub async fn kit_by_serial( user_id: Option, ) -> Result { let (_, _, kit) = helpers::fut_kit_permission_or_forbidden( - pg, + pg.clone(), user_id, kit_serial, crate::authorization::KitAction::View, ) .await?; - Ok(ResponseBuilder::ok().body(views::Kit::from(kit))) + + let conn = pg.get().await?; + + let kit_id = kit.get_id(); + let kit_last_seen = conn + .interact_flatten_err(move |conn| { + use diesel::prelude::*; + + Ok::<_, Problem>( + models::KitLastSeen::belonging_to(&kit_id) + .first(conn) + .optional()? + .map(|r: models::KitLastSeen| r.datetime_last_seen), + ) + }) + .await?; + Ok(ResponseBuilder::ok().body(views::Kit::from((kit, kit_last_seen)))) } /// Handles the `POST /kits/{kitSerial}/password` route. @@ -101,7 +147,6 @@ pub async fn create_kit( crate::extract::Json(kit): crate::extract::Json, ) -> Result { use bigdecimal::{BigDecimal, FromPrimitive}; - use diesel::Connection; use validator::Validate; #[derive(Serialize, Debug)] @@ -193,8 +238,14 @@ pub async fn patch_kit( let conn = pg.get().await?; conn.interact(move |conn| { + use diesel::prelude::*; let patched_kit = update_kit.update(conn)?; - Ok(ResponseBuilder::ok().body(views::Kit::from(patched_kit))) + + let kit_last_seen = models::KitLastSeen::belonging_to(&patched_kit.get_id()) + .first(conn) + .optional()? + .map(|r: models::KitLastSeen| r.datetime_last_seen); + Ok(ResponseBuilder::ok().body(views::Kit::from((patched_kit, kit_last_seen)))) }) .await? } @@ -268,6 +319,15 @@ pub async fn get_members( let conn = pg.get().await?; let kit_id = kit.id; + let kit_last_seen = conn + .interact_flatten_err(move |conn| { + kit_last_seen::table + .select(kit_last_seen::datetime_last_seen) + .find(kit_id) + .first(conn) + .optional() + }) + .await?; let members: Vec<(User, KitMembership)> = conn .interact_flatten_err(move |conn| { use diesel::prelude::*; @@ -285,7 +345,7 @@ pub async fn get_members( .into_iter() .map(|(user, membership)| { views::KitMembership::from(membership) - .with_kit(views::Kit::from(kit.clone())) + .with_kit(views::Kit::from((kit.clone(), kit_last_seen))) .with_user(views::User::from(user)) }) .collect(); @@ -318,20 +378,26 @@ pub async fn add_member( let conn = pg.get().await?; - let kit_id = kit.id; + let kit_id = kit.get_id(); let membership = conn .interact_flatten_err(move |conn| { use diesel::prelude::*; + use models::KitLastSeen; use schema::kit_memberships; use schema::users; + let kit_last_seen: Option> = KitLastSeen::belonging_to(&kit_id) + .first(conn) + .optional()? + .map(|r: KitLastSeen| r.datetime_last_seen); + conn.build_transaction().serializable().run(move |conn| { let user: User = users::table .filter(users::username.eq(&member.username)) .first(conn)?; let existing_membership: Option = kit_memberships::table - .filter(kit_memberships::kit_id.eq(kit_id)) + .filter(kit_memberships::kit_id.eq(kit_id.0)) .filter(kit_memberships::user_id.eq(user.id)) .get_result(conn) .optional()?; @@ -341,7 +407,7 @@ pub async fn add_member( return Ok::<_, Problem>( views::KitMembership::from(existing_membership) .with_user(views::User::from(user)) - .with_kit(views::Kit::from(kit)), + .with_kit(views::Kit::from((kit, kit_last_seen))), ); } @@ -357,7 +423,7 @@ pub async fn add_member( let membership = NewKitMembership { user_id: user.id, - kit_id, + kit_id: kit_id.0, access_super: member.access_super, access_configure: member.access_configure, datetime_linked: Utc::now(), @@ -369,7 +435,7 @@ pub async fn add_member( Ok::<_, Problem>( views::KitMembership::from(membership) .with_user(views::User::from(user)) - .with_kit(views::Kit::from(kit)), + .with_kit(views::Kit::from((kit, kit_last_seen))), ) }) }) diff --git a/astroplant-api/src/controllers/user/mod.rs b/astroplant-api/src/controllers/user/mod.rs index 57f727e..1e5cd35 100644 --- a/astroplant-api/src/controllers/user/mod.rs +++ b/astroplant-api/src/controllers/user/mod.rs @@ -1,11 +1,14 @@ use axum::extract::Path; use axum::Extension; +use diesel::prelude::*; use serde::Deserialize; use validator::Validate; use crate::database::PgPool; +use crate::models::{Kit, KitMembership}; use crate::problem::{self, Problem}; use crate::response::{Response, ResponseBuilder}; +use crate::schema::{kit_last_seen, kits}; use crate::{helpers, models, views}; // Handles the `GET /users/{username}` route. @@ -108,14 +111,23 @@ pub async fn list_kit_memberships( let user_id = user.get_id(); let conn = pg.get().await?; let kit_memberships = conn - .interact(move |conn| models::KitMembership::memberships_with_kit_of_user_id(conn, user_id)) + .interact(move |conn| { + KitMembership::by_user_id(user_id) + .inner_join(kits::table.left_join(kit_last_seen::table)) + .select(( + KitMembership::as_select(), + Kit::as_select(), + kit_last_seen::datetime_last_seen.nullable(), + )) + .get_results(conn) + }) .await??; let v: Vec> = kit_memberships .into_iter() - .map(|(kit, membership)| { + .map(|(membership, kit, kit_last_seen)| { views::KitMembership::from(membership) - .with_kit(views::Kit::from(kit)) + .with_kit(views::Kit::from((kit, kit_last_seen))) .with_user(views::User::from(user.clone())) }) .collect(); @@ -134,8 +146,6 @@ pub async fn create_user( Extension(pg): Extension, crate::extract::Json(user): crate::extract::Json, ) -> Result { - use diesel::Connection; - let username = user.username.clone(); tracing::trace!("Got request to create user with username: {}", username); diff --git a/astroplant-api/src/models/kit.rs b/astroplant-api/src/models/kit.rs index 1ea78bf..1978651 100644 --- a/astroplant-api/src/models/kit.rs +++ b/astroplant-api/src/models/kit.rs @@ -1,6 +1,7 @@ -use crate::schema::kits; +use crate::schema::{kit_last_seen, kits}; use bigdecimal::BigDecimal; +use chrono::{DateTime, Utc}; use diesel::pg::PgConnection; use diesel::prelude::*; use diesel::{Identifiable, QueryResult, Queryable, Selectable}; @@ -24,10 +25,26 @@ pub struct Kit { pub privacy_show_on_map: bool, } +#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, Associations)] +#[diesel( + table_name = kit_last_seen, + primary_key(kit_id), + belongs_to(KitId, foreign_key = kit_id), + belongs_to(Kit, foreign_key = kit_id), +)] +pub struct KitLastSeen { + pub kit_id: i32, + pub datetime_last_seen: DateTime, +} + pub type All = diesel::dsl::Select>; pub type ById = diesel::dsl::Find; pub type BySerial<'a> = diesel::dsl::Filter>; +pub type ShowOnMap = diesel::dsl::Eq; +pub type PublicDashboard = diesel::dsl::Eq; +pub type Public = diesel::dsl::And; + impl Kit { pub fn all() -> All { kits::table.select(Kit::as_select()) @@ -41,6 +58,21 @@ impl Kit { Self::all().filter(kits::columns::serial.eq(serial)) } + /// Kits that are findable on the map + pub fn show_on_map() -> ShowOnMap { + kits::privacy_show_on_map.eq(true) + } + + /// Kits that are publicly viewable by their serial + pub fn public_dashboard() -> PublicDashboard { + kits::privacy_public_dashboard.eq(true) + } + + /// Kits that are findable on the map and publicly viewable by their serial + pub fn public() -> Public { + Self::show_on_map().and(Self::public_dashboard()) + } + pub fn cursor_page( conn: &mut PgConnection, after: Option, @@ -60,6 +92,14 @@ impl Kit { pub fn get_id(&self) -> KitId { KitId(self.id) } + + pub fn last_seen(&self, conn: &mut PgConnection) -> QueryResult>> { + kit_last_seen::table + .select(kit_last_seen::datetime_last_seen) + .find(self.id) + .first(conn) + .optional() + } } #[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)] diff --git a/astroplant-api/src/models/kit_membership.rs b/astroplant-api/src/models/kit_membership.rs index 654251f..525777b 100644 --- a/astroplant-api/src/models/kit_membership.rs +++ b/astroplant-api/src/models/kit_membership.rs @@ -8,7 +8,9 @@ use diesel::{Identifiable, QueryResult, Queryable}; use super::{Kit, KitId}; use super::{User, UserId}; -#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, Associations, AsChangeset)] +#[derive( + Clone, Debug, PartialEq, Eq, Queryable, Identifiable, Associations, AsChangeset, Selectable, +)] #[diesel( table_name = kit_memberships, belongs_to(User), @@ -24,9 +26,25 @@ pub struct KitMembership { pub access_configure: bool, } +pub type All = diesel::dsl::Select< + kit_memberships::table, + diesel::dsl::AsSelect, +>; + +pub type WithUserId = diesel::dsl::Eq; +pub type ByUserId = diesel::dsl::Filter; + impl KitMembership { - pub fn memberships_of_kit(conn: &mut PgConnection, kit: &Kit) -> QueryResult> { - KitMembership::belonging_to(kit).load(conn) + pub fn all() -> All { + kit_memberships::table.select(KitMembership::as_select()) + } + + pub fn with_user_id(user_id: UserId) -> WithUserId { + kit_memberships::user_id.eq(user_id.0) + } + + pub fn by_user_id(user_id: UserId) -> ByUserId { + Self::all().filter(Self::with_user_id(user_id)) } pub fn memberships_of_user_id( @@ -48,10 +66,6 @@ impl KitMembership { .get_results(conn) } - pub fn memberships_of_user(conn: &mut PgConnection, user: &User) -> QueryResult> { - KitMembership::belonging_to(user).load(conn) - } - pub fn by_user_id_and_kit_id( conn: &mut PgConnection, user_id: UserId, diff --git a/astroplant-api/src/models/mod.rs b/astroplant-api/src/models/mod.rs index be37ac7..08d3c44 100644 --- a/astroplant-api/src/models/mod.rs +++ b/astroplant-api/src/models/mod.rs @@ -1,5 +1,5 @@ mod kit; -pub use kit::{Kit, KitId, NewKit, UpdateKit}; +pub use kit::{Kit, KitLastSeen, KitId, NewKit, UpdateKit}; mod user; pub use user::{NewUser, UpdateUser, User, UserId}; diff --git a/astroplant-api/src/views.rs b/astroplant-api/src/views.rs index 1e102a2..d1ade34 100644 --- a/astroplant-api/src/views.rs +++ b/astroplant-api/src/views.rs @@ -17,10 +17,11 @@ pub struct Kit { pub longitude: Option, pub privacy_public_dashboard: bool, pub privacy_show_on_map: bool, + pub last_seen: Option>, } -impl From for Kit { - fn from(kit: models::Kit) -> Self { +impl From<(models::Kit, Option>)> for Kit { + fn from((kit, last_seen): (models::Kit, Option>)) -> Self { use bigdecimal::ToPrimitive; let models::Kit { @@ -43,6 +44,7 @@ impl From for Kit { longitude: longitude.and_then(|l| l.to_f64()), privacy_public_dashboard, privacy_show_on_map, + last_seen, } } } diff --git a/openapi.yaml b/openapi.yaml index 0d2d2c3..6b595c8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -63,6 +63,12 @@ paths: tags: - kits parameters: + - in: query + name: lastSeenSince + schema: + type: string + format: date-time + description: Fetch all kits that have been seen after the given date and time. - in: query name: after schema: @@ -1376,6 +1382,9 @@ components: type: boolean privacyShowOnMap: type: boolean + lastSeen: + type: string + format: "date-time" PatchKit: type: object properties: