refactor: use serde-dynamo

This commit is contained in:
2026-01-20 19:49:12 -08:00
parent 33e1db94f5
commit 37fa5d4838
8 changed files with 69 additions and 74 deletions

1
api/Cargo.lock generated
View File

@@ -3381,6 +3381,7 @@ version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873a97c3f7a67dd042bceb47d056d288424b82d4c66b0a25e1a3b34675620951" checksum = "873a97c3f7a67dd042bceb47d056d288424b82d4c66b0a25e1a3b34675620951"
dependencies = [ dependencies = [
"aws-sdk-dynamodb",
"base64 0.21.7", "base64 0.21.7",
"serde", "serde",
"serde_core", "serde_core",

View File

@@ -13,7 +13,7 @@ mime_guess = "2.0.5"
reqwest = { version = "0.13.1", features = ["json", "query"] } reqwest = { version = "0.13.1", features = ["json", "query"] }
sentry = { version = "0.46.1", features = ["actix", "tracing"] } sentry = { version = "0.46.1", features = ["actix", "tracing"] }
serde = "1.0.228" serde = "1.0.228"
serde_dynamo = "4.3.0" serde_dynamo = { version = "4.3.0", features = ["aws-sdk-dynamodb+1"] }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls"] }
thiserror = "2.0.17" thiserror = "2.0.17"
tokio = "1.49.0" tokio = "1.49.0"

View File

@@ -1,4 +1,4 @@
use crate::{auth::User, error::Result, user::RepositorySchema, validate::validate_repo}; use crate::{auth::User, error::Result, user::RepositoryDefinition, validate::validate_repo};
use actix_web::{HttpResponse, web}; use actix_web::{HttpResponse, web};
use crate::AppState; use crate::AppState;
@@ -6,7 +6,7 @@ use crate::AppState;
pub async fn add_repo( pub async fn add_repo(
app_state: web::Data<AppState>, app_state: web::Data<AppState>,
user: web::ReqData<User>, user: web::ReqData<User>,
repo: web::Json<RepositorySchema>, repo: web::Json<RepositoryDefinition>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
validate_repo(app_state.clone(), &repo, &user.id).await?; validate_repo(app_state.clone(), &repo, &user.id).await?;
app_state app_state

View File

@@ -6,6 +6,7 @@ use actix_web::HttpResponse;
use aws_sdk_dynamodb::types::AttributeValue; use aws_sdk_dynamodb::types::AttributeValue;
use chrono::Utc; use chrono::Utc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_dynamo::from_item;
use sqlx::{PgPool, query_scalar}; use sqlx::{PgPool, query_scalar};
pub struct UserRepository { pub struct UserRepository {
@@ -14,8 +15,40 @@ pub struct UserRepository {
table_name: String, table_name: String,
} }
#[derive(Deserialize)]
struct RepositoryDB {
sk: String,
full_name: String,
owner_id: String,
description: String,
#[serde(default)]
approved: bool,
}
impl RepositoryDB {
fn into_repository(self) -> Option<RepositoryDefinition> {
Some(RepositoryDefinition {
id: self.sk.strip_prefix("REPO#")?.to_string(),
full_name: self.full_name,
owner_id: self.owner_id,
description: self.description,
})
}
fn into_global_response(self) -> Option<GlobalRepositoriesResponse> {
if !self.approved {
return None;
}
Some(GlobalRepositoriesResponse {
id: self.sk.strip_prefix("REPO#")?.to_string(),
full_name: self.full_name,
description: self.description,
})
}
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct RepositorySchema { pub struct RepositoryDefinition {
pub id: String, pub id: String,
pub full_name: String, pub full_name: String,
#[serde(skip)] #[serde(skip)]
@@ -48,7 +81,7 @@ impl UserRepository {
.map_err(|_| crate::error::Error::AccessToken) .map_err(|_| crate::error::Error::AccessToken)
} }
pub async fn get_repositories_user(&self, user_id: &str) -> Result<Vec<RepositorySchema>> { pub async fn get_repositories_user(&self, user_id: &str) -> Result<Vec<RepositoryDefinition>> {
let response = self let response = self
.dynamodb_client .dynamodb_client
.query() .query()
@@ -57,25 +90,18 @@ impl UserRepository {
.expression_attribute_values(":pk", AttributeValue::S(format!("USER#{user_id}"))) .expression_attribute_values(":pk", AttributeValue::S(format!("USER#{user_id}")))
.expression_attribute_values(":sk", AttributeValue::S("REPO#".into())) .expression_attribute_values(":sk", AttributeValue::S("REPO#".into()))
.send() .send()
.await? .await?;
let repos = response
.items() .items()
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
Some(RepositorySchema { let dynamo_repo: RepositoryDB = from_item(item.clone()).ok()?;
id: item dynamo_repo.into_repository()
.get("sk")?
.as_s()
.ok()?
.strip_prefix("REPO#")?
.to_string(),
full_name: item.get("full_name")?.as_s().ok()?.to_string(),
owner_id: item.get("owner_id")?.as_s().ok()?.to_string(),
description: item.get("description")?.as_s().ok()?.to_string(),
}) })
}) .collect();
.collect::<Vec<RepositorySchema>>();
Ok(response) Ok(repos)
} }
pub async fn global_repositories(&self) -> Result<HttpResponse> { pub async fn global_repositories(&self) -> Result<HttpResponse> {
@@ -87,30 +113,24 @@ impl UserRepository {
.key_condition_expression("gsi1pk = :pk") .key_condition_expression("gsi1pk = :pk")
.expression_attribute_values(":pk", AttributeValue::S("REPOS".into())) .expression_attribute_values(":pk", AttributeValue::S("REPOS".into()))
.send() .send()
.await? .await?;
let repos: Vec<GlobalRepositoriesResponse> = response
.items() .items()
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
if (*item.get("approved")?.as_bool().ok()?) == false { let dynamo_repo: RepositoryDB = from_item(item.clone()).ok()?;
return None; dynamo_repo.into_global_response()
};
Some(GlobalRepositoriesResponse {
id: item
.get("sk")?
.as_s()
.ok()?
.strip_prefix("REPO#")?
.to_string(),
full_name: item.get("full_name")?.as_s().ok()?.to_string(),
description: item.get("description")?.as_s().ok()?.to_string(),
}) })
}) .collect();
.collect::<Vec<GlobalRepositoriesResponse>>();
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(repos))
} }
pub async fn get_approved_repository(&self, repo_id: &str) -> Result<Option<RepositorySchema>> { pub async fn get_approved_repository(
&self,
repo_id: &str,
) -> Result<Option<RepositoryDefinition>> {
let response = self let response = self
.dynamodb_client .dynamodb_client
.query() .query()
@@ -125,17 +145,8 @@ impl UserRepository {
.await?; .await?;
let repo = response.items().first().and_then(|item| { let repo = response.items().first().and_then(|item| {
Some(RepositorySchema { let dynamo_repo: RepositoryDB = from_item(item.clone()).ok()?;
id: item dynamo_repo.into_repository()
.get("sk")?
.as_s()
.ok()?
.strip_prefix("REPO#")?
.to_string(),
full_name: item.get("full_name")?.as_s().ok()?.to_string(),
owner_id: item.get("owner_id")?.as_s().ok()?.to_string(),
description: item.get("description")?.as_s().ok()?.to_string(),
})
}); });
Ok(repo) Ok(repo)
@@ -144,7 +155,7 @@ impl UserRepository {
pub async fn add_repository( pub async fn add_repository(
&self, &self,
user_id: &str, user_id: &str,
repo: RepositorySchema, repo: RepositoryDefinition,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let now = Utc::now().to_rfc3339(); let now = Utc::now().to_rfc3339();
let response = self let response = self

View File

@@ -2,11 +2,11 @@ use actix_web::web;
use crate::AppState; use crate::AppState;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::user::RepositorySchema; use crate::user::RepositoryDefinition;
pub async fn validate_repo( pub async fn validate_repo(
app_state: web::Data<AppState>, app_state: web::Data<AppState>,
repo: &RepositorySchema, repo: &RepositoryDefinition,
user_id: &str, user_id: &str,
) -> Result<()> { ) -> Result<()> {
let token = app_state.user.get_access_token(&user_id).await?; let token = app_state.user.get_access_token(&user_id).await?;

View File

@@ -19,7 +19,7 @@
</script> </script>
<div class="h-14 w-full bg-[#292e42] sm:p-2"> <div class="h-14 w-full bg-[#292e42] sm:p-2">
<div class="mx-auto flex h-full max-w-[78rem] items-center justify-between"> <div class="mx-auto flex h-full max-w-312 items-center justify-between">
<Button.Root onclick={() => goto('/')}> <Button.Root onclick={() => goto('/')}>
<h1 class="header-title font-light">Project Host</h1> <h1 class="header-title font-light">Project Host</h1>
</Button.Root> </Button.Root>

View File

@@ -18,27 +18,9 @@
staleTime: 60000 staleTime: 60000
})); }));
const projectsQuery = createQuery<Project[]>(() => ({ const projects = $derived(reposQuery.data ?? []);
queryKey: ['projects', reposQuery.data], const isLoading = $derived(reposQuery.isPending);
queryFn: async () => { const hasError = $derived(reposQuery.isError);
const repos = reposQuery.data ?? [];
const projects = await Promise.all(
repos.map(async (repo: RepoDefinition) => {
const project = await resolveProjectData(repo);
return project;
})
);
return projects;
},
enabled: !!reposQuery.data && reposQuery.data.length > 0,
staleTime: 60000
}));
const projects = $derived(projectsQuery.data ?? []);
const isLoading = $derived(
reposQuery.isPending || (projectsQuery.isPending && projectsQuery.isEnabled)
);
const hasError = $derived(reposQuery.isError || projectsQuery.isError);
const isEmpty = $derived(!isLoading && !hasError && projects.length === 0); const isEmpty = $derived(!isLoading && !hasError && projects.length === 0);
</script> </script>
@@ -63,7 +45,7 @@
<div class="relative aspect-video overflow-hidden"> <div class="relative aspect-video overflow-hidden">
<Image <Image
src={'https://picsum.photos/seed/yama/400/225'} src={'https://picsum.photos/seed/yama/400/225'}
alt={project.name} alt={project.full_name}
class="min-h-56.5 w-full min-w-100 object-cover transition-transform duration-300 group-hover:scale-105" class="min-h-56.5 w-full min-w-100 object-cover transition-transform duration-300 group-hover:scale-105"
/> />
@@ -84,7 +66,7 @@
</div> </div>
<div class="p-4"> <div class="p-4">
<h3 class="text-lg font-semibold text-white">{project.name}</h3> <h3 class="text-lg font-semibold text-white">{project.full_name}</h3>
<p class="mt-1 text-sm text-gray-400">{project.description}</p> <p class="mt-1 text-sm text-gray-400">{project.description}</p>
</div> </div>
</div> </div>

View File

@@ -9,4 +9,5 @@ export type Project = {
export type RepoDefinition = { export type RepoDefinition = {
id: string; id: string;
full_name: string; full_name: string;
description: string
}; };