- app_materials table: mentor_id, title, slug, category, description, content
- domain/application/infrastructure pola articles: repo postgres, service, DTO ZodValidate
- routes: GET /materials (public, published only), GET /materials/{id|slug|categories}, POST/PUT/DELETE (auth, author-only)
- verified e2e: create 3 materi sebagai mentor, list, slug, categories
135 lines
3.5 KiB
Rust
135 lines
3.5 KiB
Rust
use paginator_utils::PaginatorResponse;
|
|
use chrono::Utc;
|
|
use uuid::Uuid;
|
|
|
|
use super::super::domain::{
|
|
CreateMaterialCommand, MaterialEntity, MaterialListItem,
|
|
MaterialRepository, MaterialService, UpdateMaterialCommand,
|
|
};
|
|
use crate::materials::domain::{MaterialRepository as _};
|
|
use imphnen_utils::AppError;
|
|
|
|
pub struct MaterialServiceImpl {
|
|
repo: Box<dyn MaterialRepository>,
|
|
}
|
|
|
|
impl MaterialServiceImpl {
|
|
pub fn new(repo: Box<dyn MaterialRepository>) -> Self {
|
|
Self { repo }
|
|
}
|
|
|
|
fn slugify(title: &str) -> String {
|
|
let slug: String = title
|
|
.to_lowercase()
|
|
.chars()
|
|
.map(|c| {
|
|
if c.is_ascii_alphanumeric() {
|
|
c
|
|
} else if c.is_whitespace() {
|
|
'-'
|
|
} else {
|
|
'-'
|
|
}
|
|
})
|
|
.collect();
|
|
slug.trim_matches('-').to_string()
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl MaterialService for MaterialServiceImpl {
|
|
async fn list_materials(
|
|
&self,
|
|
page: u64,
|
|
per_page: u64,
|
|
category: Option<String>,
|
|
published_only: bool,
|
|
) -> Result<PaginatorResponse<MaterialListItem>, AppError> {
|
|
let res = self
|
|
.repo
|
|
.find_all_paginated(page, per_page, category, published_only)
|
|
.await?;
|
|
Ok(PaginatorResponse {
|
|
data: res
|
|
.data
|
|
.into_iter()
|
|
.map(|e| MaterialListItem::from_entity(&e))
|
|
.collect(),
|
|
meta: res.meta,
|
|
})
|
|
}
|
|
|
|
async fn get_material_by_id(&self, id: Uuid) -> Result<MaterialEntity, AppError> {
|
|
self.repo.find_by_id(id).await
|
|
}
|
|
|
|
async fn get_material_by_slug(&self, slug: &str) -> Result<MaterialEntity, AppError> {
|
|
self.repo.find_by_slug(slug).await
|
|
}
|
|
|
|
async fn list_categories(&self) -> Result<Vec<String>, AppError> {
|
|
self.repo.find_categories().await
|
|
}
|
|
|
|
async fn create_material(
|
|
&self,
|
|
cmd: CreateMaterialCommand,
|
|
) -> Result<MaterialListItem, AppError> {
|
|
let now = Utc::now();
|
|
let entity = MaterialEntity {
|
|
id: Uuid::new_v4(),
|
|
mentor_id: cmd.mentor_id,
|
|
title: cmd.title.clone(),
|
|
slug: format!("{}-{}", Self::slugify(&cmd.title), Uuid::new_v4().to_string()[..8].to_string()),
|
|
category: cmd.category,
|
|
description: cmd.description,
|
|
content: cmd.content,
|
|
cover_url: cmd.cover_url,
|
|
is_published: true,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
self.repo.create(entity.clone()).await?;
|
|
Ok(MaterialListItem::from_entity(&entity))
|
|
}
|
|
|
|
async fn update_material(
|
|
&self,
|
|
id: Uuid,
|
|
actor_id: Uuid,
|
|
cmd: UpdateMaterialCommand,
|
|
) -> Result<MaterialListItem, AppError> {
|
|
let existing = self.repo.find_by_id(id).await?;
|
|
if existing.mentor_id != actor_id {
|
|
return Err(AppError::ForbiddenError(
|
|
"Only the author can update this material".into(),
|
|
));
|
|
}
|
|
// do not change mentor on update
|
|
let entity = MaterialEntity {
|
|
id,
|
|
mentor_id: existing.mentor_id,
|
|
title: cmd.title.unwrap_or(existing.title),
|
|
slug: existing.slug,
|
|
category: cmd.category.unwrap_or(existing.category),
|
|
description: cmd.description.unwrap_or(existing.description),
|
|
content: cmd.content.unwrap_or(existing.content),
|
|
cover_url: cmd.cover_url.or(existing.cover_url),
|
|
is_published: cmd.is_published.unwrap_or(existing.is_published),
|
|
created_at: existing.created_at,
|
|
updated_at: Utc::now(),
|
|
};
|
|
self.repo.update(id, entity.clone()).await?;
|
|
Ok(MaterialListItem::from_entity(&entity))
|
|
}
|
|
|
|
async fn delete_material(&self, id: Uuid, actor_id: Uuid) -> Result<(), AppError> {
|
|
let existing = self.repo.find_by_id(id).await?;
|
|
if existing.mentor_id != actor_id {
|
|
return Err(AppError::ForbiddenError(
|
|
"Only the author can delete this material".into(),
|
|
));
|
|
}
|
|
self.repo.delete(id).await
|
|
}
|
|
} |