Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

208 linesCodeBlameRaw
1//! Reading work items: one by number or id, as a viewer may see it, and
2//! `list_work_items` and `get_work_item`.
3
4use g1t_contracts::access::{self, Capability};
5use g1t_contracts::repos::{ReadableArgs, Repo};
6use g1t_contracts::work::*;
7use g1t_contracts::{Outcome, User, Viewer};
8use serde::Deserialize;
9use worker::Result;
10use worker::wasm_bindgen::JsValue;
11
12use super::rows::{Held, ITEM_COLUMNS, ItemRow, no_item, workspace_of};
13use crate::rows::{CommentRow, PULL_COLUMNS, PullRow};
14use crate::{LIST_PAGE, Work, optional, optional_number, state_name};
15
16/// What `list_work_items` reads: `?1` the workspace, then the filters, and
17/// `?8` whether the viewer is a member (else only work items filed in a
18/// public repository or one in `?9`, the repositories they were given a
19/// role on).
20const LIST_SQL: &str = "SELECT {ITEM_COLUMNS} FROM work_items
21 WHERE workspace = ?1 AND (?2 IS NULL OR state = ?2)
22 AND (?3 IS NULL OR EXISTS (SELECT 1 FROM json_each(work_items.labels) WHERE json_each.value = ?3))
23 AND (?4 IS NULL OR milestone = ?4)
24 AND (?5 IS NULL OR EXISTS (SELECT 1 FROM json_each(work_items.assignees) WHERE json_each.value = ?5))
25 AND (?6 IS NULL OR EXISTS (SELECT 1 FROM work_item_repos r
26 WHERE r.item_id = work_items.id AND r.repo_id = ?6 AND (?7 IS NULL OR r.role = ?7)))
27 AND (?8 = 1 OR EXISTS (SELECT 1 FROM work_item_repos o JOIN repo_workspaces rw ON rw.repo_id = o.repo_id
28 WHERE o.item_id = work_items.id AND o.role = 'origin'
29 AND (rw.private = 0 OR o.repo_id IN (SELECT value FROM json_each(?9)))))
30 ORDER BY number DESC LIMIT ?10";
31
32/// The work items matching `condition` (on `work_items`), with everything
33/// they are read with.
34pub(crate) fn select(condition: &str) -> String {
35 format!("SELECT {ITEM_COLUMNS} FROM work_items WHERE {condition}")
36}
37
38/// The work items a statement made by [`select`] read.
39pub(crate) fn held_rows(result: &worker::D1Result) -> Result<Vec<Held>> {
40 Ok(result.results::<ItemRow>()?.into_iter().filter_map(ItemRow::held).collect())
41}
42
43impl Work {
44 /// A work item by workspace and number, whoever is asking.
45 pub(super) async fn item(&self, workspace: &str, number: u32) -> Result<Option<Held>> {
46 Ok(self
47 .db
48 .prepare(select("workspace = ? AND number = ?"))
49 .bind(&[workspace.to_lowercase().into(), number.into()])?
50 .first::<ItemRow>(None)
51 .await?
52 .and_then(ItemRow::held))
53 }
54
55 /// A work item by id, whoever is asking.
56 pub(super) async fn item_by_id(&self, id: &str) -> Result<Option<Held>> {
57 Ok(self
58 .db
59 .prepare(select("id = ?"))
60 .bind(&[id.into()])?
61 .first::<ItemRow>(None)
62 .await?
63 .and_then(ItemRow::held))
64 }
65
66 /// The work items of these ids, in no particular order.
67 pub(super) async fn items_by_ids(&self, ids: &[String]) -> Result<Vec<Held>> {
68 if ids.is_empty() {
69 return Ok(Vec::new());
70 }
71 held_rows(
72 &self
73 .db
74 .prepare(select("id IN (SELECT value FROM json_each(?))"))
75 .bind(&[serde_json::to_string(ids)?.into()])?
76 .all()
77 .await?,
78 )
79 }
80
81 /// A work item, if `viewer` may do `capability` on it.
82 pub(super) async fn item_for(&self, viewer: &Viewer, workspace: &str, number: u32, capability: Capability) -> Result<Outcome<Held>> {
83 let Some(held) = self.item(workspace, number).await? else {
84 return Ok(no_item());
85 };
86 check!(held.allowed(viewer.as_ref(), capability));
87 Ok(Outcome::Ok(held))
88 }
89
90 /// A work item `actor` may change: theirs (`WorkItem::owner`), or one
91 /// they may triage.
92 pub(super) async fn manageable_item(&self, actor: &User, workspace: &str, number: u32) -> Result<Outcome<Held>> {
93 let held = check!(self.item_for(&Some(actor.clone()), workspace, number, Capability::Read).await?);
94 if held.item.owner().id != actor.id {
95 check!(held.allowed(Some(actor), Capability::Triage));
96 }
97 Ok(Outcome::Ok(held))
98 }
99
100 /// The ids of the work items of `workspace` with these numbers, in the
101 /// order given.
102 pub(super) async fn item_ids(&self, workspace: &str, numbers: &[u32]) -> Result<Vec<String>> {
103 #[derive(Deserialize)]
104 struct IdNumber {
105 id: String,
106 number: u32,
107 }
108 if numbers.is_empty() {
109 return Ok(Vec::new());
110 }
111 let rows = self
112 .db
113 .prepare("SELECT id, number FROM work_items WHERE workspace = ? AND number IN (SELECT value FROM json_each(?))")
114 .bind(&[workspace.into(), serde_json::to_string(numbers)?.into()])?
115 .all()
116 .await?
117 .results::<IdNumber>()?;
118 Ok(numbers.iter().filter_map(|number| rows.iter().find(|row| row.number == *number).map(|row| row.id.clone())).collect())
119 }
120
121 pub(crate) async fn list_work_items(&self, a: ListWorkItemsArgs) -> Result<Outcome<Vec<WorkItem>>> {
122 let workspace = workspace_of(&a.workspace, a.repo.as_ref());
123 let repo = match &a.repo {
124 Some(path) => {
125 let repo = check!(self.repo(path, &a.viewer).await?);
126 if !repo.namespace.eq_ignore_ascii_case(&workspace) {
127 return Ok(Outcome::Ok(Vec::new()));
128 }
129 Some(repo)
130 }
131 None => None,
132 };
133 let viewer = a.viewer.as_ref();
134 let member = viewer.and_then(|user| access::workspace_role(user, &workspace)).is_some();
135 let granted: Vec<&str> = viewer
136 .map(|user| user.grants.iter().filter(|grant| grant.workspace.eq_ignore_ascii_case(&workspace)).map(|grant| grant.repo_id.as_str()).collect())
137 .unwrap_or_default();
138 let label = a
139 .label
140 .map(|label| label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase())
141 .filter(|label| !label.is_empty());
142 let assignee = a.assignee.map(|name| name.trim().trim_start_matches('@').to_lowercase()).filter(|name| !name.is_empty());
143 let state = state_name(a.state).map_or(JsValue::NULL, JsValue::from);
144 let query = self
145 .db
146 .prepare(LIST_SQL.replace("{ITEM_COLUMNS}", ITEM_COLUMNS))
147 .bind(&[
148 workspace.as_str().into(),
149 state,
150 optional(&label),
151 optional_number(a.milestone),
152 optional(&assignee),
153 optional(&repo.as_ref().map(|repo| repo.id.clone())),
154 a.role.map_or(JsValue::NULL, |role| role.as_str().into()),
155 u32::from(member).into(),
156 serde_json::to_string(&granted)?.into(),
157 LIST_PAGE.into(),
158 ])?;
159 let rows = self.timing.db(1, query.all()).await?.results::<ItemRow>()?;
160 Ok(Outcome::Ok(
161 rows.into_iter()
162 .filter_map(ItemRow::held)
163 .filter(|held| held.may(viewer, Capability::Read))
164 .map(|held| held.item)
165 .collect(),
166 ))
167 }
168
169 /// A work item, the pull requests for it the viewer may see, and its
170 /// comments and reactions.
171 pub(crate) async fn get_work_item(&self, a: WorkItemViewArgs) -> Result<Outcome<WorkItemDetail>> {
172 let held = check!(self.item_for(&a.viewer, &a.workspace, a.number, Capability::Read).await?);
173 let key = [JsValue::from(held.item.id.as_str())];
174 let statements = vec![
175 self.db.prepare(format!("SELECT {PULL_COLUMNS} FROM pulls WHERE item_id = ?1 ORDER BY created_at")).bind(&key)?,
176 self.db.prepare("SELECT * FROM comments WHERE item_id = ?1 ORDER BY id LIMIT 500").bind(&key)?,
177 self.db.prepare(crate::reactions::ITEM_THREAD_SQL).bind(&key)?,
178 ];
179 let results = self.timing.db(3, self.db.batch(statements)).await?;
180 let rows = |index: usize| results.get(index).ok_or_else(|| worker::Error::RustError("short batch".into()));
181 let pulls: Vec<Pull> = rows(0)?.results::<PullRow>()?.into_iter().map(Pull::from).collect();
182 let mut comments: Vec<Comment> = rows(1)?.results::<CommentRow>()?.into_iter().map(Comment::from).collect();
183 let reacted = rows(2)?.results::<crate::reactions::ReactionRow>()?;
184 let reactions = crate::reactions::attach(&reacted, &held.item.id, &mut comments);
185 let pulls = self.readable_pulls(&a.viewer, pulls).await?;
186 Ok(Outcome::Ok(WorkItemDetail { item: held.item, pulls, comments, reactions }))
187 }
188
189 /// Those of `pulls` whose repositories `viewer` may read, each naming
190 /// the branch it merges into.
191 pub(super) async fn readable_pulls(&self, viewer: &Viewer, pulls: Vec<Pull>) -> Result<Vec<Pull>> {
192 if pulls.is_empty() {
193 return Ok(pulls);
194 }
195 let mut ids: Vec<String> = pulls.iter().map(|pull| pull.repo_id.clone()).collect();
196 ids.sort();
197 ids.dedup();
198 let readable: Vec<Repo> = g1t_kit::call(&self.repos, "readable", &ReadableArgs { ids, viewer: viewer.clone() }).await?;
199 Ok(pulls
200 .into_iter()
201 .filter_map(|mut pull| {
202 let repo = readable.iter().find(|repo| repo.id == pull.repo_id)?;
203 crate::fill_base(&mut pull, repo);
204 Some(pull)
205 })
206 .collect())
207 }
208}