pr_01m47d24b0e6n91zwymwxg0vpx/services/repos/src/registry.rs

177 lines5,635 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
43pub fn can_read(repo: &Repo, viewer: &Viewer) -> bool {
44 !repo.is_private || can_write(repo, viewer)
45}
46
47/// A repository belongs to its workspace, so any member may write to it. An
48/// pull request's fork belongs to whoever opened the pull request.
49pub fn can_write(repo: &Repo, viewer: &Viewer) -> bool {
50 viewer.as_ref().is_some_and(|user| {
51 if repo.fork_of.is_some() {
52 user.id == repo.owner_id
53 } else {
54 user.is_member(&repo.namespace)
55 }
56 })
57}
58
59fn optional(value: &Option<String>) -> JsValue {
60 value.as_deref().map_or(JsValue::NULL, JsValue::from)
61}
62
63pub struct Registry {
64 pub db: D1Database,
65}
66
67impl Registry {
68 pub async fn by_path(&self, path: &RepoPath) -> Result<Option<Repo>> {
69 Ok(self
70 .db
71 .prepare("SELECT * FROM repos WHERE namespace = ? AND name = ?")
72 .bind(&[
73 path.namespace.to_lowercase().into(),
74 path.name.to_lowercase().into(),
75 ])?
76 .first::<RepoRow>(None)
77 .await?
78 .map(Repo::from))
79 }
80
81 pub async fn by_id(&self, id: &str) -> Result<Option<Repo>> {
82 Ok(self
83 .db
84 .prepare("SELECT * FROM repos WHERE id = ?")
85 .bind(&[id.into()])?
86 .first::<RepoRow>(None)
87 .await?
88 .map(Repo::from))
89 }
90
91 /// Repos the viewer may see, newest first. Excludes pull request forks.
92 /// With `member_only`, only repos in the viewer's own workspaces.
93 pub async fn list(
94 &self,
95 viewer: &Viewer,
96 query: Option<&str>,
97 namespace: Option<&str>,
98 member_only: bool,
99 ) -> Result<Vec<Repo>> {
100 let workspaces: Vec<&str> = viewer
101 .iter()
102 .flat_map(|user| &user.workspaces)
103 .map(|membership| membership.slug.as_str())
104 .collect();
105 // An empty IN list is not valid SQL, so a viewer in no workspace
106 // gets a name no workspace can have.
107 let mut params: Vec<JsValue> = if workspaces.is_empty() {
108 vec!["".into()]
109 } else {
110 workspaces.iter().map(|slug| JsValue::from(*slug)).collect()
111 };
112 let mine = format!("namespace IN ({})", vec!["?"; params.len()].join(", "));
113 let mut conditions = vec![
114 "fork_of IS NULL".to_owned(),
115 if member_only {
116 mine
117 } else {
118 format!("(is_private = 0 OR {mine})")
119 },
120 ];
121 if let Some(namespace) = namespace {
122 conditions.push("namespace = ?".to_owned());
123 params.push(namespace.to_lowercase().into());
124 }
125 if let Some(query) = query.map(str::trim).filter(|query| !query.is_empty()) {
126 conditions
127 .push("(name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')".to_owned());
128 // LIKE wildcards in the query are matched literally.
129 let escaped: String = query
130 .chars()
131 .flat_map(|c| match c {
132 '\\' | '%' | '_' => vec!['\\', c],
133 _ => vec![c],
134 })
135 .collect();
136 let pattern = format!("%{escaped}%");
137 params.push(pattern.as_str().into());
138 params.push(pattern.into());
139 }
140 let sql = format!(
141 "SELECT * FROM repos WHERE {} ORDER BY created_at DESC, id DESC LIMIT 50",
142 conditions.join(" AND ")
143 );
144 let rows = self
145 .db
146 .prepare(sql)
147 .bind(&params)?
148 .all()
149 .await?
150 .results::<RepoRow>()?;
151 Ok(rows.into_iter().map(Repo::from).collect())
152 }
153
154 pub async fn insert(&self, repo: &Repo) -> Result<()> {
155 self.db
156 .prepare(
157 "INSERT INTO repos
158 (id, namespace, name, description, is_private, owner_id,
159 default_branch, fork_of, created_at)
160 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
161 )
162 .bind(&[
163 repo.id.as_str().into(),
164 repo.namespace.as_str().into(),
165 repo.name.as_str().into(),
166 optional(&repo.description),
167 (repo.is_private as u8).into(),
168 repo.owner_id.as_str().into(),
169 repo.default_branch.as_str().into(),
170 optional(&repo.fork_of),
171 repo.created_at.as_str().into(),
172 ])?
173 .run()
174 .await?;
175 Ok(())
176 }
177}