dev_server_projects.rs

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