Skip to content

Commit

Permalink
feat(api): add lastSeen kit field and lastSeenSince filter to the /ki…
Browse files Browse the repository at this point in the history
…ts route
  • Loading branch information
tomcur committed Mar 19, 2024
1 parent 858ba71 commit db0301c
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 32 deletions.
98 changes: 82 additions & 16 deletions astroplant-api/src/controllers/kit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,60 @@ 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};

mod archive;
pub use archive::{archive, archive_authorize};

#[derive(Deserialize)]
pub struct CursorPage {
#[serde(rename_all = "camelCase")]
pub struct KitsQuery {
last_seen_since: Option<chrono::DateTime<Utc>>,
after: Option<i32>,
}

/// Handles the `GET /kits/?after=afterId` route.
/// Handles the `GET /kits/?lastSeenSince=dateTime&after=afterId` route.
pub async fn kits(
Extension(pg): Extension<PgPool>,
cursor: crate::extract::Query<CursorPage>,
cursor: crate::extract::Query<KitsQuery>,
) -> Result<Response, Problem> {
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::<Vec<_>>())
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<Vec<(models::Kit, Option<DateTime<Utc>>)>> = q.load(conn);

kits.map(|kits| kits.into_iter().map(views::Kit::from).collect::<Vec<_>>())
})
.await?;

Expand All @@ -49,13 +79,29 @@ pub async fn kit_by_serial(
user_id: Option<crate::extract::UserId>,
) -> Result<Response, Problem> {
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.
Expand Down Expand Up @@ -101,7 +147,6 @@ pub async fn create_kit(
crate::extract::Json(kit): crate::extract::Json<CreateKit>,
) -> Result<Response, Problem> {
use bigdecimal::{BigDecimal, FromPrimitive};
use diesel::Connection;
use validator::Validate;

#[derive(Serialize, Debug)]
Expand Down Expand Up @@ -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?
}
Expand Down Expand Up @@ -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::*;
Expand All @@ -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();
Expand Down Expand Up @@ -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<DateTime<Utc>> = 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<KitMembership> = 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()?;
Expand All @@ -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))),
);
}

Expand All @@ -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(),
Expand All @@ -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))),
)
})
})
Expand Down
20 changes: 15 additions & 5 deletions astroplant-api/src/controllers/user/mod.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<views::KitMembership<views::User, views::Kit>> = 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();
Expand All @@ -134,8 +146,6 @@ pub async fn create_user(
Extension(pg): Extension<PgPool>,
crate::extract::Json(user): crate::extract::Json<User>,
) -> Result<Response, Problem> {
use diesel::Connection;

let username = user.username.clone();
tracing::trace!("Got request to create user with username: {}", username);

Expand Down
42 changes: 41 additions & 1 deletion astroplant-api/src/models/kit.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<Utc>,
}

pub type All = diesel::dsl::Select<kits::table, diesel::dsl::AsSelect<Kit, diesel::pg::Pg>>;
pub type ById = diesel::dsl::Find<All, i32>;
pub type BySerial<'a> = diesel::dsl::Filter<All, diesel::dsl::Eq<kits::serial, &'a str>>;

pub type ShowOnMap = diesel::dsl::Eq<kits::privacy_show_on_map, bool>;
pub type PublicDashboard = diesel::dsl::Eq<kits::privacy_public_dashboard, bool>;
pub type Public = diesel::dsl::And<ShowOnMap, PublicDashboard>;

impl Kit {
pub fn all() -> All {
kits::table.select(Kit::as_select())
Expand All @@ -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<i32>,
Expand All @@ -60,6 +92,14 @@ impl Kit {
pub fn get_id(&self) -> KitId {
KitId(self.id)
}

pub fn last_seen(&self, conn: &mut PgConnection) -> QueryResult<Option<DateTime<Utc>>> {
kit_last_seen::table
.select(kit_last_seen::datetime_last_seen)
.find(self.id)
.first(conn)
.optional()
}
}

#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
Expand Down
28 changes: 21 additions & 7 deletions astroplant-api/src/models/kit_membership.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -24,9 +26,25 @@ pub struct KitMembership {
pub access_configure: bool,
}

pub type All = diesel::dsl::Select<
kit_memberships::table,
diesel::dsl::AsSelect<KitMembership, diesel::pg::Pg>,
>;

pub type WithUserId = diesel::dsl::Eq<kit_memberships::user_id, i32>;
pub type ByUserId = diesel::dsl::Filter<All, WithUserId>;

impl KitMembership {
pub fn memberships_of_kit(conn: &mut PgConnection, kit: &Kit) -> QueryResult<Vec<Self>> {
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(
Expand All @@ -48,10 +66,6 @@ impl KitMembership {
.get_results(conn)
}

pub fn memberships_of_user(conn: &mut PgConnection, user: &User) -> QueryResult<Vec<Self>> {
KitMembership::belonging_to(user).load(conn)
}

pub fn by_user_id_and_kit_id(
conn: &mut PgConnection,
user_id: UserId,
Expand Down
2 changes: 1 addition & 1 deletion astroplant-api/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
Loading

0 comments on commit db0301c

Please sign in to comment.