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}