From 8c943b715c4f2497553394ea10f1e5bc2ce79f8a Mon Sep 17 00:00:00 2001 From: Centeno448 Date: Thu, 12 Sep 2024 20:55:29 -0500 Subject: [PATCH 1/4] wip --- backend-rs/src/api/flake.rs | 82 ++++++++++++++++++++++++++++++++++++- backend-rs/src/main.rs | 3 +- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/backend-rs/src/api/flake.rs b/backend-rs/src/api/flake.rs index 61d044c..dfbaca1 100644 --- a/backend-rs/src/api/flake.rs +++ b/backend-rs/src/api/flake.rs @@ -1,6 +1,8 @@ use anyhow::Context; use axum::{ - extract::{Query, State}, + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, Json, }; use chrono::NaiveDateTime; @@ -55,6 +57,15 @@ impl FromRow<'_, PgRow> for FlakeRelease { } } +#[derive(Debug)] +struct RepoId(i32); + +impl FromRow<'_, PgRow> for RepoId { + fn from_row(row: &PgRow) -> sqlx::Result { + Ok(Self(row.try_get("id")?)) + } +} + #[derive(serde::Serialize)] pub struct GetFlakeResponse { releases: Vec, @@ -62,6 +73,11 @@ pub struct GetFlakeResponse { query: Option, } +#[derive(serde::Serialize)] +pub struct RepoResponse { + releases: Vec, +} + pub async fn get_flake( State(state): State>, Query(mut params): Query>, @@ -89,6 +105,70 @@ pub async fn get_flake( })); } +pub async fn read_repo( + Path((owner, repo)): Path<(String, String)>, + State(state): State>, +) -> Result { + let repo_id = get_repo_id(&owner, &repo, &state.pool).await?; + + if let Some(repo_id) = repo_id { + let mut releases = get_repo_releases(&repo_id, &state.pool).await?; + + if !releases.is_empty() { + releases.sort_by(|a, b| a.version.cmp(&b.version)); + releases.reverse(); + } + + return Ok(Json(RepoResponse { releases }).into_response()); + } else { + return Ok((StatusCode::NOT_FOUND, ()).into_response()); + } +} + +async fn get_repo_id( + owner: &str, + repo: &str, + pool: &Pool, +) -> Result, sqlx::Error> { + let query = "SELECT githubrepo.id as id \ + FROM githubrepo \ + INNER JOIN githubowner ON githubowner.id = githubrepo.owner_id \ + WHERE githubrepo.name = $1 AND githubowner.name = $2 LIMIT 1"; + + let repo_id: Option = sqlx::query_as(&query) + .bind(&repo) + .bind(&owner) + .fetch_optional(pool) + .await?; + + Ok(repo_id) +} + +async fn get_repo_releases( + repo_id: &RepoId, + pool: &Pool, +) -> Result, sqlx::Error> { + let query = format!( + "SELECT release.id AS id, \ + githubowner.name AS owner, \ + githubrepo.name AS repo, \ + release.version AS version, \ + release.description AS description, \ + release.created_at AS created_at \ + FROM release \ + INNER JOIN githubrepo ON githubrepo.id = release.repo_id \ + INNER JOIN githubowner ON githubowner.id = githubrepo.owner_id \ + WHERE release.repo_id = $1", + ); + + let releases: Vec = sqlx::query_as(&query) + .bind(&repo_id.0) + .fetch_all(pool) + .await?; + + Ok(releases) +} + async fn get_flakes_by_ids( flake_ids: Vec<&i32>, pool: &Pool, diff --git a/backend-rs/src/main.rs b/backend-rs/src/main.rs index 08dc892..7188a05 100644 --- a/backend-rs/src/main.rs +++ b/backend-rs/src/main.rs @@ -20,7 +20,7 @@ use tracing::{field, info_span, Span}; use tracing_subscriber::{fmt, EnvFilter}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use crate::api::{get_flake, post_publish}; +use crate::api::{get_flake, post_publish, read_repo}; use crate::common::AppState; #[tokio::main] @@ -68,6 +68,7 @@ async fn add_ip_trace( fn app(state: Arc) -> Router { let api = Router::new() .route("/flake", get(get_flake)) + .route("/flake/github/:owner/:repo", get(read_repo)) .route("/publish", post(post_publish)); Router::new() .nest("/api", api) From 12d997ae5562f05c8c2841977e83ba0da10a3c35 Mon Sep 17 00:00:00 2001 From: Centeno448 Date: Fri, 4 Oct 2024 13:47:37 -0500 Subject: [PATCH 2/4] use updated error type --- backend-rs/src/api/flake.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend-rs/src/api/flake.rs b/backend-rs/src/api/flake.rs index dfbaca1..72267e5 100644 --- a/backend-rs/src/api/flake.rs +++ b/backend-rs/src/api/flake.rs @@ -129,7 +129,7 @@ async fn get_repo_id( owner: &str, repo: &str, pool: &Pool, -) -> Result, sqlx::Error> { +) -> Result, AppError> { let query = "SELECT githubrepo.id as id \ FROM githubrepo \ INNER JOIN githubowner ON githubowner.id = githubrepo.owner_id \ @@ -139,7 +139,8 @@ async fn get_repo_id( .bind(&repo) .bind(&owner) .fetch_optional(pool) - .await?; + .await + .context("Failed to fetch repo id from database")?; Ok(repo_id) } @@ -147,7 +148,7 @@ async fn get_repo_id( async fn get_repo_releases( repo_id: &RepoId, pool: &Pool, -) -> Result, sqlx::Error> { +) -> Result, AppError> { let query = format!( "SELECT release.id AS id, \ githubowner.name AS owner, \ @@ -164,7 +165,8 @@ async fn get_repo_releases( let releases: Vec = sqlx::query_as(&query) .bind(&repo_id.0) .fetch_all(pool) - .await?; + .await + .context("Failed to fetch repo releases from database")?; Ok(releases) } From 84f7c7e8b571febd9ff3f0e6f85477a94d33d401 Mon Sep 17 00:00:00 2001 From: Centeno448 Date: Fri, 4 Oct 2024 14:21:08 -0500 Subject: [PATCH 3/4] update tests --- backend-rs/src/main.rs | 70 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/backend-rs/src/main.rs b/backend-rs/src/main.rs index 7188a05..a28f614 100644 --- a/backend-rs/src/main.rs +++ b/backend-rs/src/main.rs @@ -111,6 +111,7 @@ mod tests { use std::env; use tokio::net::TcpListener; use tokio::task::JoinHandle; + use serde_json::Value; use url::Url; pub struct TestApp { @@ -163,39 +164,96 @@ mod tests { } } + fn json_from_str(str: &str) -> Value + { + serde_json::from_str(str).unwrap() + } + #[tokio::test] async fn test_get_flake_with_params() { let app = TestApp::new().await; - let expected_response = "{\"releases\":[{\"owner\":\"nix-community\",\"repo\":\"home-manager\",\"version\":\"23.05\",\"description\":\"\",\"created_at\":\"2024-07-12T23:08:41.029566\"}],\"count\":1,\"query\":\"search\"}"; + let expected_response = json_from_str(r#" + { + "releases": [ + { + "owner": "nix-community", + "repo": "home-manager", + "version": "23.05", + "description": "", + "created_at": "2024-09-21T16:28:15.924267" + } + ], + "count": 1, + "query": "search" + }"#); + let response = app.get("/api/flake?q=search").send().await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.text().await.unwrap(); - assert_eq!(body, expected_response); + let response: Value = json_from_str(&body); + + assert_eq!(response, expected_response); } #[tokio::test] async fn test_get_flake_with_params_no_result() { let app = TestApp::new().await; - let expected_response = "{\"releases\":[],\"count\":0,\"query\":\"nothing\"}"; + let expected_response = json_from_str(r#" + { + "releases": [], + "count": 0, + "query": "nothing" + }"#); let response = app.get("/api/flake?q=nothing").send().await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.text().await.unwrap(); - assert_eq!(body, expected_response); + let response = json_from_str(&body); + + assert_eq!(response, expected_response); } #[tokio::test] async fn test_get_flake_without_params() { let app = TestApp::new().await; - let expected_response = "{\"releases\":[{\"owner\":\"nix-community\",\"repo\":\"home-manager\",\"version\":\"23.05\",\"description\":\"\",\"created_at\":\"2024-07-12T23:08:41.029566\"},{\"owner\":\"nixos\",\"repo\":\"nixpkgs\",\"version\":\"22.05\",\"description\":\"nixpkgs is official package collection\",\"created_at\":\"2024-07-12T23:08:41.005518\"},{\"owner\":\"nixos\",\"repo\":\"nixpkgs\",\"version\":\"23.05\",\"description\":\"nixpkgs is official package collection\",\"created_at\":\"2024-07-12T23:08:41.005518\"}],\"count\":3,\"query\":null}"; + let expected_response = json_from_str(r#" + { + "releases": [ + { + "owner": "nixos", + "repo": "nixpkgs", + "version": "23.05", + "description": "nixpkgs is official package collection", + "created_at": "2024-09-21T16:28:15.924267" + }, + { + "owner": "nix-community", + "repo": "home-manager", + "version": "23.05", + "description": "", + "created_at": "2024-09-21T16:28:15.924267" + }, + { + "owner": "nixos", + "repo": "nixpkgs", + "version": "22.05", + "description": "nixpkgs is official package collection", + "created_at": "2024-09-21T16:28:15.923266" + } + ], + "count": 3, + "query": null + }"#); let response = app.get("/api/flake").send().await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.text().await.unwrap(); - assert_eq!(body, expected_response); + let response = json_from_str(&body); + + assert_eq!(expected_response, response); } } From 093208193df94919b9439f5bd295cfbcbdeb46e5 Mon Sep 17 00:00:00 2001 From: Centeno448 Date: Fri, 4 Oct 2024 16:22:54 -0500 Subject: [PATCH 4/4] handle not found case --- backend-rs/src/api/flake.rs | 85 +++++++++++++++++++++++++++++++++---- backend-rs/src/main.rs | 38 ++++++++++++++++- 2 files changed, 112 insertions(+), 11 deletions(-) diff --git a/backend-rs/src/api/flake.rs b/backend-rs/src/api/flake.rs index 72267e5..c91b598 100644 --- a/backend-rs/src/api/flake.rs +++ b/backend-rs/src/api/flake.rs @@ -2,7 +2,7 @@ use anyhow::Context; use axum::{ extract::{Path, Query, State}, http::StatusCode, - response::IntoResponse, + response::{IntoResponse, Response}, Json, }; use chrono::NaiveDateTime; @@ -13,6 +13,50 @@ use std::{cmp::Ordering, collections::HashMap, sync::Arc}; use crate::common::{AppError, AppState}; +#[derive(serde::Serialize)] +struct FlakeReleaseCompact { + #[serde(skip_serializing)] + id: i32, + owner: String, + repo: String, + version: String, + description: String, + created_at: NaiveDateTime, +} + +impl Eq for FlakeReleaseCompact {} + +impl Ord for FlakeReleaseCompact { + fn cmp(&self, other: &Self) -> Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for FlakeReleaseCompact { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for FlakeReleaseCompact { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl FromRow<'_, PgRow> for FlakeReleaseCompact { + fn from_row(row: &PgRow) -> sqlx::Result { + Ok(Self { + id: row.try_get("id")?, + owner: row.try_get("owner")?, + repo: row.try_get("repo")?, + version: row.try_get("version")?, + description: row.try_get("description").unwrap_or_default(), + created_at: row.try_get("created_at")?, + }) + } +} + #[derive(serde::Serialize)] struct FlakeRelease { #[serde(skip_serializing)] @@ -22,6 +66,8 @@ struct FlakeRelease { version: String, description: String, created_at: NaiveDateTime, + commit: String, + readme: String } impl Eq for FlakeRelease {} @@ -53,6 +99,8 @@ impl FromRow<'_, PgRow> for FlakeRelease { version: row.try_get("version")?, description: row.try_get("description").unwrap_or_default(), created_at: row.try_get("created_at")?, + commit: row.try_get("commit")?, + readme: row.try_get("readme")?, }) } } @@ -68,7 +116,7 @@ impl FromRow<'_, PgRow> for RepoId { #[derive(serde::Serialize)] pub struct GetFlakeResponse { - releases: Vec, + releases: Vec, count: usize, query: Option, } @@ -78,6 +126,23 @@ pub struct RepoResponse { releases: Vec, } +#[derive(serde::Serialize)] +pub struct NotFoundResponse +{ + detail: String, +} + +impl NotFoundResponse +{ + pub fn build() -> Self + { + NotFoundResponse + { + detail: "Not Found".into() + } + } +} + pub async fn get_flake( State(state): State>, Query(mut params): Query>, @@ -108,7 +173,7 @@ pub async fn get_flake( pub async fn read_repo( Path((owner, repo)): Path<(String, String)>, State(state): State>, -) -> Result { +) -> Result { let repo_id = get_repo_id(&owner, &repo, &state.pool).await?; if let Some(repo_id) = repo_id { @@ -119,9 +184,9 @@ pub async fn read_repo( releases.reverse(); } - return Ok(Json(RepoResponse { releases }).into_response()); + return Ok((StatusCode::OK, Json(RepoResponse { releases })).into_response()); } else { - return Ok((StatusCode::NOT_FOUND, ()).into_response()); + return Ok((StatusCode::NOT_FOUND, Json(NotFoundResponse::build())).into_response()); } } @@ -155,6 +220,8 @@ async fn get_repo_releases( githubrepo.name AS repo, \ release.version AS version, \ release.description AS description, \ + release.commit AS commit, \ + release.readme AS readme, \ release.created_at AS created_at \ FROM release \ INNER JOIN githubrepo ON githubrepo.id = release.repo_id \ @@ -174,7 +241,7 @@ async fn get_repo_releases( async fn get_flakes_by_ids( flake_ids: Vec<&i32>, pool: &Pool, -) -> Result, AppError> { +) -> Result, AppError> { if flake_ids.is_empty() { return Ok(vec![]); } @@ -195,7 +262,7 @@ async fn get_flakes_by_ids( WHERE release.id IN ({param_string})", ); - let releases: Vec = + let releases: Vec = sqlx::query_as(&query) .fetch_all(pool) .await @@ -204,8 +271,8 @@ async fn get_flakes_by_ids( Ok(releases) } -async fn get_flakes(pool: &Pool) -> Result, AppError> { - let releases: Vec = sqlx::query_as( +async fn get_flakes(pool: &Pool) -> Result, AppError> { + let releases: Vec = sqlx::query_as( "SELECT release.id AS id, \ githubowner.name AS owner, \ githubrepo.name AS repo, \ diff --git a/backend-rs/src/main.rs b/backend-rs/src/main.rs index a28f614..9ec92be 100644 --- a/backend-rs/src/main.rs +++ b/backend-rs/src/main.rs @@ -164,9 +164,9 @@ mod tests { } } - fn json_from_str(str: &str) -> Value + fn json_from_str(s: &str) -> Value { - serde_json::from_str(str).unwrap() + serde_json::from_str(s).unwrap() } #[tokio::test] @@ -256,4 +256,38 @@ mod tests { assert_eq!(expected_response, response); } + + #[tokio::test] + async fn test_read_repo_non_existent_repo() { + let app = TestApp::new().await; + let expected_response = json_from_str(r#" + { + "detail": "Not Found" + }"#); + + let response = app.get("/api/flake/github/nixos/doesnotexist").send().await.unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let body = response.text().await.unwrap(); + let response = json_from_str(&body); + + assert_eq!(expected_response, response); + } + + #[tokio::test] + async fn test_read_repo_non_existent_owner() { + let app = TestApp::new().await; + let expected_response = json_from_str(r#" + { + "detail": "Not Found" + }"#); + + let response = app.get("/api/flake/github/unkownowner/nixpkgs").send().await.unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let body = response.text().await.unwrap(); + let response = json_from_str(&body); + + assert_eq!(expected_response, response); + } }