OXYGENHUB
# ae138eea7a71 oxygenhub / crates / hub / src / db / proposals.rs
10.0 KiB · Rust · last changed Proposals from feature/ bookmarks; a landing says who and how 1 month ago source blame history raw
1
//! The `proposals`, `proposal_comments`, `proposal_reviews` and
2
//! `proposal_events` tables, and the per-repository number space.
3
4
use super::{now_ms, Pool};
5
use sqlx::FromRow;
6
7
#[derive(Clone, Debug, FromRow)]
8
pub struct ProposalRow {
9
    pub id: i64,
10
    pub repo_id: i64,
11
    pub number: i64,
12
    pub title: String,
13
    pub body: String,
14
    pub author_id: Option<i64>,
15
    pub author: String,
16
    pub state: String,
17
    pub target: String,
18
    pub source_kind: String,
19
    pub source_repo_id: Option<i64>,
20
    pub source_bookmark: String,
21
    pub source_changes: String,
22
    pub projected_state: String,
23
    pub projected_heads: String,
24
    pub projected_target: String,
25
    pub projected_changes: String,
26
    pub conflicted: bool,
27
    pub landed_state: String,
28
    pub landed_by: String,
29
    pub landed_via: String,
30
    pub landed_ms: i64,
31
    pub created_ms: i64,
32
    pub updated_ms: i64,
33
}
34
35
impl ProposalRow {
36
    pub fn projected_heads(&self) -> Vec<String> {
37
        self.projected_heads.split(',').filter(|s| !s.is_empty()).map(String::from).collect()
38
    }
39
40
    pub fn projected_changes(&self) -> Vec<String> {
41
        self.projected_changes.split(',').filter(|s| !s.is_empty()).map(String::from).collect()
42
    }
43
44
    pub fn source_changes(&self) -> Vec<String> {
45
        self.source_changes.split(',').filter(|s| !s.is_empty()).map(String::from).collect()
46
    }
47
48
    pub fn is_open(&self) -> bool {
49
        self.state == "open" || self.state == "draft"
50
    }
51
}
52
53
#[derive(Clone, Debug, FromRow)]
54
pub struct CommentRow {
55
    pub id: i64,
56
    pub proposal_id: i64,
57
    pub author_id: Option<i64>,
58
    pub author: String,
59
    pub body: String,
60
    pub path: String,
61
    pub anchor_change: String,
62
    pub anchor_index: i64,
63
    pub resolved: bool,
64
    pub created_ms: i64,
65
}
66
67
#[derive(Clone, Debug, FromRow)]
68
pub struct ReviewRow {
69
    pub id: i64,
70
    pub proposal_id: i64,
71
    pub author_id: i64,
72
    pub author: String,
73
    pub verdict: String,
74
    pub body: String,
75
    pub created_ms: i64,
76
}
77
78
#[derive(Clone, Debug, FromRow)]
79
pub struct EventRow {
80
    pub id: i64,
81
    pub at_ms: i64,
82
    pub actor: String,
83
    pub event: String,
84
    pub detail: String,
85
}
86
87
/// Take the next number of a repository's shared number space.
88
pub async fn next_number(pool: &Pool, repo_id: i64) -> sqlx::Result<i64> {
89
    let mut tx = super::begin_write(pool).await?;
90
    sqlx::query("INSERT OR IGNORE INTO numbers (repo_id, next) VALUES (?, 1)").bind(repo_id).execute(&mut *tx).await?;
91
    let n: i64 = sqlx::query_scalar("SELECT next FROM numbers WHERE repo_id = ?").bind(repo_id).fetch_one(&mut *tx).await?;
92
    sqlx::query("UPDATE numbers SET next = next + 1 WHERE repo_id = ?").bind(repo_id).execute(&mut *tx).await?;
93
    tx.commit().await?;
94
    Ok(n)
95
}
96
97
pub struct NewProposal<'a> {
98
    pub repo_id: i64,
99
    pub title: &'a str,
100
    pub body: &'a str,
101
    pub author_id: Option<i64>,
102
    pub author: &'a str,
103
    pub draft: bool,
104
    pub target: &'a str,
105
    pub source_kind: &'a str,
106
    pub source_repo_id: Option<i64>,
107
    pub source_bookmark: &'a str,
108
    pub source_changes: &'a str,
109
}
110
111
pub async fn create(pool: &Pool, p: NewProposal<'_>) -> sqlx::Result<ProposalRow> {
112
    let number = next_number(pool, p.repo_id).await?;
113
    let now = now_ms();
114
    sqlx::query_as(
115
        "INSERT INTO proposals (repo_id, number, title, body, author_id, author, state, target, source_kind, source_repo_id, source_bookmark, source_changes, created_ms, updated_ms) \
116
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *",
117
    )
118
    .bind(p.repo_id)
119
    .bind(number)
120
    .bind(p.title)
121
    .bind(p.body)
122
    .bind(p.author_id)
123
    .bind(p.author)
124
    .bind(if p.draft { "draft" } else { "open" })
125
    .bind(p.target)
126
    .bind(p.source_kind)
127
    .bind(p.source_repo_id)
128
    .bind(p.source_bookmark)
129
    .bind(p.source_changes)
130
    .bind(now)
131
    .bind(now)
132
    .fetch_one(pool)
133
    .await
134
}
135
136
pub async fn get(pool: &Pool, repo_id: i64, number: i64) -> sqlx::Result<Option<ProposalRow>> {
137
    sqlx::query_as("SELECT * FROM proposals WHERE repo_id = ? AND number = ?").bind(repo_id).bind(number).fetch_optional(pool).await
138
}
139
140
pub async fn list(pool: &Pool, repo_id: i64, states: &[&str]) -> sqlx::Result<Vec<ProposalRow>> {
141
    let all: Vec<ProposalRow> =
142
        sqlx::query_as("SELECT * FROM proposals WHERE repo_id = ? ORDER BY number DESC").bind(repo_id).fetch_all(pool).await?;
143
    Ok(all.into_iter().filter(|p| states.is_empty() || states.contains(&p.state.as_str())).collect())
144
}
145
146
/// Open proposals of every repository, for the front page.
147
pub async fn open_for_author(pool: &Pool, author_id: i64) -> sqlx::Result<Vec<ProposalRow>> {
148
    sqlx::query_as("SELECT * FROM proposals WHERE author_id = ? AND state IN ('open', 'draft') ORDER BY updated_ms DESC")
149
        .bind(author_id)
150
        .fetch_all(pool)
151
        .await
152
}
153
154
pub async fn set_projection(
155
    pool: &Pool,
156
    id: i64,
157
    state: &str,
158
    heads: &str,
159
    target_state: &str,
160
    changes: &str,
161
    conflicted: bool,
162
) -> sqlx::Result<()> {
163
    sqlx::query("UPDATE proposals SET projected_state = ?, projected_heads = ?, projected_target = ?, projected_changes = ?, conflicted = ? WHERE id = ?")
164
        .bind(state)
165
        .bind(heads)
166
        .bind(target_state)
167
        .bind(changes)
168
        .bind(conflicted)
169
        .bind(id)
170
        .execute(pool)
171
        .await?;
172
    Ok(())
173
}
174
175
pub async fn set_state(pool: &Pool, id: i64, state: &str, landed_state: &str) -> sqlx::Result<()> {
176
    sqlx::query("UPDATE proposals SET state = ?, landed_state = ?, updated_ms = ? WHERE id = ?")
177
        .bind(state)
178
        .bind(landed_state)
179
        .bind(now_ms())
180
        .bind(id)
181
        .execute(pool)
182
        .await?;
183
    Ok(())
184
}
185
186
/// Mark a proposal landed, with who landed it and how.
187
pub async fn set_landed(pool: &Pool, id: i64, landed_state: &str, by: &str, via: &str) -> sqlx::Result<()> {
188
    let now = now_ms();
189
    sqlx::query(
190
        "UPDATE proposals SET state = 'landed', landed_state = ?, landed_by = ?, landed_via = ?, landed_ms = ?, updated_ms = ? WHERE id = ?",
191
    )
192
    .bind(landed_state)
193
    .bind(by)
194
    .bind(via)
195
    .bind(now)
196
    .bind(now)
197
    .bind(id)
198
    .execute(pool)
199
    .await?;
200
    Ok(())
201
}
202
203
pub async fn edit(pool: &Pool, id: i64, title: &str, body: &str, target: &str) -> sqlx::Result<()> {
204
    sqlx::query("UPDATE proposals SET title = ?, body = ?, target = ?, updated_ms = ? WHERE id = ?")
205
        .bind(title)
206
        .bind(body)
207
        .bind(target)
208
        .bind(now_ms())
209
        .bind(id)
210
        .execute(pool)
211
        .await?;
212
    Ok(())
213
}
214
215
pub async fn touch(pool: &Pool, id: i64) -> sqlx::Result<()> {
216
    sqlx::query("UPDATE proposals SET updated_ms = ? WHERE id = ?").bind(now_ms()).bind(id).execute(pool).await?;
217
    Ok(())
218
}
219
220
pub async fn comments(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<CommentRow>> {
221
    sqlx::query_as("SELECT * FROM proposal_comments WHERE proposal_id = ? ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await
222
}
223
224
pub async fn add_comment(
225
    pool: &Pool,
226
    proposal_id: i64,
227
    author_id: Option<i64>,
228
    author: &str,
229
    body: &str,
230
    path: &str,
231
    anchor: Option<(&str, i64)>,
232
) -> sqlx::Result<CommentRow> {
233
    let row = sqlx::query_as(
234
        "INSERT INTO proposal_comments (proposal_id, author_id, author, body, path, anchor_change, anchor_index, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *",
235
    )
236
    .bind(proposal_id)
237
    .bind(author_id)
238
    .bind(author)
239
    .bind(body)
240
    .bind(path)
241
    .bind(anchor.map(|a| a.0).unwrap_or(""))
242
    .bind(anchor.map(|a| a.1).unwrap_or(0))
243
    .bind(now_ms())
244
    .fetch_one(pool)
245
    .await?;
246
    touch(pool, proposal_id).await?;
247
    Ok(row)
248
}
249
250
pub async fn set_comment_resolved(pool: &Pool, id: i64, resolved: bool) -> sqlx::Result<()> {
251
    sqlx::query("UPDATE proposal_comments SET resolved = ? WHERE id = ?").bind(resolved).bind(id).execute(pool).await?;
252
    Ok(())
253
}
254
255
pub async fn reviews(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<ReviewRow>> {
256
    sqlx::query_as("SELECT * FROM proposal_reviews WHERE proposal_id = ? ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await
257
}
258
259
pub async fn add_review(pool: &Pool, proposal_id: i64, author_id: i64, author: &str, verdict: &str, body: &str) -> sqlx::Result<ReviewRow> {
260
    let row = sqlx::query_as(
261
        "INSERT INTO proposal_reviews (proposal_id, author_id, author, verdict, body, created_ms) VALUES (?, ?, ?, ?, ?, ?) RETURNING *",
262
    )
263
    .bind(proposal_id)
264
    .bind(author_id)
265
    .bind(author)
266
    .bind(verdict)
267
    .bind(body)
268
    .bind(now_ms())
269
    .fetch_one(pool)
270
    .await?;
271
    touch(pool, proposal_id).await?;
272
    Ok(row)
273
}
274
275
pub async fn events(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<EventRow>> {
276
    sqlx::query_as("SELECT id, at_ms, actor, event, detail FROM proposal_events WHERE proposal_id = ? ORDER BY at_ms, id")
277
        .bind(proposal_id)
278
        .fetch_all(pool)
279
        .await
280
}
281
282
pub async fn add_event(pool: &Pool, proposal_id: i64, actor: &str, event: &str, detail: &str) -> sqlx::Result<()> {
283
    sqlx::query("INSERT INTO proposal_events (proposal_id, at_ms, actor, event, detail) VALUES (?, ?, ?, ?, ?)")
284
        .bind(proposal_id)
285
        .bind(now_ms())
286
        .bind(actor)
287
        .bind(event)
288
        .bind(detail)
289
        .execute(pool)
290
        .await?;
291
    Ok(())
292
}
293
294
/// Landing requirements of a repository.
295
#[derive(Clone, Copy, Debug, Default, FromRow)]
296
pub struct Requirements {
297
    pub required_approvals: i64,
298
    pub require_owners: bool,
299
    pub allow_conflicts: bool,
300
}
301
302
pub async fn requirements(pool: &Pool, repo_id: i64) -> sqlx::Result<Requirements> {
303
    sqlx::query_as("SELECT required_approvals, require_owners, allow_conflicts FROM repositories WHERE id = ?")
304
        .bind(repo_id)
305
        .fetch_one(pool)
306
        .await
307
}
308
309
pub async fn set_requirements(pool: &Pool, repo_id: i64, r: Requirements) -> sqlx::Result<()> {
310
    sqlx::query("UPDATE repositories SET required_approvals = ?, require_owners = ?, allow_conflicts = ? WHERE id = ?")
311
        .bind(r.required_approvals)
312
        .bind(r.require_owners)
313
        .bind(r.allow_conflicts)
314
        .bind(repo_id)
315
        .execute(pool)
316
        .await?;
317
    Ok(())
318
}