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