diff --git a/Cargo.lock b/Cargo.lock index b44ce59..f7d6d17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,7 @@ dependencies = [ "cfg-if", "getrandom 0.3.3", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -919,6 +920,12 @@ dependencies = [ "serde_with", ] +[[package]] +name = "borrow-or-share" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" + [[package]] name = "borsh" version = "1.5.7" @@ -1035,6 +1042,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" version = "1.23.1" @@ -2199,6 +2212,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2467,6 +2489,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "flume" version = "0.11.1" @@ -2514,6 +2547,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num 0.4.3", +] + [[package]] name = "fs4" version = "0.13.1" @@ -4086,6 +4129,33 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" +dependencies = [ + "ahash 0.8.12", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "num-traits", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax 0.8.5", + "reqwest", + "serde", + "serde_json", + "uuid-simd", +] + [[package]] name = "jwtk" version = "0.4.0" @@ -4993,13 +5063,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ - "num-complex", + "num-complex 0.2.4", "num-integer", "num-iter", "num-rational 0.2.4", "num-traits", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex 0.4.6", + "num-integer", + "num-iter", + "num-rational 0.4.2", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -5027,6 +5111,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + [[package]] name = "num-complex" version = "0.2.4" @@ -5037,6 +5127,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -5354,6 +5453,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "overload" version = "0.1.1" @@ -6779,6 +6884,7 @@ dependencies = [ "itertools 0.14.0", "jpegxl-rs", "jpegxl-sys", + "jsonschema", "jwtk", "lazy_static", "lightningcss", @@ -6803,10 +6909,12 @@ dependencies = [ "rss", "rstest", "rust_decimal", + "schemars 1.0.3", "scraper", "sea-orm", "sea-orm-migration", "seaography", + "secrecy", "serde", "serde_json", "serde_variant", @@ -6911,6 +7019,20 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "referencing" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" +dependencies = [ + "ahash 0.8.12", + "fluent-uri", + "once_cell", + "parking_lot 0.12.4", + "percent-encoding", + "serde_json", +] + [[package]] name = "reflink-copy" version = "0.1.26" @@ -6993,6 +7115,7 @@ dependencies = [ "cookie", "cookie_store", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -7438,6 +7561,31 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b13ed22d6d49fe23712e068770b5c4df4a693a2b02eeff8e7ca3135627a24f6" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.104", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -7669,6 +7817,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -7760,6 +7918,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "serde_json" version = "1.0.140" @@ -7843,7 +8012,7 @@ dependencies = [ "hex 0.4.3", "indexmap 1.9.3", "indexmap 2.9.0", - "schemars", + "schemars 0.9.0", "serde", "serde_derive", "serde_json", @@ -8017,7 +8186,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" dependencies = [ - "outref", + "outref 0.1.0", ] [[package]] @@ -8060,7 +8229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed5f6ab2122c6dec69dca18c72fa4590a27e581ad20d44960fe74c032a0b23b" dependencies = [ "generic-array 0.12.4", - "num", + "num 0.2.1", ] [[package]] @@ -9551,6 +9720,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref 0.5.2", + "uuid", + "vsimd", +] + [[package]] name = "v_frame" version = "0.3.9" @@ -9592,6 +9772,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index 913c345..b0ac53a 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -166,6 +166,9 @@ quick-xml = { version = "0.37.5", features = [ ] } croner = "2.2.0" ts-rs = "11.0.1" +secrecy = { version = "0.10.3", features = ["serde"] } +schemars = "1.0.3" +jsonschema = "0.30.0" [dev-dependencies] inquire = { workspace = true } diff --git a/apps/recorder/src/errors/app_error.rs b/apps/recorder/src/errors/app_error.rs index c968917..3b3616c 100644 --- a/apps/recorder/src/errors/app_error.rs +++ b/apps/recorder/src/errors/app_error.rs @@ -313,4 +313,10 @@ impl From for RecorderError { } } +impl From for RecorderError { + fn from(error: async_graphql::Error) -> Self { + seaography::SeaographyError::AsyncGraphQLError(error).into() + } +} + pub type RecorderResult = Result; diff --git a/apps/recorder/src/graphql/domains/cron.rs b/apps/recorder/src/graphql/domains/cron.rs index 67edeab..e35cc91 100644 --- a/apps/recorder/src/graphql/domains/cron.rs +++ b/apps/recorder/src/graphql/domains/cron.rs @@ -8,8 +8,8 @@ use crate::{ infra::{ custom::register_entity_default_writable, json::{ - convert_jsonb_output_case_for_entity, restrict_jsonb_filter_input_for_entity, - validate_jsonb_input_for_entity, + convert_jsonb_output_for_entity, restrict_jsonb_filter_input_for_entity, + try_convert_jsonb_input_for_entity, }, name::get_entity_and_column_name, }, @@ -50,14 +50,16 @@ pub fn register_cron_to_schema_context(context: &mut BuilderContext) { restrict_subscriber_for_entity::(context, &cron::Column::SubscriberId); restrict_jsonb_filter_input_for_entity::(context, &cron::Column::SubscriberTask); - convert_jsonb_output_case_for_entity::( + convert_jsonb_output_for_entity::( context, &cron::Column::SubscriberTask, - Case::Camel, + Some(Case::Camel), ); - validate_jsonb_input_for_entity::>( + try_convert_jsonb_input_for_entity::>( context, &cron::Column::SubscriberTask, + subscriber_tasks::subscriber_task_schema(), + Some(Case::Snake), ); skip_columns_for_entity_input(context); } diff --git a/apps/recorder/src/graphql/domains/subscriber_tasks.rs b/apps/recorder/src/graphql/domains/subscriber_tasks.rs index 6aa3506..2fb343a 100644 --- a/apps/recorder/src/graphql/domains/subscriber_tasks.rs +++ b/apps/recorder/src/graphql/domains/subscriber_tasks.rs @@ -1,12 +1,15 @@ use std::{ops::Deref, sync::Arc}; -use async_graphql::dynamic::{FieldValue, TypeRef, ValueAccessor}; +use async_graphql::dynamic::{FieldValue, TypeRef}; use convert_case::Case; use sea_orm::{ ColumnTrait, ConnectionTrait, EntityTrait, Iterable, QueryFilter, QuerySelect, QueryTrait, prelude::Expr, sea_query::Query, }; -use seaography::{Builder as SeaographyBuilder, BuilderContext, GuardAction}; +use seaography::{ + Builder as SeaographyBuilder, BuilderContext, EntityInputBuilder, EntityObjectBuilder, + SeaographyError, prepare_active_model, +}; use crate::{ auth::AuthUserInfo, @@ -20,59 +23,23 @@ use crate::{ generate_entity_default_insert_input_object, generate_entity_delete_mutation_field, generate_entity_filtered_mutation_field, register_entity_default_readonly, }, - json::{ - convert_jsonb_output_case_for_entity, restrict_jsonb_filter_input_for_entity, - validate_jsonb_input_for_entity, - }, + json::{convert_jsonb_output_for_entity, restrict_jsonb_filter_input_for_entity}, name::{ - get_column_name, get_entity_and_column_name, get_entity_basic_type_name, - get_entity_create_batch_mutation_data_field_name, - get_entity_create_batch_mutation_field_name, - get_entity_create_one_mutation_data_field_name, - get_entity_create_one_mutation_field_name, get_entity_custom_mutation_field_name, - get_entity_update_mutation_field_name, + get_entity_and_column_name, get_entity_basic_type_name, + get_entity_custom_mutation_field_name, }, }, }, models::subscriber_tasks, - task::{ApalisJobs, ApalisSchema}, + task::{ApalisJobs, ApalisSchema, SubscriberTaskTrait}, + utils::json::convert_json_keys, }; -pub fn check_entity_and_task_subscriber_id_matches( - value_accessor: &ValueAccessor<'_>, - subscriber_id: i32, - subscriber_id_column_name: &str, - subscriber_task_column_name: &str, -) -> bool { - value_accessor.object().is_ok_and(|input_object| { - input_object - .get(subscriber_task_column_name) - .and_then(|subscriber_task_value| subscriber_task_value.object().ok()) - .and_then(|subscriber_task_object| { - subscriber_task_object - .get("subscriber_id") - .and_then(|job_subscriber_id| job_subscriber_id.i64().ok()) - }) - .is_some_and(|subscriber_task_subscriber_id| { - subscriber_task_subscriber_id as i32 - == input_object - .get(subscriber_id_column_name) - .and_then(|subscriber_id_object| subscriber_id_object.i64().ok()) - .map(|subscriber_id| subscriber_id as i32) - .unwrap_or(subscriber_id) - }) - }) -} - fn skip_columns_for_entity_input(context: &mut BuilderContext) { for column in subscriber_tasks::Column::iter() { if matches!( column, - subscriber_tasks::Column::Job - | subscriber_tasks::Column::Id - | subscriber_tasks::Column::SubscriberId - | subscriber_tasks::Column::Priority - | subscriber_tasks::Column::MaxAttempts + subscriber_tasks::Column::Job | subscriber_tasks::Column::SubscriberId ) { continue; } @@ -82,104 +49,66 @@ fn skip_columns_for_entity_input(context: &mut BuilderContext) { } } +pub fn restrict_subscriber_tasks_for_entity( + context: &mut BuilderContext, + column: &T::Column, + case: Option>, +) where + T: EntityTrait, + ::Model: Sync, +{ + let entity_and_column = get_entity_and_column_name::(context, column); + + restrict_jsonb_filter_input_for_entity::(context, column); + convert_jsonb_output_for_entity::(context, column, Some(Case::Camel)); + let entity_column_name = get_entity_and_column_name::(context, column); + context.types.input_conversions.insert( + entity_column_name.clone(), + Box::new(move |resolve_context, accessor| { + let mut json_value = accessor.as_value().clone().into_json().map_err(|err| { + SeaographyError::TypeConversionError( + err.to_string(), + format!("Json - {entity_column_name}"), + ) + })?; + + if let Some(case) = case { + json_value = convert_json_keys(json_value, case); + } + + let subscriber_id = resolve_context + .data::()? + .subscriber_auth + .subscriber_id; + + if let Some(obj) = json_value.as_object_mut() { + obj.entry("subscriber_id") + .or_insert_with(|| serde_json::Value::from(subscriber_id)); + } + + subscriber_tasks::subscriber_task_schema() + .validate(&json_value) + .map_err(|err| { + SeaographyError::TypeConversionError( + err.to_string(), + format!("Json - {entity_column_name}"), + ) + })?; + + Ok(sea_orm::Value::Json(Some(Box::new(json_value)))) + }), + ); + + context.entity_input.update_skips.push(entity_and_column); +} + pub fn register_subscriber_tasks_to_schema_context(context: &mut BuilderContext) { restrict_subscriber_for_entity::( context, &subscriber_tasks::Column::SubscriberId, ); - restrict_jsonb_filter_input_for_entity::( - context, - &subscriber_tasks::Column::Job, - ); - convert_jsonb_output_case_for_entity::( - context, - &subscriber_tasks::Column::Job, - Case::Camel, - ); - validate_jsonb_input_for_entity::( - context, - &subscriber_tasks::Column::Job, - ); + skip_columns_for_entity_input(context); - - context.guards.field_guards.insert( - get_entity_and_column_name::( - context, - &subscriber_tasks::Column::Job, - ), - { - let create_one_mutation_field_name = - Arc::new(get_entity_create_one_mutation_field_name::< - subscriber_tasks::Entity, - >(context)); - let create_one_mutation_data_field_name = - Arc::new(get_entity_create_one_mutation_data_field_name(context).to_string()); - let create_batch_mutation_field_name = - Arc::new(get_entity_create_batch_mutation_field_name::< - subscriber_tasks::Entity, - >(context)); - let create_batch_mutation_data_field_name = - Arc::new(get_entity_create_batch_mutation_data_field_name(context).to_string()); - let update_mutation_field_name = Arc::new(get_entity_update_mutation_field_name::< - subscriber_tasks::Entity, - >(context)); - let job_column_name = Arc::new(get_column_name::( - context, - &subscriber_tasks::Column::Job, - )); - let subscriber_id_column_name = Arc::new(get_column_name::( - context, - &subscriber_tasks::Column::SubscriberId, - )); - - Box::new(move |resolve_context| { - let field_name = resolve_context.field().name(); - let subscriber_id = resolve_context - .data_opt::() - .unwrap() - .subscriber_auth - .subscriber_id; - let matched_subscriber_id = match field_name { - field if field == create_one_mutation_field_name.as_str() => resolve_context - .args - .get(create_one_mutation_data_field_name.as_str()) - .is_some_and(|value_accessor| { - check_entity_and_task_subscriber_id_matches( - &value_accessor, - subscriber_id, - subscriber_id_column_name.as_str(), - job_column_name.as_str(), - ) - }), - field if field == create_batch_mutation_field_name.as_str() => resolve_context - .args - .get(create_batch_mutation_data_field_name.as_str()) - .and_then(|value| value.list().ok()) - .is_some_and(|list| { - list.iter().all(|value| { - check_entity_and_task_subscriber_id_matches( - &value, - subscriber_id, - subscriber_id_column_name.as_str(), - job_column_name.as_str(), - ) - }) - }), - field if field == update_mutation_field_name.as_str() => { - unreachable!("subscriberTask entity do not support update job") - } - _ => true, - }; - if matched_subscriber_id { - GuardAction::Allow - } else { - GuardAction::Block(Some( - "subscriber_id mismatch between entity and job".to_string(), - )) - } - }) - }, - ); } pub fn register_subscriber_tasks_to_schema_builder( @@ -282,19 +211,35 @@ pub fn register_subscriber_tasks_to_schema_builder( builder_context, None, Arc::new(|_resolver_ctx, app_ctx, input_object| { - let job_column_name = get_column_name::( - builder_context, - &subscriber_tasks::Column::Job, - ); - let task = input_object - .get(job_column_name.as_str()) - .unwrap() - .deserialize::() - .unwrap(); + let entity_input_builder = EntityInputBuilder { + context: builder_context, + }; + let entity_object_builder = EntityObjectBuilder { + context: builder_context, + }; + + let active_model: Result = + prepare_active_model( + &entity_input_builder, + &entity_object_builder, + &input_object, + _resolver_ctx, + ); Box::pin(async move { let task_service = app_ctx.task(); + let active_model = active_model?; + + let task = active_model.job.unwrap(); + let subscriber_id = active_model.subscriber_id.unwrap(); + + if task.get_subscriber_id() != subscriber_id { + Err(async_graphql::Error::new( + "subscriber_id does not match with job.subscriber_id", + ))?; + } + let task_id = task_service.add_subscriber_task(task).await?.to_string(); let db = app_ctx.db(); diff --git a/apps/recorder/src/graphql/domains/subscribers.rs b/apps/recorder/src/graphql/domains/subscribers.rs index 1682a78..a8b15e4 100644 --- a/apps/recorder/src/graphql/domains/subscribers.rs +++ b/apps/recorder/src/graphql/domains/subscribers.rs @@ -7,7 +7,7 @@ use sea_orm::{ColumnTrait, Condition, EntityTrait, Iterable, Value as SeaValue}; use seaography::{ Builder as SeaographyBuilder, BuilderContext, FilterInfo, FilterOperation as SeaographqlFilterOperation, FilterType, FilterTypesMapHelper, - FnFilterCondition, FnGuard, FnInputTypeNoneConversion, GuardAction, SeaResult, SeaographyError, + FnFilterCondition, FnGuard, FnInputTypeNoneConversion, GuardAction, SeaResult, }; use crate::{ @@ -219,11 +219,10 @@ where if let Some(value) = filter.get("eq") { let value: i32 = value.i64()?.try_into()?; if value != subscriber_id { - return Err(SeaographyError::AsyncGraphQLError( - async_graphql::Error::new( - "subscriber_id and auth_info does not match", - ), - )); + return Err(async_graphql::Error::new( + "subscriber_id and auth_info does not match", + ) + .into()); } } } diff --git a/apps/recorder/src/graphql/infra/custom.rs b/apps/recorder/src/graphql/infra/custom.rs index bda5e25..9e95d6d 100644 --- a/apps/recorder/src/graphql/infra/custom.rs +++ b/apps/recorder/src/graphql/infra/custom.rs @@ -8,7 +8,7 @@ use sea_orm::{ ActiveModelTrait, Condition, EntityTrait, IntoActiveModel, QueryFilter, TransactionTrait, }; use seaography::{ - Builder as SeaographyBuilder, BuilderContext, GuardAction, RelationBuilder, SeaographyError, + Builder as SeaographyBuilder, BuilderContext, GuardAction, RelationBuilder, get_filter_conditions, prepare_active_model, }; @@ -274,8 +274,7 @@ where &entity_object_builder, &input_object, resolve_context, - ) - .map_err(SeaographyError::AsyncGraphQLError); + ); Box::pin(async move { if active_model_hooks { @@ -442,8 +441,7 @@ where resolve_context, ) }) - .collect::, _>>() - .map_err(SeaographyError::AsyncGraphQLError); + .collect::, _>>(); Box::pin(async move { if active_model_hooks { @@ -620,8 +618,7 @@ where &entity_object_builder, &input_object, resolve_context, - ) - .map_err(SeaographyError::AsyncGraphQLError); + ); Box::pin(async move { if active_model_hooks { diff --git a/apps/recorder/src/graphql/infra/json.rs b/apps/recorder/src/graphql/infra/json.rs index b5dff4e..1246b9e 100644 --- a/apps/recorder/src/graphql/infra/json.rs +++ b/apps/recorder/src/graphql/infra/json.rs @@ -5,6 +5,7 @@ use async_graphql::{ }; use convert_case::Case; use itertools::Itertools; +use jsonschema::Validator; use rust_decimal::{Decimal, prelude::FromPrimitive}; use sea_orm::{ Condition, EntityTrait, @@ -911,18 +912,15 @@ where Box::new( move |_resolve_context: &ResolverContext<'_>, condition, filter| { if let Some(filter) = filter { - let filter_value = to_value(filter.as_index_map()).map_err(|e| { - SeaographyError::AsyncGraphQLError(GraphqlError::new_with_source(e)) - })?; + let filter_value = + to_value(filter.as_index_map()).map_err(GraphqlError::new_with_source)?; - let filter_json: JsonValue = filter_value.into_json().map_err(|e| { - SeaographyError::AsyncGraphQLError(GraphqlError::new(format!("{e:?}"))) - })?; + let filter_json: JsonValue = filter_value + .into_json() + .map_err(GraphqlError::new_with_source)?; let cond_where = prepare_jsonb_filter_input(&Expr::col(column), filter_json) - .map_err(|e| { - SeaographyError::AsyncGraphQLError(GraphqlError::new_with_source(e)) - })?; + .map_err(GraphqlError::new_with_source)?; let condition = condition.add(cond_where); Ok(condition) @@ -952,8 +950,12 @@ where ); } -pub fn validate_jsonb_input_for_entity(context: &mut BuilderContext, column: &T::Column) -where +pub fn try_convert_jsonb_input_for_entity( + context: &mut BuilderContext, + column: &T::Column, + validator: &'static Validator, + case: Option>, +) where T: EntityTrait, ::Model: Sync, S: DeserializeOwned + Serialize, @@ -962,27 +964,33 @@ where context.types.input_conversions.insert( entity_column_name.clone(), Box::new(move |_resolve_context, accessor| { - let deserialized = accessor.deserialize::().map_err(|err| { - SeaographyError::TypeConversionError( - err.message, - format!("Json - {entity_column_name}"), - ) - })?; - let json_value = serde_json::to_value(deserialized).map_err(|err| { + let mut json_value = accessor.as_value().clone().into_json().map_err(|err| { SeaographyError::TypeConversionError( err.to_string(), format!("Json - {entity_column_name}"), ) })?; + + if let Some(case) = case { + json_value = convert_json_keys(json_value, case); + } + + validator.validate(&json_value).map_err(|err| { + SeaographyError::TypeConversionError( + err.to_string(), + format!("Json - {entity_column_name}"), + ) + })?; + Ok(sea_orm::Value::Json(Some(Box::new(json_value)))) }), ); } -pub fn convert_jsonb_output_case_for_entity( +pub fn convert_jsonb_output_for_entity( context: &mut BuilderContext, column: &T::Column, - case: Case<'static>, + case: Option>, ) where T: EntityTrait, ::Model: Sync, @@ -992,14 +1000,16 @@ pub fn convert_jsonb_output_case_for_entity( entity_column_key.clone(), Box::new(move |value| { if let sea_orm::Value::Json(Some(json)) = value { - let result = - async_graphql::Value::from_json(convert_json_keys(json.as_ref().clone(), case)) - .map_err(|err| { - SeaographyError::TypeConversionError( - err.to_string(), - format!("Json - {entity_column_key}"), - ) - })?; + let mut json_value = json.as_ref().clone(); + if let Some(case) = case { + json_value = convert_json_keys(json_value, case); + } + let result = async_graphql::Value::from_json(json_value).map_err(|err| { + SeaographyError::TypeConversionError( + err.to_string(), + format!("Json - {entity_column_key}"), + ) + })?; Ok(result) } else { Err(SeaographyError::TypeConversionError( diff --git a/apps/recorder/src/models/subscriber_tasks/mod.rs b/apps/recorder/src/models/subscriber_tasks/mod.rs index fa32e82..b33f9f7 100644 --- a/apps/recorder/src/models/subscriber_tasks/mod.rs +++ b/apps/recorder/src/models/subscriber_tasks/mod.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; pub use crate::task::{ SubscriberTask, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant, - SubscriberTaskTypeVariantIter, + SubscriberTaskTypeVariantIter, subscriber_task_schema, }; #[derive(Clone, Debug, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay)] diff --git a/apps/recorder/src/task/mod.rs b/apps/recorder/src/task/mod.rs index 3d593f2..9d327c1 100644 --- a/apps/recorder/src/task/mod.rs +++ b/apps/recorder/src/task/mod.rs @@ -16,5 +16,6 @@ pub use registry::{ SubscriberTaskTypeVariant, SubscriberTaskTypeVariantIter, SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask, SyncOneSubscriptionSourcesTask, SystemTask, SystemTaskType, SystemTaskTypeEnum, SystemTaskTypeVariant, SystemTaskTypeVariantIter, + subscriber_task_schema, }; pub use service::TaskService; diff --git a/apps/recorder/src/task/registry/mod.rs b/apps/recorder/src/task/registry/mod.rs index adc9622..2ac9e39 100644 --- a/apps/recorder/src/task/registry/mod.rs +++ b/apps/recorder/src/task/registry/mod.rs @@ -5,6 +5,7 @@ pub use subscriber::{ SubscriberTask, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant, SubscriberTaskTypeVariantIter, SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask, SyncOneSubscriptionSourcesTask, + subscriber_task_schema, }; pub use system::{ OptimizeImageTask, SystemTask, SystemTaskType, SystemTaskTypeEnum, SystemTaskTypeVariant, diff --git a/apps/recorder/src/task/registry/subscriber/base.rs b/apps/recorder/src/task/registry/subscriber/base.rs index b3ab4a6..d000e91 100644 --- a/apps/recorder/src/task/registry/subscriber/base.rs +++ b/apps/recorder/src/task/registry/subscriber/base.rs @@ -6,7 +6,7 @@ macro_rules! register_subscriber_task_type { } ) => { $(#[$type_meta])* - #[derive(typed_builder::TypedBuilder)] + #[derive(typed_builder::TypedBuilder, schemars::JsonSchema)] $task_vis struct $task_name { $($(#[$field_meta])* pub $field_name: $field_type,)* pub subscriber_id: i32, diff --git a/apps/recorder/src/task/registry/subscriber/mod.rs b/apps/recorder/src/task/registry/subscriber/mod.rs index 2541e9a..92b2f2e 100644 --- a/apps/recorder/src/task/registry/subscriber/mod.rs +++ b/apps/recorder/src/task/registry/subscriber/mod.rs @@ -1,6 +1,9 @@ mod base; mod subscription; +use jsonschema::Validator; +use once_cell::sync::OnceCell; +use schemars::JsonSchema; use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult}; use serde::{Deserialize, Serialize}; pub use subscription::{ @@ -133,7 +136,7 @@ register_subscriber_task_types!( } }, task_enum: { - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, FromJsonQueryResult)] + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, FromJsonQueryResult, JsonSchema)] pub enum SubscriberTask { SyncOneSubscriptionFeedsIncremental(SyncOneSubscriptionFeedsIncrementalTask), SyncOneSubscriptionFeedsFull(SyncOneSubscriptionFeedsFullTask), @@ -141,3 +144,15 @@ register_subscriber_task_types!( } } ); + +static SUBSCRIBER_TASK_SCHEMA: OnceCell = OnceCell::new(); + +pub fn subscriber_task_schema() -> &'static Validator { + SUBSCRIBER_TASK_SCHEMA.get_or_init(|| { + let schema = schemars::schema_for!(SubscriberTask); + jsonschema::options() + .with_draft(jsonschema::Draft::Draft7) + .build(&serde_json::to_value(&schema).unwrap()) + .unwrap() + }) +} diff --git a/apps/webui/src/domains/recorder/schema/subscriptions.ts b/apps/webui/src/domains/recorder/schema/subscriptions.ts index 1f19af6..ff34077 100644 --- a/apps/webui/src/domains/recorder/schema/subscriptions.ts +++ b/apps/webui/src/domains/recorder/schema/subscriptions.ts @@ -89,6 +89,7 @@ query GetSubscriptionDetail ($id: Int!) { } }) { nodes { id + subscriberId displayName createdAt updatedAt @@ -138,8 +139,8 @@ query GetSubscriptionDetail ($id: Int!) { `; export const SYNC_SUBSCRIPTION_FEEDS_INCREMENTAL = gql` - mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) { - subscriptionsSyncOneFeedsIncremental(filter: $filter) { + mutation SyncSubscriptionFeedsIncremental($data: SubscriberTasksInsertInput!) { + subscriberTasksCreateOne(data: $data) { id } } diff --git a/apps/webui/src/domains/recorder/services/subscriber.service.ts b/apps/webui/src/domains/recorder/services/subscriber.service.ts new file mode 100644 index 0000000..f0445bd --- /dev/null +++ b/apps/webui/src/domains/recorder/services/subscriber.service.ts @@ -0,0 +1,7 @@ +import { AuthService } from '@/domains/auth/auth.service'; +import { Injectable, inject } from '@outposts/injection-js'; + +@Injectable() +export class SubscriberService { + authService = inject(AuthService); +} diff --git a/apps/webui/src/infra/graphql/gql/gql.ts b/apps/webui/src/infra/graphql/gql/gql.ts index 35e99e4..ba6c22c 100644 --- a/apps/webui/src/infra/graphql/gql/gql.ts +++ b/apps/webui/src/infra/graphql/gql/gql.ts @@ -26,8 +26,8 @@ type Documents = { "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, - "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, + "\n mutation SyncSubscriptionFeedsIncremental($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument, "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument, @@ -47,8 +47,8 @@ const documents: Documents = { "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, - "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, + "\n mutation SyncSubscriptionFeedsIncremental($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument, "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument, @@ -121,11 +121,11 @@ export function gql(source: "\n mutation DeleteSubscriptions($filter: Subscri /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"]; +export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n"): (typeof documents)["\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n"]; +export function gql(source: "\n mutation SyncSubscriptionFeedsIncremental($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n"): (typeof documents)["\n mutation SyncSubscriptionFeedsIncremental($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/apps/webui/src/infra/graphql/gql/graphql.ts b/apps/webui/src/infra/graphql/gql/graphql.ts index 7e8813e..f125a52 100644 --- a/apps/webui/src/infra/graphql/gql/graphql.ts +++ b/apps/webui/src/infra/graphql/gql/graphql.ts @@ -1674,10 +1674,7 @@ export type SubscriberTasksFilterInput = { }; export type SubscriberTasksInsertInput = { - id?: InputMaybe; job: Scalars['Json']['input']; - maxAttempts: Scalars['Int']['input']; - priority: Scalars['Int']['input']; subscriberId?: InputMaybe; }; @@ -2184,14 +2181,14 @@ export type GetSubscriptionDetailQueryVariables = Exact<{ }>; -export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, feed: { __typename?: 'FeedsConnection', nodes: Array<{ __typename?: 'Feeds', id: number, createdAt: string, updatedAt: string, token: string, feedType: FeedTypeEnum, feedSource: FeedSourceEnum }> }, subscriberTask: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum }> }, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, homepage?: string | null }> } }> } }; +export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, subscriberId: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, feed: { __typename?: 'FeedsConnection', nodes: Array<{ __typename?: 'Feeds', id: number, createdAt: string, updatedAt: string, token: string, feedType: FeedTypeEnum, feedSource: FeedSourceEnum }> }, subscriberTask: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum }> }, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, homepage?: string | null }> } }> } }; export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{ - filter: SubscriptionsFilterInput; + data: SubscriberTasksInsertInput; }>; -export type SyncSubscriptionFeedsIncrementalMutation = { __typename?: 'Mutation', subscriptionsSyncOneFeedsIncremental: { __typename?: 'SubscriberTasksBasic', id: string } }; +export type SyncSubscriptionFeedsIncrementalMutation = { __typename?: 'Mutation', subscriberTasksCreateOne: { __typename?: 'SubscriberTasksBasic', id: string } }; export type SyncSubscriptionFeedsFullMutationVariables = Exact<{ filter: SubscriptionsFilterInput; @@ -2243,8 +2240,8 @@ export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode; export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode; export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode; -export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"feedSource"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTask"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"subscriberId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"feedSource"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTask"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasksCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const GetTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"job"}},{"kind":"Field","name":{"kind":"Name","value":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"attempts"}},{"kind":"Field","name":{"kind":"Name","value":"maxAttempts"}},{"kind":"Field","name":{"kind":"Name","value":"runAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"lockAt"}},{"kind":"Field","name":{"kind":"Name","value":"lockBy"}},{"kind":"Field","name":{"kind":"Name","value":"doneAt"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/-sync.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/-sync.tsx index c404edc..9d36cab 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/-sync.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/-sync.tsx @@ -11,13 +11,14 @@ import { SYNC_SUBSCRIPTION_FEEDS_INCREMENTAL, SYNC_SUBSCRIPTION_SOURCES, } from '@/domains/recorder/schema/subscriptions'; -import type { - SyncSubscriptionFeedsFullMutation, - SyncSubscriptionFeedsFullMutationVariables, - SyncSubscriptionFeedsIncrementalMutation, - SyncSubscriptionFeedsIncrementalMutationVariables, - SyncSubscriptionSourcesMutation, - SyncSubscriptionSourcesMutationVariables, +import { + SubscriberTaskTypeEnum, + type SyncSubscriptionFeedsFullMutation, + type SyncSubscriptionFeedsFullMutationVariables, + type SyncSubscriptionFeedsIncrementalMutation, + type SyncSubscriptionFeedsIncrementalMutationVariables, + type SyncSubscriptionSourcesMutation, + type SyncSubscriptionSourcesMutationVariables, } from '@/infra/graphql/gql/graphql'; import { useMutation } from '@apollo/client'; import { useNavigate } from '@tanstack/react-router'; @@ -43,7 +44,7 @@ export const SubscriptionSyncView = memo( >(SYNC_SUBSCRIPTION_FEEDS_INCREMENTAL, { onCompleted: (data) => { toast.success('Sync completed'); - onComplete(data.subscriptionsSyncOneFeedsIncremental); + onComplete(data.subscriberTasksCreateOne); }, onError: (error) => { toast.error('Failed to sync subscription', { @@ -103,7 +104,15 @@ export const SubscriptionSyncView = memo( variant="outline" onClick={() => syncSubscriptionFeedsIncremental({ - variables: { filter: { id: { eq: id } } }, + variables: { + data: { + job: { + subscriberId: id, + taskType: + SubscriberTaskTypeEnum.SyncOneSubscriptionFeedsIncremental, + }, + }, + }, }) } > diff --git a/justfile b/justfile index 1ef8eeb..d64d184 100644 --- a/justfile +++ b/justfile @@ -27,7 +27,7 @@ dev-proxy: pnpm run --parallel --filter=proxy dev dev-recorder: - watchexec -r -e rs,toml,yaml,json,env -- cargo run -p recorder --bin recorder_cli -- --environment=development --graceful-shutdown=false + watchexec -r -e rs,toml,yaml,json -- cargo run -p recorder --bin recorder_cli -- --environment=development --graceful-shutdown=false prod-recorder: prod-webui cargo run --release -p recorder --bin recorder_cli -- --environment=production --working-dir=apps/recorder --graceful-shutdown=false