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