refactor: refactor graphql

This commit is contained in:
2025-06-27 04:06:58 +08:00
parent 3a8eb88e1a
commit 65505f91b2
43 changed files with 2199 additions and 818 deletions

View File

@@ -64,7 +64,9 @@ impl Model {
.one(db)
.await?
.ok_or_else(|| {
RecorderError::from_model_not_found_detail("auth", format!("pid {pid} not found"))
RecorderError::from_entity_not_found_detail::<Entity, _>(format!(
"pid {pid} not found"
))
})?;
Ok(subscriber_auth)
}

View File

@@ -1,3 +1,5 @@
use serde::{Deserialize, Serialize};
pub const CRON_DUE_EVENT: &str = "cron_due";
pub const CHECK_AND_CLEANUP_EXPIRED_CRON_LOCKS_FUNCTION_NAME: &str =
@@ -7,3 +9,15 @@ pub const CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME: &str = "check_and_trigger_d
pub const NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME: &str = "notify_due_cron_when_mutating";
pub const NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME: &str =
"notify_due_cron_when_mutating_trigger";
pub const SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME: &str = "setup_cron_extra_foreign_keys";
pub const SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME: &str =
"setup_cron_extra_foreign_keys_trigger";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CronCreateOptions {
pub cron_expr: String,
pub priority: Option<i32>,
pub timeout_ms: Option<i32>,
pub max_attempts: Option<i32>,
pub enabled: Option<bool>,
}

View File

@@ -3,8 +3,9 @@ mod registry;
pub use core::{
CHECK_AND_CLEANUP_EXPIRED_CRON_LOCKS_FUNCTION_NAME, CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME,
CRON_DUE_EVENT, NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME,
NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME,
CRON_DUE_EVENT, CronCreateOptions, NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME,
NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME,
SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
};
use async_trait::async_trait;
@@ -17,21 +18,7 @@ use sea_orm::{
};
use serde::{Deserialize, Serialize};
use crate::{
app::AppContextTrait,
errors::{RecorderError, RecorderResult},
models::subscriptions::{self},
};
#[derive(
Debug, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay, Serialize, Deserialize,
)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "cron_source")]
#[serde(rename_all = "snake_case")]
pub enum CronSource {
#[sea_orm(string_value = "subscription")]
Subscription,
}
use crate::{app::AppContextTrait, errors::RecorderResult, models::subscriber_tasks};
#[derive(
Debug, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay, Serialize, Deserialize,
@@ -58,7 +45,6 @@ pub struct Model {
pub updated_at: DateTimeUtc,
#[sea_orm(primary_key)]
pub id: i32,
pub cron_source: CronSource,
pub subscriber_id: Option<i32>,
pub subscription_id: Option<i32>,
pub cron_expr: String,
@@ -67,6 +53,7 @@ pub struct Model {
pub last_error: Option<String>,
pub locked_by: Option<String>,
pub locked_at: Option<DateTimeUtc>,
#[sea_orm(default_expr = "5000")]
pub timeout_ms: i32,
#[sea_orm(default_expr = "0")]
pub attempts: i32,
@@ -77,6 +64,7 @@ pub struct Model {
pub status: CronStatus,
#[sea_orm(default_expr = "true")]
pub enabled: bool,
pub subscriber_task: Option<subscriber_tasks::SubscriberTask>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -119,6 +107,38 @@ pub enum RelatedEntity {
Subscription,
}
impl ActiveModel {
pub fn from_subscriber_task(
subscriber_task: subscriber_tasks::SubscriberTask,
cron_options: CronCreateOptions,
) -> RecorderResult<Self> {
let mut active_model = Self {
next_run: Set(Some(Model::calculate_next_run(&cron_options.cron_expr)?)),
cron_expr: Set(cron_options.cron_expr),
subscriber_task: Set(Some(subscriber_task)),
..Default::default()
};
if let Some(priority) = cron_options.priority {
active_model.priority = Set(priority);
}
if let Some(timeout_ms) = cron_options.timeout_ms {
active_model.timeout_ms = Set(timeout_ms);
}
if let Some(max_attempts) = cron_options.max_attempts {
active_model.max_attempts = Set(max_attempts);
}
if let Some(enabled) = cron_options.enabled {
active_model.enabled = Set(enabled);
}
Ok(active_model)
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {}
@@ -196,19 +216,13 @@ impl Model {
}
async fn exec_cron(&self, ctx: &dyn AppContextTrait) -> RecorderResult<()> {
match self.cron_source {
CronSource::Subscription => {
let subscription_id = self.subscription_id.unwrap_or_else(|| {
unreachable!("Subscription cron must have a subscription id")
});
let subscription = subscriptions::Entity::find_by_id(subscription_id)
.one(ctx.db())
.await?
.ok_or_else(|| RecorderError::from_model_not_found("Subscription"))?;
subscription.exec_cron(ctx).await?;
}
if let Some(subscriber_task) = self.subscriber_task.as_ref() {
let task_service = ctx.task();
task_service
.add_subscriber_task(subscriber_task.clone())
.await?;
} else {
unimplemented!("Cron without subscriber task is not supported now");
}
Ok(())
@@ -217,7 +231,7 @@ impl Model {
async fn mark_cron_completed(&self, ctx: &dyn AppContextTrait) -> RecorderResult<()> {
let db = ctx.db();
let next_run = self.calculate_next_run(&self.cron_expr)?;
let next_run = Self::calculate_next_run(&self.cron_expr)?;
ActiveModel {
id: Set(self.id),
@@ -250,7 +264,7 @@ impl Model {
let next_run = if should_retry {
Some(Utc::now() + chrono::Duration::seconds(5))
} else {
Some(self.calculate_next_run(&self.cron_expr)?)
Some(Self::calculate_next_run(&self.cron_expr)?)
};
ActiveModel {
@@ -295,7 +309,7 @@ impl Model {
Ok(())
}
fn calculate_next_run(&self, cron_expr: &str) -> RecorderResult<DateTime<Utc>> {
pub fn calculate_next_run(cron_expr: &str) -> RecorderResult<DateTime<Utc>> {
let cron_expr = Cron::new(cron_expr).parse()?;
let next = cron_expr.find_next_occurrence(&Utc::now(), false)?;

View File

@@ -122,7 +122,7 @@ impl Model {
.filter(Column::FeedType.eq(FeedType::Rss))
.one(db)
.await?
.ok_or(RecorderError::from_model_not_found("Feed"))?;
.ok_or(RecorderError::from_entity_not_found::<Entity>())?;
let feed = Feed::from_model(ctx, feed_model).await?;

View File

@@ -44,7 +44,7 @@ impl Feed {
.await?;
(subscription, episodes)
} else {
return Err(RecorderError::from_model_not_found("Subscription"));
return Err(RecorderError::from_entity_not_found::<subscriptions::Entity>());
};
Ok(Feed::SubscritpionEpisodes(

View File

@@ -131,7 +131,7 @@ impl Model {
let db = ctx.db();
let subscriber = Entity::find_by_id(id).one(db).await?.ok_or_else(|| {
RecorderError::from_model_not_found_detail("subscribers", format!("id {id} not found"))
RecorderError::from_entity_not_found_detail::<Entity, _>(format!("id {id} not found"))
})?;
Ok(subscriber)
}

View File

@@ -11,10 +11,7 @@ pub use registry::{
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{
app::AppContextTrait,
errors::{RecorderError, RecorderResult},
};
use crate::{app::AppContextTrait, errors::RecorderResult};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "subscriptions")]
@@ -155,50 +152,6 @@ impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {}
impl Model {
pub async fn toggle_with_ids(
ctx: &dyn AppContextTrait,
ids: impl Iterator<Item = i32>,
enabled: bool,
) -> RecorderResult<()> {
let db = ctx.db();
Entity::update_many()
.col_expr(Column::Enabled, Expr::value(enabled))
.filter(Column::Id.is_in(ids))
.exec(db)
.await?;
Ok(())
}
pub async fn delete_with_ids(
ctx: &dyn AppContextTrait,
ids: impl Iterator<Item = i32>,
) -> RecorderResult<()> {
let db = ctx.db();
Entity::delete_many()
.filter(Column::Id.is_in(ids))
.exec(db)
.await?;
Ok(())
}
pub async fn find_by_id_and_subscriber_id(
ctx: &dyn AppContextTrait,
subscriber_id: i32,
subscription_id: i32,
) -> RecorderResult<Self> {
let db = ctx.db();
let subscription_model = Entity::find_by_id(subscription_id)
.one(db)
.await?
.ok_or_else(|| RecorderError::from_model_not_found("Subscription"))?;
if subscription_model.subscriber_id != subscriber_id {
Err(RecorderError::from_model_not_found("Subscription"))?;
}
Ok(subscription_model)
}
pub async fn exec_cron(&self, _ctx: &dyn AppContextTrait) -> RecorderResult<()> {
todo!()
}

View File

@@ -1,129 +1,147 @@
use std::{fmt::Debug, sync::Arc};
use async_trait::async_trait;
use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter};
use serde::{Deserialize, Serialize};
use crate::{
app::AppContextTrait,
errors::{RecorderError, RecorderResult},
errors::RecorderResult,
extract::mikan::{
MikanBangumiSubscription, MikanSeasonSubscription, MikanSubscriberSubscription,
},
models::subscriptions::{self, SubscriptionTrait},
};
#[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, DeriveDisplay,
)]
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "subscription_category"
)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionCategory {
#[sea_orm(string_value = "mikan_subscriber")]
MikanSubscriber,
#[sea_orm(string_value = "mikan_season")]
MikanSeason,
#[sea_orm(string_value = "mikan_bangumi")]
MikanBangumi,
#[sea_orm(string_value = "manual")]
Manual,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "category")]
pub enum Subscription {
#[serde(rename = "mikan_subscriber")]
MikanSubscriber(MikanSubscriberSubscription),
#[serde(rename = "mikan_season")]
MikanSeason(MikanSeasonSubscription),
#[serde(rename = "mikan_bangumi")]
MikanBangumi(MikanBangumiSubscription),
#[serde(rename = "manual")]
Manual,
}
impl Subscription {
pub fn category(&self) -> SubscriptionCategory {
match self {
Self::MikanSubscriber(_) => SubscriptionCategory::MikanSubscriber,
Self::MikanSeason(_) => SubscriptionCategory::MikanSeason,
Self::MikanBangumi(_) => SubscriptionCategory::MikanBangumi,
Self::Manual => SubscriptionCategory::Manual,
}
}
}
#[async_trait]
impl SubscriptionTrait for Subscription {
fn get_subscriber_id(&self) -> i32 {
match self {
Self::MikanSubscriber(subscription) => subscription.get_subscriber_id(),
Self::MikanSeason(subscription) => subscription.get_subscriber_id(),
Self::MikanBangumi(subscription) => subscription.get_subscriber_id(),
Self::Manual => unreachable!(),
}
}
fn get_subscription_id(&self) -> i32 {
match self {
Self::MikanSubscriber(subscription) => subscription.get_subscription_id(),
Self::MikanSeason(subscription) => subscription.get_subscription_id(),
Self::MikanBangumi(subscription) => subscription.get_subscription_id(),
Self::Manual => unreachable!(),
}
}
async fn sync_feeds_incremental(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
match self {
Self::MikanSubscriber(subscription) => subscription.sync_feeds_incremental(ctx).await,
Self::MikanSeason(subscription) => subscription.sync_feeds_incremental(ctx).await,
Self::MikanBangumi(subscription) => subscription.sync_feeds_incremental(ctx).await,
Self::Manual => Ok(()),
}
}
async fn sync_feeds_full(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
match self {
Self::MikanSubscriber(subscription) => subscription.sync_feeds_full(ctx).await,
Self::MikanSeason(subscription) => subscription.sync_feeds_full(ctx).await,
Self::MikanBangumi(subscription) => subscription.sync_feeds_full(ctx).await,
Self::Manual => Ok(()),
}
}
async fn sync_sources(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
match self {
Self::MikanSubscriber(subscription) => subscription.sync_sources(ctx).await,
Self::MikanSeason(subscription) => subscription.sync_sources(ctx).await,
Self::MikanBangumi(subscription) => subscription.sync_sources(ctx).await,
Self::Manual => Ok(()),
}
}
fn try_from_model(model: &subscriptions::Model) -> RecorderResult<Self> {
match model.category {
SubscriptionCategory::MikanSubscriber => {
MikanSubscriberSubscription::try_from_model(model).map(Self::MikanSubscriber)
macro_rules! register_subscription_type {
(
subscription_category_enum: {
$(#[$subscription_category_enum_meta:meta])*
pub enum $type_enum_name:ident {
$(
$(#[$variant_meta:meta])*
$variant:ident => $string_value:literal
),* $(,)?
}
SubscriptionCategory::MikanSeason => {
MikanSeasonSubscription::try_from_model(model).map(Self::MikanSeason)
}$(,)?
subscription_enum: {
$(#[$subscription_enum_meta:meta])*
pub enum $subscription_enum_name:ident {
$(
$subscription_variant:ident($subscription_type:ty)
),* $(,)?
}
SubscriptionCategory::MikanBangumi => {
MikanBangumiSubscription::try_from_model(model).map(Self::MikanBangumi)
}
) => {
$(#[$subscription_category_enum_meta])*
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "subscription_category"
)]
pub enum $type_enum_name {
$(
$(#[$variant_meta])*
#[serde(rename = $string_value)]
#[sea_orm(string_value = $string_value)]
$variant,
)*
}
$(#[$subscription_enum_meta])*
#[serde(tag = "category")]
pub enum $subscription_enum_name {
$(
#[serde(rename = $string_value)]
$subscription_variant($subscription_type),
)*
}
impl $subscription_enum_name {
pub fn category(&self) -> $type_enum_name {
match self {
$(Self::$subscription_variant(_) => $type_enum_name::$variant,)*
}
}
SubscriptionCategory::Manual => Ok(Self::Manual),
}
#[async_trait::async_trait]
impl $crate::models::subscriptions::SubscriptionTrait for $subscription_enum_name {
fn get_subscriber_id(&self) -> i32 {
match self {
$(Self::$subscription_variant(subscription) => subscription.get_subscriber_id(),)*
}
}
fn get_subscription_id(&self) -> i32 {
match self {
$(Self::$subscription_variant(subscription) => subscription.get_subscription_id(),)*
}
}
async fn sync_feeds_incremental(&self, ctx: Arc<dyn $crate::app::AppContextTrait>) -> $crate::errors::RecorderResult<()> {
match self {
$(Self::$subscription_variant(subscription) => subscription.sync_feeds_incremental(ctx).await,)*
}
}
async fn sync_feeds_full(&self, ctx: Arc<dyn $crate::app::AppContextTrait>) -> $crate::errors::RecorderResult<()> {
match self {
$(Self::$subscription_variant(subscription) => subscription.sync_feeds_full(ctx).await,)*
}
}
async fn sync_sources(&self, ctx: Arc<dyn $crate::app::AppContextTrait>) -> $crate::errors::RecorderResult<()> {
match self {
$(Self::$subscription_variant(subscription) => subscription.sync_sources(ctx).await,)*
}
}
fn try_from_model(model: &subscriptions::Model) -> RecorderResult<Self> {
match model.category {
$($type_enum_name::$variant => {
<$subscription_type as $crate::models::subscriptions::SubscriptionTrait>::try_from_model(model).map(Self::$subscription_variant)
})*
}
}
}
impl TryFrom<&$crate::models::subscriptions::Model> for $subscription_enum_name {
type Error = $crate::errors::RecorderError;
fn try_from(model: &$crate::models::subscriptions::Model) -> Result<Self, Self::Error> {
Self::try_from_model(model)
}
}
};
}
register_subscription_type! {
subscription_category_enum: {
#[derive(
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
Copy,
DeriveActiveEnum,
DeriveDisplay,
EnumIter,
)]
pub enum SubscriptionCategory {
MikanSubscriber => "mikan_subscriber",
MikanSeason => "mikan_season",
MikanBangumi => "mikan_bangumi",
}
}
subscription_enum: {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Subscription {
MikanSubscriber(MikanSubscriberSubscription),
MikanSeason(MikanSeasonSubscription),
MikanBangumi(MikanBangumiSubscription)
}
}
}
impl TryFrom<&subscriptions::Model> for Subscription {
type Error = RecorderError;
fn try_from(model: &subscriptions::Model) -> Result<Self, Self::Error> {
Self::try_from_model(model)
}
}