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