gitdataai/libs/service/skill/manage.rs
2026-04-14 19:02:01 +08:00

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,
})
}
}