feat(dimentorin): materials module (CRUD konten mentoring, public+protected routes)
- 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
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
use crate::materials::domain::{MaterialEntity, MaterialListItem};
|
||||
use imphnen_libs::ZodValidate;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateMaterialRequest {
|
||||
#[zod(min_length(3), max_length(200))]
|
||||
pub title: String,
|
||||
#[zod(min_length(1), max_length(100))]
|
||||
pub category: String,
|
||||
#[zod(min_length(3), max_length(500))]
|
||||
pub description: String,
|
||||
#[zod(min_length(10))]
|
||||
pub content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cover_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for CreateMaterialRequest {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateMaterialRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cover_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_published: Option<bool>,
|
||||
}
|
||||
|
||||
impl ZodValidate for UpdateMaterialRequest {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MaterialResponse {
|
||||
pub id: Uuid,
|
||||
pub mentor_id: Uuid,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub category: String,
|
||||
pub description: String,
|
||||
pub content: String,
|
||||
pub cover_url: Option<String>,
|
||||
pub is_published: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl MaterialResponse {
|
||||
pub fn from_entity(e: &MaterialEntity) -> Self {
|
||||
Self {
|
||||
id: e.id,
|
||||
mentor_id: e.mentor_id,
|
||||
title: e.title.clone(),
|
||||
slug: e.slug.clone(),
|
||||
category: e.category.clone(),
|
||||
description: e.description.clone(),
|
||||
content: e.content.clone(),
|
||||
cover_url: e.cover_url.clone(),
|
||||
is_published: e.is_published,
|
||||
created_at: e.created_at.to_rfc3339(),
|
||||
updated_at: e.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MaterialListItemResponse {
|
||||
pub id: Uuid,
|
||||
pub mentor_id: Uuid,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub category: String,
|
||||
pub description: String,
|
||||
pub cover_url: Option<String>,
|
||||
pub is_published: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<&MaterialEntity> for MaterialListItemResponse {
|
||||
fn from(e: &MaterialEntity) -> Self {
|
||||
Self {
|
||||
id: e.id,
|
||||
mentor_id: e.mentor_id,
|
||||
title: e.title.clone(),
|
||||
slug: e.slug.clone(),
|
||||
category: e.category.clone(),
|
||||
description: e.description.clone(),
|
||||
cover_url: e.cover_url.clone(),
|
||||
is_published: e.is_published,
|
||||
created_at: e.created_at.to_rfc3339(),
|
||||
updated_at: e.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MaterialListItem> for MaterialListItemResponse {
|
||||
fn from(e: &MaterialListItem) -> Self {
|
||||
Self {
|
||||
id: e.id,
|
||||
mentor_id: e.mentor_id,
|
||||
title: e.title.clone(),
|
||||
slug: e.slug.clone(),
|
||||
category: e.category.clone(),
|
||||
description: e.description.clone(),
|
||||
cover_url: e.cover_url.clone(),
|
||||
is_published: e.is_published,
|
||||
created_at: e.created_at.to_rfc3339(),
|
||||
updated_at: e.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type MaterialListResponse = PaginatorResponse<MaterialListItemResponse>;
|
||||
Reference in New Issue
Block a user