175 lines
5.5 KiB
Rust
175 lines
5.5 KiB
Rust
//! Create, update, delete project skills.
|
|
|
|
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use super::info::SkillResponse;
|
|
use chrono::Utc;
|
|
use models::projects::project_skill::{Column as C, Entity as SkillEntity};
|
|
use models::ActiveModelTrait;
|
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set};
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use utoipa::ToSchema;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
|
pub struct CreateSkillRequest {
|
|
pub slug: String,
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub content: String,
|
|
pub metadata: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
|
pub struct UpdateSkillRequest {
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub content: Option<String>,
|
|
pub metadata: Option<serde_json::Value>,
|
|
pub enabled: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
|
pub struct DeleteSkillResponse {
|
|
pub deleted: bool,
|
|
pub slug: String,
|
|
}
|
|
|
|
fn validate_slug(slug: &str) -> Result<(), AppError> {
|
|
if slug.is_empty() || slug.len() > 255 {
|
|
return Err(AppError::BadRequest("Invalid slug".to_string()));
|
|
}
|
|
if !slug
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
|
{
|
|
return Err(AppError::BadRequest(
|
|
"Slug must contain only ASCII letters, numbers, hyphens, and underscores".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl AppService {
|
|
/// Add a skill manually to a project.
|
|
pub async fn skill_create(
|
|
&self,
|
|
project_uuid: String,
|
|
request: CreateSkillRequest,
|
|
ctx: &Session,
|
|
) -> Result<SkillResponse, AppError> {
|
|
validate_slug(&request.slug)?;
|
|
|
|
let project_id = Uuid::parse_str(&project_uuid)
|
|
.map_err(|_| AppError::BadRequest("Invalid project UUID".to_string()))?;
|
|
|
|
let user_id = ctx
|
|
.user()
|
|
.ok_or_else(|| AppError::Unauthorized)?;
|
|
|
|
// Check for duplicate slug within project
|
|
let exists = SkillEntity::find()
|
|
.filter(C::ProjectUuid.eq(project_id))
|
|
.filter(C::Slug.eq(&request.slug))
|
|
.one(&self.db)
|
|
.await?;
|
|
if exists.is_some() {
|
|
return Err(AppError::Conflict(format!(
|
|
"Skill '{}' already exists in this project",
|
|
request.slug
|
|
)));
|
|
}
|
|
|
|
let now = Utc::now();
|
|
let metadata = request.metadata.unwrap_or(serde_json::Value::Object(Default::default()));
|
|
let name = request.name.unwrap_or_else(|| request.slug.clone());
|
|
|
|
let active = models::projects::project_skill::ActiveModel {
|
|
id: Set(0), // auto-increment
|
|
project_uuid: Set(project_id),
|
|
slug: Set(request.slug),
|
|
name: Set(name),
|
|
description: Set(request.description),
|
|
source: Set("manual".to_string()),
|
|
repo_id: Set(None),
|
|
commit_sha: Set(None),
|
|
blob_hash: Set(None),
|
|
content: Set(request.content),
|
|
metadata: Set(metadata),
|
|
enabled: Set(true),
|
|
created_by: Set(Some(user_id)),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
|
|
let inserted = active.insert(&self.db).await?;
|
|
Ok(SkillResponse::from(inserted))
|
|
}
|
|
|
|
/// Update an existing skill.
|
|
pub async fn skill_update(
|
|
&self,
|
|
project_uuid: String,
|
|
slug: String,
|
|
request: UpdateSkillRequest,
|
|
_ctx: &Session,
|
|
) -> Result<SkillResponse, AppError> {
|
|
let project_id = Uuid::parse_str(&project_uuid)
|
|
.map_err(|_| AppError::BadRequest("Invalid project UUID".to_string()))?;
|
|
|
|
let skill = SkillEntity::find()
|
|
.filter(C::ProjectUuid.eq(project_id))
|
|
.filter(C::Slug.eq(&slug))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Skill not found".to_string()))?;
|
|
|
|
let mut active: models::projects::project_skill::ActiveModel = skill.into();
|
|
if let Some(name) = request.name {
|
|
active.name = Set(name);
|
|
}
|
|
if let Some(description) = request.description {
|
|
active.description = Set(Some(description));
|
|
}
|
|
if let Some(content) = request.content {
|
|
active.content = Set(content);
|
|
}
|
|
if let Some(metadata) = request.metadata {
|
|
active.metadata = Set(metadata);
|
|
}
|
|
if let Some(enabled) = request.enabled {
|
|
active.enabled = Set(enabled);
|
|
}
|
|
active.updated_at = Set(Utc::now());
|
|
|
|
let updated = active.update(&self.db).await?;
|
|
Ok(SkillResponse::from(updated))
|
|
}
|
|
|
|
/// Delete a skill from a project.
|
|
pub async fn skill_delete(
|
|
&self,
|
|
project_uuid: String,
|
|
slug: String,
|
|
_ctx: &Session,
|
|
) -> Result<DeleteSkillResponse, AppError> {
|
|
let project_id = Uuid::parse_str(&project_uuid)
|
|
.map_err(|_| AppError::BadRequest("Invalid project UUID".to_string()))?;
|
|
|
|
let skill = SkillEntity::find()
|
|
.filter(C::ProjectUuid.eq(project_id))
|
|
.filter(C::Slug.eq(&slug))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Skill not found".to_string()))?;
|
|
|
|
let deleted = SkillEntity::delete_by_id(skill.id).exec(&self.db).await?;
|
|
|
|
Ok(DeleteSkillResponse {
|
|
deleted: deleted.rows_affected > 0,
|
|
slug,
|
|
})
|
|
}
|
|
}
|