Skip to main content

max / makenotwork

4.3 KB · 166 lines History Blame Raw
1 //! Per-repo collaborator access queries.
2
3 use chrono::{DateTime, Utc};
4 use sqlx::{FromRow, PgPool};
5 use uuid::Uuid;
6
7 use super::{GitRepoId, UserId};
8 use crate::error::Result;
9
10 #[derive(Debug, FromRow)]
11 pub struct DbRepoCollaborator {
12 pub id: Uuid,
13 pub repo_id: GitRepoId,
14 pub user_id: UserId,
15 pub can_push: bool,
16 pub created_at: DateTime<Utc>,
17 }
18
19 /// Collaborator with joined username for display.
20 #[derive(Debug, FromRow)]
21 pub struct CollaboratorWithUsername {
22 pub id: Uuid,
23 pub user_id: UserId,
24 pub username: String,
25 pub can_push: bool,
26 pub created_at: DateTime<Utc>,
27 }
28
29 /// Add a collaborator to a repo. Returns the new record.
30 #[tracing::instrument(skip_all)]
31 pub async fn add_collaborator(
32 pool: &PgPool,
33 repo_id: GitRepoId,
34 user_id: UserId,
35 can_push: bool,
36 ) -> Result<DbRepoCollaborator> {
37 let row = sqlx::query_as::<_, DbRepoCollaborator>(
38 r"
39 INSERT INTO repo_collaborators (repo_id, user_id, can_push)
40 VALUES ($1, $2, $3)
41 RETURNING *
42 ",
43 )
44 .bind(repo_id)
45 .bind(user_id)
46 .bind(can_push)
47 .fetch_one(pool)
48 .await?;
49
50 Ok(row)
51 }
52
53 /// Remove a collaborator from a repo. Returns true if a row was deleted.
54 #[tracing::instrument(skip_all)]
55 pub async fn remove_collaborator(
56 pool: &PgPool,
57 repo_id: GitRepoId,
58 user_id: UserId,
59 ) -> Result<bool> {
60 let result = sqlx::query("DELETE FROM repo_collaborators WHERE repo_id = $1 AND user_id = $2")
61 .bind(repo_id)
62 .bind(user_id)
63 .execute(pool)
64 .await?;
65
66 Ok(result.rows_affected() > 0)
67 }
68
69 /// List collaborators for a repo, with usernames.
70 #[tracing::instrument(skip_all)]
71 pub async fn list_collaborators(
72 pool: &PgPool,
73 repo_id: GitRepoId,
74 ) -> Result<Vec<CollaboratorWithUsername>> {
75 let rows = sqlx::query_as::<_, CollaboratorWithUsername>(
76 r"
77 SELECT rc.id, rc.user_id, u.username, rc.can_push, rc.created_at
78 FROM repo_collaborators rc
79 JOIN users u ON u.id = rc.user_id
80 WHERE rc.repo_id = $1
81 ORDER BY rc.created_at ASC
82 ",
83 )
84 .bind(repo_id)
85 .fetch_all(pool)
86 .await?;
87
88 Ok(rows)
89 }
90
91 /// A collaborator row tagged with its `repo_id`, for batched listing across
92 /// many repos in a single query (see [`list_collaborators_for_repos`]).
93 #[derive(Debug, FromRow)]
94 pub struct CollaboratorForRepo {
95 pub repo_id: GitRepoId,
96 pub user_id: UserId,
97 pub username: String,
98 pub can_push: bool,
99 }
100
101 /// List collaborators for many repos in ONE query, each row tagged with its
102 /// `repo_id` so the caller can group them. Replaces a per-repo `list_collaborators`
103 /// loop (the N+1 in the project code tab). Returns rows ordered by repo then
104 /// join time; an empty input short-circuits without a query.
105 #[tracing::instrument(skip_all)]
106 pub async fn list_collaborators_for_repos(
107 pool: &PgPool,
108 repo_ids: &[GitRepoId],
109 ) -> Result<Vec<CollaboratorForRepo>> {
110 if repo_ids.is_empty() {
111 return Ok(Vec::new());
112 }
113 let rows = sqlx::query_as::<_, CollaboratorForRepo>(
114 r"
115 SELECT rc.repo_id, rc.user_id, u.username, rc.can_push
116 FROM repo_collaborators rc
117 JOIN users u ON u.id = rc.user_id
118 WHERE rc.repo_id = ANY($1)
119 ORDER BY rc.repo_id, rc.created_at ASC
120 ",
121 )
122 .bind(repo_ids)
123 .fetch_all(pool)
124 .await?;
125
126 Ok(rows)
127 }
128
129 /// Check if a user has push access to a repo (either owner or collaborator with can_push).
130 #[tracing::instrument(skip_all)]
131 pub async fn can_user_push(pool: &PgPool, repo_id: GitRepoId, user_id: UserId) -> Result<bool> {
132 let row: (bool,) = sqlx::query_as(
133 r"
134 SELECT EXISTS(
135 SELECT 1 FROM repo_collaborators
136 WHERE repo_id = $1 AND user_id = $2 AND can_push = true
137 )
138 ",
139 )
140 .bind(repo_id)
141 .bind(user_id)
142 .fetch_one(pool)
143 .await?;
144
145 Ok(row.0)
146 }
147
148 /// Check if a user has read access to a repo (any collaborator record, regardless of can_push).
149 #[tracing::instrument(skip_all)]
150 pub async fn is_collaborator(pool: &PgPool, repo_id: GitRepoId, user_id: UserId) -> Result<bool> {
151 let row: (bool,) = sqlx::query_as(
152 r"
153 SELECT EXISTS(
154 SELECT 1 FROM repo_collaborators
155 WHERE repo_id = $1 AND user_id = $2
156 )
157 ",
158 )
159 .bind(repo_id)
160 .bind(user_id)
161 .fetch_one(pool)
162 .await?;
163
164 Ok(row.0)
165 }
166