Skip to main content

max / goingson

5.6 KB · 185 lines History Blame Raw
1 //! SQLite implementation of the ProjectRepository.
2 //!
3 //! Manages projects, which are the top-level organizational unit in GoingsOn.
4 //! Projects can be of various types (Job, SideProject, Company, etc.) and have
5 //! associated tasks, events, and emails.
6
7 use async_trait::async_trait;
8 use sqlx::SqlitePool;
9 use goingson_core::{
10 CoreError, DbValue, NewProject, ParseableEnum, Project, ProjectId, ProjectRepository,
11 ProjectStatus, ProjectType, Result, UpdateProject, UserId,
12 };
13
14 use crate::utils::{format_datetime_now, parse_datetime, parse_uuid};
15
16 /// Database row struct for Project
17 #[derive(Debug, Clone, sqlx::FromRow)]
18 struct ProjectRow {
19 pub id: String,
20 pub name: String,
21 pub description: String,
22 pub project_type: String,
23 pub status: String,
24 pub created_at: String,
25 }
26
27 impl TryFrom<ProjectRow> for Project {
28 type Error = CoreError;
29
30 fn try_from(row: ProjectRow) -> std::result::Result<Self, Self::Error> {
31 Ok(Project {
32 id: parse_uuid(&row.id)?.into(),
33 name: row.name,
34 description: row.description,
35 project_type: ProjectType::from_str_or_default(&row.project_type),
36 status: ProjectStatus::from_str_or_default(&row.status),
37 created_at: parse_datetime(&row.created_at)?,
38 })
39 }
40 }
41
42 /// SQLite-backed implementation of [`ProjectRepository`].
43 ///
44 /// Provides CRUD operations for projects with automatic UUID generation
45 /// and timestamp management.
46 pub struct SqliteProjectRepository {
47 pool: SqlitePool,
48 }
49
50 impl SqliteProjectRepository {
51 /// Creates a new repository instance with the given connection pool.
52 #[tracing::instrument(skip_all)]
53 pub fn new(pool: SqlitePool) -> Self {
54 Self { pool }
55 }
56 }
57
58 #[async_trait]
59 impl ProjectRepository for SqliteProjectRepository {
60 #[tracing::instrument(skip_all)]
61 async fn list_all(&self, user_id: UserId) -> Result<Vec<Project>> {
62 let rows = sqlx::query_as::<_, ProjectRow>(
63 r#"
64 SELECT id, name, description, project_type, status, created_at
65 FROM projects
66 WHERE user_id = ?
67 ORDER BY created_at DESC
68 "#,
69 )
70 .bind(user_id.to_string())
71 .fetch_all(&self.pool)
72 .await
73 .map_err(CoreError::database)?;
74
75 rows.into_iter().map(Project::try_from).collect()
76 }
77
78 #[tracing::instrument(skip_all)]
79 async fn get_by_id(&self, id: ProjectId, user_id: UserId) -> Result<Option<Project>> {
80 let row = sqlx::query_as::<_, ProjectRow>(
81 r#"
82 SELECT id, name, description, project_type, status, created_at
83 FROM projects
84 WHERE id = ? AND user_id = ?
85 "#,
86 )
87 .bind(id.to_string())
88 .bind(user_id.to_string())
89 .fetch_optional(&self.pool)
90 .await
91 .map_err(CoreError::database)?;
92
93 row.map(Project::try_from).transpose()
94 }
95
96 #[tracing::instrument(skip_all)]
97 async fn create(&self, user_id: UserId, project: NewProject) -> Result<Project> {
98 let id = ProjectId::new();
99 let now = format_datetime_now();
100
101 sqlx::query(
102 r#"
103 INSERT INTO projects (id, user_id, name, description, project_type, status, created_at)
104 VALUES (?, ?, ?, ?, ?, ?, ?)
105 "#,
106 )
107 .bind(id.to_string())
108 .bind(user_id.to_string())
109 .bind(&project.name)
110 .bind(&project.description)
111 .bind(project.project_type.db_value())
112 .bind(project.status.db_value())
113 .bind(&now)
114 .execute(&self.pool)
115 .await
116 .map_err(CoreError::database)?;
117
118 // Fetch the created project
119 self.get_by_id(id, user_id)
120 .await?
121 .ok_or_else(|| CoreError::internal("Failed to retrieve created project"))
122 }
123
124 #[tracing::instrument(skip_all)]
125 async fn update(
126 &self,
127 id: ProjectId,
128 user_id: UserId,
129 project: UpdateProject,
130 ) -> Result<Option<Project>> {
131 let result = sqlx::query(
132 r#"
133 UPDATE projects
134 SET name = ?, description = ?, project_type = ?, status = ?
135 WHERE id = ? AND user_id = ?
136 "#,
137 )
138 .bind(&project.name)
139 .bind(&project.description)
140 .bind(project.project_type.db_value())
141 .bind(project.status.db_value())
142 .bind(id.to_string())
143 .bind(user_id.to_string())
144 .execute(&self.pool)
145 .await
146 .map_err(CoreError::database)?;
147
148 if result.rows_affected() > 0 {
149 self.get_by_id(id, user_id).await
150 } else {
151 Ok(None)
152 }
153 }
154
155 #[tracing::instrument(skip_all)]
156 async fn delete(&self, id: ProjectId, user_id: UserId) -> Result<bool> {
157 let result = sqlx::query("DELETE FROM projects WHERE id = ? AND user_id = ?")
158 .bind(id.to_string())
159 .bind(user_id.to_string())
160 .execute(&self.pool)
161 .await
162 .map_err(CoreError::database)?;
163
164 Ok(result.rows_affected() > 0)
165 }
166
167 #[tracing::instrument(skip_all)]
168 async fn find_by_name(&self, user_id: UserId, name: &str) -> Result<Option<Project>> {
169 let row = sqlx::query_as::<_, ProjectRow>(
170 r#"
171 SELECT id, name, description, project_type, status, created_at
172 FROM projects
173 WHERE user_id = ? AND LOWER(name) = LOWER(?)
174 "#,
175 )
176 .bind(user_id.to_string())
177 .bind(name)
178 .fetch_optional(&self.pool)
179 .await
180 .map_err(CoreError::database)?;
181
182 row.map(Project::try_from).transpose()
183 }
184 }
185