1use anyhow::anyhow;
2use rpc::{
3 proto::{self},
4 ConnectionId,
5};
6use sea_orm::{
7 ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
8 IntoActiveModel, ModelTrait, QueryFilter,
9};
10
11use crate::db::ProjectId;
12
13use super::{
14 dev_server, dev_server_project, project, project_collaborator, worktree, Database, DevServerId,
15 DevServerProjectId, RejoinedProject, ResharedProject, ServerId, UserId,
16};
17
18impl Database {
19 pub async fn get_dev_server_project(
20 &self,
21 dev_server_project_id: DevServerProjectId,
22 ) -> crate::Result<dev_server_project::Model> {
23 self.transaction(|tx| async move {
24 Ok(
25 dev_server_project::Entity::find_by_id(dev_server_project_id)
26 .one(&*tx)
27 .await?
28 .ok_or_else(|| {
29 anyhow!("no dev server project with id {}", dev_server_project_id)
30 })?,
31 )
32 })
33 .await
34 }
35
36 pub async fn get_projects_for_dev_server(
37 &self,
38 dev_server_id: DevServerId,
39 ) -> crate::Result<Vec<proto::DevServerProject>> {
40 self.transaction(|tx| async move {
41 self.get_projects_for_dev_server_internal(dev_server_id, &tx)
42 .await
43 })
44 .await
45 }
46
47 pub async fn get_projects_for_dev_server_internal(
48 &self,
49 dev_server_id: DevServerId,
50 tx: &DatabaseTransaction,
51 ) -> crate::Result<Vec<proto::DevServerProject>> {
52 let servers = dev_server_project::Entity::find()
53 .filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
54 .find_also_related(project::Entity)
55 .all(tx)
56 .await?;
57 Ok(servers
58 .into_iter()
59 .map(|(dev_server_project, project)| dev_server_project.to_proto(project))
60 .collect())
61 }
62
63 pub async fn dev_server_project_ids_for_user(
64 &self,
65 user_id: UserId,
66 tx: &DatabaseTransaction,
67 ) -> crate::Result<Vec<DevServerProjectId>> {
68 let dev_servers = dev_server::Entity::find()
69 .filter(dev_server::Column::UserId.eq(user_id))
70 .find_with_related(dev_server_project::Entity)
71 .all(tx)
72 .await?;
73
74 Ok(dev_servers
75 .into_iter()
76 .flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
77 .collect())
78 }
79
80 pub async fn owner_for_dev_server_project(
81 &self,
82 dev_server_project_id: DevServerProjectId,
83 tx: &DatabaseTransaction,
84 ) -> crate::Result<UserId> {
85 let dev_server = dev_server_project::Entity::find_by_id(dev_server_project_id)
86 .find_also_related(dev_server::Entity)
87 .one(tx)
88 .await?
89 .and_then(|(_, dev_server)| dev_server)
90 .ok_or_else(|| anyhow!("no dev server project"))?;
91
92 Ok(dev_server.user_id)
93 }
94
95 pub async fn get_stale_dev_server_projects(
96 &self,
97 connection: ConnectionId,
98 ) -> crate::Result<Vec<ProjectId>> {
99 self.transaction(|tx| async move {
100 let projects = project::Entity::find()
101 .filter(
102 Condition::all()
103 .add(project::Column::HostConnectionId.eq(connection.id))
104 .add(project::Column::HostConnectionServerId.eq(connection.owner_id)),
105 )
106 .all(&*tx)
107 .await?;
108
109 Ok(projects.into_iter().map(|p| p.id).collect())
110 })
111 .await
112 }
113
114 pub async fn create_dev_server_project(
115 &self,
116 dev_server_id: DevServerId,
117 path: &str,
118 user_id: UserId,
119 ) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
120 self.transaction(|tx| async move {
121 let dev_server = dev_server::Entity::find_by_id(dev_server_id)
122 .one(&*tx)
123 .await?
124 .ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
125 if dev_server.user_id != user_id {
126 return Err(anyhow!("not your dev server"))?;
127 }
128
129 let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
130 id: ActiveValue::NotSet,
131 dev_server_id: ActiveValue::Set(dev_server_id),
132 paths: ActiveValue::Set(dev_server_project::JSONPaths(vec![path.to_string()])),
133 })
134 .exec_with_returning(&*tx)
135 .await?;
136
137 let status = self
138 .dev_server_projects_update_internal(user_id, &tx)
139 .await?;
140
141 Ok((project, status))
142 })
143 .await
144 }
145
146 pub async fn update_dev_server_project(
147 &self,
148 id: DevServerProjectId,
149 paths: &[String],
150 user_id: UserId,
151 ) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
152 self.transaction(move |tx| async move {
153 let paths = paths.to_owned();
154 let Some((project, Some(dev_server))) = dev_server_project::Entity::find_by_id(id)
155 .find_also_related(dev_server::Entity)
156 .one(&*tx)
157 .await?
158 else {
159 return Err(anyhow!("no such dev server project"))?;
160 };
161
162 if dev_server.user_id != user_id {
163 return Err(anyhow!("not your dev server"))?;
164 }
165 let mut project = project.into_active_model();
166 project.paths = ActiveValue::Set(dev_server_project::JSONPaths(paths));
167 let project = project.update(&*tx).await?;
168
169 let status = self
170 .dev_server_projects_update_internal(user_id, &tx)
171 .await?;
172
173 Ok((project, status))
174 })
175 .await
176 }
177
178 pub async fn delete_dev_server_project(
179 &self,
180 dev_server_project_id: DevServerProjectId,
181 dev_server_id: DevServerId,
182 user_id: UserId,
183 ) -> crate::Result<(Vec<proto::DevServerProject>, proto::DevServerProjectsUpdate)> {
184 self.transaction(|tx| async move {
185 project::Entity::delete_many()
186 .filter(project::Column::DevServerProjectId.eq(dev_server_project_id))
187 .exec(&*tx)
188 .await?;
189 let result = dev_server_project::Entity::delete_by_id(dev_server_project_id)
190 .exec(&*tx)
191 .await?;
192 if result.rows_affected != 1 {
193 return Err(anyhow!(
194 "no dev server project with id {}",
195 dev_server_project_id
196 ))?;
197 }
198
199 let status = self
200 .dev_server_projects_update_internal(user_id, &tx)
201 .await?;
202
203 let projects = self
204 .get_projects_for_dev_server_internal(dev_server_id, &tx)
205 .await?;
206 Ok((projects, status))
207 })
208 .await
209 }
210
211 pub async fn share_dev_server_project(
212 &self,
213 dev_server_project_id: DevServerProjectId,
214 dev_server_id: DevServerId,
215 connection: ConnectionId,
216 worktrees: &[proto::WorktreeMetadata],
217 ) -> crate::Result<(
218 proto::DevServerProject,
219 UserId,
220 proto::DevServerProjectsUpdate,
221 )> {
222 self.transaction(|tx| async move {
223 let dev_server = dev_server::Entity::find_by_id(dev_server_id)
224 .one(&*tx)
225 .await?
226 .ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
227
228 let dev_server_project = dev_server_project::Entity::find_by_id(dev_server_project_id)
229 .one(&*tx)
230 .await?
231 .ok_or_else(|| {
232 anyhow!("no dev server project with id {}", dev_server_project_id)
233 })?;
234
235 if dev_server_project.dev_server_id != dev_server_id {
236 return Err(anyhow!("dev server project shared from wrong server"))?;
237 }
238
239 let project = project::ActiveModel {
240 room_id: ActiveValue::Set(None),
241 host_user_id: ActiveValue::Set(None),
242 host_connection_id: ActiveValue::set(Some(connection.id as i32)),
243 host_connection_server_id: ActiveValue::set(Some(ServerId(
244 connection.owner_id as i32,
245 ))),
246 id: ActiveValue::NotSet,
247 hosted_project_id: ActiveValue::Set(None),
248 dev_server_project_id: ActiveValue::Set(Some(dev_server_project_id)),
249 }
250 .insert(&*tx)
251 .await?;
252
253 if !worktrees.is_empty() {
254 worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
255 worktree::ActiveModel {
256 id: ActiveValue::set(worktree.id as i64),
257 project_id: ActiveValue::set(project.id),
258 abs_path: ActiveValue::set(worktree.abs_path.clone()),
259 root_name: ActiveValue::set(worktree.root_name.clone()),
260 visible: ActiveValue::set(worktree.visible),
261 scan_id: ActiveValue::set(0),
262 completed_scan_id: ActiveValue::set(0),
263 }
264 }))
265 .exec(&*tx)
266 .await?;
267 }
268
269 let status = self
270 .dev_server_projects_update_internal(dev_server.user_id, &tx)
271 .await?;
272
273 Ok((
274 dev_server_project.to_proto(Some(project)),
275 dev_server.user_id,
276 status,
277 ))
278 })
279 .await
280 }
281
282 pub async fn reshare_dev_server_projects(
283 &self,
284 reshared_projects: &Vec<proto::UpdateProject>,
285 dev_server_id: DevServerId,
286 connection: ConnectionId,
287 ) -> crate::Result<Vec<ResharedProject>> {
288 self.transaction(|tx| async move {
289 let mut ret = Vec::new();
290 for reshared_project in reshared_projects {
291 let project_id = ProjectId::from_proto(reshared_project.project_id);
292 let (project, dev_server_project) = project::Entity::find_by_id(project_id)
293 .find_also_related(dev_server_project::Entity)
294 .one(&*tx)
295 .await?
296 .ok_or_else(|| anyhow!("project does not exist"))?;
297
298 if dev_server_project.map(|rp| rp.dev_server_id) != Some(dev_server_id) {
299 return Err(anyhow!("dev server project reshared from wrong server"))?;
300 }
301
302 let Ok(old_connection_id) = project.host_connection() else {
303 return Err(anyhow!("dev server project was not shared"))?;
304 };
305
306 project::Entity::update(project::ActiveModel {
307 id: ActiveValue::set(project_id),
308 host_connection_id: ActiveValue::set(Some(connection.id as i32)),
309 host_connection_server_id: ActiveValue::set(Some(ServerId(
310 connection.owner_id as i32,
311 ))),
312 ..Default::default()
313 })
314 .exec(&*tx)
315 .await?;
316
317 let collaborators = project
318 .find_related(project_collaborator::Entity)
319 .all(&*tx)
320 .await?;
321
322 self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
323 .await?;
324
325 ret.push(super::ResharedProject {
326 id: project_id,
327 old_connection_id,
328 collaborators: collaborators
329 .iter()
330 .map(|collaborator| super::ProjectCollaborator {
331 connection_id: collaborator.connection(),
332 user_id: collaborator.user_id,
333 replica_id: collaborator.replica_id,
334 is_host: collaborator.is_host,
335 })
336 .collect(),
337 worktrees: reshared_project.worktrees.clone(),
338 });
339 }
340 Ok(ret)
341 })
342 .await
343 }
344
345 pub async fn rejoin_dev_server_projects(
346 &self,
347 rejoined_projects: &Vec<proto::RejoinProject>,
348 user_id: UserId,
349 connection_id: ConnectionId,
350 ) -> crate::Result<Vec<RejoinedProject>> {
351 self.transaction(|tx| async move {
352 let mut ret = Vec::new();
353 for rejoined_project in rejoined_projects {
354 if let Some(project) = self
355 .rejoin_project_internal(&tx, rejoined_project, user_id, connection_id)
356 .await?
357 {
358 ret.push(project);
359 }
360 }
361 Ok(ret)
362 })
363 .await
364 }
365}