From 7e82ae845ccee22c4bcd7724b5477b95ae9f6af9 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 19 Jul 2023 13:34:39 +0200 Subject: [PATCH] WIP: use sea-query for dynamic paginated queries --- Cargo.lock | 38 +++++ crates/graphql/src/model/browser_sessions.rs | 12 +- crates/graphql/src/model/users.rs | 19 ++- crates/storage-pg/Cargo.toml | 1 + crates/storage-pg/src/lib.rs | 1 + crates/storage-pg/src/pagination.rs | 43 ++++- crates/storage-pg/src/sea_query_sqlx.rs | 49 ++++++ crates/storage-pg/src/user/session.rs | 147 +++++++++++------- crates/storage/src/user/mod.rs | 4 +- crates/storage/src/user/session.rs | 89 ++++++++--- frontend/schema.graphql | 15 ++ .../src/components/BrowserSessionList.tsx | 1 + frontend/src/gql/gql.ts | 6 +- frontend/src/gql/graphql.ts | 14 ++ frontend/src/gql/schema.ts | 7 + 15 files changed, 360 insertions(+), 86 deletions(-) create mode 100644 crates/storage-pg/src/sea_query_sqlx.rs diff --git a/Cargo.lock b/Cargo.lock index 2d932488..0ba88b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3559,6 +3559,7 @@ dependencies = [ "oauth2-types", "rand 0.8.5", "rand_chacha 0.3.1", + "sea-query", "serde", "serde_json", "sqlx", @@ -5211,6 +5212,43 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sea-query" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbab99b8cd878ab7786157b7eb8df96333a6807cc6e45e8888c85b51534b401a" +dependencies = [ + "chrono", + "sea-query-attr", + "sea-query-derive", + "uuid 1.4.1", +] + +[[package]] +name = "sea-query-attr" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "878cf3d57f0e5bfacd425cdaccc58b4c06d68a7b71c63fc28710a20c88676808" +dependencies = [ + "darling 0.14.4", + "heck", + "quote 1.0.31", + "syn 1.0.109", +] + +[[package]] +name = "sea-query-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f62030c60f3a691f5fe251713b4e220b306e50a71e1d6f9cce1f24bb781978" +dependencies = [ + "heck", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 1.0.109", + "thiserror", +] + [[package]] name = "sec1" version = "0.7.3" diff --git a/crates/graphql/src/model/browser_sessions.rs b/crates/graphql/src/model/browser_sessions.rs index 2602433a..140a2f51 100644 --- a/crates/graphql/src/model/browser_sessions.rs +++ b/crates/graphql/src/model/browser_sessions.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_graphql::{Description, Object, ID}; +use async_graphql::{Description, Enum, Object, ID}; use chrono::{DateTime, Utc}; use super::{NodeType, User}; @@ -21,6 +21,16 @@ use super::{NodeType, User}; #[derive(Description)] pub struct BrowserSession(pub mas_data_model::BrowserSession); +/// The state of a browser session. +#[derive(Enum, Copy, Clone, Eq, PartialEq)] +pub enum BrowserSessionState { + /// The session is active. + Active, + + /// The session is no longer active. + Finished, +} + impl From for BrowserSession { fn from(v: mas_data_model::BrowserSession) -> Self { Self(v) diff --git a/crates/graphql/src/model/users.rs b/crates/graphql/src/model/users.rs index b816204b..986c4335 100644 --- a/crates/graphql/src/model/users.rs +++ b/crates/graphql/src/model/users.rs @@ -21,7 +21,7 @@ use mas_storage::{ compat::CompatSsoLoginRepository, oauth2::OAuth2SessionRepository, upstream_oauth2::UpstreamOAuthLinkRepository, - user::{BrowserSessionRepository, UserEmailRepository}, + user::{BrowserSessionFilter, BrowserSessionRepository, UserEmailRepository}, Pagination, RepositoryAccess, }; @@ -30,7 +30,7 @@ use super::{ UpstreamOAuth2Link, }; use crate::{ - model::{matrix::MatrixUser, CompatSession}, + model::{browser_sessions::BrowserSessionState, matrix::MatrixUser, CompatSession}, state::ContextExt, }; @@ -189,6 +189,9 @@ impl User { &self, ctx: &Context<'_>, + #[graphql(name = "state", desc = "List only sessions in the given state.")] + state_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.")] @@ -213,10 +216,14 @@ impl User { .transpose()?; let pagination = Pagination::try_new(before_id, after_id, first, last)?; - let page = repo - .browser_session() - .list_active_paginated(&self.0, pagination) - .await?; + let filter = BrowserSessionFilter::new().for_user(&self.0); + let filter = match state_param { + Some(BrowserSessionState::Active) => filter.active_only(), + Some(BrowserSessionState::Finished) => filter.finished_only(), + None => filter, + }; + + let page = repo.browser_session().list(filter, pagination).await?; repo.cancel().await?; diff --git a/crates/storage-pg/Cargo.toml b/crates/storage-pg/Cargo.toml index a2ecda4c..ccbc06f8 100644 --- a/crates/storage-pg/Cargo.toml +++ b/crates/storage-pg/Cargo.toml @@ -8,6 +8,7 @@ license = "Apache-2.0" [dependencies] async-trait = "0.1.71" sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "json", "uuid"] } +sea-query = { version = "0.28.5", features = ["derive", "attr", "with-uuid", "with-chrono"] } chrono = { version = "0.4.26", features = ["serde"] } serde = { version = "1.0.171", features = ["derive"] } serde_json = "1.0.103" diff --git a/crates/storage-pg/src/lib.rs b/crates/storage-pg/src/lib.rs index d4a340d6..44804b32 100644 --- a/crates/storage-pg/src/lib.rs +++ b/crates/storage-pg/src/lib.rs @@ -217,6 +217,7 @@ pub mod user; mod errors; pub(crate) mod pagination; pub(crate) mod repository; +mod sea_query_sqlx; pub(crate) mod tracing; pub(crate) use self::errors::DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/pagination.rs b/crates/storage-pg/src/pagination.rs index 97e5220f..06c8aff6 100644 --- a/crates/storage-pg/src/pagination.rs +++ b/crates/storage-pg/src/pagination.rs @@ -21,9 +21,11 @@ use uuid::Uuid; /// An extension trait to the `sqlx` [`QueryBuilder`], to help adding pagination /// to a query pub trait QueryBuilderExt { + type Iden; + /// Add cursor-based pagination to a query, as used in paginated GraphQL /// connections - fn generate_pagination(&mut self, id_field: &'static str, pagination: Pagination) -> &mut Self; + fn generate_pagination(&mut self, id_field: Self::Iden, pagination: Pagination) -> &mut Self; } impl<'a, DB> QueryBuilderExt for QueryBuilder<'a, DB> @@ -32,6 +34,8 @@ where Uuid: sqlx::Type + sqlx::Encode<'a, DB>, i64: sqlx::Type + sqlx::Encode<'a, DB>, { + type Iden = &'static str; + fn generate_pagination(&mut self, id_field: &'static str, pagination: Pagination) -> &mut Self { // ref: https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564 // 1. Start from the greedy query: SELECT * FROM table @@ -76,3 +80,40 @@ where self } } + +impl QueryBuilderExt for sea_query::SelectStatement { + type Iden = sea_query::ColumnRef; + fn generate_pagination(&mut self, id_field: Self::Iden, pagination: Pagination) -> &mut Self { + // ref: https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564 + // 1. Start from the greedy query: SELECT * FROM table + + // 2. If the after argument is provided, add `id > parsed_cursor` to the `WHERE` + // clause + if let Some(after) = pagination.after { + self.and_where(sea_query::Expr::col(id_field.clone()).gt(Uuid::from(after))); + } + + // 3. If the before argument is provided, add `id < parsed_cursor` to the + // `WHERE` clause + if let Some(before) = pagination.before { + self.and_where(sea_query::Expr::col(id_field.clone()).lt(Uuid::from(before))); + } + + match pagination.direction { + // 4. If the first argument is provided, add `ORDER BY id ASC LIMIT first+1` to the + // query + PaginationDirection::Forward => { + self.order_by(id_field, sea_query::Order::Asc) + .limit((pagination.count + 1) as u64); + } + // 5. If the first argument is provided, add `ORDER BY id DESC LIMIT last+1` to the + // query + PaginationDirection::Backward => { + self.order_by(id_field, sea_query::Order::Desc) + .limit((pagination.count + 1) as u64); + } + }; + + self + } +} diff --git a/crates/storage-pg/src/sea_query_sqlx.rs b/crates/storage-pg/src/sea_query_sqlx.rs new file mode 100644 index 00000000..653f7feb --- /dev/null +++ b/crates/storage-pg/src/sea_query_sqlx.rs @@ -0,0 +1,49 @@ +// Copyright 2021-2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use sea_query::Value; +use sqlx::Arguments; + +pub(crate) fn map_values(values: sea_query::Values) -> sqlx::postgres::PgArguments { + let mut arguments = sqlx::postgres::PgArguments::default(); + + for value in values { + match value { + Value::Bool(b) => arguments.add(b), + Value::TinyInt(i) => arguments.add(i), + Value::SmallInt(i) => arguments.add(i), + Value::Int(i) => arguments.add(i), + Value::BigInt(i) => arguments.add(i), + Value::TinyUnsigned(u) => arguments.add(u.map(|u| u as i16)), + Value::SmallUnsigned(u) => arguments.add(u.map(|u| u as i32)), + Value::Unsigned(u) => arguments.add(u.map(|u| u as i64)), + Value::BigUnsigned(u) => arguments.add(u.map(|u| i64::try_from(u).unwrap_or(i64::MAX))), + Value::Float(f) => arguments.add(f), + Value::Double(d) => arguments.add(d), + Value::String(s) => arguments.add(s.as_deref()), + Value::Char(c) => arguments.add(c.map(|c| c.to_string())), + Value::Bytes(b) => arguments.add(b.as_deref()), + Value::ChronoDate(d) => arguments.add(d.as_deref()), + Value::ChronoTime(t) => arguments.add(t.as_deref()), + Value::ChronoDateTime(dt) => arguments.add(dt.as_deref()), + Value::ChronoDateTimeUtc(dt) => arguments.add(dt.as_deref()), + Value::ChronoDateTimeLocal(dt) => arguments.add(dt.as_deref()), + Value::ChronoDateTimeWithTimeZone(dt) => arguments.add(dt.as_deref()), + Value::Uuid(u) => arguments.add(u.as_deref()), + _ => unimplemented!(), + } + } + + arguments +} diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index 82ea9ca2..d4623917 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -17,13 +17,14 @@ use chrono::{DateTime, Utc}; use mas_data_model::{Authentication, BrowserSession, Password, UpstreamOAuthLink, User}; use mas_storage::{user::BrowserSessionRepository, Clock, Page, Pagination}; use rand::RngCore; +use sea_query::{Expr, IntoColumnRef, PostgresQueryBuilder}; use sqlx::{PgConnection, QueryBuilder}; use ulid::Ulid; use uuid::Uuid; use crate::{ - pagination::QueryBuilderExt, tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, - LookupResultExt, + pagination::QueryBuilderExt, sea_query_sqlx::map_values, tracing::ExecuteExt, DatabaseError, + DatabaseInconsistencyError, LookupResultExt, }; /// An implementation of [`BrowserSessionRepository`] for a PostgreSQL @@ -41,6 +42,7 @@ impl<'c> PgBrowserSessionRepository<'c> { } #[derive(sqlx::FromRow)] +#[sea_query::enum_def] struct SessionLookup { user_session_id: Uuid, user_session_created_at: DateTime, @@ -52,6 +54,31 @@ struct SessionLookup { last_authd_at: Option>, } +#[derive(sea_query::Iden)] +enum UserSessions { + Table, + UserSessionId, + CreatedAt, + FinishedAt, + UserId, +} + +#[derive(sea_query::Iden)] +enum Users { + Table, + UserId, + Username, + PrimaryUserEmailId, +} + +#[derive(sea_query::Iden)] +enum SessionAuthentication { + Table, + UserSessionAuthenticationId, + UserSessionId, + CreatedAt, +} + impl TryFrom for BrowserSession { type Error = DatabaseInconsistencyError; @@ -214,46 +241,78 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { } #[tracing::instrument( - name = "db.browser_session.list_active_paginated", + name = "db.browser_session.list", skip_all, fields( db.statement, - %user.id, ), err, )] - async fn list_active_paginated( + async fn list( &mut self, - user: &User, + filter: mas_storage::user::BrowserSessionFilter<'_>, pagination: Pagination, ) -> Result, Self::Error> { - // TODO: ordering of last authentication is wrong - let mut query = QueryBuilder::new( - r#" - SELECT DISTINCT ON (s.user_session_id) - s.user_session_id, - s.created_at AS "user_session_created_at", - s.finished_at AS "user_session_finished_at", - u.user_id, - u.username AS "user_username", - u.primary_user_email_id AS "user_primary_user_email_id", - a.user_session_authentication_id AS "last_authentication_id", - a.created_at AS "last_authd_at" - FROM user_sessions s - INNER JOIN users u - USING (user_id) - LEFT JOIN user_session_authentications a - USING (user_session_id) - "#, - ); + let (sql, values) = sea_query::Query::select() + .expr_as( + Expr::col((UserSessions::Table, UserSessions::UserSessionId)), + SessionLookupIden::UserSessionId, + ) + .expr_as( + Expr::col((UserSessions::Table, UserSessions::CreatedAt)), + SessionLookupIden::UserSessionCreatedAt, + ) + .expr_as( + Expr::col((UserSessions::Table, UserSessions::FinishedAt)), + SessionLookupIden::UserSessionFinishedAt, + ) + .expr_as( + Expr::col((Users::Table, Users::UserId)), + SessionLookupIden::UserId, + ) + .expr_as( + Expr::col((Users::Table, Users::Username)), + SessionLookupIden::UserUsername, + ) + .expr_as( + Expr::col((Users::Table, Users::PrimaryUserEmailId)), + SessionLookupIden::UserPrimaryUserEmailId, + ) + .expr_as( + Expr::value(None::), + SessionLookupIden::LastAuthenticationId, + ) + .expr_as( + Expr::value(None::>), + SessionLookupIden::LastAuthdAt, + ) + .from(UserSessions::Table) + .inner_join( + Users::Table, + Expr::col((UserSessions::Table, UserSessions::UserId)) + .equals((Users::Table, Users::UserId)), + ) + .and_where_option( + filter + .user() + .map(|user| Expr::col((Users::Table, Users::UserId)).eq(Uuid::from(user.id))), + ) + .and_where_option(filter.state().map(|state| { + if state.is_active() { + Expr::col((UserSessions::Table, UserSessions::FinishedAt)).is_null() + } else { + Expr::col((UserSessions::Table, UserSessions::FinishedAt)).is_not_null() + } + })) + .generate_pagination( + (UserSessions::Table, UserSessions::UserSessionId).into_column_ref(), + pagination, + ) + .build(PostgresQueryBuilder); - query - .push(" WHERE s.finished_at IS NULL AND s.user_id = ") - .push_bind(Uuid::from(user.id)) - .generate_pagination("s.user_session_id", pagination); + let arguments = map_values(values); - let edges: Vec = query - .build_query_as() + let edges: Vec = sqlx::query_as_with(&sql, arguments) .traced() .fetch_all(&mut *self.conn) .await?; @@ -261,34 +320,10 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { let page = pagination .process(edges) .try_map(BrowserSession::try_from)?; + Ok(page) } - #[tracing::instrument( - name = "db.browser_session.count_active", - skip_all, - fields( - db.statement, - %user.id, - ), - err, - )] - async fn count_active(&mut self, user: &User) -> Result { - let res = sqlx::query_scalar!( - r#" - SELECT COUNT(*) as "count!" - FROM user_sessions s - WHERE s.user_id = $1 AND s.finished_at IS NULL - "#, - Uuid::from(user.id), - ) - .traced() - .fetch_one(&mut *self.conn) - .await?; - - res.try_into().map_err(DatabaseError::to_invalid_operation) - } - #[tracing::instrument( name = "db.browser_session.authenticate_with_password", skip_all, diff --git a/crates/storage/src/user/mod.rs b/crates/storage/src/user/mod.rs index a611b459..9a3da3d6 100644 --- a/crates/storage/src/user/mod.rs +++ b/crates/storage/src/user/mod.rs @@ -26,7 +26,9 @@ mod password; mod session; pub use self::{ - email::UserEmailRepository, password::UserPasswordRepository, session::BrowserSessionRepository, + email::UserEmailRepository, + password::UserPasswordRepository, + session::{BrowserSessionFilter, BrowserSessionRepository}, }; /// A [`UserRepository`] helps interacting with [`User`] saved in the storage diff --git a/crates/storage/src/user/session.rs b/crates/storage/src/user/session.rs index 5e9defbe..46e14ee0 100644 --- a/crates/storage/src/user/session.rs +++ b/crates/storage/src/user/session.rs @@ -19,6 +19,70 @@ use ulid::Ulid; use crate::{pagination::Page, repository_impl, Clock, Pagination}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BrowserSessionState { + Active, + Finished, +} + +impl BrowserSessionState { + pub fn is_active(self) -> bool { + matches!(self, Self::Active) + } + + pub fn is_finished(self) -> bool { + matches!(self, Self::Finished) + } +} + +/// Filter parameters for listing browser sessions +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct BrowserSessionFilter<'a> { + user: Option<&'a User>, + state: Option, +} + +impl<'a> BrowserSessionFilter<'a> { + /// Create a new [`BrowserSessionFilter`] with default values + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Set the user who owns the browser sessions + #[must_use] + pub fn for_user(mut self, user: &'a User) -> Self { + self.user = Some(user); + self + } + + /// Get the user filter + #[must_use] + pub fn user(&self) -> Option<&User> { + self.user + } + + /// Only return active browser sessions + #[must_use] + pub fn active_only(mut self) -> Self { + self.state = Some(BrowserSessionState::Active); + self + } + + /// Only return finished browser sessions + #[must_use] + pub fn finished_only(mut self) -> Self { + self.state = Some(BrowserSessionState::Finished); + self + } + + /// Get the state filter + #[must_use] + pub fn state(&self) -> Option { + self.state + } +} + /// A [`BrowserSessionRepository`] helps interacting with [`BrowserSession`] /// saved in the storage backend #[async_trait] @@ -77,33 +141,22 @@ pub trait BrowserSessionRepository: Send + Sync { user_session: BrowserSession, ) -> Result; - /// List active [`BrowserSession`] for a [`User`] with the given pagination + /// List [`BrowserSession`] with the given filter and pagination /// /// # Parameters /// - /// * `user`: The user to list the sessions for + /// * `filter`: The filter to apply /// * `pagination`: The pagination parameters /// /// # Errors /// /// Returns [`Self::Error`] if the underlying repository fails - async fn list_active_paginated( + async fn list( &mut self, - user: &User, + filter: BrowserSessionFilter<'_>, pagination: Pagination, ) -> Result, Self::Error>; - /// Count active [`BrowserSession`] for a [`User`] - /// - /// # Parameters - /// - /// * `user`: The user to count the sessions for - /// - /// # Errors - /// - /// Returns [`Self::Error`] if the underlying repository fails - async fn count_active(&mut self, user: &User) -> Result; - /// Authenticate a [`BrowserSession`] with the given [`Password`] /// /// Returns the updated [`BrowserSession`] @@ -163,12 +216,12 @@ repository_impl!(BrowserSessionRepository: clock: &dyn Clock, user_session: BrowserSession, ) -> Result; - async fn list_active_paginated( + + async fn list( &mut self, - user: &User, + filter: BrowserSessionFilter<'_>, pagination: Pagination, ) -> Result, Self::Error>; - async fn count_active(&mut self, user: &User) -> Result; async fn authenticate_with_password( &mut self, diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 475f3ccd..6b42cb62 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -118,6 +118,20 @@ type BrowserSessionEdge { cursor: String! } +""" +The state of a browser session. +""" +enum BrowserSessionState { + """ + The session is active. + """ + ACTIVE + """ + The session is no longer active. + """ + FINISHED +} + """ A compat session represents a client session which used the legacy Matrix login API. @@ -871,6 +885,7 @@ type User implements Node { Get the list of active browser sessions, chronologically sorted """ browserSessions( + state: BrowserSessionState after: String before: String first: Int diff --git a/frontend/src/components/BrowserSessionList.tsx b/frontend/src/components/BrowserSessionList.tsx index 3f5503b5..a20ed222 100644 --- a/frontend/src/components/BrowserSessionList.tsx +++ b/frontend/src/components/BrowserSessionList.tsx @@ -48,6 +48,7 @@ const QUERY = graphql(/* GraphQL */ ` after: $after last: $last before: $before + state: ACTIVE ) { edges { cursor diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index c50c3a2a..f74ff549 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -23,7 +23,7 @@ const documents = { types.BrowserSession_SessionFragmentDoc, "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n": types.EndBrowserSessionDocument, - "\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": + "\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": types.BrowserSessionListDocument, "\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\n }\n": types.CompatSession_Sso_LoginFragmentDoc, @@ -109,8 +109,8 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n" -): typeof documents["\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"]; + source: "\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n" +): typeof documents["\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 4a821e18..54d23e13 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -117,6 +117,14 @@ export type BrowserSessionEdge = { node: BrowserSession; }; +/** The state of a browser session. */ +export enum BrowserSessionState { + /** The session is active. */ + Active = "ACTIVE", + /** The session is no longer active. */ + Finished = "FINISHED", +} + /** * A compat session represents a client session which used the legacy Matrix * login API. @@ -670,6 +678,7 @@ export type UserBrowserSessionsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + state?: InputMaybe; }; /** A user is an individual's account. */ @@ -1797,6 +1806,11 @@ export const BrowserSessionListDocument = { name: { kind: "Name", value: "before" }, }, }, + { + kind: "Argument", + name: { kind: "Name", value: "state" }, + value: { kind: "EnumValue", value: "ACTIVE" }, + }, ], selectionSet: { kind: "SelectionSet", diff --git a/frontend/src/gql/schema.ts b/frontend/src/gql/schema.ts index 82a4bf96..82bdeb1a 100644 --- a/frontend/src/gql/schema.ts +++ b/frontend/src/gql/schema.ts @@ -1970,6 +1970,13 @@ export default { name: "Any", }, }, + { + name: "state", + type: { + kind: "SCALAR", + name: "Any", + }, + }, ], }, {