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