mabel/src/database/site.rs

174 lines
5.2 KiB
Rust

use async_trait::async_trait;
use sqlx::Postgres;
use uuid::Uuid;
use crate::content::{
collection::CollectionData,
site::{CreateSiteData, SiteData, SiteRepository, UpdateSiteData},
Error,
};
use super::Database;
impl sqlx::Type<Postgres> for CollectionData {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("collection_data")
}
}
impl<'r> sqlx::Decode<'r, Postgres> for CollectionData {
fn decode(
value: sqlx::postgres::PgValueRef<'r>,
) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
let id = decoder.try_decode::<Uuid>()?;
let name = decoder.try_decode::<String>()?;
let slug = decoder.try_decode::<String>()?;
let parent = decoder.try_decode::<Option<Uuid>>()?;
Ok(Self {
id,
name,
slug,
parent,
})
}
}
#[async_trait]
impl SiteRepository for Database {
async fn get_site_by_name(&self, name: &str) -> Result<SiteData, Error> {
let site = sqlx::query_as!(
SiteData,
r#"SELECT
sites.name,
users.name as owner,
COALESCE(users.display_name,users.name) as owner_display_name,
title,
description,
sites.created_at,
default_collection,
array_agg(row(collections.id, collections.slug, collections.name, parent)) as "collections!: Vec<CollectionData>"
FROM sites
JOIN
users ON sites.owner = users.id
JOIN
collections ON collections.site = sites.id
WHERE
sites.name = $1
AND sites.deleted_at IS NULL
GROUP BY
sites.name, users.name, users.display_name, title, description, sites.created_at, default_collection"#,
name
)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => Error::NotFound,
_ => e.into(),
})?;
Ok(site)
}
async fn create_site(&self, owner: &Uuid, options: CreateSiteData) -> Result<Uuid, Error> {
let result = sqlx::query!(
"INSERT INTO sites (id, name, owner, title, description)
VALUES ($1, $2, $3, $4, $5) RETURNING id",
Uuid::now_v7(),
options.name,
owner,
options.title,
options.description,
)
.fetch_one(&self.pool)
.await
.map_err(|err| match err {
sqlx::Error::Database(dberr) if dberr.constraint() == Some("sites_name_key") => {
Error::IdentifierNotAvailable
}
_ => err.into(),
})?;
Ok(result.id)
}
async fn update_site(
&self,
owner: &Uuid,
name: &str,
options: UpdateSiteData,
) -> Result<(), Error> {
let result = sqlx::query!(
r#"UPDATE sites SET
name = COALESCE($1, name),
title = COALESCE($2, title),
description = COALESCE($3, description),
default_collection = COALESCE($4, default_collection),
modified_at = NOW()
WHERE
name = $5
AND owner = $6
AND deleted_at IS NULL"#,
options.name,
options.title,
options.description,
options.default_collection,
name,
owner,
)
.execute(&self.pool)
.await
.map_err(|err| match err {
sqlx::Error::Database(dberr) if dberr.constraint() == Some("sites_name_key") => {
Error::IdentifierNotAvailable
}
_ => err.into(),
})?;
if result.rows_affected() == 0 {
return Err(Error::NotFound);
}
Ok(())
}
async fn delete_site(&self, owner: &Uuid, name: &str, soft_delete: bool) -> Result<(), Error> {
let result = if soft_delete {
sqlx::query!(
// soft delete
"UPDATE sites SET deleted_at = NOW() WHERE name = $1 AND owner = $2 AND deleted_at IS NULL",
name, owner
)
.execute(&self.pool)
.await?
} else {
sqlx::query!(
// hard delete
"DELETE FROM sites WHERE name = $1 AND owner = $2",
name,
owner,
)
.execute(&self.pool)
.await?
};
if result.rows_affected() == 0 {
return Err(Error::NotFound);
}
Ok(())
}
async fn get_site_id_and_owner(&self, name: &str) -> Result<(Uuid, Uuid), Error> {
let record = sqlx::query!(
"SELECT id, owner FROM sites WHERE name = $1 AND deleted_at IS NULL",
name,
)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => Error::NotFound,
_ => e.into(),
})?;
Ok((record.id, record.owner))
}
}