diff --git a/backend-rs/src/api/flake.rs b/backend-rs/src/api/flake.rs index 61d044c..c91b598 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, Response}, Json, }; use chrono::NaiveDateTime; @@ -11,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)] @@ -20,6 +66,8 @@ struct FlakeRelease { version: String, description: String, created_at: NaiveDateTime, + commit: String, + readme: String } impl Eq for FlakeRelease {} @@ -51,17 +99,50 @@ 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")?, }) } } +#[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, + releases: Vec, count: usize, query: Option, } +#[derive(serde::Serialize)] +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>, @@ -89,10 +170,78 @@ 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((StatusCode::OK, Json(RepoResponse { releases })).into_response()); + } else { + return Ok((StatusCode::NOT_FOUND, Json(NotFoundResponse::build())).into_response()); + } +} + +async fn get_repo_id( + owner: &str, + repo: &str, + pool: &Pool, +) -> Result, AppError> { + 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 + .context("Failed to fetch repo id from database")?; + + Ok(repo_id) +} + +async fn get_repo_releases( + repo_id: &RepoId, + pool: &Pool, +) -> Result, AppError> { + 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.commit AS commit, \ + release.readme AS readme, \ + 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 + .context("Failed to fetch repo releases from database")?; + + Ok(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![]); } @@ -113,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 @@ -122,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 08dc892..9ec92be 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) @@ -110,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 { @@ -162,39 +164,130 @@ mod tests { } } + fn json_from_str(s: &str) -> Value + { + serde_json::from_str(s).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); + } + + #[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); } }