Add explicit pagination cursors in the admin API (#5062)

This commit is contained in:
Quentin Gliech
2025-09-30 11:40:09 +02:00
committed by GitHub
39 changed files with 921 additions and 214 deletions

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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<T: Resource> PaginatedResponse<T> {
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<T: Resource> PaginatedResponse<T> {
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<T> {
/// 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<SingleResourceMetaPage>,
}
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<T: Resource> SingleResource<T> {
@@ -153,8 +182,16 @@ impl<T: Resource> SingleResource<T> {
id: resource.id(),
attributes: resource,
links: SelfLinks { self_ },
meta: SingleResourceMeta { page: None },
}
}
fn from_edge<C: ToString>(edge: Edge<T, C>) -> Self {
let cursor = edge.cursor.to_string();
let mut resource = Self::new(edge.node);
resource.meta.page = Some(SingleResourceMetaPage { cursor });
resource
}
}
/// Related links

View File

@@ -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"
}
}
}
],

View File

@@ -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"
}
}
}
],

View File

@@ -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"
}
}
"###);
"#);
}
}

View File

@@ -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"
}
}
}
],

View File

@@ -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"
}
}
"###);
"#);
}
}

View File

@@ -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"
}
}
}
],

View File

@@ -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"
}
}
"###);
"#);
}
}

View File

@@ -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,
};

View File

@@ -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))),

View File

@@ -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))),
),
}));

View File

@@ -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?;

View File

@@ -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),
)
}));

View File

@@ -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)
},

View File

@@ -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 {

View File

@@ -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")]

View File

@@ -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<DateTime<Utc>>,
pub(super) last_active_ip: Option<IpAddr>,
}
impl Node<Ulid> 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()))
);

View File

@@ -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);
}
}

View File

@@ -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<IpAddr>,
}
impl Node<Ulid> for CompatSessionLookup {
fn cursor(&self) -> Ulid {
self.compat_session_id.into()
}
}
impl From<CompatSessionLookup> 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<DateTime<Utc>>,
}
impl Node<Ulid> for CompatSessionAndSsoLoginLookup {
fn cursor(&self) -> Ulid {
self.compat_session_id.into()
}
}
impl TryFrom<CompatSessionAndSsoLoginLookup> for (CompatSession, Option<CompatSsoLogin>) {
type Error = DatabaseInconsistencyError;

View File

@@ -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<Uuid>,
}
impl Node<Ulid> for CompatSsoLoginLookup {
fn cursor(&self) -> Ulid {
self.compat_sso_login_id.into()
}
}
impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin {
type Error = DatabaseInconsistencyError;

View File

@@ -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

View File

@@ -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<String>,
}
impl Node<Ulid> for OAuthSessionLookup {
fn cursor(&self) -> Ulid {
self.oauth2_session_id.into()
}
}
impl TryFrom<OAuthSessionLookup> for Session {
type Error = DatabaseInconsistencyError;

View File

@@ -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<Utc>,
}
impl Node<Ulid> for LinkLookup {
fn cursor(&self) -> Ulid {
self.upstream_oauth_link_id.into()
}
}
impl From<LinkLookup> for UpstreamOAuthLink {
fn from(value: LinkLookup) -> Self {
UpstreamOAuthLink {

View File

@@ -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")
);
}

View File

@@ -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<Ulid> for ProviderLookup {
fn cursor(&self) -> Ulid {
self.upstream_oauth_provider_id.into()
}
}
impl TryFrom<ProviderLookup> for UpstreamOAuthProvider {
type Error = DatabaseInconsistencyError;

View File

@@ -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<DateTime<Utc>>,
}
impl Node<Ulid> for SessionLookup {
fn cursor(&self) -> Ulid {
self.upstream_oauth_authorization_session_id.into()
}
}
impl TryFrom<SessionLookup> for UpstreamOAuthAuthorizationSession {
type Error = DatabaseInconsistencyError;

View File

@@ -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<Utc>,
}
impl Node<Ulid> for UserEmailLookup {
fn cursor(&self) -> Ulid {
self.user_email_id.into()
}
}
impl From<UserEmailLookup> for UserEmail {
fn from(e: UserEmailLookup) -> UserEmail {
UserEmail {

View File

@@ -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<Ulid> for UserLookup {
fn cursor(&self) -> Ulid {
self.user_id.into()
}
}
}
use priv_::{UserLookup, UserLookupIden};

View File

@@ -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<DateTime<Utc>>,
}
impl Node<Ulid> 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<Page<UserRegistrationToken>, 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<UserRegistrationTokenLookup> = sqlx::query_as_with(&sql, arguments)
.traced()
.fetch_all(&mut *self.conn)
.await?
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?;
.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

View File

@@ -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<Ulid> for SessionLookup {
fn cursor(&self) -> Ulid {
self.user_id.into()
}
}
impl TryFrom<SessionLookup> for BrowserSession {
type Error = DatabaseInconsistencyError;

View File

@@ -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);

View File

@@ -16,12 +16,12 @@ pub struct InvalidPagination;
/// Pagination parameters
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pagination {
pub struct Pagination<Cursor = Ulid> {
/// The cursor to start from
pub before: Option<Ulid>,
pub before: Option<Cursor>,
/// The cursor to end at
pub after: Option<Ulid>,
pub after: Option<Cursor>,
/// 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<C = Ulid> {
/// The cursor of that particular node
fn cursor(&self) -> C;
}
impl<C> Pagination<C> {
/// 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<Ulid>,
after: Option<Ulid>,
pub fn try_new(
before: Option<C>,
after: Option<C>,
first: Option<usize>,
last: Option<usize>,
) -> Result<Self, InvalidPagination> {
@@ -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<T>(&self, mut edges: Vec<T>) -> Page<T> {
let is_full = edges.len() == (self.count + 1);
pub fn process<T: Node<C>>(&self, mut nodes: Vec<T>) -> Page<T, C> {
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<T, C = Ulid> {
/// 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<T> {
pub struct Page<T, C = Ulid> {
/// When paginating forwards, this is true if there are more items after
pub has_next_page: bool,
@@ -152,21 +175,28 @@ pub struct Page<T> {
pub has_previous_page: bool,
/// The items in the page
pub edges: Vec<T>,
pub edges: Vec<Edge<T, C>>,
}
impl<T> Page<T> {
impl<T, C> Page<T, C> {
/// Map the items in this page with the given function
///
/// # Parameters
///
/// * `f`: The function to map the items with
#[must_use]
pub fn map<F, T2>(self, f: F) -> Page<T2>
pub fn map<F, T2>(self, mut f: F) -> Page<T2, C>
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<T> Page<T> {
/// # Errors
///
/// Returns the first error encountered while mapping the items
pub fn try_map<F, E, T2>(self, f: F) -> Result<Page<T2>, E>
pub fn try_map<F, E, T2>(self, mut f: F) -> Result<Page<T2, C>, E>
where
F: FnMut(T) -> Result<T2, E>,
{
let edges: Result<Vec<T2>, E> = self.edges.into_iter().map(f).collect();
let edges: Result<Vec<Edge<T2, C>>, 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,

View File

@@ -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),
})
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)?;
}

View File

@@ -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"
}
}
},