From 2a100ab927c0605e9307e3d11d2dce0138fd1687 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 6 Oct 2023 15:51:49 +0200 Subject: [PATCH] graphql: allow filtering appsessions on device_id --- crates/graphql/src/model/users.rs | 11 +++++++ crates/storage-pg/src/app_session.rs | 44 ++++++++++++++++++++++++++-- crates/storage/src/app_session.rs | 16 +++++++++- frontend/schema.graphql | 4 +++ frontend/src/gql/graphql.ts | 1 + frontend/src/gql/schema.ts | 7 +++++ 6 files changed, 79 insertions(+), 4 deletions(-) diff --git a/crates/graphql/src/model/users.rs b/crates/graphql/src/model/users.rs index ad540c0f..d2b1b497 100644 --- a/crates/graphql/src/model/users.rs +++ b/crates/graphql/src/model/users.rs @@ -17,6 +17,7 @@ use async_graphql::{ Context, Description, Enum, Object, Union, ID, }; use chrono::{DateTime, Utc}; +use mas_data_model::Device; use mas_storage::{ app_session::AppSessionFilter, compat::{CompatSessionFilter, CompatSsoLoginFilter, CompatSsoLoginRepository}, @@ -535,6 +536,9 @@ impl User { #[graphql(name = "state", desc = "List only sessions in the given state.")] state_param: Option, + #[graphql(name = "device", desc = "List only sessions for the given device.")] + device_param: Option, + #[graphql(desc = "Returns the elements in the list that come after the cursor.")] after: Option, #[graphql(desc = "Returns the elements in the list that come before the cursor.")] @@ -563,6 +567,8 @@ impl User { .transpose()?; let pagination = Pagination::try_new(before_id, after_id, first, last)?; + let device_param = device_param.map(Device::try_from).transpose()?; + let filter = AppSessionFilter::new().for_user(&self.0); let filter = match state_param { @@ -571,6 +577,11 @@ impl User { None => filter, }; + let filter = match device_param.as_ref() { + Some(device) => filter.for_device(device), + None => filter, + }; + let page = repo.app_session().list(filter, pagination).await?; let count = if ctx.look_ahead().field("totalCount").exists() { diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index 422fdf2e..fcda4626 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -22,7 +22,7 @@ use mas_storage::{ }; use oauth2_types::scope::{Scope, ScopeToken}; use sea_query::{ - Alias, ColumnRef, CommonTableExpression, Expr, PostgresQueryBuilder, Query, UnionType, + Alias, ColumnRef, CommonTableExpression, Expr, PgFunc, PostgresQueryBuilder, Query, UnionType, }; use sea_query_binder::SqlxBinder; use sqlx::PgConnection; @@ -269,6 +269,12 @@ impl<'c> AppSessionRepository for PgAppSessionRepository<'c> { Expr::col((OAuth2Sessions::Table, OAuth2Sessions::FinishedAt)).is_not_null() } })) + .and_where_option(filter.device().map(|device| { + Expr::val(device.to_scope_token().to_string()).eq(PgFunc::any(Expr::col(( + OAuth2Sessions::Table, + OAuth2Sessions::ScopeList, + )))) + })) .clone(); let compat_session_select = Query::select() @@ -323,6 +329,9 @@ impl<'c> AppSessionRepository for PgAppSessionRepository<'c> { Expr::col((CompatSessions::Table, CompatSessions::FinishedAt)).is_not_null() } })) + .and_where_option(filter.device().map(|device| { + Expr::col((CompatSessions::Table, CompatSessions::DeviceId)).eq(device.to_string()) + })) .clone(); let common_table_expression = CommonTableExpression::new() @@ -376,6 +385,12 @@ impl<'c> AppSessionRepository for PgAppSessionRepository<'c> { Expr::col((OAuth2Sessions::Table, OAuth2Sessions::FinishedAt)).is_not_null() } })) + .and_where_option(filter.device().map(|device| { + Expr::val(device.to_scope_token().to_string()).eq(PgFunc::any(Expr::col(( + OAuth2Sessions::Table, + OAuth2Sessions::ScopeList, + )))) + })) .clone(); let compat_session_select = Query::select() @@ -391,6 +406,9 @@ impl<'c> AppSessionRepository for PgAppSessionRepository<'c> { Expr::col((CompatSessions::Table, CompatSessions::FinishedAt)).is_not_null() } })) + .and_where_option(filter.device().map(|device| { + Expr::col((CompatSessions::Table, CompatSessions::DeviceId)).eq(device.to_string()) + })) .clone(); let common_table_expression = CommonTableExpression::new() @@ -475,7 +493,7 @@ mod tests { let device = Device::generate(&mut rng); let compat_session = repo .compat_session() - .add(&mut rng, &clock, &user, device, false) + .add(&mut rng, &clock, &user, device.clone(), false) .await .unwrap(); @@ -552,7 +570,8 @@ mod tests { .await .unwrap(); - let scope = Scope::from_iter([OPENID]); + let device2 = Device::generate(&mut rng); + let scope = Scope::from_iter([OPENID, device2.to_scope_token()]); // We're moving the clock forward by 1 minute between each session to ensure // we're getting consistent ordering in lists. @@ -629,6 +648,25 @@ mod tests { AppSession::OAuth2(Box::new(oauth_session.clone())) ); + // Query by device + let filter = AppSessionFilter::new().for_device(&device); + assert_eq!(repo.app_session().count(filter).await.unwrap(), 1); + let list = repo.app_session().list(filter, pagination).await.unwrap(); + assert_eq!(list.edges.len(), 1); + assert_eq!( + list.edges[0], + AppSession::Compat(Box::new(compat_session.clone())) + ); + + let filter = AppSessionFilter::new().for_device(&device2); + assert_eq!(repo.app_session().count(filter).await.unwrap(), 1); + let list = repo.app_session().list(filter, pagination).await.unwrap(); + assert_eq!(list.edges.len(), 1); + assert_eq!( + list.edges[0], + AppSession::OAuth2(Box::new(oauth_session.clone())) + ); + // Create a second user let user2 = repo .user() diff --git a/crates/storage/src/app_session.rs b/crates/storage/src/app_session.rs index 8c2987ac..4411aa1e 100644 --- a/crates/storage/src/app_session.rs +++ b/crates/storage/src/app_session.rs @@ -15,7 +15,7 @@ //! Repositories to interact with all kinds of sessions use async_trait::async_trait; -use mas_data_model::{CompatSession, Session, User}; +use mas_data_model::{CompatSession, Device, Session, User}; use crate::{repository_impl, Page, Pagination}; @@ -57,6 +57,7 @@ pub enum AppSession { pub struct AppSessionFilter<'a> { user: Option<&'a User>, state: Option, + device_id: Option<&'a Device>, } impl<'a> AppSessionFilter<'a> { @@ -79,6 +80,19 @@ impl<'a> AppSessionFilter<'a> { self.user } + /// Set the device ID filter + #[must_use] + pub fn for_device(mut self, device_id: &'a Device) -> Self { + self.device_id = Some(device_id); + self + } + + /// Get the device ID filter + #[must_use] + pub fn device(&self) -> Option<&'a Device> { + self.device_id + } + /// Only return active compatibility sessions #[must_use] pub fn active_only(mut self) -> Self { diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 2dfe6103..d1149b82 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -1400,6 +1400,10 @@ type User implements Node { """ state: SessionState """ + List only sessions for the given device. + """ + device: String + """ Returns the elements in the list that come after the cursor. """ after: String diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index b2aade61..719ba341 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -919,6 +919,7 @@ export type User = Node & { export type UserAppSessionsArgs = { after?: InputMaybe; before?: InputMaybe; + device?: InputMaybe; first?: InputMaybe; last?: InputMaybe; state?: InputMaybe; diff --git a/frontend/src/gql/schema.ts b/frontend/src/gql/schema.ts index 30d90440..01fc1ed4 100644 --- a/frontend/src/gql/schema.ts +++ b/frontend/src/gql/schema.ts @@ -2545,6 +2545,13 @@ export default { name: "Any", }, }, + { + name: "device", + type: { + kind: "SCALAR", + name: "Any", + }, + }, { name: "first", type: {