Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/controllers/trustpub/github_configs/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,18 @@ pub struct CreateResponse {
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct ListResponse {
pub github_configs: Vec<GitHubConfig>,

#[schema(inline)]
pub meta: ListResponseMeta,
}

#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct ListResponseMeta {
/// The total number of GitHub configs belonging to the crate.
#[schema(example = 42)]
pub total: i64,

/// Query string to the next page of results, if any.
#[schema(example = "?seek=abc123")]
pub next_page: Option<String>,
}
108 changes: 101 additions & 7 deletions src/controllers/trustpub/github_configs/list/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use crate::app::AppState;
use crate::auth::AuthCheck;
use crate::controllers::helpers::pagination::{
Page, PaginationOptions, PaginationQueryParams, encode_seek,
};
use crate::controllers::krate::load_crate;
use crate::controllers::trustpub::github_configs::json::{self, ListResponse};
use crate::controllers::trustpub::github_configs::json::{self, ListResponse, ListResponseMeta};
use crate::util::RequestUtils;
use crate::util::errors::{AppResult, bad_request};
use axum::Json;
use axum::extract::{FromRequestParts, Query};
Expand All @@ -13,6 +17,7 @@ use diesel::dsl::{exists, select};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use http::request::Parts;
use indexmap::IndexMap;
use serde::Deserialize;

#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
Expand All @@ -28,7 +33,7 @@ pub struct ListQueryParams {
#[utoipa::path(
get,
path = "/api/v1/trusted_publishing/github_configs",
params(ListQueryParams),
params(ListQueryParams, PaginationQueryParams),
security(("cookie" = []), ("api_token" = [])),
tag = "trusted_publishing",
responses((status = 200, description = "Successful Response", body = inline(ListResponse))),
Expand Down Expand Up @@ -64,10 +69,13 @@ pub async fn list_trustpub_github_configs(
return Err(bad_request("You are not an owner of this crate"));
}

let configs = GitHubConfig::query()
.filter(trustpub_configs_github::crate_id.eq(krate.id))
.load(&mut conn)
.await?;
let pagination = PaginationOptions::builder()
.enable_seek(true)
.enable_pages(false)
.gather(&parts)?;

let (configs, total, next_page) =
list_configs(&mut conn, krate.id, &pagination, &parts).await?;

let github_configs = configs
.into_iter()
Expand All @@ -83,5 +91,91 @@ pub async fn list_trustpub_github_configs(
})
.collect();

Ok(Json(ListResponse { github_configs }))
Ok(Json(ListResponse {
github_configs,
meta: ListResponseMeta { total, next_page },
}))
}

async fn list_configs(
conn: &mut diesel_async::AsyncPgConnection,
crate_id: i32,
options: &PaginationOptions,
req: &Parts,
) -> AppResult<(Vec<GitHubConfig>, i64, Option<String>)> {
use seek::*;

let seek = Seek::Id;

assert!(
!matches!(&options.page, Page::Numeric(_)),
"?page= is not supported"
);

let make_base_query = || {
GitHubConfig::query()
.filter(trustpub_configs_github::crate_id.eq(crate_id))
.into_boxed()
};

let mut query = make_base_query();
query = query.limit(options.per_page);
query = query.order(trustpub_configs_github::id.asc());

if let Some(SeekPayload::Id(Id { id })) = seek.after(&options.page)? {
query = query.filter(trustpub_configs_github::id.gt(id));
}

let data: Vec<GitHubConfig> = query.load(conn).await?;

let next_page = next_seek_params(&data, options, |last| seek.to_payload(last))?
.map(|p| req.query_with_params(p));

// Avoid the count query if we're on the first page and got fewer results than requested
let total =
if matches!(options.page, Page::Unspecified) && data.len() < options.per_page as usize {
data.len() as i64
} else {
make_base_query().count().get_result(conn).await?
};

Ok((data, total, next_page))
}

fn next_seek_params<T, S, F>(
records: &[T],
options: &PaginationOptions,
f: F,
) -> AppResult<Option<IndexMap<String, String>>>
where
F: Fn(&T) -> S,
S: serde::Serialize,
{
if records.len() < options.per_page as usize {
return Ok(None);
}

let seek = f(records.last().unwrap());
let mut opts = IndexMap::new();
opts.insert("seek".into(), encode_seek(seek)?);
Ok(Some(opts))
}

mod seek {
use crate::controllers::helpers::pagination::seek;
use crates_io_database::models::trustpub::GitHubConfig;

seek!(
pub enum Seek {
Id { id: i32 },
}
);

impl Seek {
pub(crate) fn to_payload(&self, record: &GitHubConfig) -> SeekPayload {
match *self {
Seek::Id => SeekPayload::Id(Id { id: record.id }),
}
}
}
}
64 changes: 64 additions & 0 deletions src/tests/routes/trustpub/github_configs/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,67 @@ async fn test_token_auth_with_wildcard_crate_scope() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_pagination() -> anyhow::Result<()> {
let (app, _, cookie_client) = TestApp::full().with_user().await;
let mut conn = app.db_conn().await;

let owner_id = cookie_client.as_model().id;
let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?;

// Create 15 configs
for i in 0..15 {
create_config(&mut conn, krate.id, &format!("repo-{i}")).await?;
}

// Request first page with per_page=5
let response = cookie_client
.get_with_query::<()>(URL, "crate=foo&per_page=5")
.await;
assert_snapshot!(response.status(), @"200 OK");
let json = response.json();
assert_json_snapshot!(json, {
".github_configs[].created_at" => "[datetime]",
});

// Extract the next_page URL and make a second request
let next_page = json["meta"]["next_page"]
.as_str()
.expect("next_page should be present");
let next_url = format!("{}{}", URL, next_page);
let response = cookie_client.get::<()>(&next_url).await;
assert_snapshot!(response.status(), @"200 OK");
let json = response.json();
assert_json_snapshot!(json, {
".github_configs[].created_at" => "[datetime]",
});

// Third page (last page with data)
let next_page = json["meta"]["next_page"]
.as_str()
.expect("next_page should be present");
let next_url = format!("{}{}", URL, next_page);
let response = cookie_client.get::<()>(&next_url).await;
assert_snapshot!(response.status(), @"200 OK");
let json = response.json();
assert_json_snapshot!(json, {
".github_configs[].created_at" => "[datetime]",
});

// The third page has exactly 5 items, so next_page will be present
// (cursor-based pagination is conservative about indicating more pages)
// Following it should give us an empty fourth page
let next_page = json["meta"]["next_page"]
.as_str()
.expect("next_page should be present on third page");
let next_url = format!("{}{}", URL, next_page);
let response = cookie_client.get::<()>(&next_url).await;
assert_snapshot!(response.status(), @"200 OK");
let json = response.json();
assert_json_snapshot!(json, {
".github_configs[].created_at" => "[datetime]",
});

Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ source: src/tests/routes/trustpub/github_configs/list.rs
expression: response.json()
---
{
"github_configs": []
"github_configs": [],
"meta": {
"next_page": null,
"total": 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ expression: response.json()
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
}
]
],
"meta": {
"next_page": null,
"total": 2
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ expression: response.json()
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
}
]
],
"meta": {
"next_page": null,
"total": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ expression: response.json()
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
}
]
],
"meta": {
"next_page": null,
"total": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
source: src/tests/routes/trustpub/github_configs/list.rs
expression: json
---
{
"github_configs": [
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 1,
"repository_name": "repo-0",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 2,
"repository_name": "repo-1",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 3,
"repository_name": "repo-2",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 4,
"repository_name": "repo-3",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 5,
"repository_name": "repo-4",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
}
],
"meta": {
"next_page": "?crate=foo&per_page=5&seek=NQ",
"total": 15
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
source: src/tests/routes/trustpub/github_configs/list.rs
expression: json
---
{
"github_configs": [
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 6,
"repository_name": "repo-5",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 7,
"repository_name": "repo-6",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 8,
"repository_name": "repo-7",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 9,
"repository_name": "repo-8",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
},
{
"crate": "foo",
"created_at": "[datetime]",
"environment": null,
"id": 10,
"repository_name": "repo-9",
"repository_owner": "rust-lang",
"repository_owner_id": 42,
"workflow_filename": "publish.yml"
}
],
"meta": {
"next_page": "?crate=foo&per_page=5&seek=MTA",
"total": 15
}
}
Loading