···11+#[cfg(feature = "postgres")]
22+pub mod postgres;
33+44+pub use tranquil_db_traits::*;
55+66+#[cfg(feature = "postgres")]
77+pub use postgres::PostgresRepositories;
+99
crates/tranquil-db/src/postgres/backlink.rs
···11+use async_trait::async_trait;
22+use sqlx::PgPool;
33+use tranquil_db_traits::{Backlink, BacklinkRepository, DbError};
44+use tranquil_types::{AtUri, Nsid};
55+use uuid::Uuid;
66+77+use super::user::map_sqlx_error;
88+99+pub struct PostgresBacklinkRepository {
1010+ pool: PgPool,
1111+}
1212+1313+impl PostgresBacklinkRepository {
1414+ pub fn new(pool: PgPool) -> Self {
1515+ Self { pool }
1616+ }
1717+}
1818+1919+#[async_trait]
2020+impl BacklinkRepository for PostgresBacklinkRepository {
2121+ async fn get_backlink_conflicts(
2222+ &self,
2323+ repo_id: Uuid,
2424+ collection: &Nsid,
2525+ backlinks: &[Backlink],
2626+ ) -> Result<Vec<AtUri>, DbError> {
2727+ if backlinks.is_empty() {
2828+ return Ok(Vec::new());
2929+ }
3030+3131+ let paths: Vec<&str> = backlinks.iter().map(|b| b.path.as_str()).collect();
3232+ let link_tos: Vec<&str> = backlinks.iter().map(|b| b.link_to.as_str()).collect();
3333+ let collection_pattern = format!("%/{}/%", collection.as_str());
3434+3535+ let results = sqlx::query_scalar!(
3636+ r#"
3737+ SELECT DISTINCT uri
3838+ FROM backlinks
3939+ WHERE repo_id = $1
4040+ AND uri LIKE $4
4141+ AND (path, link_to) IN (SELECT unnest($2::text[]), unnest($3::text[]))
4242+ "#,
4343+ repo_id,
4444+ &paths as &[&str],
4545+ &link_tos as &[&str],
4646+ collection_pattern
4747+ )
4848+ .fetch_all(&self.pool)
4949+ .await
5050+ .map_err(map_sqlx_error)?;
5151+5252+ Ok(results.into_iter().map(Into::into).collect())
5353+ }
5454+5555+ async fn add_backlinks(&self, repo_id: Uuid, backlinks: &[Backlink]) -> Result<(), DbError> {
5656+ if backlinks.is_empty() {
5757+ return Ok(());
5858+ }
5959+6060+ let uris: Vec<&str> = backlinks.iter().map(|b| b.uri.as_str()).collect();
6161+ let paths: Vec<&str> = backlinks.iter().map(|b| b.path.as_str()).collect();
6262+ let link_tos: Vec<&str> = backlinks.iter().map(|b| b.link_to.as_str()).collect();
6363+6464+ sqlx::query!(
6565+ r#"
6666+ INSERT INTO backlinks (uri, path, link_to, repo_id)
6767+ SELECT unnest($1::text[]), unnest($2::text[]), unnest($3::text[]), $4
6868+ ON CONFLICT (uri, path) DO NOTHING
6969+ "#,
7070+ &uris as &[&str],
7171+ &paths as &[&str],
7272+ &link_tos as &[&str],
7373+ repo_id
7474+ )
7575+ .execute(&self.pool)
7676+ .await
7777+ .map_err(map_sqlx_error)?;
7878+7979+ Ok(())
8080+ }
8181+8282+ async fn remove_backlinks_by_uri(&self, uri: &AtUri) -> Result<(), DbError> {
8383+ sqlx::query!("DELETE FROM backlinks WHERE uri = $1", uri.as_str())
8484+ .execute(&self.pool)
8585+ .await
8686+ .map_err(map_sqlx_error)?;
8787+8888+ Ok(())
8989+ }
9090+9191+ async fn remove_backlinks_by_repo(&self, repo_id: Uuid) -> Result<(), DbError> {
9292+ sqlx::query!("DELETE FROM backlinks WHERE repo_id = $1", repo_id)
9393+ .execute(&self.pool)
9494+ .await
9595+ .map_err(map_sqlx_error)?;
9696+9797+ Ok(())
9898+ }
9999+}