fabrica

hanna/fabrica

4687 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Per-repository issue/PR labels (optionally scoped `kind: value`).
6
7use model::Label;
8use sqlx::any::AnyRow;
9use sqlx::{AssertSqlSafe, Row};
10
11use crate::{Store, StoreError, map_conflict, new_id, now_ms};
12
13/// The columns of `labels` in a fixed order.
14const LABEL_COLUMNS: &str = "id, repo_id, name, color, description, created_at";
15
16impl Store {
17 /// Create a label on a repo. Names are unique per repo, case-insensitively.
18 ///
19 /// # Errors
20 ///
21 /// * [`StoreError::Conflict`] if the name is already used in the repo.
22 /// * [`StoreError::Query`] for any other database failure.
23 pub async fn create_label(
24 &self,
25 repo_id: &str,
26 name: &str,
27 color: &str,
28 description: Option<&str>,
29 ) -> Result<Label, StoreError> {
30 let label = Label {
31 id: new_id(),
32 repo_id: repo_id.to_string(),
33 name: name.to_string(),
34 color: color.to_string(),
35 description: description.map(str::to_string),
36 created_at: now_ms(),
37 };
38 let sql = "INSERT INTO labels (id, repo_id, name, name_lower, color, description, created_at) \
39 VALUES ($1, $2, $3, $4, $5, $6, $7)";
40 sqlx::query(sql)
41 .bind(&label.id)
42 .bind(&label.repo_id)
43 .bind(&label.name)
44 .bind(label.name.to_lowercase())
45 .bind(&label.color)
46 .bind(&label.description)
47 .bind(label.created_at)
48 .execute(&self.pool)
49 .await
50 .map_err(|e| map_conflict(e, "label", &[("name", "name")]))?;
51 Ok(label)
52 }
53
54 /// List a repo's labels, alphabetical (scoped labels group naturally).
55 ///
56 /// # Errors
57 ///
58 /// Returns [`StoreError::Query`] on a database failure.
59 pub async fn list_labels(&self, repo_id: &str) -> Result<Vec<Label>, StoreError> {
60 let sql = format!(
61 "SELECT {LABEL_COLUMNS} FROM labels WHERE repo_id = $1 ORDER BY name_lower ASC"
62 );
63 let rows = sqlx::query(AssertSqlSafe(sql))
64 .bind(repo_id)
65 .fetch_all(&self.pool)
66 .await?;
67 rows.iter()
68 .map(map_label)
69 .collect::<Result<_, _>>()
70 .map_err(Into::into)
71 }
72
73 /// Fetch a label by id.
74 ///
75 /// # Errors
76 ///
77 /// Returns [`StoreError::Query`] on a database failure.
78 pub async fn label_by_id(&self, id: &str) -> Result<Option<Label>, StoreError> {
79 let sql = format!("SELECT {LABEL_COLUMNS} FROM labels WHERE id = $1");
80 let row = sqlx::query(AssertSqlSafe(sql))
81 .bind(id)
82 .fetch_optional(&self.pool)
83 .await?;
84 Ok(row.as_ref().map(map_label).transpose()?)
85 }
86
87 /// Update a label's name, colour, and description.
88 ///
89 /// # Errors
90 ///
91 /// * [`StoreError::Conflict`] if the new name collides in the repo.
92 /// * [`StoreError::Query`] for any other database failure.
93 pub async fn update_label(
94 &self,
95 id: &str,
96 name: &str,
97 color: &str,
98 description: Option<&str>,
99 ) -> Result<bool, StoreError> {
100 let sql = "UPDATE labels SET name = $1, name_lower = $2, color = $3, description = $4 \
101 WHERE id = $5";
102 let updated = sqlx::query(sql)
103 .bind(name)
104 .bind(name.to_lowercase())
105 .bind(color)
106 .bind(description)
107 .bind(id)
108 .execute(&self.pool)
109 .await
110 .map_err(|e| map_conflict(e, "label", &[("name", "name")]))?
111 .rows_affected();
112 Ok(updated > 0)
113 }
114
115 /// Delete a label (cascading to `issue_labels`). Returns `true` if removed.
116 ///
117 /// # Errors
118 ///
119 /// Returns [`StoreError::Query`] on a database failure.
120 pub async fn delete_label(&self, id: &str) -> Result<bool, StoreError> {
121 let deleted = sqlx::query("DELETE FROM labels WHERE id = $1")
122 .bind(id)
123 .execute(&self.pool)
124 .await?
125 .rows_affected();
126 Ok(deleted > 0)
127 }
128}
129
130/// Map a `labels` row to a [`Label`].
131fn map_label(row: &AnyRow) -> Result<Label, sqlx::Error> {
132 Ok(Label {
133 id: row.try_get("id")?,
134 repo_id: row.try_get("repo_id")?,
135 name: row.try_get("name")?,
136 color: row.try_get("color")?,
137 description: row.try_get("description")?,
138 created_at: row.try_get("created_at")?,
139 })
140}