OXYGENHUB

Coverage / crates/hub/src/db/activity.rs 91.2%

125 of 137 lines run, 19 of 44 functions · run #488 on proposal/340 · run missed

1
//! Cross-repository reads for the signed-in dashboard (hub spec §6):
2
//! the activity feed and the proposals waiting on a reviewer. Callers
3
//! pass the repository ids the viewer may see, so nothing here decides
4
//! access.
5
6
use super::Pool;
7
use sqlx::FromRow;
8
9
/// One line of the activity feed.
10
#[derive(Clone, Debug, FromRow)]
11
pub struct Activity {
12
    pub owner: String,
13
    pub name: String,
14
    pub at_ms: i64,
15
    pub actor: String,
16
    /// `push`, `release`, `proposal` or `issue`.
17
    pub kind: String,
18
    /// What happened: the event name, or the bookmark for a push.
19
    pub event: String,
20
    /// The proposal or issue title, the release tag.
21
    pub title: String,
22
    /// The proposal or issue number, 0 otherwise.
23
    pub number: i64,
24
}
25
26
impl Activity {
27
    /// Where the line links to.
288×
    pub fn url(&self) -> String {
298×
        let base = format!("/{}/{}", self.owner, self.name);
308×
        match self.kind.as_str() {
318×
            "proposal" => format!("{base}/proposal/{}", self.number),
321×
            "issue" => format!("{base}/issue/{}", self.number),
331×
            "release" => format!("{base}/releases/tag/{}", self.title),
340×
            _ => format!("{base}/pushes"),
35
        }
368×
    }
37
}
38
39226×
fn ids_json(ids: &[i64]) -> String {
40226×
    serde_json::to_string(ids).unwrap_or_else(|_| "[]".into())
41226×
}
42
43
/// The SQL with each `SELECT value FROM json_each($n)` (the ids bound as a
44
/// JSON array) in this engine's words: Postgres has `json_array_elements_text`.
45248×
fn each(sql: &'static str) -> std::borrow::Cow<'static, str> {
46248×
    if cfg!(feature = "postgres") {
470×
        let mut out = sql.to_string();
480×
        for n in 1..=4 {
490×
            out = out.replace(
500×
                &format!("SELECT value FROM json_each(${n})"),
510×
                &format!("SELECT CAST(value AS BIGINT) FROM json_array_elements_text(CAST(${n} AS json)) AS t(value)"),
520×
            );
530×
        }
540×
        out.into()
55
    } else {
56248×
        sql.into()
57
    }
58248×
}
59
60
/// The newest `limit` events across the repositories in `repo_ids`.
6110×
pub async fn feed(pool: &Pool, repo_ids: &[i64], limit: i64) -> sqlx::Result<Vec<Activity>> {
6210×
    feed_by(pool, repo_ids, None, limit).await
6310×
}
64
65
/// Like `feed`, only what `actor` did when one is given (profile pages).
6621×
pub async fn feed_by(pool: &Pool, repo_ids: &[i64], actor: Option<&str>, limit: i64) -> sqlx::Result<Vec<Activity>> {
6721×
    sqlx::query_as(
6821×
        &each("WITH seen(id) AS (SELECT value FROM json_each($1)), ev AS ( \
6921×
           SELECT repo_id, at_ms, pusher AS actor, 'push' AS kind, reference AS event, '' AS title, 0 AS number FROM pushes WHERE ok = TRUE \
7021×
           UNION ALL SELECT repo_id, created_ms, author, 'release', 'published', tag, 0 FROM releases \
7121×
           UNION ALL SELECT p.repo_id, e.at_ms, e.actor, 'proposal', e.event, p.title, p.number FROM proposal_events e JOIN proposals p ON p.id = e.proposal_id \
7221×
           UNION ALL SELECT i.repo_id, e.at_ms, e.actor, 'issue', e.event, i.title, i.number FROM issue_events e JOIN issues i ON i.id = e.issue_id) \
7321×
         SELECT r.owner, r.name, ev.at_ms, ev.actor, ev.kind, ev.event, ev.title, ev.number \
7421×
         FROM ev JOIN repositories r ON r.id = ev.repo_id WHERE ev.repo_id IN (SELECT id FROM seen) \
7521×
           AND ($2 IS NULL OR lower(ev.actor) = lower($3)) \
7621×
         ORDER BY ev.at_ms DESC LIMIT $4"),
7721×
    )
7821×
    .bind(ids_json(repo_ids))
7921×
    .bind(actor)
8021×
    .bind(actor)
8121×
    .bind(limit)
8221×
    .fetch_all(pool)
8321×
    .await
8421×
}
85
86
/// A proposal on the dashboard, with its repository's name.
87
#[derive(Clone, Debug, FromRow)]
88
pub struct ProposalLine {
89
    pub owner: String,
90
    pub name: String,
91
    pub number: i64,
92
    pub title: String,
93
    pub author: String,
94
    pub state: String,
95
    pub conflicted: bool,
96
    pub updated_ms: i64,
97
}
98
99
/// Open proposals in `repo_ids` by someone else that `account_id` has
100
/// not reviewed yet: the review queue.
10113×
pub async fn awaiting_review(pool: &Pool, account_id: i64, repo_ids: &[i64]) -> sqlx::Result<Vec<ProposalLine>> {
10213×
    sqlx::query_as(&each(
10313×
        "SELECT r.owner, r.name, p.number, p.title, p.author, p.state, p.conflicted, p.updated_ms \
10413×
         FROM proposals p JOIN repositories r ON r.id = p.repo_id \
10513×
         WHERE p.repo_id IN (SELECT value FROM json_each($1)) AND p.state = 'open' \
10613×
           AND (p.author_id IS NULL OR p.author_id != $2) \
10713×
           AND NOT EXISTS (SELECT 1 FROM proposal_reviews v WHERE v.proposal_id = p.id AND v.author_id = $3) \
10813×
         ORDER BY p.updated_ms DESC",
10913×
    ))
11013×
    .bind(ids_json(repo_ids))
11113×
    .bind(account_id)
11213×
    .bind(account_id)
11313×
    .fetch_all(pool)
11413×
    .await
11513×
}
116
117
/// `account_id`'s own open and draft proposals in repositories it can
118
/// still see.
11913×
pub async fn authored(pool: &Pool, account_id: i64, repo_ids: &[i64]) -> sqlx::Result<Vec<ProposalLine>> {
12013×
    sqlx::query_as(&each(
12113×
        "SELECT r.owner, r.name, p.number, p.title, p.author, p.state, p.conflicted, p.updated_ms \
12213×
         FROM proposals p JOIN repositories r ON r.id = p.repo_id \
12313×
         WHERE p.author_id = $1 AND p.state IN ('open', 'draft') AND p.repo_id IN (SELECT value FROM json_each($2)) \
12413×
         ORDER BY p.updated_ms DESC",
12513×
    ))
12613×
    .bind(account_id)
12713×
    .bind(ids_json(repo_ids))
12813×
    .fetch_all(pool)
12913×
    .await
13013×
}
131
132
/// An issue on the dashboard, with its repository's name.
133
#[derive(Clone, Debug, FromRow)]
134
pub struct IssueLine {
135
    pub owner: String,
136
    pub name: String,
137
    pub number: i64,
138
    pub title: String,
139
    pub updated_ms: i64,
140
}
141
142
/// Open issues assigned to `account_id`, in repositories it can see.
1438×
pub async fn assigned(pool: &Pool, account_id: i64, repo_ids: &[i64]) -> sqlx::Result<Vec<IssueLine>> {
1448×
    sqlx::query_as(&each(
1458×
        "SELECT r.owner, r.name, i.number, i.title, i.updated_ms \
1468×
         FROM issues i JOIN issue_assignees a ON a.issue_id = i.id JOIN repositories r ON r.id = i.repo_id \
1478×
         WHERE a.account_id = $1 AND i.state = 'open' AND i.repo_id IN (SELECT value FROM json_each($2)) \
1488×
         ORDER BY i.updated_ms DESC",
1498×
    ))
1508×
    .bind(account_id)
1518×
    .bind(ids_json(repo_ids))
1528×
    .fetch_all(pool)
1538×
    .await
1548×
}
155
156
/// One event of Your sky (docs/backgrounds.md §5): its repository, when,
157
/// its kind (0 a push, 1 a proposal event, 2 an issue event, 3 a release,
158
/// 4 a Flow run) and how many marks it makes: a push's changes, else one.
159
#[derive(Clone, Debug, FromRow)]
160
pub struct SkyEvent {
161
    pub repo_id: i64,
162
    pub at_ms: i64,
163
    pub kind: i64,
164
    pub n: i64,
165
}
166
167
/// The events since `since_ms` in `repo_ids`, newest first, at most `limit`.
168171×
pub async fn sky(pool: &Pool, repo_ids: &[i64], since_ms: i64, limit: i64) -> sqlx::Result<Vec<SkyEvent>> {
169171×
    sqlx::query_as(
170171×
        &each("WITH seen(id) AS (SELECT value FROM json_each($1)), ev AS ( \
171171×
           SELECT repo_id, at_ms, CAST(0 AS BIGINT) AS kind, CASE WHEN changes > 1 THEN changes ELSE 1 END AS n FROM pushes WHERE ok = TRUE \
172171×
           UNION ALL SELECT p.repo_id, e.at_ms, 1, 1 FROM proposal_events e JOIN proposals p ON p.id = e.proposal_id \
173171×
           UNION ALL SELECT i.repo_id, e.at_ms, 2, 1 FROM issue_events e JOIN issues i ON i.id = e.issue_id \
174171×
           UNION ALL SELECT repo_id, created_ms, 3, 1 FROM releases \
175171×
           UNION ALL SELECT repo_id, created_ms, 4, 1 FROM flow_runs) \
176171×
         SELECT repo_id, at_ms, kind, n FROM ev WHERE repo_id IN (SELECT id FROM seen) AND at_ms >= $2 \
177171×
         ORDER BY at_ms DESC LIMIT $3"),
178171×
    )
179171×
    .bind(ids_json(repo_ids))
180171×
    .bind(since_ms)
181171×
    .bind(limit)
182171×
    .fetch_all(pool)
183171×
    .await
184171×
}
185
186
/// One account's pushes of the last year in the repositories a viewer may
187
/// see, for the profile's pulse (#217): `(repo_id, at_ms, changes, via)`.
188
/// Landings are left out (their changes were pushed once already), as are
189
/// failed pushes and pushes that brought nothing.
19011×
pub async fn pulse(pool: &Pool, account_id: i64, repo_ids: &[i64], since_ms: i64) -> sqlx::Result<Vec<(i64, i64, i64, String)>> {
19111×
    let ids = serde_json::to_string(repo_ids).unwrap_or_else(|_| "[]".into());
19211×
    sqlx::query_as(&each(
19311×
        "SELECT repo_id, at_ms, changes, via FROM pushes \
19411×
         WHERE account_id = $1 AND ok = TRUE AND changes > 0 AND at_ms >= $2 AND reason NOT LIKE 'landed%' \
19511×
           AND repo_id IN (SELECT value FROM json_each($3)) ORDER BY at_ms",
19611×
    ))
19711×
    .bind(account_id)
19811×
    .bind(since_ms)
19911×
    .bind(ids)
20011×
    .fetch_all(pool)
20111×
    .await
20211×
}
203
204
/// Releases an account made in the last year in repositories a viewer may
205
/// see: `(repo_id, tag, created_ms)`, to label the pulse's peaks.
20611×
pub async fn releases_by(pool: &Pool, account_id: i64, repo_ids: &[i64], since_ms: i64) -> sqlx::Result<Vec<(i64, String, i64)>> {
20711×
    let ids = serde_json::to_string(repo_ids).unwrap_or_else(|_| "[]".into());
20811×
    sqlx::query_as(&each(
20911×
        "SELECT repo_id, tag, created_ms FROM releases WHERE author_id = $1 AND created_ms >= $2 \
21011×
           AND repo_id IN (SELECT value FROM json_each($3)) ORDER BY created_ms",
21111×
    ))
21211×
    .bind(account_id)
21311×
    .bind(since_ms)
21411×
    .bind(ids)
21511×
    .fetch_all(pool)
21611×
    .await
21711×
}