pr_01m47d15m3e54sn21z27rpy5n9/services/repos/src/registry.rs

180 lines5,839 bytesCodeBlame
1//! Repository metadata in D1.
2
3use g1t_contracts::Viewer;
4use g1t_contracts::repos::{Repo, RepoPath};
5use serde::Deserialize;
6use worker::wasm_bindgen::JsValue;
7use worker::{D1Database, Result};
8
9#[derive(Deserialize)]
10struct RepoRow {
11 id: String,
12 namespace: String,
13 name: String,
14 description: Option<String>,
15 is_private: u8,
16 owner_id: String,
17 default_branch: String,
18 fork_of: Option<String>,
19 created_at: String,
20}
21
22impl From<RepoRow> for Repo {
23 fn from(row: RepoRow) -> Self {
24 Repo {
25 id: row.id,
26 namespace: row.namespace,
27 name: row.name,
28 description: row.description,
29 is_private: row.is_private != 0,
30 owner_id: row.owner_id,
31 default_branch: row.default_branch,
32 fork_of: row.fork_of,
33 created_at: row.created_at,
34 }
35 }
36}
37
38/// The key a repo is stored under in the git store.
39pub fn store_key(repo: &Repo) -> String {
40 format!("{}--{}", repo.namespace, repo.name)
41}
42
43/// Whether the viewer may read `repo`, going by the repository alone. A
44/// private pull request fork is also readable by whoever can read the
45/// repository it came from, which `Repos::may_read` checks.
46pub fn can_read(repo: &Repo, viewer: &Viewer) -> bool {
47 !repo.is_private || can_write(repo, viewer)
48}
49
50/// A repository belongs to its workspace, so any member may write to it. A
51/// pull request's fork belongs to whoever opened the pull request.
52pub fn can_write(repo: &Repo, viewer: &Viewer) -> bool {
53 viewer.as_ref().is_some_and(|user| {
54 if repo.fork_of.is_some() {
55 user.id == repo.owner_id
56 } else {
57 user.is_member(&repo.namespace)
58 }
59 })
60}
61
62fn optional(value: &Option<String>) -> JsValue {
63 value.as_deref().map_or(JsValue::NULL, JsValue::from)
64}
65
66pub struct Registry {
67 pub db: D1Database,
68}
69
70impl Registry {
71 pub async fn by_path(&self, path: &RepoPath) -> Result<Option<Repo>> {
72 Ok(self
73 .db
74 .prepare("SELECT * FROM repos WHERE namespace = ? AND name = ?")
75 .bind(&[
76 path.namespace.to_lowercase().into(),
77 path.name.to_lowercase().into(),
78 ])?
79 .first::<RepoRow>(None)
80 .await?
81 .map(Repo::from))
82 }
83
84 pub async fn by_id(&self, id: &str) -> Result<Option<Repo>> {
85 Ok(self
86 .db
87 .prepare("SELECT * FROM repos WHERE id = ?")
88 .bind(&[id.into()])?
89 .first::<RepoRow>(None)
90 .await?
91 .map(Repo::from))
92 }
93
94 /// Repos the viewer may see, newest first. Excludes pull request forks.
95 /// With `member_only`, only repos in the viewer's own workspaces.
96 pub async fn list(
97 &self,
98 viewer: &Viewer,
99 query: Option<&str>,
100 namespace: Option<&str>,
101 member_only: bool,
102 ) -> Result<Vec<Repo>> {
103 let workspaces: Vec<&str> = viewer
104 .iter()
105 .flat_map(|user| &user.workspaces)
106 .map(|membership| membership.slug.as_str())
107 .collect();
108 // An empty IN list is not valid SQL, so a viewer in no workspace
109 // gets a name no workspace can have.
110 let mut params: Vec<JsValue> = if workspaces.is_empty() {
111 vec!["".into()]
112 } else {
113 workspaces.iter().map(|slug| JsValue::from(*slug)).collect()
114 };
115 let mine = format!("namespace IN ({})", vec!["?"; params.len()].join(", "));
116 let mut conditions = vec![
117 "fork_of IS NULL".to_owned(),
118 if member_only {
119 mine
120 } else {
121 format!("(is_private = 0 OR {mine})")
122 },
123 ];
124 if let Some(namespace) = namespace {
125 conditions.push("namespace = ?".to_owned());
126 params.push(namespace.to_lowercase().into());
127 }
128 if let Some(query) = query.map(str::trim).filter(|query| !query.is_empty()) {
129 conditions
130 .push("(name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')".to_owned());
131 // LIKE wildcards in the query are matched literally.
132 let escaped: String = query
133 .chars()
134 .flat_map(|c| match c {
135 '\\' | '%' | '_' => vec!['\\', c],
136 _ => vec![c],
137 })
138 .collect();
139 let pattern = format!("%{escaped}%");
140 params.push(pattern.as_str().into());
141 params.push(pattern.into());
142 }
143 let sql = format!(
144 "SELECT * FROM repos WHERE {} ORDER BY created_at DESC, id DESC LIMIT 50",
145 conditions.join(" AND ")
146 );
147 let rows = self
148 .db
149 .prepare(sql)
150 .bind(&params)?
151 .all()
152 .await?
153 .results::<RepoRow>()?;
154 Ok(rows.into_iter().map(Repo::from).collect())
155 }
156
157 pub async fn insert(&self, repo: &Repo) -> Result<()> {
158 self.db
159 .prepare(
160 "INSERT INTO repos
161 (id, namespace, name, description, is_private, owner_id,
162 default_branch, fork_of, created_at)
163 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
164 )
165 .bind(&[
166 repo.id.as_str().into(),
167 repo.namespace.as_str().into(),
168 repo.name.as_str().into(),
169 optional(&repo.description),
170 (repo.is_private as u8).into(),
171 repo.owner_id.as_str().into(),
172 repo.default_branch.as_str().into(),
173 optional(&repo.fork_of),
174 repo.created_at.as_str().into(),
175 ])?
176 .run()
177 .await?;
178 Ok(())
179 }
180}