diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 0b0121078..5bbd870ea 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -390,9 +390,10 @@ impl Options { info!("The following users can request admin privileges ({total} total):"); loop { let page = repo.user().list(filter, cursor).await?; - for user in page.edges { + for edge in page.edges { + let user = edge.node; info!(%user.id, username = %user.username); - cursor = cursor.after(user.id); + cursor = cursor.after(edge.cursor); } if !page.has_next_page { diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index 4b8c388c3..e66b3aa50 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -132,7 +132,8 @@ pub async fn config_sync( let mut existing_enabled_ids = BTreeSet::new(); let mut existing_disabled = BTreeMap::new(); // Process the existing providers - for provider in page.edges { + for edge in page.edges { + let provider = edge.node; if provider.enabled() { if config_ids.contains(&provider.id) { existing_enabled_ids.insert(provider.id); diff --git a/crates/handlers/src/admin/response.rs b/crates/handlers/src/admin/response.rs index 19f0e8040..986ec2479 100644 --- a/crates/handlers/src/admin/response.rs +++ b/crates/handlers/src/admin/response.rs @@ -6,7 +6,7 @@ #![allow(clippy::module_name_repetitions)] -use mas_storage::Pagination; +use mas_storage::{Pagination, pagination::Edge}; use schemars::JsonSchema; use serde::Serialize; use ulid::Ulid; @@ -102,7 +102,7 @@ impl PaginatedResponse { base, current_pagination .clear_before() - .after(page.edges.last().unwrap().id()), + .after(page.edges.last().unwrap().cursor), ) }), prev: if page.has_previous_page { @@ -110,14 +110,18 @@ impl PaginatedResponse { base, current_pagination .clear_after() - .before(page.edges.first().unwrap().id()), + .before(page.edges.first().unwrap().cursor), )) } else { None }, }; - let data = page.edges.into_iter().map(SingleResource::new).collect(); + let data = page + .edges + .into_iter() + .map(SingleResource::from_edge) + .collect(); Self { meta: PaginationMeta { count }, @@ -143,6 +147,31 @@ struct SingleResource { /// Related links links: SelfLinks, + + /// Metadata about the resource + #[serde(skip_serializing_if = "SingleResourceMeta::is_empty")] + meta: SingleResourceMeta, +} + +/// Metadata associated with a resource +#[derive(Serialize, JsonSchema)] +struct SingleResourceMeta { + /// Information about the pagination of the resource + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl SingleResourceMeta { + fn is_empty(&self) -> bool { + self.page.is_none() + } +} + +/// Pagination metadata for a resource +#[derive(Serialize, JsonSchema)] +struct SingleResourceMetaPage { + /// The cursor of this resource in the paginated result + cursor: String, } impl SingleResource { @@ -153,8 +182,16 @@ impl SingleResource { id: resource.id(), attributes: resource, links: SelfLinks { self_ }, + meta: SingleResourceMeta { page: None }, } } + + fn from_edge(edge: Edge) -> Self { + let cursor = edge.cursor.to_string(); + let mut resource = Self::new(edge.node); + resource.meta.page = Some(SingleResourceMetaPage { cursor }); + resource + } } /// Related links diff --git a/crates/handlers/src/admin/v1/compat_sessions/list.rs b/crates/handlers/src/admin/v1/compat_sessions/list.rs index debb2a304..2ab788a2b 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/list.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/list.rs @@ -137,7 +137,13 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p let sessions = CompatSession::samples(); let pagination = mas_storage::Pagination::first(sessions.len()); let page = Page { - edges: sessions.into(), + edges: sessions + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; @@ -299,6 +305,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } } }, { @@ -318,6 +329,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW" + }, + "meta": { + "page": { + "cursor": "01FSHNCZP0PPF7X0EVMJNECPZW" + } } } ], @@ -362,6 +378,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } } } ], @@ -403,6 +424,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } } } ], @@ -444,6 +470,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW" + }, + "meta": { + "page": { + "cursor": "01FSHNCZP0PPF7X0EVMJNECPZW" + } } } ], diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs index 52b597edc..b24877c45 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs @@ -192,7 +192,13 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p let sessions = OAuth2Session::samples(); let pagination = mas_storage::Pagination::first(sessions.len()); let page = Page { - edges: sessions.into(), + edges: sessions + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; @@ -354,6 +360,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MKGTBNZ16RDR3PVY" + } } } ], diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs index 59efe6541..4c2eeb7d5 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs @@ -112,7 +112,13 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let links = UpstreamOAuthLink::samples(); let pagination = mas_storage::Pagination::first(links.len()); let page = Page { - edges: links.into(), + edges: links + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; @@ -296,7 +302,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 3 @@ -314,6 +320,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } }, { @@ -328,6 +339,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4" + } } }, { @@ -342,6 +358,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07" + } } } ], @@ -351,7 +372,7 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?page[last]=10" } } - "###); + "#); // Filter by user ID let request = Request::get(format!( @@ -364,7 +385,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -382,6 +403,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } }, { @@ -396,6 +422,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07" + } } } ], @@ -405,7 +436,7 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10" } } - "###); + "#); // Filter by provider let request = Request::get(format!( @@ -418,7 +449,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -436,6 +467,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } }, { @@ -450,6 +486,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4" + } } } ], @@ -459,7 +500,7 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?filter[provider]=01FSHN9AG09NMZYX8MFYH578R9&page[last]=10" } } - "###); + "#); // Filter by subject let request = Request::get(format!( @@ -472,7 +513,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -490,6 +531,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } } ], @@ -499,6 +545,6 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?filter[subject]=subject1&page[last]=10" } } - "###); + "#); } } diff --git a/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs index dc5f2cc9c..6439e2fdd 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs @@ -84,7 +84,13 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let providers = UpstreamOAuthProvider::samples(); let pagination = mas_storage::Pagination::first(providers.len()); let page = Page { - edges: providers.into(), + edges: providers + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; @@ -291,6 +297,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -305,6 +316,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -319,6 +335,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -364,6 +385,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -378,6 +404,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -423,6 +454,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } } ], @@ -469,6 +505,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -483,6 +524,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } } ], @@ -525,6 +571,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], diff --git a/crates/handlers/src/admin/v1/user_emails/list.rs b/crates/handlers/src/admin/v1/user_emails/list.rs index 92dfe12c2..edf64e989 100644 --- a/crates/handlers/src/admin/v1/user_emails/list.rs +++ b/crates/handlers/src/admin/v1/user_emails/list.rs @@ -99,7 +99,13 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let emails = UserEmail::samples(); let pagination = mas_storage::Pagination::first(emails.len()); let page = Page { - edges: emails.into(), + edges: emails + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; @@ -209,7 +215,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -225,6 +231,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } } }, { @@ -237,6 +248,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG0KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0KEPHYQQXW9XPTX6Z" + } } } ], @@ -246,7 +262,7 @@ mod tests { "last": "/api/admin/v1/user-emails?page[last]=10" } } - "###); + "#); // Filter by user let request = Request::get(format!( @@ -258,7 +274,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -274,6 +290,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } } } ], @@ -283,7 +304,7 @@ mod tests { "last": "/api/admin/v1/user-emails?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10" } } - "###); + "#); // Filter by email let request = Request::get("/api/admin/v1/user-emails?filter[email]=alice@example.com") @@ -292,7 +313,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -308,6 +329,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } } } ], @@ -317,6 +343,6 @@ mod tests { "last": "/api/admin/v1/user-emails?filter[email]=alice@example.com&page[last]=10" } } - "###); + "#); } } diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs index 546491536..08acad4df 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs @@ -112,7 +112,13 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let tokens = UserRegistrationToken::samples(); let pagination = mas_storage::Pagination::first(tokens.len()); let page = Page { - edges: tokens.into(), + edges: tokens + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; @@ -300,6 +306,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -317,6 +328,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -334,6 +350,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -351,6 +372,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } }, { @@ -368,6 +394,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -416,6 +447,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -433,6 +469,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -473,6 +514,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -490,6 +536,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -507,6 +558,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -555,6 +611,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -572,6 +633,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -612,6 +678,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -629,6 +700,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -646,6 +722,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -694,6 +775,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } } ], @@ -734,6 +820,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -751,6 +842,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -768,6 +864,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } }, { @@ -785,6 +886,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -833,6 +939,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -850,6 +961,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -890,6 +1006,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -907,6 +1028,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -924,6 +1050,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -974,6 +1105,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -1022,6 +1158,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -1039,6 +1180,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } } ], @@ -1080,6 +1226,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -1097,6 +1248,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -1138,6 +1294,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], diff --git a/crates/handlers/src/admin/v1/user_sessions/list.rs b/crates/handlers/src/admin/v1/user_sessions/list.rs index 28a52edf2..555d15d23 100644 --- a/crates/handlers/src/admin/v1/user_sessions/list.rs +++ b/crates/handlers/src/admin/v1/user_sessions/list.rs @@ -123,7 +123,13 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p let sessions = UserSession::samples(); let pagination = mas_storage::Pagination::first(sessions.len()); let page = Page { - edges: sessions.into(), + edges: sessions + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; @@ -241,7 +247,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -260,6 +266,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } }, { @@ -275,6 +286,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB530KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AJ6AC5HQ9X6H4RP4" + } } } ], @@ -284,7 +300,7 @@ mod tests { "last": "/api/admin/v1/user-sessions?page[last]=10" } } - "###); + "#); // Filter by user let request = Request::get(format!( @@ -296,7 +312,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -315,6 +331,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -324,7 +345,7 @@ mod tests { "last": "/api/admin/v1/user-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10" } } - "###); + "#); // Filter by status (active) let request = Request::get("/api/admin/v1/user-sessions?filter[status]=active") @@ -333,7 +354,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -352,6 +373,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -361,7 +387,7 @@ mod tests { "last": "/api/admin/v1/user-sessions?filter[status]=active&page[last]=10" } } - "###); + "#); // Filter by status (finished) let request = Request::get("/api/admin/v1/user-sessions?filter[status]=finished") @@ -370,7 +396,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -389,6 +415,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB530KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AJ6AC5HQ9X6H4RP4" + } } } ], @@ -398,6 +429,6 @@ mod tests { "last": "/api/admin/v1/user-sessions?filter[status]=finished&page[last]=10" } } - "###); + "#); } } diff --git a/crates/handlers/src/admin/v1/users/list.rs b/crates/handlers/src/admin/v1/users/list.rs index da70e5807..cdfe59d4a 100644 --- a/crates/handlers/src/admin/v1/users/list.rs +++ b/crates/handlers/src/admin/v1/users/list.rs @@ -137,7 +137,13 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let users = User::samples(); let pagination = mas_storage::Pagination::first(users.len()); let page = Page { - edges: users.into(), + edges: users + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; diff --git a/crates/handlers/src/graphql/model/browser_sessions.rs b/crates/handlers/src/graphql/model/browser_sessions.rs index 925288067..08ba25830 100644 --- a/crates/handlers/src/graphql/model/browser_sessions.rs +++ b/crates/handlers/src/graphql/model/browser_sessions.rs @@ -172,7 +172,7 @@ impl BrowserSession { connection .edges - .extend(page.edges.into_iter().map(|s| match s { + .extend(page.edges.into_iter().map(|edge| match edge.node { mas_storage::app_session::AppSession::Compat(session) => Edge::new( OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), AppSession::CompatSession(Box::new(CompatSession::new(*session))), diff --git a/crates/handlers/src/graphql/model/users.rs b/crates/handlers/src/graphql/model/users.rs index 11522c6b4..7e615df7d 100644 --- a/crates/handlers/src/graphql/model/users.rs +++ b/crates/handlers/src/graphql/model/users.rs @@ -125,10 +125,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|u| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, u.id)), - CompatSsoLogin(u), + OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, edge.cursor)), + CompatSsoLogin(edge.node), ) })); @@ -219,14 +219,13 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection - .edges - .extend(page.edges.into_iter().map(|(session, sso_login)| { - Edge::new( - OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), - CompatSession::new(session).with_loaded_sso_login(sso_login), - ) - })); + connection.edges.extend(page.edges.into_iter().map(|edge| { + let (session, sso_login) = edge.node; + Edge::new( + OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), + CompatSession::new(session).with_loaded_sso_login(sso_login), + ) + })); Ok::<_, async_graphql::Error>(connection) }, @@ -305,10 +304,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|u| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)), - BrowserSession(u), + OpaqueCursor(NodeCursor(NodeType::BrowserSession, edge.cursor)), + BrowserSession(edge.node), ) })); @@ -373,10 +372,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|u| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::UserEmail, u.id)), - UserEmail(u), + OpaqueCursor(NodeCursor(NodeType::UserEmail, edge.cursor)), + UserEmail(edge.node), ) })); @@ -480,10 +479,10 @@ impl User { PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|s| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::OAuth2Session, s.id)), - OAuth2Session(s), + OpaqueCursor(NodeCursor(NodeType::OAuth2Session, edge.cursor)), + OAuth2Session(edge.node), ) })); @@ -547,10 +546,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|s| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Link, s.id)), - UpstreamOAuth2Link::new(s), + OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Link, edge.cursor)), + UpstreamOAuth2Link::new(edge.node), ) })); @@ -689,13 +688,13 @@ impl User { connection .edges - .extend(page.edges.into_iter().map(|s| match s { + .extend(page.edges.into_iter().map(|edge| match edge.node { mas_storage::app_session::AppSession::Compat(session) => Edge::new( - OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), + OpaqueCursor(NodeCursor(NodeType::CompatSession, edge.cursor)), AppSession::CompatSession(Box::new(CompatSession::new(*session))), ), mas_storage::app_session::AppSession::OAuth2(session) => Edge::new( - OpaqueCursor(NodeCursor(NodeType::OAuth2Session, session.id)), + OpaqueCursor(NodeCursor(NodeType::OAuth2Session, edge.cursor)), AppSession::OAuth2Session(Box::new(OAuth2Session(*session))), ), })); diff --git a/crates/handlers/src/graphql/query/session.rs b/crates/handlers/src/graphql/query/session.rs index 921009ee9..82ca55fd9 100644 --- a/crates/handlers/src/graphql/query/session.rs +++ b/crates/handlers/src/graphql/query/session.rs @@ -68,7 +68,8 @@ impl SessionQuery { ); } - if let Some((compat_session, sso_login)) = compat_sessions.edges.into_iter().next() { + if let Some(edge) = compat_sessions.edges.into_iter().next() { + let (compat_session, sso_login) = edge.node; repo.cancel().await?; return Ok(Some(Session::CompatSession(Box::new( @@ -92,10 +93,10 @@ impl SessionQuery { ); } - if let Some(session) = sessions.edges.into_iter().next() { + if let Some(edge) = sessions.edges.into_iter().next() { repo.cancel().await?; return Ok(Some(Session::OAuth2Session(Box::new(OAuth2Session( - session, + edge.node, ))))); } repo.cancel().await?; diff --git a/crates/handlers/src/graphql/query/upstream_oauth.rs b/crates/handlers/src/graphql/query/upstream_oauth.rs index f52c21f82..f0b4ceee6 100644 --- a/crates/handlers/src/graphql/query/upstream_oauth.rs +++ b/crates/handlers/src/graphql/query/upstream_oauth.rs @@ -130,10 +130,10 @@ impl UpstreamOAuthQuery { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|p| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Provider, p.id)), - UpstreamOAuth2Provider::new(p), + OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Provider, edge.cursor)), + UpstreamOAuth2Provider::new(edge.node), ) })); diff --git a/crates/handlers/src/graphql/query/user.rs b/crates/handlers/src/graphql/query/user.rs index 364319e57..bb55ef67b 100644 --- a/crates/handlers/src/graphql/query/user.rs +++ b/crates/handlers/src/graphql/query/user.rs @@ -143,11 +143,12 @@ impl UserQuery { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend( - page.edges.into_iter().map(|p| { - Edge::new(OpaqueCursor(NodeCursor(NodeType::User, p.id)), User(p)) - }), - ); + connection.edges.extend(page.edges.into_iter().map(|edge| { + Edge::new( + OpaqueCursor(NodeCursor(NodeType::User, edge.cursor)), + User(edge.node), + ) + })); Ok::<_, async_graphql::Error>(connection) }, diff --git a/crates/handlers/src/upstream_oauth2/backchannel_logout.rs b/crates/handlers/src/upstream_oauth2/backchannel_logout.rs index 76a7b574c..63454741c 100644 --- a/crates/handlers/src/upstream_oauth2/backchannel_logout.rs +++ b/crates/handlers/src/upstream_oauth2/backchannel_logout.rs @@ -267,9 +267,9 @@ pub(crate) async fn post( .browser_session() .list(browser_session_filter, cursor) .await?; - for browser_session in browser_sessions.edges { - user_ids.insert(browser_session.user.id); - cursor = cursor.after(browser_session.id); + for edge in browser_sessions.edges { + user_ids.insert(edge.node.user.id); + cursor = cursor.after(edge.cursor); } if !browser_sessions.has_next_page { diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index a3d4c1bb9..d9577bafd 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -1212,9 +1212,9 @@ mod tests { .list(UserEmailFilter::new().for_user(&user), Pagination::first(1)) .await .unwrap(); - let email = page.edges.first().expect("email exists"); + let edge = page.edges.first().expect("email exists"); - assert_eq!(email.email, "john@example.com"); + assert_eq!(edge.node.email, "john@example.com"); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index 62f3979a9..4e12810cc 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -55,7 +55,9 @@ mod priv_ { use std::net::IpAddr; use chrono::{DateTime, Utc}; + use mas_storage::pagination::Node; use sea_query::enum_def; + use ulid::Ulid; use uuid::Uuid; #[derive(sqlx::FromRow)] @@ -77,6 +79,12 @@ mod priv_ { pub(super) last_active_at: Option>, pub(super) last_active_ip: Option, } + + impl Node for AppSessionLookup { + fn cursor(&self) -> Ulid { + self.cursor.into() + } + } } use priv_::{AppSessionLookup, AppSessionLookupIden}; @@ -592,13 +600,13 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); let active_list = repo.app_session().list(active, pagination).await.unwrap(); assert_eq!(active_list.edges.len(), 1); assert_eq!( - active_list.edges[0], + active_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); @@ -618,7 +626,7 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); let active_list = repo.app_session().list(active, pagination).await.unwrap(); @@ -626,7 +634,7 @@ mod tests { let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); assert_eq!(finished_list.edges.len(), 1); assert_eq!( - finished_list.edges[0], + finished_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); @@ -680,25 +688,25 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 2); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); assert_eq!( - full_list.edges[1], + full_list.edges[1].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); let active_list = repo.app_session().list(active, pagination).await.unwrap(); assert_eq!(active_list.edges.len(), 1); assert_eq!( - active_list.edges[0], + active_list.edges[0].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); assert_eq!(finished_list.edges.len(), 1); assert_eq!( - finished_list.edges[0], + finished_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); @@ -716,11 +724,11 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 2); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); assert_eq!( - full_list.edges[1], + full_list.edges[1].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); @@ -730,11 +738,11 @@ mod tests { let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); assert_eq!(finished_list.edges.len(), 2); assert_eq!( - finished_list.edges[0], + finished_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); assert_eq!( - full_list.edges[1], + full_list.edges[1].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); @@ -744,7 +752,7 @@ mod tests { let list = repo.app_session().list(filter, pagination).await.unwrap(); assert_eq!(list.edges.len(), 1); assert_eq!( - list.edges[0], + list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); @@ -753,7 +761,7 @@ mod tests { let list = repo.app_session().list(filter, pagination).await.unwrap(); assert_eq!(list.edges.len(), 1); assert_eq!( - list.edges[0], + list.edges[0].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); diff --git a/crates/storage-pg/src/compat/mod.rs b/crates/storage-pg/src/compat/mod.rs index db190db71..d42c9b1af 100644 --- a/crates/storage-pg/src/compat/mod.rs +++ b/crates/storage-pg/src/compat/mod.rs @@ -92,14 +92,14 @@ mod tests { let full_list = repo.compat_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); - assert_eq!(full_list.edges[0].0.id, session.id); + assert_eq!(full_list.edges[0].node.0.id, session.id); let active_list = repo .compat_session() .list(active, pagination) .await .unwrap(); assert_eq!(active_list.edges.len(), 1); - assert_eq!(active_list.edges[0].0.id, session.id); + assert_eq!(active_list.edges[0].node.0.id, session.id); let finished_list = repo .compat_session() .list(finished, pagination) @@ -150,7 +150,7 @@ mod tests { .await .unwrap(); assert_eq!(list.edges.len(), 1); - let session_lookup = &list.edges[0].0; + let session_lookup = &list.edges[0].node.0; assert_eq!(session_lookup.id, session.id); assert_eq!(session_lookup.user_id, user.id); assert_eq!(session.device.as_ref().unwrap().as_str(), device_str); @@ -168,7 +168,7 @@ mod tests { let full_list = repo.compat_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); - assert_eq!(full_list.edges[0].0.id, session.id); + assert_eq!(full_list.edges[0].node.0.id, session.id); let active_list = repo .compat_session() .list(active, pagination) @@ -181,7 +181,7 @@ mod tests { .await .unwrap(); assert_eq!(finished_list.edges.len(), 1); - assert_eq!(finished_list.edges[0].0.id, session.id); + assert_eq!(finished_list.edges[0].node.0.id, session.id); // Reload the session and check again let session_lookup = repo @@ -260,14 +260,14 @@ mod tests { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].0.id, sso_login_session.id); + assert_eq!(list.edges[0].node.0.id, sso_login_session.id); let list = repo .compat_session() .list(unknown, pagination) .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].0.id, unknown_session.id); + assert_eq!(list.edges[0].node.0.id, unknown_session.id); // Check that combining the two filters works // At this point, there is one active SSO login session and one finished unknown @@ -696,7 +696,8 @@ mod tests { // List all logins let logins = repo.compat_sso_login().list(all, pagination).await.unwrap(); assert!(!logins.has_next_page); - assert_eq!(logins.edges, vec![login.clone()]); + assert_eq!(logins.edges.len(), 1); + assert_eq!(logins.edges[0].node, login); // List the logins for the user let logins = repo @@ -705,7 +706,8 @@ mod tests { .await .unwrap(); assert!(!logins.has_next_page); - assert_eq!(logins.edges, vec![login.clone()]); + assert_eq!(logins.edges.len(), 1); + assert_eq!(logins.edges[0].node, login); // List only the pending logins for the user let logins = repo @@ -732,6 +734,7 @@ mod tests { .await .unwrap(); assert!(!logins.has_next_page); - assert_eq!(logins.edges, &[login]); + assert_eq!(logins.edges.len(), 1); + assert_eq!(logins.edges[0].node, login); } } diff --git a/crates/storage-pg/src/compat/session.rs b/crates/storage-pg/src/compat/session.rs index 4ba5ee726..0fb21c487 100644 --- a/crates/storage-pg/src/compat/session.rs +++ b/crates/storage-pg/src/compat/session.rs @@ -15,6 +15,7 @@ use mas_data_model::{ use mas_storage::{ Page, Pagination, compat::{CompatSessionFilter, CompatSessionRepository}, + pagination::Node, }; use rand::RngCore; use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def}; @@ -59,6 +60,12 @@ struct CompatSessionLookup { last_active_ip: Option, } +impl Node for CompatSessionLookup { + fn cursor(&self) -> Ulid { + self.compat_session_id.into() + } +} + impl From for CompatSession { fn from(value: CompatSessionLookup) -> Self { let id = value.compat_session_id.into(); @@ -106,6 +113,12 @@ struct CompatSessionAndSsoLoginLookup { compat_sso_login_exchanged_at: Option>, } +impl Node for CompatSessionAndSsoLoginLookup { + fn cursor(&self) -> Ulid { + self.compat_session_id.into() + } +} + impl TryFrom for (CompatSession, Option) { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/compat/sso_login.rs b/crates/storage-pg/src/compat/sso_login.rs index eeadff164..43ad4bead 100644 --- a/crates/storage-pg/src/compat/sso_login.rs +++ b/crates/storage-pg/src/compat/sso_login.rs @@ -10,6 +10,7 @@ use mas_data_model::{BrowserSession, Clock, CompatSession, CompatSsoLogin, Compa use mas_storage::{ Page, Pagination, compat::{CompatSsoLoginFilter, CompatSsoLoginRepository}, + pagination::Node, }; use rand::RngCore; use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def}; @@ -54,6 +55,12 @@ struct CompatSsoLoginLookup { compat_session_id: Option, } +impl Node for CompatSsoLoginLookup { + fn cursor(&self) -> Ulid { + self.compat_sso_login_id.into() + } +} + impl TryFrom for CompatSsoLogin { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index 6b1c81d46..bf741b5f2 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -511,10 +511,10 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 4); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); - assert_eq!(list.edges[2], session21); - assert_eq!(list.edges[3], session22); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); + assert_eq!(list.edges[2].node, session21); + assert_eq!(list.edges[3].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4); @@ -527,8 +527,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session21); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session21); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -541,8 +541,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -557,7 +557,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session22); + assert_eq!(list.edges[0].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -570,8 +570,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session12); - assert_eq!(list.edges[1], session21); + assert_eq!(list.edges[0].node, session12); + assert_eq!(list.edges[1].node, session21); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -584,8 +584,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session22); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -598,7 +598,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session22); + assert_eq!(list.edges[0].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -613,7 +613,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session22); + assert_eq!(list.edges[0].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -626,7 +626,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session12); + assert_eq!(list.edges[0].node, session12); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -641,7 +641,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session21); + assert_eq!(list.edges[0].node, session21); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -655,10 +655,10 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 4); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); - assert_eq!(list.edges[2], session21); - assert_eq!(list.edges[3], session22); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); + assert_eq!(list.edges[2].node, session21); + assert_eq!(list.edges[3].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4); // We should get all sessions with the "openid" and "email" scope @@ -671,8 +671,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); // Try combining the scope filter with the user filter @@ -685,7 +685,7 @@ mod tests { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session11); + assert_eq!(list.edges[0].node, session11); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); // Finish all sessions of a client in batch diff --git a/crates/storage-pg/src/oauth2/session.rs b/crates/storage-pg/src/oauth2/session.rs index c04e03df0..072691a06 100644 --- a/crates/storage-pg/src/oauth2/session.rs +++ b/crates/storage-pg/src/oauth2/session.rs @@ -12,6 +12,7 @@ use mas_data_model::{BrowserSession, Client, Clock, Session, SessionState, User} use mas_storage::{ Page, Pagination, oauth2::{OAuth2SessionFilter, OAuth2SessionRepository}, + pagination::Node, }; use oauth2_types::scope::{Scope, ScopeToken}; use rand::RngCore; @@ -61,6 +62,12 @@ struct OAuthSessionLookup { human_name: Option, } +impl Node for OAuthSessionLookup { + fn cursor(&self) -> Ulid { + self.oauth2_session_id.into() + } +} + impl TryFrom for Session { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/upstream_oauth2/link.rs b/crates/storage-pg/src/upstream_oauth2/link.rs index 6f671655d..c43dd8a18 100644 --- a/crates/storage-pg/src/upstream_oauth2/link.rs +++ b/crates/storage-pg/src/upstream_oauth2/link.rs @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc}; use mas_data_model::{Clock, UpstreamOAuthLink, UpstreamOAuthProvider, User}; use mas_storage::{ Page, Pagination, + pagination::Node, upstream_oauth2::{UpstreamOAuthLinkFilter, UpstreamOAuthLinkRepository}, }; use opentelemetry_semantic_conventions::trace::DB_QUERY_TEXT; @@ -53,6 +54,12 @@ struct LinkLookup { created_at: DateTime, } +impl Node for LinkLookup { + fn cursor(&self) -> Ulid { + self.upstream_oauth_link_id.into() + } +} + impl From for UpstreamOAuthLink { fn from(value: LinkLookup) -> Self { UpstreamOAuthLink { diff --git a/crates/storage-pg/src/upstream_oauth2/mod.rs b/crates/storage-pg/src/upstream_oauth2/mod.rs index 6381c9b7f..d98e840b6 100644 --- a/crates/storage-pg/src/upstream_oauth2/mod.rs +++ b/crates/storage-pg/src/upstream_oauth2/mod.rs @@ -206,8 +206,8 @@ mod tests { assert!(!links.has_previous_page); assert!(!links.has_next_page); assert_eq!(links.edges.len(), 1); - assert_eq!(links.edges[0].id, link.id); - assert_eq!(links.edges[0].user_id, Some(user.id)); + assert_eq!(links.edges[0].node.id, link.id); + assert_eq!(links.edges[0].node.user_id, Some(user.id)); assert_eq!(repo.upstream_oauth_link().count(filter).await.unwrap(), 1); @@ -282,7 +282,7 @@ mod tests { .unwrap(); assert_eq!(session_page.edges.len(), 1); - assert_eq!(session_page.edges[0].id, session.id); + assert_eq!(session_page.edges[0].node.id, session.id); assert!(!session_page.has_next_page); assert!(!session_page.has_previous_page); @@ -374,7 +374,7 @@ mod tests { // It returned the first 10 items assert!(page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Getting the same page with the "enabled only" filter should return the same @@ -396,7 +396,7 @@ mod tests { // It returned the next 10 items assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the last 10 items @@ -408,7 +408,7 @@ mod tests { // It returned the last 10 items assert!(page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the previous 10 items @@ -420,7 +420,7 @@ mod tests { // It returned the previous 10 items assert!(!page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Lookup 10 items between two IDs @@ -432,7 +432,7 @@ mod tests { // It returned the items in between assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[6..8]); // There should not be any disabled providers @@ -560,7 +560,7 @@ mod tests { // It returned the first 10 items assert!(page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Lookup the next 10 items @@ -572,7 +572,7 @@ mod tests { // It returned the next 10 items assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the last 10 items @@ -584,7 +584,7 @@ mod tests { // It returned the last 10 items assert!(page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the previous 10 items @@ -596,7 +596,7 @@ mod tests { // It returned the previous 10 items assert!(!page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Lookup 5 items between two IDs @@ -608,7 +608,7 @@ mod tests { // It returned the items in between assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[6..11]); // Check the sub/sid filters @@ -638,11 +638,21 @@ mod tests { assert_eq!(page.edges.len(), 4); for edge in page.edges { assert_eq!( - edge.id_token_claims().unwrap().get("sub").unwrap().as_str(), + edge.node + .id_token_claims() + .unwrap() + .get("sub") + .unwrap() + .as_str(), Some("alice") ); assert_eq!( - edge.id_token_claims().unwrap().get("sid").unwrap().as_str(), + edge.node + .id_token_claims() + .unwrap() + .get("sid") + .unwrap() + .as_str(), Some("one") ); } diff --git a/crates/storage-pg/src/upstream_oauth2/provider.rs b/crates/storage-pg/src/upstream_oauth2/provider.rs index 583f8ec0c..caade738d 100644 --- a/crates/storage-pg/src/upstream_oauth2/provider.rs +++ b/crates/storage-pg/src/upstream_oauth2/provider.rs @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc}; use mas_data_model::{Clock, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports}; use mas_storage::{ Page, Pagination, + pagination::Node, upstream_oauth2::{ UpstreamOAuthProviderFilter, UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository, }, @@ -74,6 +75,12 @@ struct ProviderLookup { on_backchannel_logout: String, } +impl Node for ProviderLookup { + fn cursor(&self) -> Ulid { + self.upstream_oauth_provider_id.into() + } +} + impl TryFrom for UpstreamOAuthProvider { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/upstream_oauth2/session.rs b/crates/storage-pg/src/upstream_oauth2/session.rs index 8b37aae2e..b961c4f8c 100644 --- a/crates/storage-pg/src/upstream_oauth2/session.rs +++ b/crates/storage-pg/src/upstream_oauth2/session.rs @@ -12,6 +12,7 @@ use mas_data_model::{ }; use mas_storage::{ Page, Pagination, + pagination::Node, upstream_oauth2::{UpstreamOAuthSessionFilter, UpstreamOAuthSessionRepository}, }; use rand::RngCore; @@ -91,6 +92,12 @@ struct SessionLookup { unlinked_at: Option>, } +impl Node for SessionLookup { + fn cursor(&self) -> Ulid { + self.upstream_oauth_authorization_session_id.into() + } +} + impl TryFrom for UpstreamOAuthAuthorizationSession { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/user/email.rs b/crates/storage-pg/src/user/email.rs index 916874ae8..0f998e55f 100644 --- a/crates/storage-pg/src/user/email.rs +++ b/crates/storage-pg/src/user/email.rs @@ -12,6 +12,7 @@ use mas_data_model::{ }; use mas_storage::{ Page, Pagination, + pagination::Node, user::{UserEmailFilter, UserEmailRepository}, }; use rand::RngCore; @@ -51,6 +52,12 @@ struct UserEmailLookup { created_at: DateTime, } +impl Node for UserEmailLookup { + fn cursor(&self) -> Ulid { + self.user_email_id.into() + } +} + impl From for UserEmail { fn from(e: UserEmailLookup) -> UserEmail { UserEmail { diff --git a/crates/storage-pg/src/user/mod.rs b/crates/storage-pg/src/user/mod.rs index 0be594556..bbf02567e 100644 --- a/crates/storage-pg/src/user/mod.rs +++ b/crates/storage-pg/src/user/mod.rs @@ -61,7 +61,9 @@ mod priv_ { #![allow(missing_docs)] use chrono::{DateTime, Utc}; + use mas_storage::pagination::Node; use sea_query::enum_def; + use ulid::Ulid; use uuid::Uuid; #[derive(Debug, Clone, sqlx::FromRow)] @@ -75,6 +77,12 @@ mod priv_ { pub(super) can_request_admin: bool, pub(super) is_guest: bool, } + + impl Node for UserLookup { + fn cursor(&self) -> Ulid { + self.user_id.into() + } + } } use priv_::{UserLookup, UserLookupIden}; diff --git a/crates/storage-pg/src/user/registration_token.rs b/crates/storage-pg/src/user/registration_token.rs index f64c8136d..5c9231aa1 100644 --- a/crates/storage-pg/src/user/registration_token.rs +++ b/crates/storage-pg/src/user/registration_token.rs @@ -8,6 +8,7 @@ use chrono::{DateTime, Utc}; use mas_data_model::{Clock, UserRegistrationToken}; use mas_storage::{ Page, Pagination, + pagination::Node, user::{UserRegistrationTokenFilter, UserRegistrationTokenRepository}, }; use rand::RngCore; @@ -53,6 +54,12 @@ struct UserRegistrationTokenLookup { revoked_at: Option>, } +impl Node for UserRegistrationTokenLookup { + fn cursor(&self) -> Ulid { + self.user_registration_token_id.into() + } +} + impl Filter for UserRegistrationTokenFilter { fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition { sea_query::Condition::all() @@ -230,7 +237,7 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { filter: UserRegistrationTokenFilter, pagination: Pagination, ) -> Result, Self::Error> { - let (sql, values) = Query::select() + let (sql, arguments) = Query::select() .expr_as( Expr::col(( UserRegistrationTokens::Table, @@ -295,15 +302,14 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { ) .build_sqlx(PostgresQueryBuilder); - let tokens = sqlx::query_as_with::<_, UserRegistrationTokenLookup, _>(&sql, values) + let edges: Vec = sqlx::query_as_with(&sql, arguments) .traced() .fetch_all(&mut *self.conn) - .await? - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()?; + .await?; - let page = pagination.process(tokens); + let page = pagination + .process(edges) + .try_map(UserRegistrationToken::try_from)?; Ok(page) } @@ -705,7 +711,7 @@ mod tests { .await .unwrap(); - assert!(page.edges.iter().any(|t| t.id == unrevoked_token.id)); + assert!(page.edges.iter().any(|t| t.node.id == unrevoked_token.id)); } #[sqlx::test(migrator = "crate::MIGRATOR")] @@ -867,7 +873,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token2.id); + assert_eq!(page.edges[0].node.id, token2.id); // Test unused filter let unused_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(false); @@ -886,7 +892,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token3.id); + assert_eq!(page.edges[0].node.id, token3.id); let not_expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(false); let page = repo @@ -904,7 +910,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token4.id); + assert_eq!(page.edges[0].node.id, token4.id); let not_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false); let page = repo @@ -941,7 +947,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token4.id); + assert_eq!(page.edges[0].node.id, token4.id); // Test pagination let page = repo diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index 54645b3cb..8644d7666 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -14,6 +14,7 @@ use mas_data_model::{ }; use mas_storage::{ Page, Pagination, + pagination::Node, user::{BrowserSessionFilter, BrowserSessionRepository}, }; use rand::RngCore; @@ -64,6 +65,12 @@ struct SessionLookup { user_is_guest: bool, } +impl Node for SessionLookup { + fn cursor(&self) -> Ulid { + self.user_id.into() + } +} + impl TryFrom for BrowserSession { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/user/tests.rs b/crates/storage-pg/src/user/tests.rs index 0ee978914..98489d68d 100644 --- a/crates/storage-pg/src/user/tests.rs +++ b/crates/storage-pg/src/user/tests.rs @@ -190,7 +190,7 @@ async fn test_user_repo(pool: PgPool) { // Check the list method let list = repo.user().list(all, Pagination::first(10)).await.unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].id, user.id); + assert_eq!(list.edges[0].node.id, user.id); let list = repo .user() @@ -205,7 +205,7 @@ async fn test_user_repo(pool: PgPool) { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].id, user.id); + assert_eq!(list.edges[0].node.id, user.id); let list = repo .user() @@ -227,7 +227,7 @@ async fn test_user_repo(pool: PgPool) { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].id, user.id); + assert_eq!(list.edges[0].node.id, user.id); repo.save().await.unwrap(); } @@ -348,7 +348,7 @@ async fn test_user_email_repo(pool: PgPool) { .unwrap(); assert!(!emails.has_next_page); assert_eq!(emails.edges.len(), 1); - assert_eq!(emails.edges[0], user_email); + assert_eq!(emails.edges[0].node, user_email); // Listing emails from the email address should work let emails = repo @@ -358,7 +358,7 @@ async fn test_user_email_repo(pool: PgPool) { .unwrap(); assert!(!emails.has_next_page); assert_eq!(emails.edges.len(), 1); - assert_eq!(emails.edges[0], user_email); + assert_eq!(emails.edges[0].node, user_email); // Filtering on another email should not return anything let emails = repo @@ -648,7 +648,7 @@ async fn test_user_session(pool: PgPool) { .unwrap(); assert!(!session_list.has_next_page); assert_eq!(session_list.edges.len(), 1); - assert_eq!(session_list.edges[0], session); + assert_eq!(session_list.edges[0].node, session); let session_lookup = repo .browser_session() @@ -809,7 +809,7 @@ async fn test_user_session(pool: PgPool) { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, session.id); + assert_eq!(page.edges[0].node.id, session.id); // Try counting assert_eq!(repo.browser_session().count(filter).await.unwrap(), 1); diff --git a/crates/storage/src/pagination.rs b/crates/storage/src/pagination.rs index 01b8ed197..ad632cb10 100644 --- a/crates/storage/src/pagination.rs +++ b/crates/storage/src/pagination.rs @@ -16,12 +16,12 @@ pub struct InvalidPagination; /// Pagination parameters #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Pagination { +pub struct Pagination { /// The cursor to start from - pub before: Option, + pub before: Option, /// The cursor to end at - pub after: Option, + pub after: Option, /// The maximum number of items to return pub count: usize, @@ -40,16 +40,22 @@ pub enum PaginationDirection { Backward, } -impl Pagination { +/// A node in a page, with a cursor +pub trait Node { + /// The cursor of that particular node + fn cursor(&self) -> C; +} + +impl Pagination { /// Creates a new [`Pagination`] from user-provided parameters. /// /// # Errors /// /// Either `first` or `last` must be provided, else this function will /// return an [`InvalidPagination`] error. - pub const fn try_new( - before: Option, - after: Option, + pub fn try_new( + before: Option, + after: Option, first: Option, last: Option, ) -> Result { @@ -91,49 +97,57 @@ impl Pagination { /// Get items before the given cursor #[must_use] - pub const fn before(mut self, id: Ulid) -> Self { - self.before = Some(id); + pub fn before(mut self, cursor: C) -> Self { + self.before = Some(cursor); self } /// Clear the before cursor #[must_use] - pub const fn clear_before(mut self) -> Self { + pub fn clear_before(mut self) -> Self { self.before = None; self } /// Get items after the given cursor #[must_use] - pub const fn after(mut self, id: Ulid) -> Self { - self.after = Some(id); + pub fn after(mut self, cursor: C) -> Self { + self.after = Some(cursor); self } /// Clear the after cursor #[must_use] - pub const fn clear_after(mut self) -> Self { + pub fn clear_after(mut self) -> Self { self.after = None; self } /// Process a page returned by a paginated query #[must_use] - pub fn process(&self, mut edges: Vec) -> Page { - let is_full = edges.len() == (self.count + 1); + pub fn process>(&self, mut nodes: Vec) -> Page { + let is_full = nodes.len() == (self.count + 1); if is_full { - edges.pop(); + nodes.pop(); } let (has_previous_page, has_next_page) = match self.direction { PaginationDirection::Forward => (false, is_full), PaginationDirection::Backward => { // 6. If the last argument is provided, I reverse the order of the results - edges.reverse(); + nodes.reverse(); (is_full, false) } }; + let edges = nodes + .into_iter() + .map(|node| Edge { + cursor: node.cursor(), + node, + }) + .collect(); + Page { has_next_page, has_previous_page, @@ -142,9 +156,18 @@ impl Pagination { } } +/// An edge in a paginated result +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Edge { + /// The cursor of the edge + pub cursor: C, + /// The node of the edge + pub node: T, +} + /// A page of results returned by a paginated query #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Page { +pub struct Page { /// When paginating forwards, this is true if there are more items after pub has_next_page: bool, @@ -152,21 +175,28 @@ pub struct Page { pub has_previous_page: bool, /// The items in the page - pub edges: Vec, + pub edges: Vec>, } -impl Page { +impl Page { /// Map the items in this page with the given function /// /// # Parameters /// /// * `f`: The function to map the items with #[must_use] - pub fn map(self, f: F) -> Page + pub fn map(self, mut f: F) -> Page where F: FnMut(T) -> T2, { - let edges = self.edges.into_iter().map(f).collect(); + let edges = self + .edges + .into_iter() + .map(|edge| Edge { + cursor: edge.cursor, + node: f(edge.node), + }) + .collect(); Page { has_next_page: self.has_next_page, has_previous_page: self.has_previous_page, @@ -183,11 +213,21 @@ impl Page { /// # Errors /// /// Returns the first error encountered while mapping the items - pub fn try_map(self, f: F) -> Result, E> + pub fn try_map(self, mut f: F) -> Result, E> where F: FnMut(T) -> Result, { - let edges: Result, E> = self.edges.into_iter().map(f).collect(); + let edges: Result>, E> = self + .edges + .into_iter() + .map(|edge| { + Ok(Edge { + cursor: edge.cursor, + node: f(edge.node)?, + }) + }) + .collect(); + Ok(Page { has_next_page: self.has_next_page, has_previous_page: self.has_previous_page, diff --git a/crates/storage/src/queue/tasks.rs b/crates/storage/src/queue/tasks.rs index eb16f6e29..3558314cf 100644 --- a/crates/storage/src/queue/tasks.rs +++ b/crates/storage/src/queue/tasks.rs @@ -384,7 +384,7 @@ impl ExpireInactiveOAuthSessionsJob { let last_edge = page.edges.last()?; Some(Self { threshold: self.threshold, - after: Some(last_edge.id), + after: Some(last_edge.cursor), }) } } @@ -441,7 +441,7 @@ impl ExpireInactiveCompatSessionsJob { let last_edge = page.edges.last()?; Some(Self { threshold: self.threshold, - after: Some(last_edge.id), + after: Some(last_edge.cursor), }) } } @@ -498,7 +498,7 @@ impl ExpireInactiveUserSessionsJob { let last_edge = page.edges.last()?; Some(Self { threshold: self.threshold, - after: Some(last_edge.id), + after: Some(last_edge.cursor), }) } } diff --git a/crates/tasks/src/matrix.rs b/crates/tasks/src/matrix.rs index 87e052d20..2acd9372e 100644 --- a/crates/tasks/src/matrix.rs +++ b/crates/tasks/src/matrix.rs @@ -203,11 +203,12 @@ impl RunnableJob for SyncDevicesJob { .await .map_err(JobError::retry)?; - for (compat_session, _) in page.edges { + for edge in page.edges { + let (compat_session, _) = edge.node; if let Some(ref device) = compat_session.device { devices.insert(device.as_str().to_owned()); } - cursor = cursor.after(compat_session.id); + cursor = cursor.after(edge.cursor); } if !page.has_next_page { @@ -227,14 +228,14 @@ impl RunnableJob for SyncDevicesJob { .await .map_err(JobError::retry)?; - for oauth2_session in page.edges { - for scope in &*oauth2_session.scope { + for edge in page.edges { + for scope in &*edge.node.scope { if let Some(device) = Device::from_scope_token(scope) { devices.insert(device.as_str().to_owned()); } } - cursor = cursor.after(oauth2_session.id); + cursor = cursor.after(edge.cursor); } if !page.has_next_page { diff --git a/crates/tasks/src/recovery.rs b/crates/tasks/src/recovery.rs index 03e02d57b..51afcc295 100644 --- a/crates/tasks/src/recovery.rs +++ b/crates/tasks/src/recovery.rs @@ -70,26 +70,18 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { .await .map_err(JobError::retry)?; - for email in page.edges { + for edge in page.edges { let ticket = Alphanumeric.sample_string(&mut rng, 32); let ticket = repo .user_recovery() - .add_ticket(&mut rng, clock, &session, &email, ticket) + .add_ticket(&mut rng, clock, &session, &edge.node, ticket) .await .map_err(JobError::retry)?; - let user_email = repo - .user_email() - .lookup(email.id) - .await - .map_err(JobError::retry)? - .context("User email not found") - .map_err(JobError::fail)?; - let user = repo .user() - .lookup(user_email.user_id) + .lookup(edge.node.user_id) .await .map_err(JobError::retry)? .context("User not found") @@ -97,7 +89,7 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { let url = url_builder.account_recovery_link(ticket.ticket); - let address: Address = user_email.email.parse().map_err(JobError::fail)?; + let address: Address = edge.node.email.parse().map_err(JobError::fail)?; let mailbox = Mailbox::new(Some(user.username.clone()), address); info!("Sending recovery email to {}", mailbox); @@ -112,7 +104,7 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { ); } - cursor = cursor.after(email.id); + cursor = cursor.after(edge.cursor); } if !page.has_next_page { diff --git a/crates/tasks/src/sessions.rs b/crates/tasks/src/sessions.rs index d10d908da..eede69d51 100644 --- a/crates/tasks/src/sessions.rs +++ b/crates/tasks/src/sessions.rs @@ -110,7 +110,7 @@ impl RunnableJob for ExpireInactiveOAuthSessionsJob { } for edge in page.edges { - if let Some(user_id) = edge.user_id { + if let Some(user_id) = edge.node.user_id { let inserted = users_synced.insert(user_id); if inserted { tracing::info!(user.id = %user_id, "Scheduling devices sync for user"); @@ -128,7 +128,7 @@ impl RunnableJob for ExpireInactiveOAuthSessionsJob { } repo.oauth2_session() - .finish(clock, edge) + .finish(clock, edge.node) .await .map_err(JobError::retry)?; } @@ -174,14 +174,14 @@ impl RunnableJob for ExpireInactiveCompatSessionsJob { } for edge in page.edges { - let inserted = users_synced.insert(edge.user_id); + let inserted = users_synced.insert(edge.node.user_id); if inserted { - tracing::info!(user.id = %edge.user_id, "Scheduling devices sync for user"); + tracing::info!(user.id = %edge.node.user_id, "Scheduling devices sync for user"); repo.queue_job() .schedule_job_later( &mut rng, clock, - SyncDevicesJob::new_for_id(edge.user_id), + SyncDevicesJob::new_for_id(edge.node.user_id), clock.now() + delay, ) .await @@ -190,7 +190,7 @@ impl RunnableJob for ExpireInactiveCompatSessionsJob { } repo.compat_session() - .finish(clock, edge) + .finish(clock, edge.node) .await .map_err(JobError::retry)?; } @@ -230,7 +230,7 @@ impl RunnableJob for ExpireInactiveUserSessionsJob { for edge in page.edges { repo.browser_session() - .finish(clock, edge) + .finish(clock, edge.node) .await .map_err(JobError::retry)?; } diff --git a/docs/api/spec.json b/docs/api/spec.json index 166436454..b730e30ce 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -171,6 +171,11 @@ }, "links": { "self": "/api/admin/v1/compat-sessions/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -190,6 +195,11 @@ }, "links": { "self": "/api/admin/v1/compat-sessions/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -209,6 +219,11 @@ }, "links": { "self": "/api/admin/v1/compat-sessions/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -473,6 +488,11 @@ }, "links": { "self": "/api/admin/v1/oauth2-sessions/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -492,6 +512,11 @@ }, "links": { "self": "/api/admin/v1/oauth2-sessions/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -511,6 +536,11 @@ }, "links": { "self": "/api/admin/v1/oauth2-sessions/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -964,6 +994,11 @@ }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -979,6 +1014,11 @@ }, "links": { "self": "/api/admin/v1/users/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -994,6 +1034,11 @@ }, "links": { "self": "/api/admin/v1/users/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -1798,6 +1843,11 @@ }, "links": { "self": "/api/admin/v1/user-emails/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } } ], @@ -2146,6 +2196,11 @@ }, "links": { "self": "/api/admin/v1/user-sessions/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -2161,6 +2216,11 @@ }, "links": { "self": "/api/admin/v1/user-sessions/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -2176,6 +2236,11 @@ }, "links": { "self": "/api/admin/v1/user-sessions/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -2408,6 +2473,11 @@ }, "links": { "self": "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -2425,6 +2495,11 @@ }, "links": { "self": "/api/admin/v1/user-registration-tokens/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } } ], @@ -2941,6 +3016,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -2955,6 +3035,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-links/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -2969,6 +3054,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-links/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -3316,6 +3406,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -3330,6 +3425,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -3344,6 +3444,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-providers/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -3568,6 +3673,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -3586,6 +3692,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -3674,6 +3784,30 @@ } } }, + "SingleResourceMeta": { + "description": "Metadata associated with a resource", + "type": "object", + "properties": { + "page": { + "description": "Information about the pagination of the resource", + "$ref": "#/components/schemas/SingleResourceMetaPage", + "nullable": true + } + } + }, + "SingleResourceMetaPage": { + "description": "Pagination metadata for a resource", + "type": "object", + "required": [ + "cursor" + ], + "properties": { + "cursor": { + "description": "The cursor of this resource in the paginated result", + "type": "string" + } + } + }, "PaginationLinks": { "description": "Related links", "type": "object", @@ -3849,6 +3983,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -3867,6 +4002,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -3989,6 +4128,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -4007,6 +4147,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -4094,6 +4238,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -4112,6 +4257,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -4295,6 +4444,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -4313,6 +4463,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -4430,6 +4584,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -4448,6 +4603,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -4567,6 +4726,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -4585,6 +4745,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -4756,6 +4920,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -4774,6 +4939,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } }, @@ -4898,6 +5067,7 @@ "attributes", "id", "links", + "meta", "type" ], "properties": { @@ -4916,6 +5086,10 @@ "links": { "description": "Related links", "$ref": "#/components/schemas/SelfLinks" + }, + "meta": { + "description": "Metadata about the resource", + "$ref": "#/components/schemas/SingleResourceMeta" } } },