1337 lines
49 KiB
Rust
1337 lines
49 KiB
Rust
use crate::auth::get_auth_user;
|
|
use crate::graphql::guards::{
|
|
RequireMultiplePermissions, RequirePermission, RequireReadPermission,
|
|
};
|
|
use crate::graphql::types::*;
|
|
use crate::models::blog::{
|
|
Blog, BlogCategory, BlogCategoryFilterInput, BlogDetail, BlogFilterInput, BlogSortInput,
|
|
BlogStats, BlogTag, BlogTagFilterInput,
|
|
};
|
|
use crate::models::invite_code::InviteCode;
|
|
use crate::models::page_block::{PaginatedResult, PaginationInput};
|
|
use crate::models::settings::SettingFilter;
|
|
use crate::models::user::{User, UserInfoRow};
|
|
use crate::services::blog_service::BlogService;
|
|
use crate::services::casbin_service::CasbinService;
|
|
use crate::services::invite_code_service::InviteCodeService;
|
|
use crate::services::page_block_service::PageBlockService;
|
|
use crate::services::settings_service::SettingsService;
|
|
use crate::services::user_service::UserService;
|
|
use async_graphql::Error as GraphQLError;
|
|
use async_graphql::{Context, Object, Result};
|
|
use tracing::info;
|
|
use uuid::Uuid;
|
|
|
|
pub struct QueryRoot;
|
|
|
|
#[Object]
|
|
impl QueryRoot {
|
|
async fn health_check(&self) -> &str {
|
|
"OK"
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
|
async fn current_user(&self, ctx: &Context<'_>) -> Result<User> {
|
|
let auth_user = get_auth_user(ctx).await?;
|
|
let user_service = ctx.data::<UserService>()?;
|
|
|
|
user_service
|
|
.get_user_by_id(auth_user.id)
|
|
.await?
|
|
.ok_or_else(|| async_graphql::Error::new("User not found"))
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"admin\")")]
|
|
async fn secret_data(&self, _ctx: &Context<'_>) -> &str {
|
|
"This is super secret admin data!"
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
|
async fn users(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
offset: Option<u64>,
|
|
limit: Option<u64>,
|
|
sort_by: Option<String>,
|
|
sort_order: Option<String>,
|
|
filter: Option<String>,
|
|
) -> Result<Vec<UserInfoRow>> {
|
|
let user_service = ctx.data::<UserService>()?;
|
|
info!("users im here");
|
|
let offset = offset.unwrap_or(0);
|
|
let limit = limit.unwrap_or(20);
|
|
let sort_by = sort_by.unwrap_or("created_at".to_string());
|
|
let sort_order = sort_order.unwrap_or("desc".to_string());
|
|
user_service
|
|
.get_all_users(offset, limit, sort_by, sort_order, filter)
|
|
.await
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"invite_codes\")")]
|
|
async fn my_invite_codes(&self, ctx: &Context<'_>) -> Result<Vec<InviteCode>> {
|
|
let auth_user = get_auth_user(ctx).await?;
|
|
let invite_code_service = ctx.data::<InviteCodeService>()?;
|
|
invite_code_service
|
|
.get_invite_codes_by_creator(auth_user.id)
|
|
.await
|
|
}
|
|
|
|
#[graphql(guard = "RequirePermission::new(\"invite_codes\", \"write\")")]
|
|
async fn validate_invite_code(&self, ctx: &Context<'_>, code: String) -> Result<bool> {
|
|
let invite_code_service = ctx.data::<InviteCodeService>()?;
|
|
invite_code_service
|
|
.validate_invite_code(crate::models::invite_code::ValidateInviteCodeInput { code })
|
|
.await
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
|
async fn users_info(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
offset: Option<u64>,
|
|
limit: Option<u64>,
|
|
sort_by: Option<String>,
|
|
sort_order: Option<String>,
|
|
filter: Option<String>,
|
|
) -> Result<UserInfoRespnose> {
|
|
let user_service = ctx.data::<UserService>()?;
|
|
let offset = offset.unwrap_or(0);
|
|
let limit = limit.unwrap_or(20);
|
|
let sort_by = sort_by.unwrap_or("created_at".to_string());
|
|
let sort_order = sort_order.unwrap_or("desc".to_string());
|
|
|
|
user_service
|
|
.users_info(offset, limit, sort_by, sort_order, filter)
|
|
.await
|
|
}
|
|
|
|
// Settings queries
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn settings(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
filter: Option<SettingFilterInput>,
|
|
) -> Result<Vec<SettingType>> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
let filter = filter.map(|f| SettingFilter {
|
|
category: f.category,
|
|
is_system: f.is_system,
|
|
is_editable: f.is_editable,
|
|
search: f.search,
|
|
});
|
|
|
|
let settings = if let Some(filter) = filter {
|
|
settings_service.get_settings_with_filter(&filter).await?
|
|
} else {
|
|
settings_service.get_all_settings().await?
|
|
};
|
|
|
|
Ok(settings
|
|
.into_iter()
|
|
.map(|s| SettingType {
|
|
id: s.id,
|
|
key: s.key,
|
|
value: s.value,
|
|
value_type: s.value_type,
|
|
description: s.description,
|
|
category: s.category,
|
|
is_encrypted: s.is_encrypted,
|
|
is_system: s.is_system,
|
|
is_editable: s.is_editable,
|
|
created_at: s.created_at,
|
|
updated_at: s.updated_at,
|
|
created_by: s.created_by,
|
|
updated_by: s.updated_by,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn setting_by_key(&self, ctx: &Context<'_>, key: String) -> Result<Option<SettingType>> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
let setting = settings_service.get_setting_by_key(&key).await?;
|
|
|
|
Ok(setting.map(|s| SettingType {
|
|
id: s.id,
|
|
key: s.key,
|
|
value: s.value,
|
|
value_type: s.value_type,
|
|
description: s.description,
|
|
category: s.category,
|
|
is_encrypted: s.is_encrypted,
|
|
is_system: s.is_system,
|
|
is_editable: s.is_editable,
|
|
created_at: s.created_at,
|
|
updated_at: s.updated_at,
|
|
created_by: s.created_by,
|
|
updated_by: s.updated_by,
|
|
}))
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn setting_by_id(&self, ctx: &Context<'_>, id: Uuid) -> Result<Option<SettingType>> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
let setting = settings_service.get_setting_by_id(id).await?;
|
|
|
|
Ok(setting.map(|s| SettingType {
|
|
id: s.id,
|
|
key: s.key,
|
|
value: s.value,
|
|
value_type: s.value_type,
|
|
description: s.description,
|
|
category: s.category,
|
|
is_encrypted: s.is_encrypted,
|
|
is_system: s.is_system,
|
|
is_editable: s.is_editable,
|
|
created_at: s.created_at,
|
|
updated_at: s.updated_at,
|
|
created_by: s.created_by,
|
|
updated_by: s.updated_by,
|
|
}))
|
|
}
|
|
|
|
#[graphql(
|
|
guard = "RequireMultiplePermissions::new(&[(\"settings\", \"read\"), (\"pages\", \"read\")])"
|
|
)]
|
|
async fn setting_categories(&self, ctx: &Context<'_>) -> Result<Vec<CategoryPageType>> {
|
|
let page_block_service = ctx.data::<PageBlockService>()?;
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
|
|
// 获取所有配置分类
|
|
let categories = settings_service.get_categories().await?;
|
|
|
|
let mut category_pages = Vec::new();
|
|
|
|
// 为每个分类创建 CategoryPageType
|
|
for category in categories {
|
|
// 获取页面和统计信息
|
|
let (page, total_count, system_count, editable_count) = page_block_service
|
|
.get_category_page_with_stats(&category)
|
|
.await?;
|
|
|
|
// 获取该分类下的所有配置项
|
|
let filter = SettingFilter {
|
|
category: Some(category.clone()),
|
|
is_system: None,
|
|
is_editable: None,
|
|
search: None,
|
|
};
|
|
let settings = settings_service.get_settings_with_filter(&filter).await?;
|
|
|
|
// 转换为 GraphQL 类型
|
|
let settings_types = settings
|
|
.into_iter()
|
|
.map(|s| SettingCenterType {
|
|
id: s.id,
|
|
key: s.key,
|
|
value: if s.is_encrypted.unwrap_or(false) {
|
|
Some("***".to_string()) // 占位符,不返回明文
|
|
} else {
|
|
s.value
|
|
},
|
|
value_type: s.value_type,
|
|
is_encrypted: s.is_encrypted,
|
|
is_editable: s.is_editable,
|
|
is_system: s.is_system,
|
|
description: s.description,
|
|
updated_at: s.updated_at,
|
|
})
|
|
.collect();
|
|
|
|
// 转换页面类型
|
|
let page_type = page.map(|p| PageType {
|
|
id: p.id,
|
|
title: p.title,
|
|
slug: p.slug,
|
|
description: p.description,
|
|
is_active: p.is_active,
|
|
created_at: p.created_at,
|
|
updated_at: p.updated_at,
|
|
created_by: p.created_by,
|
|
updated_by: p.updated_by,
|
|
});
|
|
|
|
category_pages.push(CategoryPageType {
|
|
page: page_type,
|
|
settings: settings_types,
|
|
category,
|
|
settings_count: total_count,
|
|
system_settings_count: system_count,
|
|
editable_settings_count: editable_count,
|
|
});
|
|
}
|
|
|
|
Ok(category_pages)
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn settings_stats(&self, ctx: &Context<'_>) -> Result<SettingsStatsType> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
let categories = settings_service.get_categories().await?;
|
|
let stats = settings_service.get_settings_stats().await?;
|
|
|
|
Ok(SettingsStatsType { categories, stats })
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn setting_history(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
setting_id: Uuid,
|
|
) -> Result<Vec<SettingHistoryType>> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
let history = settings_service.get_setting_history(setting_id).await?;
|
|
|
|
Ok(history
|
|
.into_iter()
|
|
.map(|h| SettingHistoryType {
|
|
id: h.id,
|
|
setting_id: h.setting_id,
|
|
old_value: h.old_value,
|
|
new_value: h.new_value,
|
|
changed_by: h.changed_by,
|
|
change_reason: h.change_reason,
|
|
created_at: h.created_at,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
// Page Block queries
|
|
#[graphql(guard = "RequireReadPermission::new(\"pages\")")]
|
|
async fn pages(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
filter: Option<PageFilterInputType>,
|
|
limit: Option<i64>,
|
|
offset: Option<i64>,
|
|
) -> Result<Vec<PageType>> {
|
|
let page_block_service = ctx.data::<PageBlockService>()?;
|
|
let filter = filter.map(|f| crate::models::page_block::PageFilterInput {
|
|
title: f.title,
|
|
slug: f.slug,
|
|
is_active: f.is_active,
|
|
search: f.search,
|
|
});
|
|
|
|
let pages = page_block_service.get_pages(filter, limit, offset).await?;
|
|
|
|
Ok(pages
|
|
.into_iter()
|
|
.map(|p| PageType {
|
|
id: p.id,
|
|
title: p.title,
|
|
slug: p.slug,
|
|
description: p.description,
|
|
is_active: p.is_active,
|
|
created_at: p.created_at,
|
|
updated_at: p.updated_at,
|
|
created_by: p.created_by,
|
|
updated_by: p.updated_by,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"pages\")")]
|
|
async fn page_by_id(&self, ctx: &Context<'_>, id: Uuid) -> Result<Option<PageType>> {
|
|
let page_block_service = ctx.data::<PageBlockService>()?;
|
|
|
|
match page_block_service.get_page_by_id(id).await {
|
|
Ok(page) => Ok(Some(PageType {
|
|
id: page.id,
|
|
title: page.title,
|
|
slug: page.slug,
|
|
description: page.description,
|
|
is_active: page.is_active,
|
|
created_at: page.created_at,
|
|
updated_at: page.updated_at,
|
|
created_by: page.created_by,
|
|
updated_by: page.updated_by,
|
|
})),
|
|
Err(_) => Ok(None),
|
|
}
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"pages\")")]
|
|
async fn page_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result<Option<PageType>> {
|
|
let page_block_service = ctx.data::<PageBlockService>()?;
|
|
|
|
match page_block_service.get_page_by_slug(&slug).await {
|
|
Ok(page) => Ok(Some(PageType {
|
|
id: page.id,
|
|
title: page.title,
|
|
slug: page.slug,
|
|
description: page.description,
|
|
is_active: page.is_active,
|
|
created_at: page.created_at,
|
|
updated_at: page.updated_at,
|
|
created_by: page.created_by,
|
|
updated_by: page.updated_by,
|
|
})),
|
|
Err(_) => Ok(None),
|
|
}
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"page_blocks\")")]
|
|
async fn page_blocks(&self, ctx: &Context<'_>, page_id: Uuid) -> Result<Vec<BlockType>> {
|
|
let page_block_service = ctx.data::<PageBlockService>()?;
|
|
let blocks = page_block_service.get_page_blocks(page_id).await?;
|
|
|
|
Ok(blocks
|
|
.into_iter()
|
|
.map(|block| match block {
|
|
crate::models::page_block::Block::TextBlock(tb) => {
|
|
BlockType::TextBlock(TextBlockType {
|
|
id: tb.id,
|
|
page_id: tb.page_id,
|
|
block_order: tb.block_order,
|
|
title: tb.title,
|
|
markdown: tb.markdown,
|
|
is_active: tb.is_active,
|
|
created_at: tb.created_at,
|
|
updated_at: tb.updated_at,
|
|
})
|
|
}
|
|
crate::models::page_block::Block::ChartBlock(cb) => {
|
|
BlockType::ChartBlock(ChartBlockType {
|
|
id: cb.id,
|
|
page_id: cb.page_id,
|
|
block_order: cb.block_order,
|
|
title: cb.title,
|
|
chart_type: cb.chart_type,
|
|
series: cb
|
|
.series
|
|
.into_iter()
|
|
.map(|dp| DataPointType {
|
|
id: dp.id,
|
|
chart_block_id: dp.chart_block_id,
|
|
x: dp.x,
|
|
y: dp.y,
|
|
label: dp.label,
|
|
color: dp.color,
|
|
})
|
|
.collect(),
|
|
config: cb.config,
|
|
is_active: cb.is_active,
|
|
created_at: cb.created_at,
|
|
updated_at: cb.updated_at,
|
|
})
|
|
}
|
|
crate::models::page_block::Block::SettingsBlock(sb) => {
|
|
BlockType::SettingsBlock(SettingsBlockType {
|
|
id: sb.id,
|
|
page_id: sb.page_id,
|
|
block_order: sb.block_order,
|
|
title: sb.title,
|
|
category: sb.category,
|
|
editable: sb.editable,
|
|
display_mode: sb.display_mode,
|
|
is_active: sb.is_active,
|
|
created_at: sb.created_at,
|
|
updated_at: sb.updated_at,
|
|
})
|
|
}
|
|
crate::models::page_block::Block::TableBlock(tb) => {
|
|
BlockType::TableBlock(TableBlockType {
|
|
id: tb.id,
|
|
page_id: tb.page_id,
|
|
block_order: tb.block_order,
|
|
title: tb.title,
|
|
columns: tb
|
|
.columns
|
|
.into_iter()
|
|
.map(|col| TableColumnType {
|
|
id: col.id,
|
|
table_block_id: col.table_block_id,
|
|
name: col.name,
|
|
label: col.label,
|
|
data_type: col.data_type,
|
|
is_sortable: col.is_sortable,
|
|
is_filterable: col.is_filterable,
|
|
width: col.width,
|
|
order: col.order,
|
|
})
|
|
.collect(),
|
|
data_source: tb.data_source,
|
|
data_config: tb.data_config,
|
|
is_active: tb.is_active,
|
|
created_at: tb.created_at,
|
|
updated_at: tb.updated_at,
|
|
})
|
|
}
|
|
crate::models::page_block::Block::HeroBlock(hb) => {
|
|
BlockType::HeroBlock(HeroBlockType {
|
|
id: hb.id,
|
|
page_id: hb.page_id,
|
|
block_order: hb.block_order,
|
|
title: hb.title,
|
|
subtitle: hb.subtitle,
|
|
background_image: hb.background_image,
|
|
background_color: hb.background_color,
|
|
text_color: hb.text_color,
|
|
cta_text: hb.cta_text,
|
|
cta_link: hb.cta_link,
|
|
is_active: hb.is_active,
|
|
created_at: hb.created_at,
|
|
updated_at: hb.updated_at,
|
|
})
|
|
}
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
/// 根据配置分类获取对应的页面
|
|
#[graphql(
|
|
guard = "RequireMultiplePermissions::new(&[(\"pages\", \"read\"), (\"settings\", \"read\")])"
|
|
)]
|
|
async fn page_by_category(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
category: String,
|
|
) -> Result<CategoryPageType> {
|
|
let page_block_service = ctx.data::<PageBlockService>()?;
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
|
|
// 获取页面和统计信息
|
|
let (page, total_count, system_count, editable_count) = page_block_service
|
|
.get_category_page_with_stats(&category)
|
|
.await?;
|
|
|
|
// 获取该分类下的所有配置项
|
|
let filter = SettingFilter {
|
|
category: Some(category.clone()),
|
|
is_system: None,
|
|
is_editable: None,
|
|
search: None,
|
|
};
|
|
let settings = settings_service.get_settings_with_filter(&filter).await?;
|
|
|
|
// 转换为 GraphQL 类型
|
|
let settings_types = settings
|
|
.into_iter()
|
|
.map(|s| SettingCenterType {
|
|
id: s.id,
|
|
key: s.key,
|
|
value: if s.is_encrypted.unwrap_or(false) {
|
|
Some("***".to_string()) // 占位符,不返回明文
|
|
} else {
|
|
s.value
|
|
},
|
|
value_type: s.value_type,
|
|
is_encrypted: s.is_encrypted,
|
|
is_editable: s.is_editable,
|
|
is_system: s.is_system,
|
|
description: s.description,
|
|
updated_at: s.updated_at,
|
|
})
|
|
.collect();
|
|
|
|
// 转换页面类型
|
|
let page_type = page.map(|p| PageType {
|
|
id: p.id,
|
|
title: p.title,
|
|
slug: p.slug,
|
|
description: p.description,
|
|
is_active: p.is_active,
|
|
created_at: p.created_at,
|
|
updated_at: p.updated_at,
|
|
created_by: p.created_by,
|
|
updated_by: p.updated_by,
|
|
});
|
|
|
|
Ok(CategoryPageType {
|
|
page: page_type,
|
|
settings: settings_types,
|
|
category,
|
|
settings_count: total_count,
|
|
system_settings_count: system_count,
|
|
editable_settings_count: editable_count,
|
|
})
|
|
}
|
|
|
|
/// 获取所有配置分类页面
|
|
#[graphql(guard = "RequireReadPermission::new(\"pages\")")]
|
|
async fn all_category_pages(&self, ctx: &Context<'_>) -> Result<Vec<PageType>> {
|
|
let page_block_service = ctx.data::<PageBlockService>()?;
|
|
let pages = page_block_service.get_all_category_pages().await?;
|
|
|
|
Ok(pages
|
|
.into_iter()
|
|
.map(|p| PageType {
|
|
id: p.id,
|
|
title: p.title,
|
|
slug: p.slug,
|
|
description: p.description,
|
|
is_active: p.is_active,
|
|
created_at: p.created_at,
|
|
updated_at: p.updated_at,
|
|
created_by: p.created_by,
|
|
updated_by: p.updated_by,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
// Enhanced Settings queries for settings center
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn settings_by_category(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
category: String,
|
|
) -> Result<Vec<SettingCenterType>> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
let filter = SettingFilter {
|
|
category: Some(category),
|
|
is_system: None,
|
|
is_editable: None,
|
|
search: None,
|
|
};
|
|
|
|
let settings = settings_service.get_settings_with_filter(&filter).await?;
|
|
|
|
Ok(settings
|
|
.into_iter()
|
|
.map(|s| SettingCenterType {
|
|
id: s.id,
|
|
key: s.key,
|
|
value: if s.is_encrypted.unwrap_or(false) {
|
|
Some("***".to_string()) // 占位符,不返回明文
|
|
} else {
|
|
s.value
|
|
},
|
|
value_type: s.value_type,
|
|
is_encrypted: s.is_encrypted,
|
|
is_editable: s.is_editable,
|
|
is_system: s.is_system,
|
|
description: s.description,
|
|
updated_at: s.updated_at,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
// 权限管理查询
|
|
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
|
async fn check_permission(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
resource: String,
|
|
action: String,
|
|
) -> Result<PermissionCheckResult> {
|
|
let user = get_auth_user(ctx).await?;
|
|
let casbin_service = ctx.data::<CasbinService>()?;
|
|
|
|
let has_permission = casbin_service
|
|
.check_permission(&user.id.to_string(), &resource, &action)
|
|
.await?;
|
|
|
|
let roles = casbin_service.get_user_roles(&user.id.to_string()).await?;
|
|
|
|
Ok(PermissionCheckResult {
|
|
user_id: user.id.to_string(),
|
|
resource,
|
|
action,
|
|
has_permission,
|
|
roles,
|
|
})
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
|
async fn get_user_roles(&self, ctx: &Context<'_>) -> Result<Vec<String>> {
|
|
let user = get_auth_user(ctx).await?;
|
|
let casbin_service = ctx.data::<CasbinService>()?;
|
|
|
|
let roles = casbin_service.get_user_roles(&user.id.to_string()).await?;
|
|
Ok(roles)
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
|
async fn get_all_policies(&self, ctx: &Context<'_>) -> Result<Vec<PolicyType>> {
|
|
let casbin_service = ctx.data::<CasbinService>()?;
|
|
|
|
let policies = casbin_service.get_all_policies().await?;
|
|
|
|
Ok(policies
|
|
.into_iter()
|
|
.filter(|p| p.len() >= 3)
|
|
.map(|p| PolicyType {
|
|
role: p[0].clone(),
|
|
resource: p[1].clone(),
|
|
action: p[2].clone(),
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
|
async fn get_role_permissions(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
role_name: String,
|
|
) -> Result<Vec<PermissionPair>> {
|
|
let casbin_service = ctx.data::<CasbinService>()?;
|
|
|
|
let permissions = casbin_service.get_role_permissions(&role_name).await?;
|
|
Ok(permissions
|
|
.into_iter()
|
|
.map(|p| PermissionPair {
|
|
resource: p.0,
|
|
action: p.1,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
|
async fn can_read(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
|
|
let user = get_auth_user(ctx).await?;
|
|
let casbin_service = ctx.data::<CasbinService>()?;
|
|
|
|
let can_read = casbin_service
|
|
.can_read(&user.id.to_string(), &resource)
|
|
.await?;
|
|
Ok(can_read)
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
|
async fn can_write(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
|
|
let user = get_auth_user(ctx).await?;
|
|
let casbin_service = ctx.data::<CasbinService>()?;
|
|
|
|
let can_write = casbin_service
|
|
.can_write(&user.id.to_string(), &resource)
|
|
.await?;
|
|
Ok(can_write)
|
|
}
|
|
|
|
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
|
async fn can_delete(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
|
|
let user = get_auth_user(ctx).await?;
|
|
let casbin_service = ctx.data::<CasbinService>()?;
|
|
|
|
let can_delete = casbin_service
|
|
.can_delete(&user.id.to_string(), &resource)
|
|
.await?;
|
|
Ok(can_delete)
|
|
}
|
|
|
|
// 站点与运营配置查询方法
|
|
async fn site_ops_config(&self, ctx: &Context<'_>) -> Result<SiteOpsConfigType> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
|
|
// 获取站点配置
|
|
let site_name = settings_service
|
|
.get_setting_by_key("site.name")
|
|
.await?
|
|
.and_then(|s| s.get_string().ok())
|
|
.unwrap_or_else(|| "MMAP System".to_string());
|
|
|
|
let locale_default = settings_service
|
|
.get_setting_by_key("site.locale_default")
|
|
.await?
|
|
.and_then(|s| s.get_string().ok())
|
|
.unwrap_or_else(|| "zh-CN".to_string());
|
|
|
|
let locales_supported = settings_service
|
|
.get_setting_by_key("site.locales_supported")
|
|
.await?
|
|
.and_then(|s| s.get_json().ok())
|
|
.and_then(|v| v.as_array().cloned())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_else(|| vec!["zh-CN".to_string(), "en".to_string()]);
|
|
|
|
// 获取品牌配置
|
|
let logo_url = settings_service
|
|
.get_setting_by_key("site.brand.logo_url")
|
|
.await?
|
|
.and_then(|s| s.get_string().ok())
|
|
.unwrap_or_else(|| "/images/logo.png".to_string());
|
|
|
|
let primary_color = settings_service
|
|
.get_setting_by_key("site.brand.primary_color")
|
|
.await?
|
|
.and_then(|s| s.get_string().ok())
|
|
.unwrap_or_else(|| "#3B82F6".to_string());
|
|
|
|
let dark_mode_default = settings_service
|
|
.get_setting_by_key("site.brand.dark_mode_default")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(false);
|
|
|
|
// 获取页脚链接
|
|
let footer_links = settings_service
|
|
.get_setting_by_key("site.footer_links")
|
|
.await?
|
|
.and_then(|s| s.get_json().ok())
|
|
.and_then(|v| v.as_array().cloned())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| {
|
|
if let (Some(name), Some(url), Some(visible)) = (
|
|
v.get("name").and_then(|n| n.as_str()),
|
|
v.get("url").and_then(|u| u.as_str()),
|
|
v.get("visible_to_guest").and_then(|vis| vis.as_bool()),
|
|
) {
|
|
Some(FooterLinkType {
|
|
name: name.to_string(),
|
|
url: url.to_string(),
|
|
visible_to_guest: visible,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_else(|| {
|
|
vec![
|
|
FooterLinkType {
|
|
name: "关于我们".to_string(),
|
|
url: "/about".to_string(),
|
|
visible_to_guest: true,
|
|
},
|
|
FooterLinkType {
|
|
name: "联系我们".to_string(),
|
|
url: "/contact".to_string(),
|
|
visible_to_guest: true,
|
|
},
|
|
]
|
|
});
|
|
|
|
// 获取横幅公告配置
|
|
let banner_enabled = settings_service
|
|
.get_setting_by_key("notice.banner.enabled")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(false);
|
|
|
|
let banner_text = settings_service
|
|
.get_setting_by_key("notice.banner.text")
|
|
.await?
|
|
.and_then(|s| s.get_json().ok())
|
|
.and_then(|v| v.as_object().cloned())
|
|
.map(|obj| {
|
|
obj.iter()
|
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
|
.collect()
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let mut map = std::collections::HashMap::new();
|
|
map.insert("zh-CN".to_string(), "欢迎使用MMAP系统".to_string());
|
|
map.insert("en".to_string(), "Welcome to MMAP System".to_string());
|
|
map
|
|
});
|
|
|
|
// 获取维护窗口配置
|
|
let maintenance_config = settings_service.get_setting_by_key("maintenance.window").await?
|
|
.and_then(|s| s.get_json().ok())
|
|
.unwrap_or_else(|| serde_json::json!({
|
|
"enabled": false,
|
|
"start_time": null,
|
|
"end_time": null,
|
|
"message": {"zh-CN": "系统维护中,请稍后再试", "en": "System maintenance in progress"}
|
|
}));
|
|
|
|
let maintenance_enabled = maintenance_config
|
|
.get("enabled")
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false);
|
|
|
|
let maintenance_start_time = maintenance_config
|
|
.get("start_time")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
|
.map(|dt| dt.with_timezone(&chrono::Utc));
|
|
|
|
let maintenance_end_time = maintenance_config
|
|
.get("end_time")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
|
.map(|dt| dt.with_timezone(&chrono::Utc));
|
|
|
|
let maintenance_message = maintenance_config
|
|
.get("message")
|
|
.and_then(|v| v.as_object())
|
|
.map(|obj| {
|
|
obj.iter()
|
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
|
.collect()
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let mut map = std::collections::HashMap::new();
|
|
map.insert("zh-CN".to_string(), "系统维护中,请稍后再试".to_string());
|
|
map.insert(
|
|
"en".to_string(),
|
|
"System maintenance in progress".to_string(),
|
|
);
|
|
map
|
|
});
|
|
|
|
// 获取弹窗公告
|
|
let modal_announcements = settings_service
|
|
.get_setting_by_key("modal.announcements")
|
|
.await?
|
|
.and_then(|s| s.get_json().ok())
|
|
.and_then(|v| v.as_array().cloned())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| {
|
|
if let (
|
|
Some(id),
|
|
Some(title),
|
|
Some(content),
|
|
Some(start_time),
|
|
Some(end_time),
|
|
Some(audience),
|
|
Some(priority),
|
|
) = (
|
|
v.get("id").and_then(|id| id.as_str()),
|
|
v.get("title").and_then(|t| t.as_object()),
|
|
v.get("content").and_then(|c| c.as_object()),
|
|
v.get("start_time").and_then(|st| st.as_str()),
|
|
v.get("end_time").and_then(|et| et.as_str()),
|
|
v.get("audience").and_then(|a| a.as_array()),
|
|
v.get("priority").and_then(|p| p.as_str()),
|
|
) {
|
|
let title_map: std::collections::HashMap<String, String> = title
|
|
.iter()
|
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
|
.collect();
|
|
|
|
let content_map: std::collections::HashMap<String, String> = content
|
|
.iter()
|
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
|
.collect();
|
|
|
|
let audience_vec: Vec<String> = audience
|
|
.iter()
|
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
.collect();
|
|
|
|
if let (Ok(start_dt), Ok(end_dt)) = (
|
|
chrono::DateTime::parse_from_rfc3339(start_time),
|
|
chrono::DateTime::parse_from_rfc3339(end_time),
|
|
) {
|
|
Some(ModalAnnouncementType {
|
|
id: id.to_string(),
|
|
title: title_map,
|
|
content: content_map,
|
|
start_time: start_dt.with_timezone(&chrono::Utc),
|
|
end_time: end_dt.with_timezone(&chrono::Utc),
|
|
audience: audience_vec,
|
|
priority: priority.to_string(),
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
// 获取文档链接
|
|
let docs_links = settings_service
|
|
.get_setting_by_key("docs.links")
|
|
.await?
|
|
.and_then(|s| s.get_json().ok())
|
|
.and_then(|v| v.as_array().cloned())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| {
|
|
if let (Some(name), Some(url), Some(description)) = (
|
|
v.get("name").and_then(|n| n.as_str()),
|
|
v.get("url").and_then(|u| u.as_str()),
|
|
v.get("description").and_then(|d| d.as_str()),
|
|
) {
|
|
Some(DocLinkType {
|
|
name: name.to_string(),
|
|
url: url.to_string(),
|
|
description: description.to_string(),
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
// 获取支持渠道
|
|
let support_channels = settings_service
|
|
.get_setting_by_key("support.channels")
|
|
.await?
|
|
.and_then(|s| s.get_json().ok())
|
|
.unwrap_or_else(|| {
|
|
serde_json::json!({
|
|
"email": "support@mapp.com",
|
|
"ticket_system": "/support/tickets",
|
|
"chat_groups": [],
|
|
"working_hours": {"zh-CN": "周一至周五 9:00-18:00", "en": "Mon-Fri 9:00-18:00"}
|
|
})
|
|
});
|
|
|
|
let support_email = support_channels
|
|
.get("email")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("support@mapp.com")
|
|
.to_string();
|
|
|
|
let ticket_system = support_channels
|
|
.get("ticket_system")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("/support/tickets")
|
|
.to_string();
|
|
|
|
let chat_groups = support_channels
|
|
.get("chat_groups")
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| {
|
|
if let (Some(name), Some(description)) = (
|
|
v.get("name").and_then(|n| n.as_str()),
|
|
v.get("description").and_then(|d| d.as_str()),
|
|
) {
|
|
let url = v.get("url").and_then(|u| u.as_str()).map(|s| s.to_string());
|
|
let qr_code = v
|
|
.get("qr_code")
|
|
.and_then(|q| q.as_str())
|
|
.map(|s| s.to_string());
|
|
|
|
Some(ChatGroupType {
|
|
name: name.to_string(),
|
|
url,
|
|
qr_code,
|
|
description: description.to_string(),
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let working_hours = support_channels
|
|
.get("working_hours")
|
|
.and_then(|v| v.as_object())
|
|
.map(|obj| {
|
|
obj.iter()
|
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
|
.collect()
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let mut map = std::collections::HashMap::new();
|
|
map.insert("zh-CN".to_string(), "周一至周五 9:00-18:00".to_string());
|
|
map.insert("en".to_string(), "Mon-Fri 9:00-18:00".to_string());
|
|
map
|
|
});
|
|
|
|
// 获取运营配置
|
|
let ops_features = settings_service
|
|
.get_setting_by_key("ops.features.registration_enabled")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(true);
|
|
|
|
let invite_code_required = settings_service
|
|
.get_setting_by_key("ops.features.invite_code_required")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(true);
|
|
|
|
let email_verification = settings_service
|
|
.get_setting_by_key("ops.features.email_verification")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(false);
|
|
|
|
let max_users = settings_service
|
|
.get_setting_by_key("ops.limits.max_users")
|
|
.await?
|
|
.and_then(|s| s.get_number().ok())
|
|
.map(|n| n as i32)
|
|
.unwrap_or(1000);
|
|
|
|
let max_invite_codes = settings_service
|
|
.get_setting_by_key("ops.limits.max_invite_codes_per_user")
|
|
.await?
|
|
.and_then(|s| s.get_number().ok())
|
|
.map(|n| n as i32)
|
|
.unwrap_or(10);
|
|
|
|
let session_timeout = settings_service
|
|
.get_setting_by_key("ops.limits.session_timeout_hours")
|
|
.await?
|
|
.and_then(|s| s.get_number().ok())
|
|
.map(|n| n as i32)
|
|
.unwrap_or(24);
|
|
|
|
let welcome_email = settings_service
|
|
.get_setting_by_key("ops.notifications.welcome_email")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(true);
|
|
|
|
let system_announcements = settings_service
|
|
.get_setting_by_key("ops.notifications.system_announcements")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(true);
|
|
|
|
let maintenance_alerts = settings_service
|
|
.get_setting_by_key("ops.notifications.maintenance_alerts")
|
|
.await?
|
|
.and_then(|s| s.get_bool().ok())
|
|
.unwrap_or(true);
|
|
|
|
Ok(SiteOpsConfigType {
|
|
site: SiteConfigType {
|
|
info: SiteInfoType {
|
|
name: site_name,
|
|
locale_default,
|
|
locales_supported,
|
|
},
|
|
brand: BrandConfigType {
|
|
logo_url,
|
|
primary_color,
|
|
dark_mode_default,
|
|
},
|
|
footer_links,
|
|
},
|
|
notice_maintenance: NoticeMaintenanceType {
|
|
banner: BannerNoticeType {
|
|
enabled: banner_enabled,
|
|
text: banner_text,
|
|
},
|
|
maintenance_window: MaintenanceWindowType {
|
|
enabled: maintenance_enabled,
|
|
start_time: maintenance_start_time,
|
|
end_time: maintenance_end_time,
|
|
message: maintenance_message,
|
|
},
|
|
modal_announcements,
|
|
},
|
|
docs_support: DocsSupportType {
|
|
links: docs_links,
|
|
channels: SupportChannelsType {
|
|
email: support_email,
|
|
ticket_system,
|
|
chat_groups,
|
|
working_hours,
|
|
},
|
|
},
|
|
ops: OpsConfigType {
|
|
features: FeatureSwitchesType {
|
|
registration_enabled: ops_features,
|
|
invite_code_required,
|
|
email_verification,
|
|
},
|
|
limits: LimitsConfigType {
|
|
max_users,
|
|
max_invite_codes_per_user: max_invite_codes,
|
|
session_timeout_hours: session_timeout,
|
|
},
|
|
notifications: NotificationConfigType {
|
|
welcome_email,
|
|
system_announcements,
|
|
maintenance_alerts,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
/// 获取站点配置
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn site_config(&self, ctx: &Context<'_>) -> Result<SiteConfigType> {
|
|
let full_config = self.site_ops_config(ctx).await?;
|
|
Ok(full_config.site)
|
|
}
|
|
|
|
/// 获取公告维护配置
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn notice_maintenance_config(&self, ctx: &Context<'_>) -> Result<NoticeMaintenanceType> {
|
|
let full_config = self.site_ops_config(ctx).await?;
|
|
Ok(full_config.notice_maintenance)
|
|
}
|
|
|
|
/// 获取文档支持配置
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn docs_support_config(&self, ctx: &Context<'_>) -> Result<DocsSupportType> {
|
|
let full_config = self.site_ops_config(ctx).await?;
|
|
Ok(full_config.docs_support)
|
|
}
|
|
|
|
/// 获取运营配置
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn ops_config(&self, ctx: &Context<'_>) -> Result<OpsConfigType> {
|
|
let full_config = self.site_ops_config(ctx).await?;
|
|
Ok(full_config.ops)
|
|
}
|
|
|
|
/// 验证配置有效性
|
|
#[graphql(guard = "RequireReadPermission::new(\"settings\")")]
|
|
async fn validate_config(&self, ctx: &Context<'_>) -> Result<ConfigValidationResultType> {
|
|
let settings_service = ctx.data::<SettingsService>()?;
|
|
let mut errors = Vec::new();
|
|
let mut warnings = Vec::new();
|
|
|
|
// 验证站点名称
|
|
if let Some(setting) = settings_service.get_setting_by_key("site.name").await? {
|
|
if let Ok(name) = setting.get_string() {
|
|
if name.trim().is_empty() {
|
|
errors.push("站点名称不能为空".to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
// 验证默认语言
|
|
if let Some(setting) = settings_service
|
|
.get_setting_by_key("site.locale_default")
|
|
.await?
|
|
{
|
|
if let Ok(locale) = setting.get_string() {
|
|
if !["zh-CN", "en"].contains(&locale.as_str()) {
|
|
errors.push("默认语言必须是 zh-CN 或 en".to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
// 验证主题色格式
|
|
if let Some(setting) = settings_service
|
|
.get_setting_by_key("site.brand.primary_color")
|
|
.await?
|
|
{
|
|
if let Ok(color) = setting.get_string() {
|
|
if !color.starts_with('#') || color.len() != 7 {
|
|
warnings.push("主题色格式建议使用 #RRGGBB 格式".to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
// 验证维护窗口时间
|
|
if let Some(setting) = settings_service
|
|
.get_setting_by_key("maintenance.window")
|
|
.await?
|
|
{
|
|
if let Ok(config) = setting.get_json() {
|
|
if let (Some(enabled), Some(start), Some(end)) = (
|
|
config.get("enabled").and_then(|v| v.as_bool()),
|
|
config.get("start_time").and_then(|v| v.as_str()),
|
|
config.get("end_time").and_then(|v| v.as_str()),
|
|
) {
|
|
if enabled {
|
|
if let (Ok(start_dt), Ok(end_dt)) = (
|
|
chrono::DateTime::parse_from_rfc3339(start),
|
|
chrono::DateTime::parse_from_rfc3339(end),
|
|
) {
|
|
if start_dt >= end_dt {
|
|
errors.push("维护开始时间必须早于结束时间".to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(ConfigValidationResultType {
|
|
valid: errors.is_empty(),
|
|
errors,
|
|
warnings,
|
|
})
|
|
}
|
|
|
|
// ==================== Blog 相关查询 ====================
|
|
|
|
async fn blogs(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
filter: Option<BlogFilterInput>,
|
|
sort: Option<BlogSortInput>,
|
|
pagination: Option<PaginationInput>,
|
|
) -> Result<PaginatedResult<Blog>> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_blogs(filter, sort, pagination)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
/// 根据ID获取博客文章
|
|
async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result<Blog> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_blog_by_id(id)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
/// 根据slug获取博客文章
|
|
async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result<Blog> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_blog_by_slug(&slug)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogDetail> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_blog_detail(id)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
async fn blog_stats(&self, ctx: &Context<'_>) -> Result<BlogStats> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_blog_stats()
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
async fn blog_categories(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
filter: Option<BlogCategoryFilterInput>,
|
|
) -> Result<Vec<BlogCategory>> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_categories(filter)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogCategory> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_category_by_id(id)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
async fn blog_tags(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
filter: Option<BlogTagFilterInput>,
|
|
) -> Result<Vec<BlogTag>> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_tags(filter)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
|
|
async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogTag> {
|
|
let blog_service = ctx.data::<BlogService>()?;
|
|
blog_service
|
|
.get_tag_by_id(id)
|
|
.await
|
|
.map_err(|e| GraphQLError::new(e.to_string()))
|
|
}
|
|
}
|