Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read repo endpoint #24

Draft
wants to merge 4 commits into
base: rewrite_backend_rust
Choose a base branch
from
Draft
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
161 changes: 155 additions & 6 deletions backend-rs/src/api/flake.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Ordering> {
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<Self> {
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)]
Expand All @@ -20,6 +66,8 @@ struct FlakeRelease {
version: String,
description: String,
created_at: NaiveDateTime,
commit: String,
readme: String
}

impl Eq for FlakeRelease {}
Expand Down Expand Up @@ -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<Self> {
Ok(Self(row.try_get("id")?))
}
}

#[derive(serde::Serialize)]
pub struct GetFlakeResponse {
releases: Vec<FlakeRelease>,
releases: Vec<FlakeReleaseCompact>,
count: usize,
query: Option<String>,
}

#[derive(serde::Serialize)]
pub struct RepoResponse {
releases: Vec<FlakeRelease>,
}

#[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<Arc<AppState>>,
Query(mut params): Query<HashMap<String, String>>,
Expand Down Expand Up @@ -89,10 +170,78 @@ pub async fn get_flake(
}));
}

pub async fn read_repo(
Path((owner, repo)): Path<(String, String)>,
State(state): State<Arc<AppState>>,
) -> Result<Response, AppError> {
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<Postgres>,
) -> Result<Option<RepoId>, 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<RepoId> = 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<Postgres>,
) -> Result<Vec<FlakeRelease>, 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<FlakeRelease> = 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<Postgres>,
) -> Result<Vec<FlakeRelease>, AppError> {
) -> Result<Vec<FlakeReleaseCompact>, AppError> {
if flake_ids.is_empty() {
return Ok(vec![]);
}
Expand All @@ -113,7 +262,7 @@ async fn get_flakes_by_ids(
WHERE release.id IN ({param_string})",
);

let releases: Vec<FlakeRelease> =
let releases: Vec<FlakeReleaseCompact> =
sqlx::query_as(&query)
.fetch_all(pool)
.await
Expand All @@ -122,8 +271,8 @@ async fn get_flakes_by_ids(
Ok(releases)
}

async fn get_flakes(pool: &Pool<Postgres>) -> Result<Vec<FlakeRelease>, AppError> {
let releases: Vec<FlakeRelease> = sqlx::query_as(
async fn get_flakes(pool: &Pool<Postgres>) -> Result<Vec<FlakeReleaseCompact>, AppError> {
let releases: Vec<FlakeReleaseCompact> = sqlx::query_as(
"SELECT release.id AS id, \
githubowner.name AS owner, \
githubrepo.name AS repo, \
Expand Down
107 changes: 100 additions & 7 deletions backend-rs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -68,6 +68,7 @@ async fn add_ip_trace(
fn app(state: Arc<AppState>) -> 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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
Loading