use app_error::AppError; use database_entity::dto::{ AccountLink, Template, TemplateCategory, TemplateCategoryType, TemplateCreator, TemplateGroup, TemplateMinimal, }; use sqlx::{Executor, Postgres, QueryBuilder}; use uuid::Uuid; use crate::pg_row::{ AFTemplateCategoryMinimalRow, AFTemplateCategoryRow, AFTemplateCategoryTypeColumn, AFTemplateCreatorRow, AFTemplateGroupRow, AFTemplateMinimalRow, AFTemplateRow, AccountLinkColumn, }; pub async fn insert_new_template_category<'a, E: Executor<'a, Database = Postgres>>( executor: E, name: &str, description: &str, icon: &str, bg_color: &str, category_type: TemplateCategoryType, priority: i32, ) -> Result { let category_type_column: AFTemplateCategoryTypeColumn = category_type.into(); let new_template_category = sqlx::query_as!( TemplateCategory, r#" INSERT INTO af_template_category (name, description, icon, bg_color, category_type, priority) VALUES ($1, $2, $3, $4, $5, $6) RETURNING category_id AS id, name, description, icon, bg_color, category_type AS "category_type: AFTemplateCategoryTypeColumn", priority "#, name, description, icon, bg_color, category_type_column as AFTemplateCategoryTypeColumn, priority, ) .fetch_one(executor) .await?; Ok(new_template_category) } #[allow(clippy::too_many_arguments)] pub async fn update_template_category_by_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, id: Uuid, name: &str, description: &str, icon: &str, bg_color: &str, category_type: TemplateCategoryType, priority: i32, ) -> Result { let category_type_column: AFTemplateCategoryTypeColumn = category_type.into(); let new_template_category = sqlx::query_as!( TemplateCategory, r#" UPDATE af_template_category SET name = $2, description = $3, icon = $4, bg_color = $5, category_type = $6, priority = $7, updated_at = NOW() WHERE category_id = $1 RETURNING category_id AS id, name, description, icon, bg_color, category_type AS "category_type: AFTemplateCategoryTypeColumn", priority "#, id, name, description, icon, bg_color, category_type_column as AFTemplateCategoryTypeColumn, priority, ) .fetch_one(executor) .await?; Ok(new_template_category) } pub async fn select_template_categories<'a, E: Executor<'a, Database = Postgres>>( executor: E, name_contains: Option<&str>, category_type: Option, ) -> Result, AppError> { let mut query_builder: QueryBuilder = QueryBuilder::new( r#" SELECT category_id, name, description, icon, bg_color, category_type, priority FROM af_template_category WHERE TRUE "#, ); if let Some(category_type) = category_type { let category_type_column: AFTemplateCategoryTypeColumn = category_type.into(); query_builder.push(" AND category_type = "); query_builder.push_bind(category_type_column); }; if let Some(name_contains) = name_contains { query_builder.push(" AND name ILIKE CONCAT('%', "); query_builder.push_bind(name_contains); query_builder.push(" , '%')"); }; query_builder.push(" ORDER BY priority DESC, created_at ASC"); let query = query_builder.build_query_as::(); let category_rows: Vec = query.fetch_all(executor).await?; let categories = category_rows.into_iter().map(|row| row.into()).collect(); Ok(categories) } pub async fn select_template_category_by_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, category_id: Uuid, ) -> Result { let category = sqlx::query_as!( TemplateCategory, r#" SELECT category_id AS id, name, description, icon, bg_color, category_type AS "category_type: AFTemplateCategoryTypeColumn", priority FROM af_template_category WHERE category_id = $1 "#, category_id, ) .fetch_one(executor) .await?; Ok(category) } pub async fn delete_template_category_by_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, category_id: Uuid, ) -> Result<(), AppError> { sqlx::query!( r#" DELETE FROM af_template_category WHERE category_id = $1 "#, category_id, ) .execute(executor) .await?; Ok(()) } pub async fn insert_template_creator<'a, E: Executor<'a, Database = Postgres>>( executor: E, name: &str, avatar_url: &str, account_links: &[AccountLink], ) -> Result { let link_types: Vec = account_links .iter() .map(|link| link.link_type.clone()) .collect(); let url: Vec = account_links.iter().map(|link| link.url.clone()).collect(); let new_template_creator_row = sqlx::query_as!( AFTemplateCreatorRow, r#" WITH new_creator AS ( INSERT INTO af_template_creator (name, avatar_url) VALUES ($1, $2) RETURNING creator_id, name, avatar_url ), account_links AS ( INSERT INTO af_template_creator_account_link (creator_id, link_type, url) SELECT new_creator.creator_id as creator_id, link_type, url FROM UNNEST($3::text[], $4::text[]) AS t(link_type, url) CROSS JOIN new_creator RETURNING creator_id, link_type, url ) SELECT new_creator.creator_id AS id, name, avatar_url, ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec", 0 AS "number_of_templates!" FROM new_creator LEFT OUTER JOIN account_links USING (creator_id) GROUP BY (id, name, avatar_url) "#, name, avatar_url, link_types.as_slice(), url.as_slice(), ) .fetch_one(executor) .await?; let new_template_creator = new_template_creator_row.into(); Ok(new_template_creator) } pub async fn update_template_creator_by_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, creator_id: Uuid, name: &str, avatar_url: &str, account_links: &[AccountLink], ) -> Result { let link_types: Vec = account_links .iter() .map(|link| link.link_type.clone()) .collect(); let url: Vec = account_links.iter().map(|link| link.url.clone()).collect(); let updated_template_creator_row = sqlx::query_as!( AFTemplateCreatorRow, r#" WITH updated_creator AS ( UPDATE af_template_creator SET name = $2, avatar_url = $3, updated_at = NOW() WHERE creator_id = $1 RETURNING creator_id, name, avatar_url ), account_links AS ( INSERT INTO af_template_creator_account_link (creator_id, link_type, url) SELECT updated_creator.creator_id as creator_id, link_type, url FROM UNNEST($4::text[], $5::text[]) AS t(link_type, url) CROSS JOIN updated_creator RETURNING creator_id, link_type, url ), creator_number_of_templates AS ( SELECT creator_id, COUNT(1)::int AS number_of_templates FROM af_template_view WHERE creator_id = $1 GROUP BY creator_id ) SELECT updated_creator.creator_id AS id, name, avatar_url, ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec", COALESCE(number_of_templates, 0) AS "number_of_templates!" FROM updated_creator LEFT OUTER JOIN account_links USING (creator_id) LEFT OUTER JOIN creator_number_of_templates USING (creator_id) GROUP BY (id, name, avatar_url, number_of_templates) "#, creator_id, name, avatar_url, link_types.as_slice(), url.as_slice(), ) .fetch_one(executor) .await?; let updated_template_creator = updated_template_creator_row.into(); Ok(updated_template_creator) } pub async fn delete_template_creator_account_links<'a, E: Executor<'a, Database = Postgres>>( executor: E, creator_id: Uuid, ) -> Result<(), AppError> { sqlx::query!( r#" DELETE FROM af_template_creator_account_link WHERE creator_id = $1 "#, creator_id, ) .execute(executor) .await?; Ok(()) } pub async fn select_template_creators_by_name<'a, E: Executor<'a, Database = Postgres>>( executor: E, substr_match: &str, ) -> Result, AppError> { let creator_rows = sqlx::query_as!( AFTemplateCreatorRow, r#" WITH creator_number_of_templates AS ( SELECT creator_id, COUNT(1)::int AS number_of_templates FROM af_template_view WHERE name ILIKE $1 GROUP BY creator_id ) SELECT creator.creator_id AS "id!", name AS "name!", avatar_url AS "avatar_url!", ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec", COALESCE(number_of_templates, 0) AS "number_of_templates!" FROM af_template_creator creator LEFT OUTER JOIN af_template_creator_account_link account_link USING (creator_id) LEFT OUTER JOIN creator_number_of_templates USING (creator_id) WHERE name ILIKE $1 GROUP BY (creator.creator_id, name, avatar_url, number_of_templates) ORDER BY created_at ASC "#, format!("%{}%", substr_match) ) .fetch_all(executor) .await?; let creators = creator_rows.into_iter().map(|row| row.into()).collect(); Ok(creators) } pub async fn select_template_creator_by_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, creator_id: Uuid, ) -> Result { let creator_row = sqlx::query_as!( AFTemplateCreatorRow, r#" WITH creator_number_of_templates AS ( SELECT creator_id, COUNT(1)::int AS number_of_templates FROM af_template_view WHERE creator_id = $1 GROUP BY creator_id ) SELECT creator.creator_id AS "id!", name AS "name!", avatar_url AS "avatar_url!", ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec", COALESCE(number_of_templates, 0) AS "number_of_templates!" FROM af_template_creator creator LEFT OUTER JOIN af_template_creator_account_link account_link USING (creator_id) LEFT OUTER JOIN creator_number_of_templates USING (creator_id) WHERE creator.creator_id = $1 GROUP BY (creator.creator_id, name, avatar_url, number_of_templates) "#, creator_id ) .fetch_one(executor) .await?; let creator = creator_row.into(); Ok(creator) } pub async fn delete_template_creator_by_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, creator_id: Uuid, ) -> Result<(), AppError> { sqlx::query!( r#" DELETE FROM af_template_creator WHERE creator_id = $1 "#, creator_id, ) .execute(executor) .await?; Ok(()) } pub async fn insert_template_view_template_category<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, category_ids: &[Uuid], ) -> Result<(), AppError> { let rows_affected = sqlx::query!( r#" INSERT INTO af_template_view_template_category (view_id, category_id) SELECT $1 as view_id, category_id FROM UNNEST($2::uuid[]) AS category_id "#, view_id, category_ids ) .execute(executor) .await? .rows_affected(); if rows_affected == 0 { tracing::error!( "at least one category id is expected to be inserted for view_id {}", view_id ); } Ok(()) } pub async fn delete_template_view_template_categories<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, ) -> Result<(), AppError> { sqlx::query!( r#" DELETE FROM af_template_view_template_category WHERE view_id = $1 "#, view_id, ) .execute(executor) .await?; Ok(()) } pub async fn insert_related_templates<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, category_ids: &[Uuid], ) -> Result<(), AppError> { sqlx::query!( r#" INSERT INTO af_related_template_view (view_id, related_view_id) SELECT $1 AS view_id, related_view_id FROM UNNEST($2::uuid[]) AS t(related_view_id) "#, view_id, category_ids ) .execute(executor) .await?; Ok(()) } pub async fn delete_related_templates<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, ) -> Result<(), AppError> { sqlx::query!( r#" DELETE FROM af_related_template_view WHERE view_id = $1 "#, view_id, ) .execute(executor) .await?; Ok(()) } #[allow(clippy::too_many_arguments)] pub async fn insert_template_view<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, name: &str, description: &str, about: &str, view_url: &str, creator_id: Uuid, is_new_template: bool, is_featured: bool, ) -> Result<(), AppError> { sqlx::query!( r#" INSERT INTO af_template_view ( view_id, name, description, about, view_url, creator_id, is_new_template, is_featured ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) "#, view_id, name, description, about, view_url, creator_id, is_new_template, is_featured ) .execute(executor) .await?; Ok(()) } #[allow(clippy::too_many_arguments)] pub async fn update_template_view<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, name: &str, description: &str, about: &str, view_url: &str, creator_id: Uuid, is_new_template: bool, is_featured: bool, ) -> Result<(), AppError> { sqlx::query!( r#" UPDATE af_template_view SET updated_at = NOW(), name = $2, description = $3, about = $4, view_url = $5, creator_id = $6, is_new_template = $7, is_featured = $8 WHERE view_id = $1 "#, view_id, name, description, about, view_url, creator_id, is_new_template, is_featured ) .execute(executor) .await?; Ok(()) } pub async fn select_template_view_by_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, ) -> Result { let view_row = sqlx::query_as!( AFTemplateRow, r#" WITH template_with_creator_account_link AS ( SELECT template.view_id, template.creator_id, COALESCE( ARRAY_AGG((link_type, url)::account_link_type) FILTER (WHERE link_type IS NOT NULL), '{}' ) AS account_links FROM af_template_view template JOIN af_published_collab USING (view_id) JOIN af_template_creator creator USING (creator_id) LEFT OUTER JOIN af_template_creator_account_link account_link USING (creator_id) WHERE view_id = $1 GROUP BY (view_id, template.creator_id) ), related_template_with_category AS ( SELECT template.related_view_id, ARRAY_AGG( ( template_category.category_id, template_category.name, template_category.icon, template_category.bg_color )::template_category_minimal_type ) AS categories FROM af_related_template_view template JOIN af_template_view_template_category template_template_category ON template.related_view_id = template_template_category.view_id JOIN af_template_category template_category USING (category_id) WHERE template.view_id = $1 GROUP BY template.related_view_id ), template_with_related_template AS ( SELECT template.view_id, ARRAY_AGG( ( template.related_view_id, related_template.created_at, related_template.updated_at, related_template.name, related_template.description, related_template.view_url, ( creator.creator_id, creator.name, creator.avatar_url )::template_creator_minimal_type, related_template_with_category.categories, related_template.is_new_template, related_template.is_featured )::template_minimal_type ) AS related_templates FROM af_related_template_view template JOIN af_template_view related_template ON template.related_view_id = related_template.view_id JOIN af_template_creator creator ON related_template.creator_id = creator.creator_id JOIN related_template_with_category ON template.related_view_id = related_template_with_category.related_view_id WHERE template.view_id = $1 GROUP BY template.view_id ), template_with_category AS ( SELECT view_id, COALESCE( ARRAY_AGG(( vtc.category_id, name, icon, bg_color, description, category_type, priority )) FILTER (WHERE vtc.category_id IS NOT NULL), '{}' ) AS categories FROM af_template_view_template_category vtc JOIN af_template_category tc ON vtc.category_id = tc.category_id WHERE view_id = $1 GROUP BY view_id ), creator_number_of_templates AS ( SELECT creator_id, COUNT(*) AS number_of_templates FROM af_template_view GROUP BY creator_id ) SELECT template.view_id, template.created_at, template.updated_at, template.name, template.description, template.about, template.view_url, ( creator.creator_id, creator.name, creator.avatar_url, template_with_creator_account_link.account_links, creator_number_of_templates.number_of_templates )::template_creator_type AS "creator!: AFTemplateCreatorRow", template_with_category.categories AS "categories!: Vec", COALESCE(template_with_related_template.related_templates, '{}') AS "related_templates!: Vec", template.is_new_template, template.is_featured FROM af_template_view template JOIN af_template_creator creator USING (creator_id) JOIN template_with_creator_account_link ON template.view_id = template_with_creator_account_link.view_id LEFT OUTER JOIN template_with_related_template ON template.view_id = template_with_related_template.view_id JOIN template_with_category ON template.view_id = template_with_category.view_id LEFT OUTER JOIN creator_number_of_templates ON template.creator_id = creator_number_of_templates.creator_id WHERE template.view_id = $1 "#, view_id ) .fetch_one(executor) .await?; let view = view_row.into(); Ok(view) } pub async fn select_templates<'a, E: Executor<'a, Database = Postgres>>( executor: E, category_id: Option, is_featured: Option, is_new_template: Option, name_contains: Option<&str>, limit: Option, ) -> Result, AppError> { let mut query_builder: QueryBuilder = QueryBuilder::new( r#" WITH template_with_template_category AS ( SELECT template_template_category.view_id, ARRAY_AGG(( template_template_category.category_id, category.name, category.icon, category.bg_color )::template_category_minimal_type) AS categories FROM af_template_view_template_category template_template_category JOIN af_template_category category USING (category_id) JOIN af_template_view template USING (view_id) JOIN af_published_collab USING (view_id) WHERE TRUE "#, ); if let Some(category_id) = category_id { query_builder.push(" AND template_template_category.category_id = "); query_builder.push_bind(category_id); }; if let Some(is_featured) = is_featured { query_builder.push(" AND template.is_featured = "); query_builder.push_bind(is_featured); }; if let Some(is_new_template) = is_new_template { query_builder.push(" AND template.is_new_template = "); query_builder.push_bind(is_new_template); }; if let Some(name_contains) = name_contains { query_builder.push(" AND template.name ILIKE CONCAT('%', "); query_builder.push_bind(name_contains); query_builder.push(" , '%')"); }; query_builder.push( r#" GROUP BY template_template_category.view_id ) SELECT template.view_id, template.created_at, template.updated_at, template.name, template.description, template.view_url, ( template_creator.creator_id, template_creator.name, template_creator.avatar_url )::template_creator_minimal_type AS creator, tc.categories AS categories, template.is_new_template, template.is_featured FROM template_with_template_category tc JOIN af_template_view template USING (view_id) JOIN af_template_creator template_creator USING (creator_id) ORDER BY template.created_at DESC "#, ); if let Some(limit) = limit { query_builder.push(" LIMIT "); query_builder.push_bind(limit); }; let query = query_builder.build_query_as::(); let template_rows: Vec = query.fetch_all(executor).await?; Ok(template_rows.into_iter().map(|row| row.into()).collect()) } pub async fn select_template_homepage<'a, E: Executor<'a, Database = Postgres>>( executor: E, per_count: i64, ) -> Result, AppError> { let template_group_rows = sqlx::query_as!( AFTemplateGroupRow, r#" WITH recent_template AS ( SELECT template_template_category.category_id, template_template_category.view_id, category.name, category.icon, category.bg_color, ROW_NUMBER() OVER (PARTITION BY template_template_category.category_id ORDER BY template.created_at DESC) AS recency FROM af_template_view_template_category template_template_category JOIN af_template_category category USING (category_id) JOIN af_template_view template USING (view_id) JOIN af_published_collab USING (view_id) ), template_group_by_category_and_view AS ( SELECT category_id, view_id, ARRAY_AGG(( category_id, name, icon, bg_color )::template_category_minimal_type) AS categories FROM recent_template WHERE recency <= $1 GROUP BY category_id, view_id ), template_group_by_category_and_view_with_creator_and_template_details AS ( SELECT template_group_by_category_and_view.category_id, ( template.view_id, template.created_at, template.updated_at, template.name, template.description, template.view_url, ( creator.creator_id, creator.name, creator.avatar_url )::template_creator_minimal_type, template_group_by_category_and_view.categories, template.is_new_template, template.is_featured )::template_minimal_type AS template FROM template_group_by_category_and_view JOIN af_template_view template USING (view_id) JOIN af_template_creator creator USING (creator_id) ), template_group_by_category AS ( SELECT category_id, ARRAY_AGG(template) AS templates FROM template_group_by_category_and_view_with_creator_and_template_details GROUP BY category_id ) SELECT ( template_group_by_category.category_id, category.name, category.icon, category.bg_color )::template_category_minimal_type AS "category!: AFTemplateCategoryMinimalRow", templates AS "templates!: Vec" FROM template_group_by_category JOIN af_template_category category USING (category_id) "#, per_count, ) .fetch_all(executor) .await?; Ok( template_group_rows .into_iter() .map(|row| row.into()) .collect(), ) } pub async fn delete_template_by_view_id<'a, E: Executor<'a, Database = Postgres>>( executor: E, view_id: Uuid, ) -> Result<(), AppError> { sqlx::query!( r#" DELETE FROM af_template_view WHERE view_id = $1 "#, view_id, ) .execute(executor) .await?; Ok(()) }