// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Per-repository issue/PR labels (optionally scoped `kind: value`). use model::Label; use sqlx::any::AnyRow; use sqlx::{AssertSqlSafe, Row}; use crate::{Store, StoreError, map_conflict, new_id, now_ms}; /// The columns of `labels` in a fixed order. const LABEL_COLUMNS: &str = "id, repo_id, name, color, description, created_at"; impl Store { /// Create a label on a repo. Names are unique per repo, case-insensitively. /// /// # Errors /// /// * [`StoreError::Conflict`] if the name is already used in the repo. /// * [`StoreError::Query`] for any other database failure. pub async fn create_label( &self, repo_id: &str, name: &str, color: &str, description: Option<&str>, ) -> Result { let label = Label { id: new_id(), repo_id: repo_id.to_string(), name: name.to_string(), color: color.to_string(), description: description.map(str::to_string), created_at: now_ms(), }; let sql = "INSERT INTO labels (id, repo_id, name, name_lower, color, description, created_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7)"; sqlx::query(sql) .bind(&label.id) .bind(&label.repo_id) .bind(&label.name) .bind(label.name.to_lowercase()) .bind(&label.color) .bind(&label.description) .bind(label.created_at) .execute(&self.pool) .await .map_err(|e| map_conflict(e, "label", &[("name", "name")]))?; Ok(label) } /// List a repo's labels, alphabetical (scoped labels group naturally). /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn list_labels(&self, repo_id: &str) -> Result, StoreError> { let sql = format!( "SELECT {LABEL_COLUMNS} FROM labels WHERE repo_id = $1 ORDER BY name_lower ASC" ); let rows = sqlx::query(AssertSqlSafe(sql)) .bind(repo_id) .fetch_all(&self.pool) .await?; rows.iter() .map(map_label) .collect::>() .map_err(Into::into) } /// Fetch a label by id. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn label_by_id(&self, id: &str) -> Result, StoreError> { let sql = format!("SELECT {LABEL_COLUMNS} FROM labels WHERE id = $1"); let row = sqlx::query(AssertSqlSafe(sql)) .bind(id) .fetch_optional(&self.pool) .await?; Ok(row.as_ref().map(map_label).transpose()?) } /// Update a label's name, colour, and description. /// /// # Errors /// /// * [`StoreError::Conflict`] if the new name collides in the repo. /// * [`StoreError::Query`] for any other database failure. pub async fn update_label( &self, id: &str, name: &str, color: &str, description: Option<&str>, ) -> Result { let sql = "UPDATE labels SET name = $1, name_lower = $2, color = $3, description = $4 \ WHERE id = $5"; let updated = sqlx::query(sql) .bind(name) .bind(name.to_lowercase()) .bind(color) .bind(description) .bind(id) .execute(&self.pool) .await .map_err(|e| map_conflict(e, "label", &[("name", "name")]))? .rows_affected(); Ok(updated > 0) } /// Delete a label (cascading to `issue_labels`). Returns `true` if removed. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn delete_label(&self, id: &str) -> Result { let deleted = sqlx::query("DELETE FROM labels WHERE id = $1") .bind(id) .execute(&self.pool) .await? .rows_affected(); Ok(deleted > 0) } } /// Map a `labels` row to a [`Label`]. fn map_label(row: &AnyRow) -> Result { Ok(Label { id: row.try_get("id")?, repo_id: row.try_get("repo_id")?, name: row.try_get("name")?, color: row.try_get("color")?, description: row.try_get("description")?, created_at: row.try_get("created_at")?, }) }