refactor: use serde-dynamo
This commit is contained in:
1
api/Cargo.lock
generated
1
api/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
101
api/src/user.rs
101
api/src/user.rs
@@ -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
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user