forge_core_db/models/
project.rs

1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sqlx::{FromRow, SqlitePool};
6use thiserror::Error;
7use ts_rs_forge::TS;
8use uuid::Uuid;
9
10#[derive(Debug, Error)]
11pub enum ProjectError {
12    #[error(transparent)]
13    Database(#[from] sqlx::Error),
14    #[error("Project not found")]
15    ProjectNotFound,
16    #[error("Project with git repository path already exists")]
17    GitRepoPathExists,
18    #[error("Failed to check existing git repository path: {0}")]
19    GitRepoCheckFailed(String),
20    #[error("Failed to create project: {0}")]
21    CreateFailed(String),
22}
23
24#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
25pub struct Project {
26    pub id: Uuid,
27    pub name: String,
28    pub git_repo_path: PathBuf,
29    pub setup_script: Option<String>,
30    pub dev_script: Option<String>,
31    pub cleanup_script: Option<String>,
32    pub copy_files: Option<String>,
33    /// Custom prompt template for generating commit messages for this project
34    pub commit_prompt: Option<String>,
35
36    #[ts(type = "Date")]
37    pub created_at: DateTime<Utc>,
38    #[ts(type = "Date")]
39    pub updated_at: DateTime<Utc>,
40}
41
42#[derive(Debug, Deserialize, TS)]
43pub struct CreateProject {
44    pub name: String,
45    pub git_repo_path: String,
46    pub use_existing_repo: bool,
47    pub setup_script: Option<String>,
48    pub dev_script: Option<String>,
49    pub cleanup_script: Option<String>,
50    pub copy_files: Option<String>,
51    pub commit_prompt: Option<String>,
52}
53
54#[derive(Debug, Deserialize, TS)]
55pub struct UpdateProject {
56    pub name: Option<String>,
57    pub git_repo_path: Option<String>,
58    pub setup_script: Option<String>,
59    pub dev_script: Option<String>,
60    pub cleanup_script: Option<String>,
61    pub copy_files: Option<String>,
62    pub commit_prompt: Option<String>,
63}
64
65#[derive(Debug, Serialize, TS)]
66pub struct SearchResult {
67    pub path: String,
68    pub is_file: bool,
69    pub match_type: SearchMatchType,
70}
71
72#[derive(Debug, Clone, Serialize, TS)]
73pub enum SearchMatchType {
74    FileName,
75    DirectoryName,
76    FullPath,
77}
78
79impl Project {
80    pub async fn count(pool: &SqlitePool) -> Result<i64, sqlx::Error> {
81        sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!: i64" FROM projects"#)
82            .fetch_one(pool)
83            .await
84    }
85
86    pub async fn find_all(pool: &SqlitePool) -> Result<Vec<Self>, sqlx::Error> {
87        sqlx::query_as!(
88            Project,
89            r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, cleanup_script, copy_files, commit_prompt, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects ORDER BY created_at DESC"#
90        )
91        .fetch_all(pool)
92        .await
93    }
94
95    /// Find the most actively used projects based on recent task activity
96    pub async fn find_most_active(pool: &SqlitePool, limit: i32) -> Result<Vec<Self>, sqlx::Error> {
97        sqlx::query_as!(
98            Project,
99            r#"
100            SELECT p.id as "id!: Uuid", p.name, p.git_repo_path, p.setup_script, p.dev_script, p.cleanup_script, p.copy_files, p.commit_prompt,
101                   p.created_at as "created_at!: DateTime<Utc>", p.updated_at as "updated_at!: DateTime<Utc>"
102            FROM projects p
103            WHERE p.id IN (
104                SELECT DISTINCT t.project_id
105                FROM tasks t
106                INNER JOIN task_attempts ta ON ta.task_id = t.id
107                ORDER BY ta.updated_at DESC
108            )
109            LIMIT $1
110            "#,
111            limit
112        )
113        .fetch_all(pool)
114        .await
115    }
116
117    pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
118        sqlx::query_as!(
119            Project,
120            r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, cleanup_script, copy_files, commit_prompt, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE id = $1"#,
121            id
122        )
123        .fetch_optional(pool)
124        .await
125    }
126
127    pub async fn find_by_git_repo_path(
128        pool: &SqlitePool,
129        git_repo_path: &str,
130    ) -> Result<Option<Self>, sqlx::Error> {
131        sqlx::query_as!(
132            Project,
133            r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, cleanup_script, copy_files, commit_prompt, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE git_repo_path = $1"#,
134            git_repo_path
135        )
136        .fetch_optional(pool)
137        .await
138    }
139
140    pub async fn find_by_git_repo_path_excluding_id(
141        pool: &SqlitePool,
142        git_repo_path: &str,
143        exclude_id: Uuid,
144    ) -> Result<Option<Self>, sqlx::Error> {
145        sqlx::query_as!(
146            Project,
147            r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, cleanup_script, copy_files, commit_prompt, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE git_repo_path = $1 AND id != $2"#,
148            git_repo_path,
149            exclude_id
150        )
151        .fetch_optional(pool)
152        .await
153    }
154
155    pub async fn create(
156        pool: &SqlitePool,
157        data: &CreateProject,
158        project_id: Uuid,
159    ) -> Result<Self, sqlx::Error> {
160        sqlx::query_as!(
161            Project,
162            r#"INSERT INTO projects (id, name, git_repo_path, setup_script, dev_script, cleanup_script, copy_files, commit_prompt) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, cleanup_script, copy_files, commit_prompt, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
163            project_id,
164            data.name,
165            data.git_repo_path,
166            data.setup_script,
167            data.dev_script,
168            data.cleanup_script,
169            data.copy_files,
170            data.commit_prompt
171        )
172        .fetch_one(pool)
173        .await
174    }
175
176    #[allow(clippy::too_many_arguments)]
177    pub async fn update(
178        pool: &SqlitePool,
179        id: Uuid,
180        name: String,
181        git_repo_path: String,
182        setup_script: Option<String>,
183        dev_script: Option<String>,
184        cleanup_script: Option<String>,
185        copy_files: Option<String>,
186        commit_prompt: Option<String>,
187    ) -> Result<Self, sqlx::Error> {
188        sqlx::query_as!(
189            Project,
190            r#"UPDATE projects SET name = $2, git_repo_path = $3, setup_script = $4, dev_script = $5, cleanup_script = $6, copy_files = $7, commit_prompt = $8 WHERE id = $1 RETURNING id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, cleanup_script, copy_files, commit_prompt, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
191            id,
192            name,
193            git_repo_path,
194            setup_script,
195            dev_script,
196            cleanup_script,
197            copy_files,
198            commit_prompt
199        )
200        .fetch_one(pool)
201        .await
202    }
203
204    pub async fn delete(pool: &SqlitePool, id: Uuid) -> Result<u64, sqlx::Error> {
205        let result = sqlx::query!("DELETE FROM projects WHERE id = $1", id)
206            .execute(pool)
207            .await?;
208        Ok(result.rows_affected())
209    }
210
211    pub async fn exists(pool: &SqlitePool, id: Uuid) -> Result<bool, sqlx::Error> {
212        let result = sqlx::query!(
213            r#"
214                SELECT COUNT(*) as "count!: i64"
215                FROM projects
216                WHERE id = $1
217            "#,
218            id
219        )
220        .fetch_one(pool)
221        .await?;
222
223        Ok(result.count > 0)
224    }
225}